diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index f0bdb1481bb..5010571e453 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -1,5 +1,6 @@ import atexit import contextlib +import errno import fnmatch import importlib.util import itertools @@ -549,17 +550,40 @@ def resolve_package_path(path: Path) -> Optional[Path]: def visit( - path: str, recurse: Callable[["os.DirEntry[str]"], bool] + path: str, + recurse: Callable[["os.DirEntry[str]"], bool], + *, + _seen: Optional[Set[str]] = None, ) -> Iterator["os.DirEntry[str]"]: """Walk a directory recursively, in breadth-first order. Entries at each directory level are sorted. """ + if _seen is None: + _seen = set() + + real_path = os.path.realpath(path) + if real_path in _seen: + return + _seen.add(real_path) + entries = sorted(os.scandir(path), key=lambda entry: entry.name) yield from entries for entry in entries: - if entry.is_dir(follow_symlinks=False) and recurse(entry): - yield from visit(entry.path, recurse) + try: + is_directory = entry.is_dir(follow_symlinks=True) + except OSError as exc: + if exc.errno in ( + errno.ENOENT, + errno.ENOTDIR, + errno.ELOOP, + errno.EACCES, + errno.EPERM, + ): + continue + raise + if is_directory and recurse(entry): + yield from visit(entry.path, recurse, _seen=_seen) def absolutepath(path: Union[Path, str]) -> Path: diff --git a/testing/test_collection.py b/testing/test_collection.py index 841aa358b96..fac08cd8723 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1256,6 +1256,32 @@ def test_collect_sub_with_symlinks(use_pkg, testdir): ) +def test_collect_symlinked_directory_argument(testdir): + target = testdir.mkdir("real_dir") + target.join("test_linked.py").write("def test_linked(): pass") + + link = testdir.tmpdir.join("link_dir") + symlink_or_skip(target, link, target_is_directory=True) + + result = testdir.runpytest("-v", str(link)) + result.stdout.fnmatch_lines( + ("link_dir/test_linked.py::test_linked PASSED*", "*1 passed in*",) + ) + + +def test_collect_symlinked_subdirectory(testdir): + target = testdir.mkdir("real_pkg") + target.join("test_inside.py").write("def test_inside(): pass") + + root = testdir.mkdir("root") + symlink_or_skip(target, root.join("link_pkg"), target_is_directory=True) + + result = testdir.runpytest("-v", str(root)) + result.stdout.fnmatch_lines( + ("root/link_pkg/test_inside.py::test_inside PASSED*", "*1 passed in*",) + ) + + def test_collector_respects_tbstyle(testdir): p1 = testdir.makepyfile("assert 0") result = testdir.runpytest(p1, "--tb=native")