tests/test_cache_windows_case_4925.py
Rama v8 · réplica de lectura del repositorio original.
"""Windows path normalization must not lowercase the file it opens (#4925).
The failure needs Windows path semantics and a case-sensitive filesystem. A
Linux host with a case-sensitive temp directory can exercise that combination
by taking the win32 branch of cache._normalize_path and making normcase
lowercase, which is what ntpath.normcase does on Windows.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import pytest
from graphify import cache
def _reset_stat_index() -> None:
cache._stat_index = {}
cache._stat_index_root = None
cache._stat_index_anchor = None
cache._stat_index_path = None
cache._stat_index_dirty = False
@pytest.fixture
def win32_on_case_sensitive_fs(monkeypatch, tmp_path):
probe = tmp_path / "CaseProbe"
probe.mkdir()
if (tmp_path / "caseprobe").exists():
pytest.skip("needs a case-sensitive filesystem")
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(os.path, "normcase", lambda s: s.lower())
# The temp path is short. The extended-length prefix is not under test.
monkeypatch.setattr(cache, "_os_long_path", lambda p: str(p))
_reset_stat_index()
yield tmp_path
_reset_stat_index()
def _write(path: Path, text: str) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
def test_file_hash_accepts_uppercase_file(win32_on_case_sensitive_fs):
root = win32_on_case_sensitive_fs / "Repo"
f = _write(root / "README.md", "# Repro\nhello\n")
assert cache.file_hash(f, root)
def test_file_hash_accepts_uppercase_root(win32_on_case_sensitive_fs):
root = win32_on_case_sensitive_fs / "Repo"
f = _write(root / "readme.md", "# Repro\nhello\n")
assert cache.file_hash(f, root)
def test_save_and_load_roundtrip_uppercase(win32_on_case_sensitive_fs):
root = win32_on_case_sensitive_fs / "Repo"
f = _write(root / "README.md", "# Repro\nhello\n")
result = {
"nodes": [{"id": "readme", "label": "README"}],
"edges": [],
"hyperedges": [],
}
cache.save_cached(f, result, root=root, kind="semantic")
assert cache.load_cached(f, root, kind="semantic") == result
def test_case_variants_with_same_content_have_different_hash(win32_on_case_sensitive_fs):
root = win32_on_case_sensitive_fs / "Repo"
upper = _write(root / "README.md", "# same\n")
lower = _write(root / "readme.md", "# same\n")
assert upper.read_bytes() == lower.read_bytes()
assert cache.file_hash(upper, root) != cache.file_hash(lower, root)
def test_case_variants_do_not_cross_hit_the_semantic_cache(win32_on_case_sensitive_fs):
root = win32_on_case_sensitive_fs / "Repo"
upper = _write(root / "README.md", "# same\n")
lower = _write(root / "readme.md", "# same\n")
upper_result = {"nodes": [{"id": "upper"}], "edges": [], "hyperedges": []}
lower_result = {"nodes": [{"id": "lower"}], "edges": [], "hyperedges": []}
cache.save_cached(upper, upper_result, root=root, kind="semantic")
# A case-sensitive volume has two files here. The lower one has no entry.
assert cache.load_cached(lower, root, kind="semantic") is None
cache.save_cached(lower, lower_result, root=root, kind="semantic")
assert cache.load_cached(upper, root, kind="semantic") == upper_result
assert cache.load_cached(lower, root, kind="semantic") == lower_result
def test_hardlink_aliases_keep_distinct_hashes(win32_on_case_sensitive_fs):
root = win32_on_case_sensitive_fs / "repo"
target = _write(root / "target.txt", "same bytes\n")
alias = root / "TARGET.txt"
os.link(target, alias)
assert os.path.samefile(target, alias)
# #1989 keeps the walked path distinct even when two names share an inode.
assert cache.file_hash(alias, root) != cache.file_hash(target, root)
def test_symlink_aliases_keep_distinct_hashes(win32_on_case_sensitive_fs):
root = win32_on_case_sensitive_fs / "repo"
target = _write(root / "target.txt", "same bytes\n")
alias = root / "TARGET.txt"
try:
alias.symlink_to(target)
except OSError:
pytest.skip("symlinks are not available here")
assert os.path.samefile(target, alias)
assert cache.file_hash(alias, root) != cache.file_hash(target, root)
def test_cached_word_count_uses_stat_index_for_uppercase(win32_on_case_sensitive_fs):
root = win32_on_case_sensitive_fs / "Repo"
f = _write(root / "README.md", "one two three\n")
old = f.stat().st_mtime_ns - 10 * 1_000_000_000
os.utime(f, ns=(old, old))
calls = []
def compute(p: Path) -> int:
calls.append(p)
return len(p.read_text().split())
assert cache.cached_word_count(f, root, compute) == 3
assert len(calls) == 1
assert cache._stat_index, "word count must write a stat-index entry"
assert cache.cached_word_count(f, root, compute) == 3
assert len(calls) == 1, "second call must use the stat fastpath"