diff --git a/AUTHORS b/AUTHORS index e25156cde19..deb42adbe30 100644 --- a/AUTHORS +++ b/AUTHORS @@ -353,6 +353,7 @@ Ran Benita Raphael Castaneda Raphael Pierzina Rafal Semik +Reza Mousavi Raquel Alegre Ravi Chandra Reagan Lee diff --git a/changelog/13083.bugfix.rst b/changelog/13083.bugfix.rst new file mode 100644 index 00000000000..fc4564755ba --- /dev/null +++ b/changelog/13083.bugfix.rst @@ -0,0 +1 @@ +Fixed issue where pytest could crash if one of the collected directories got removed during collection. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index decf8af0b4f..ca8a1d2d4f9 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -955,17 +955,24 @@ def scandir( The returned entries are sorted according to the given key. The default is to sort by name. + If the directory does not exist, return an empty list. """ entries = [] - with os.scandir(path) as s: - # Skip entries with symlink loops and other brokenness, so the caller - # doesn't have to deal with it. + # Attempt to create a scandir iterator for the given path. + try: + scandir_iter = os.scandir(path) + except FileNotFoundError: + # If the directory does not exist, return an empty list. + return [] + # Use the scandir iterator in a context manager to ensure it is properly closed. + with scandir_iter as s: for entry in s: try: entry.is_file() except OSError as err: if _ignore_error(err): continue + # Reraise non-ignorable errors to avoid hiding issues. raise entries.append(entry) entries.sort(key=sort_key) # type: ignore[arg-type] diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 1acc96915d4..dbf76887859 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -38,6 +38,7 @@ from _pytest.pathlib import resolve_package_path from _pytest.pathlib import resolve_pkg_root_and_module_name from _pytest.pathlib import safe_exists +from _pytest.pathlib import scandir from _pytest.pathlib import spec_matches_module_path from _pytest.pathlib import symlink_or_skip from _pytest.pathlib import visit @@ -569,6 +570,29 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N assert getattr(module, "foo")() == 42 +def test_scandir_with_non_existent_directory() -> None: + # Test with a directory that does not exist + non_existent_dir = "path_to_non_existent_dir" + result = scandir(non_existent_dir) + # Assert that the result is an empty list + assert result == [] + + +def test_scandir_handles_os_error() -> None: + # Create a mock entry that will raise an OSError when is_file is called + mock_entry = unittest.mock.MagicMock() + mock_entry.is_file.side_effect = OSError("some permission error") + # Mock os.scandir to return an iterator with our mock entry + with unittest.mock.patch("os.scandir") as mock_scandir: + mock_scandir.return_value.__enter__.return_value = [mock_entry] + # Call the scandir function with a path + # We expect an OSError to be raised here + with pytest.raises(OSError, match="some permission error"): + scandir("/fake/path") + # Verify that the is_file method was called on the mock entry + mock_entry.is_file.assert_called_once() + + class TestImportLibMode: def test_importmode_importlib_with_dataclass( self, tmp_path: Path, ns_param: bool