Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/14864.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed collection on Windows collecting the whole suite instead of the given path, on file systems which do not support file IDs (``st_ino`` is ``0`` for every file, as seen for example on ``sshfs-win``/WinFsp mounts). The Windows-only short-path fallback used when matching collection arguments now ignores a zero file ID and compares paths instead.
31 changes: 26 additions & 5 deletions src/_pytest/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1090,9 +1090,30 @@ def safe_exists(p: Path) -> bool:
return False


def samefile_nofollow(p1: Path, p2: Path) -> bool:
"""Test whether two paths reference the same actual file or directory.
if sys.platform == "win32":

def samefile_nofollow(p1: Path, p2: Path) -> bool:
"""Test whether two paths reference the same actual file or directory.

Unlike Path.samefile(), does not resolve symlinks.

On Windows st_ino is the file ID, which file systems are free to not

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this in a code comment, not in the docstring (it's an implementation detail IMO).

support, in which case it is 0 for every file -- WinFsp mounts such as
sshfs-win are one example. os.path.samestat() would then consider any two
files on the volume to be the same (python/cpython#78116), so a zero file
ID is treated as "unknown", leaving the caller with plain path comparison
like on the other platforms (#14864).
"""
s1, s2 = p1.lstat(), p2.lstat()
if not s1.st_ino or not s2.st_ino:
return False
return os.path.samestat(s1, s2)

Unlike Path.samefile(), does not resolve symlinks.
"""
return os.path.samestat(p1.lstat(), p2.lstat())
else:

def samefile_nofollow(p1: Path, p2: Path) -> bool:
"""Test whether two paths reference the same actual file or directory.

Unlike Path.samefile(), does not resolve symlinks.
"""
return os.path.samestat(p1.lstat(), p2.lstat())
34 changes: 34 additions & 0 deletions testing/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 samefile_nofollow
from _pytest.pathlib import scandir
from _pytest.pathlib import spec_matches_module_path
from _pytest.pathlib import symlink_or_skip
Expand Down Expand Up @@ -570,6 +571,39 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N
assert getattr(module, "foo")() == 42


def test_samefile_nofollow(tmp_path: Path) -> None:
p1 = tmp_path / "test_one.py"
p2 = tmp_path / "test_two.py"
p1.touch()
p2.touch()

assert samefile_nofollow(p1, p1)
assert not samefile_nofollow(p1, p2)


@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only")
def test_samefile_nofollow_zero_file_id(
tmp_path: Path, monkeypatch: MonkeyPatch
) -> None:
"""On Windows file systems which do not support file IDs, st_ino is 0 for every
file, which must not make two distinct files compare equal (#14864)."""
p1 = tmp_path / "test_one.py"
p2 = tmp_path / "test_two.py"
p1.touch()
p2.touch()

# st_dev is a real value (the volume serial number), only the file ID is missing.
zero_file_id = os.stat_result((0o100644, 0, 3816903231, 1, 0, 0, 0, 0, 0, 0))
with monkeypatch.context() as mp:
# Use a context to narrow the patch as much as possible, given how central
# Path.lstat() is.
mp.setattr(Path, "lstat", lambda self: zero_file_id)

assert not samefile_nofollow(p1, p2)
# Also for the same file -- the caller compares paths first anyway.
assert not samefile_nofollow(p1, p1)


def test_scandir_with_non_existent_directory() -> None:
# Test with a directory that does not exist
non_existent_dir = "path_to_non_existent_dir"
Expand Down