Skip to content

Commit 1142d13

Browse files
authored
Merge pull request #7986 from bluetech/backport-7956
[6.1.x] Fix handling recursive symlinks
2 parents 296f468 + 1adbb04 commit 1142d13

File tree

5 files changed

+66
-1
lines changed

5 files changed

+66
-1
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,4 @@ Xuecong Liao
310310
Yoav Caspi
311311
Zac Hatfield-Dodds
312312
Zoltán Máté
313+
Zsolt Cserna

changelog/7951.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed handling of recursive symlinks when collecting tests.

src/_pytest/pathlib.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import uuid
1010
import warnings
1111
from enum import Enum
12+
from errno import EBADF
13+
from errno import ELOOP
14+
from errno import ENOENT
15+
from errno import ENOTDIR
1216
from functools import partial
1317
from os.path import expanduser
1418
from os.path import expandvars
@@ -43,6 +47,24 @@
4347

4448
_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
4549

50+
# The following function, variables and comments were
51+
# copied from cpython 3.9 Lib/pathlib.py file.
52+
53+
# EBADF - guard against macOS `stat` throwing EBADF
54+
_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP)
55+
56+
_IGNORED_WINERRORS = (
57+
21, # ERROR_NOT_READY - drive exists but is not accessible
58+
1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself
59+
)
60+
61+
62+
def _ignore_error(exception):
63+
return (
64+
getattr(exception, "errno", None) in _IGNORED_ERRORS
65+
or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
66+
)
67+
4668

4769
def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
4870
return path.joinpath(".lock")
@@ -563,8 +585,23 @@ def visit(
563585
564586
Entries at each directory level are sorted.
565587
"""
566-
entries = sorted(os.scandir(path), key=lambda entry: entry.name)
588+
589+
# Skip entries with symlink loops and other brokenness, so the caller doesn't
590+
# have to deal with it.
591+
entries = []
592+
for entry in os.scandir(path):
593+
try:
594+
entry.is_file()
595+
except OSError as err:
596+
if _ignore_error(err):
597+
continue
598+
raise
599+
entries.append(entry)
600+
601+
entries.sort(key=lambda entry: entry.name)
602+
567603
yield from entries
604+
568605
for entry in entries:
569606
if entry.is_dir() and recurse(entry):
570607
yield from visit(entry.path, recurse)

testing/test_collection.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,3 +1416,17 @@ def a(): return 4
14161416
result = testdir.runpytest()
14171417
# Not INTERNAL_ERROR
14181418
assert result.ret == ExitCode.INTERRUPTED
1419+
1420+
1421+
def test_does_not_crash_on_recursive_symlink(testdir: Testdir) -> None:
1422+
"""Regression test for an issue around recursive symlinks (#7951)."""
1423+
symlink_or_skip("recursive", testdir.tmpdir.join("recursive"))
1424+
testdir.makepyfile(
1425+
"""
1426+
def test_foo(): assert True
1427+
"""
1428+
)
1429+
result = testdir.runpytest()
1430+
1431+
assert result.ret == ExitCode.OK
1432+
assert result.parseoutcomes() == {"passed": 1}

testing/test_pathlib.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from _pytest.pathlib import maybe_delete_a_numbered_dir
1818
from _pytest.pathlib import Path
1919
from _pytest.pathlib import resolve_package_path
20+
from _pytest.pathlib import symlink_or_skip
21+
from _pytest.pathlib import visit
2022

2123

2224
class TestFNMatcherPort:
@@ -401,3 +403,13 @@ def test_commonpath() -> None:
401403
assert commonpath(subpath, path) == path
402404
assert commonpath(Path(str(path) + "suffix"), path) == path.parent
403405
assert commonpath(path, path.parent.parent) == path.parent.parent
406+
407+
408+
def test_visit_ignores_errors(tmpdir: py.path.local) -> None:
409+
symlink_or_skip("recursive", tmpdir.join("recursive"))
410+
tmpdir.join("foo").write_binary(b"")
411+
tmpdir.join("bar").write_binary(b"")
412+
413+
assert [
414+
entry.name for entry in visit(str(tmpdir), recurse=lambda entry: False)
415+
] == ["bar", "foo"]

0 commit comments

Comments
 (0)