From dd8bff644ea9b644a8b54b4724381a98194b0789 Mon Sep 17 00:00:00 2001 From: delta87 Date: Thu, 26 Dec 2024 01:17:57 +0330 Subject: [PATCH] Fix scandir() crash by returning [] when directory is not found (#13083) --- AUTHORS | 1 + changelog/13083.bugfix.rst | 6 ++++++ src/_pytest/pathlib.py | 32 ++++++++++++++++++-------------- testing/test_pathlib.py | 24 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 changelog/13083.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 9629e00bcfb..826556f1464 100644 --- a/AUTHORS +++ b/AUTHORS @@ -360,6 +360,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..4298258642b --- /dev/null +++ b/changelog/13083.bugfix.rst @@ -0,0 +1,6 @@ +13083.bugfix.rst: + +Fix issue where the `scandir` function in `pathlib.py` caused symlink loops under certain conditions. + +- Issue: https://github.com/pytest-dev/pytest/issues/13083 +- Authors: Reza Mousavi diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 25dc69b6349..e8fc1a421b5 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -955,21 +955,25 @@ 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. - for entry in s: - try: - entry.is_file() - except OSError as err: - if _ignore_error(err): - continue - raise - entries.append(entry) - entries.sort(key=sort_key) # type: ignore[arg-type] - return entries + try: + 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. + for entry in s: + try: + entry.is_file() + except OSError as err: + if _ignore_error(err): + continue + raise + entries.append(entry) + entries.sort(key=sort_key) # type: ignore[arg-type] + return entries + except FileNotFoundError: + return [] def visit( diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 5a13cd5a400..da3a9ad06a2 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(): + # 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("Permission denied") + # 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="Permission denied"): + 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