Fix handling recursive symlinks
When pytest was run on a directory containing a recursive symlink it failed with ELOOP as the library was not able to determine the type of the direntry: src/_pytest/main.py:685: in collect if not direntry.is_file(): E OSError: [Errno 40] Too many levels of symbolic links: '/home/florian/proj/pytest/tests/recursive' This is fixed by handling ELOOP and other errors in the visit function in pathlib.py, so the entries whose is_file() call raises an OSError with the pre-defined list of error numbers will be exluded from the result. The _ignore_errors function was copied from Lib/pathlib.py of cpython 3.9. Fixes #7951
This commit is contained in:
parent
b95991aeea
commit
8a38e7a6e8
1
AUTHORS
1
AUTHORS
|
@ -314,3 +314,4 @@ Xuecong Liao
|
|||
Yoav Caspi
|
||||
Zac Hatfield-Dodds
|
||||
Zoltán Máté
|
||||
Zsolt Cserna
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Fixed handling of recursive symlinks when collecting tests.
|
|
@ -9,6 +9,10 @@ import sys
|
|||
import uuid
|
||||
import warnings
|
||||
from enum import Enum
|
||||
from errno import EBADF
|
||||
from errno import ELOOP
|
||||
from errno import ENOENT
|
||||
from errno import ENOTDIR
|
||||
from functools import partial
|
||||
from os.path import expanduser
|
||||
from os.path import expandvars
|
||||
|
@ -37,6 +41,24 @@ LOCK_TIMEOUT = 60 * 60 * 24 * 3
|
|||
|
||||
_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
|
||||
|
||||
# The following function, variables and comments were
|
||||
# copied from cpython 3.9 Lib/pathlib.py file.
|
||||
|
||||
# EBADF - guard against macOS `stat` throwing EBADF
|
||||
_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP)
|
||||
|
||||
_IGNORED_WINERRORS = (
|
||||
21, # ERROR_NOT_READY - drive exists but is not accessible
|
||||
1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself
|
||||
)
|
||||
|
||||
|
||||
def _ignore_error(exception):
|
||||
return (
|
||||
getattr(exception, "errno", None) in _IGNORED_ERRORS
|
||||
or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
|
||||
)
|
||||
|
||||
|
||||
def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||
return path.joinpath(".lock")
|
||||
|
@ -555,8 +577,23 @@ def visit(
|
|||
|
||||
Entries at each directory level are sorted.
|
||||
"""
|
||||
entries = sorted(os.scandir(path), key=lambda entry: entry.name)
|
||||
|
||||
# Skip entries with symlink loops and other brokenness, so the caller doesn't
|
||||
# have to deal with it.
|
||||
entries = []
|
||||
for entry in os.scandir(path):
|
||||
try:
|
||||
entry.is_file()
|
||||
except OSError as err:
|
||||
if _ignore_error(err):
|
||||
continue
|
||||
raise
|
||||
entries.append(entry)
|
||||
|
||||
entries.sort(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)
|
||||
|
|
|
@ -1404,3 +1404,17 @@ def test_does_not_crash_on_error_from_decorated_function(testdir: Testdir) -> No
|
|||
result = testdir.runpytest()
|
||||
# Not INTERNAL_ERROR
|
||||
assert result.ret == ExitCode.INTERRUPTED
|
||||
|
||||
|
||||
def test_does_not_crash_on_recursive_symlink(testdir: Testdir) -> None:
|
||||
"""Regression test for an issue around recursive symlinks (#7951)."""
|
||||
symlink_or_skip("recursive", testdir.tmpdir.join("recursive"))
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
def test_foo(): assert True
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
|
||||
assert result.ret == ExitCode.OK
|
||||
assert result.parseoutcomes() == {"passed": 1}
|
||||
|
|
|
@ -17,6 +17,8 @@ from _pytest.pathlib import import_path
|
|||
from _pytest.pathlib import ImportPathMismatchError
|
||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||
from _pytest.pathlib import resolve_package_path
|
||||
from _pytest.pathlib import symlink_or_skip
|
||||
from _pytest.pathlib import visit
|
||||
|
||||
|
||||
class TestFNMatcherPort:
|
||||
|
@ -401,3 +403,14 @@ def test_commonpath() -> None:
|
|||
assert commonpath(subpath, path) == path
|
||||
assert commonpath(Path(str(path) + "suffix"), path) == path.parent
|
||||
assert commonpath(path, path.parent.parent) == path.parent.parent
|
||||
|
||||
|
||||
def test_visit_ignores_errors(tmpdir) -> None:
|
||||
symlink_or_skip("recursive", tmpdir.join("recursive"))
|
||||
tmpdir.join("foo").write_binary(b"")
|
||||
tmpdir.join("bar").write_binary(b"")
|
||||
|
||||
assert [entry.name for entry in visit(tmpdir, recurse=lambda entry: False)] == [
|
||||
"bar",
|
||||
"foo",
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue