Skip to content

Commit 91ebef2

Browse files
pathlib: don't trust a zero Windows file ID in samefile_nofollow
On Windows st_ino holds the file ID, which file systems are free to not support; WinFsp mounts such as sshfs-win report 0 for every file, because SFTP has no inode field to fill it from. os.path.samestat() then reports any two files on the volume as the same file, so the Windows short-path fallback in Session.collect() matched every node and nothing got pruned: passing a single file collected the whole suite. Treat a zero file ID as unknown and fall back to path comparison, which is what the other platforms do anyway. Fixes #14864. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5fd2be0 commit 91ebef2

3 files changed

Lines changed: 58 additions & 5 deletions

File tree

changelog/14864.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

src/_pytest/pathlib.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,9 +1090,30 @@ def safe_exists(p: Path) -> bool:
10901090
return False
10911091

10921092

1093-
def samefile_nofollow(p1: Path, p2: Path) -> bool:
1094-
"""Test whether two paths reference the same actual file or directory.
1093+
if sys.platform == "win32":
1094+
1095+
def samefile_nofollow(p1: Path, p2: Path) -> bool:
1096+
"""Test whether two paths reference the same actual file or directory.
1097+
1098+
Unlike Path.samefile(), does not resolve symlinks.
1099+
1100+
On Windows st_ino is the file ID, which file systems are free to not
1101+
support, in which case it is 0 for every file -- WinFsp mounts such as
1102+
sshfs-win are one example. os.path.samestat() would then consider any two
1103+
files on the volume to be the same (python/cpython#78116), so a zero file
1104+
ID is treated as "unknown", leaving the caller with plain path comparison
1105+
like on the other platforms (#14864).
1106+
"""
1107+
s1, s2 = p1.lstat(), p2.lstat()
1108+
if not s1.st_ino or not s2.st_ino:
1109+
return False
1110+
return os.path.samestat(s1, s2)
10951111

1096-
Unlike Path.samefile(), does not resolve symlinks.
1097-
"""
1098-
return os.path.samestat(p1.lstat(), p2.lstat())
1112+
else:
1113+
1114+
def samefile_nofollow(p1: Path, p2: Path) -> bool:
1115+
"""Test whether two paths reference the same actual file or directory.
1116+
1117+
Unlike Path.samefile(), does not resolve symlinks.
1118+
"""
1119+
return os.path.samestat(p1.lstat(), p2.lstat())

testing/test_pathlib.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from _pytest.pathlib import resolve_package_path
3939
from _pytest.pathlib import resolve_pkg_root_and_module_name
4040
from _pytest.pathlib import safe_exists
41+
from _pytest.pathlib import samefile_nofollow
4142
from _pytest.pathlib import scandir
4243
from _pytest.pathlib import spec_matches_module_path
4344
from _pytest.pathlib import symlink_or_skip
@@ -570,6 +571,36 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N
570571
assert getattr(module, "foo")() == 42
571572

572573

574+
def test_samefile_nofollow(tmp_path: Path) -> None:
575+
p1 = tmp_path / "test_one.py"
576+
p2 = tmp_path / "test_two.py"
577+
p1.touch()
578+
p2.touch()
579+
580+
assert samefile_nofollow(p1, p1)
581+
assert not samefile_nofollow(p1, p2)
582+
583+
584+
@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows only")
585+
def test_samefile_nofollow_zero_file_id(
586+
tmp_path: Path, monkeypatch: MonkeyPatch
587+
) -> None:
588+
"""On Windows file systems which do not support file IDs, st_ino is 0 for every
589+
file, which must not make two distinct files compare equal (#14864)."""
590+
p1 = tmp_path / "test_one.py"
591+
p2 = tmp_path / "test_two.py"
592+
p1.touch()
593+
p2.touch()
594+
595+
# st_dev is a real value (the volume serial number), only the file ID is missing.
596+
zero_file_id = os.stat_result((0o100644, 0, 3816903231, 1, 0, 0, 0, 0, 0, 0))
597+
monkeypatch.setattr(Path, "lstat", lambda self: zero_file_id)
598+
599+
assert not samefile_nofollow(p1, p2)
600+
# Also for the same file -- the caller compares paths first anyway.
601+
assert not samefile_nofollow(p1, p1)
602+
603+
573604
def test_scandir_with_non_existent_directory() -> None:
574605
# Test with a directory that does not exist
575606
non_existent_dir = "path_to_non_existent_dir"

0 commit comments

Comments
 (0)