Skip to content
9 changes: 9 additions & 0 deletions dandi/cli/tests/test_cmd_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ def redirected_logdir(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
return logdir


@pytest.mark.ai_generated
def test_validate_missing_path(tmp_path: Path) -> None:
missing = tmp_path / "missing.nwb"
result = CliRunner().invoke(validate, [str(missing)])
assert result.exit_code == 2
assert "does not exist" in result.output
assert missing.name in result.output


@pytest.mark.parametrize(
"ds_name, expected_err_location",
[
Expand Down
8 changes: 8 additions & 0 deletions dandi/files/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from collections import deque
from collections.abc import Iterator
import errno
import os.path
from pathlib import Path

Expand Down Expand Up @@ -84,6 +85,10 @@ def find_dandi_files(
they are of a type represented by a `LocalDirectoryAsset` subclass, in
which case they are not recursed into.

All requested paths are checked before any files are yielded. A missing
path raises `FileNotFoundError`; a broken symlink is still discovered so
that the caller can apply its missing-content policy.

:param dandiset_path:
The path to the root of the Dandiset in which the paths are located.
All paths in ``paths`` must be equal to or subpaths of
Expand Down Expand Up @@ -111,6 +116,9 @@ def find_dandi_files(
raise ValueError(
f"Path {str(p)!r} is not inside Dandiset path {str(dandiset_path)!r}"
)
# Do not follow symlinks: unfetched DataLad content can be a broken link.
if not os.path.lexists(p):
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(p))
path_queue.append((Path(p), None))
bids_roots = []
while path_queue:
Expand Down
35 changes: 35 additions & 0 deletions dandi/tests/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,41 @@ def mkpaths(root: Path, *paths: str) -> None:
pp.touch()


@pytest.mark.ai_generated
@pytest.mark.parametrize(
"name", ["missing.nwb", "missing.txt", "missing.zarr", ".missing"]
)
@pytest.mark.parametrize("allow_all", [False, True])
def test_find_dandi_files_missing_path(
tmp_path: Path, name: str, allow_all: bool
) -> None:
existing = tmp_path / "existing.nwb"
existing.touch()
missing = tmp_path / name
discovery = find_dandi_files(
existing, missing, dandiset_path=tmp_path, allow_all=allow_all
)
with pytest.raises(FileNotFoundError) as exc:
next(discovery)
assert exc.value.filename == str(missing)


@pytest.mark.ai_generated
@pytest.mark.skipif(os.name == "nt", reason="Creating symlinks requires Windows privileges")
def test_find_dandi_files_broken_symlink(tmp_path: Path) -> None:
link = tmp_path / "unfetched.nwb"
link.symlink_to(tmp_path / "absent.nwb")
assets = list(find_dandi_files(link))
assert len(assets) == 1
assert isinstance(assets[0], NWBAsset)
assert assets[0].filepath == link


@pytest.mark.ai_generated
def test_find_dandi_files_empty_directory(tmp_path: Path) -> None:
assert list(find_dandi_files(tmp_path, dandiset_path=tmp_path)) == []


def test_find_dandi_files(tmp_path: Path) -> None:
mkpaths(
tmp_path,
Expand Down
Loading