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 | ||||
| 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() and recurse(entry): | ||||
|             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() | ||||
|     # 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