Merge pull request #7956 from csernazs/fix-7951
Fix handling recursive symlinks
This commit is contained in:
		
						commit
						7fb0ea3f68
					
				
							
								
								
									
										1
									
								
								AUTHORS
								
								
								
								
							
							
						
						
									
										1
									
								
								AUTHORS
								
								
								
								
							|  | @ -316,3 +316,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() and recurse(entry): |         if entry.is_dir() and recurse(entry): | ||||||
|             yield from visit(entry.path, recurse) |             yield from visit(entry.path, recurse) | ||||||
|  |  | ||||||
|  | @ -1414,3 +1414,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