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
|
Yoav Caspi
|
||||||
Zac Hatfield-Dodds
|
Zac Hatfield-Dodds
|
||||||
Zoltán Máté
|
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 uuid
|
||||||
import warnings
|
import warnings
|
||||||
from enum import Enum
|
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 functools import partial
|
||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
from os.path import expandvars
|
from os.path import expandvars
|
||||||
|
@ -37,6 +41,24 @@ LOCK_TIMEOUT = 60 * 60 * 24 * 3
|
||||||
|
|
||||||
_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
|
_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:
|
def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||||
return path.joinpath(".lock")
|
return path.joinpath(".lock")
|
||||||
|
@ -555,8 +577,23 @@ def visit(
|
||||||
|
|
||||||
Entries at each directory level are sorted.
|
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
|
yield from entries
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if entry.is_dir(follow_symlinks=False) and recurse(entry):
|
if entry.is_dir(follow_symlinks=False) and recurse(entry):
|
||||||
yield from visit(entry.path, recurse)
|
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()
|
result = testdir.runpytest()
|
||||||
# Not INTERNAL_ERROR
|
# Not INTERNAL_ERROR
|
||||||
assert result.ret == ExitCode.INTERRUPTED
|
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 ImportPathMismatchError
|
||||||
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
from _pytest.pathlib import maybe_delete_a_numbered_dir
|
||||||
from _pytest.pathlib import resolve_package_path
|
from _pytest.pathlib import resolve_package_path
|
||||||
|
from _pytest.pathlib import symlink_or_skip
|
||||||
|
from _pytest.pathlib import visit
|
||||||
|
|
||||||
|
|
||||||
class TestFNMatcherPort:
|
class TestFNMatcherPort:
|
||||||
|
@ -401,3 +403,14 @@ def test_commonpath() -> None:
|
||||||
assert commonpath(subpath, path) == path
|
assert commonpath(subpath, path) == path
|
||||||
assert commonpath(Path(str(path) + "suffix"), path) == path.parent
|
assert commonpath(Path(str(path) + "suffix"), path) == path.parent
|
||||||
assert commonpath(path, path.parent.parent) == path.parent.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