mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
_all_project_files carried @lru_cache(maxsize=1) and read PROJECT_ROOT from the
module global inside its body, so the root was not part of the cache key. The
first call decided the answer for the rest of the process: point PROJECT_ROOT at
a fixture afterwards and the walk still reports the real repository, or, in the
other order, a sweep after the fixture is torn down enumerates a directory that
no longer exists.
Both failure modes are silent. iter_project_files, iter_project_files_with_content
and iter_non_ignored_files all sit on this one walk, 202 files in the tree use
them, and a lint whose corpus collapses to zero paths reports zero offenders and
passes. Nothing raises.
The root is now a parameter and therefore the key, with room for four of them.
_reset() still clears it through cache_clear(), and the only other caller is
iter_project_files, which passes str(PROJECT_ROOT) explicitly.
Live impact today is none, and that is worth stating: exactly one test patches
PROJECT_ROOT in this module, and it calls _reset() in both setUp and
addCleanup, so the defect was fully masked. Every other patch site in the repo
targets PROJECT_ROOT in a different module. This is about the next test that
forgets, not about a failure anyone has seen.
A regression test covers it rather than a comment. Negative control: with the
key removed the new test fails on the second root with
Items in the first set but not the second: '/tmp/.../first.txt'
and passes once the key is back; the file is byte-identical to its pre-probe
state afterwards.
The lint docstring that quoted the old cache shape is deleted rather than
updated, because a cross-module implementation reference goes stale the moment
that module changes, which is exactly what happened here.
Same class as c4556fe72d, which keyed the applications SPOT by the groups its
payload reads. That one was a live defect in utils/cache/base.py; this is the
latent twin one module over.
Residual: maxsize=4 is a choice, not a measurement. More than four roots alive
at once evicts the oldest, which re-walks rather than answers wrongly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
197 lines
7.3 KiB
Python
197 lines
7.3 KiB
Python
"""Unit tests for `utils.cache.files`.
|
|
|
|
Covers the project-tree walk cache, the file-content cache, and the
|
|
combined ``iter_project_files_with_content`` helper. All tests run
|
|
against a synthetic tempdir; ``PROJECT_ROOT`` is monkey-patched onto
|
|
the module so the real repo tree is never touched.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from unittest.mock import patch
|
|
|
|
from utils.cache import files as files_module
|
|
from utils.cache.files import (
|
|
_reset,
|
|
iter_project_files,
|
|
iter_project_files_with_content,
|
|
read_text,
|
|
)
|
|
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
|
|
|
|
|
def _touch(path: Path, content: str = "") -> Path:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
class _ProjectRootFixture:
|
|
"""Mixin that sets up a synthetic PROJECT_ROOT for every test in the
|
|
subclass. Combined with ``unittest.TestCase`` via multiple inheritance
|
|
so the lint guard ``test_all_test_files_have_tests`` can see the
|
|
``TestCase`` base directly on the concrete test classes.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
_reset()
|
|
self._tmp = TemporaryDirectory()
|
|
self.addCleanup(self._tmp.cleanup)
|
|
self.root = Path(self._tmp.name)
|
|
self._patcher = patch.object(files_module, "PROJECT_ROOT", self.root)
|
|
self._patcher.start()
|
|
self.addCleanup(self._patcher.stop)
|
|
self.addCleanup(_reset)
|
|
|
|
|
|
class TestReadText(_ProjectRootFixture, unittest.TestCase):
|
|
def test_returns_utf8_content(self) -> None:
|
|
path = _touch(self.root / "a.txt", "héllo")
|
|
self.assertEqual(read_text(str(path)), "héllo")
|
|
|
|
def test_second_call_hits_cache(self) -> None:
|
|
path = _touch(self.root / "a.txt", "first")
|
|
self.assertEqual(read_text(str(path)), "first")
|
|
path.write_text("changed", encoding="utf-8")
|
|
self.assertEqual(read_text(str(path)), "first")
|
|
|
|
def test_reset_invalidates_cache(self) -> None:
|
|
path = _touch(self.root / "a.txt", "first")
|
|
self.assertEqual(read_text(str(path)), "first")
|
|
path.write_text("second", encoding="utf-8")
|
|
_reset()
|
|
self.assertEqual(read_text(str(path)), "second")
|
|
|
|
def test_missing_file_raises(self) -> None:
|
|
with self.assertRaises(FileNotFoundError):
|
|
read_text(str(self.root / "nope.txt"))
|
|
|
|
|
|
class TestIterProjectFiles(_ProjectRootFixture, unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
_touch(self.root / f"roles/web-app-foo/{ROLE_FILE_META_SERVICES}", "foo: 1")
|
|
_touch(self.root / "roles/web-app-foo/templates/x.j2", "{{ x }}")
|
|
_touch(self.root / "docs/readme.md", "# Hi")
|
|
_touch(self.root / "tests/unit/test_a.py", "import unittest")
|
|
_touch(self.root / ".git/HEAD", "ref: refs/heads/main")
|
|
_touch(self.root / "node_modules/pkg/index.js", "module.exports = {};")
|
|
_touch(self.root / "vendor/nette/utils/readme.md", "# vendored")
|
|
_touch(self.root / "__pycache__/dead.pyc", "")
|
|
|
|
def test_skip_dirs_pruned(self) -> None:
|
|
all_paths = set(iter_project_files())
|
|
self.assertFalse(
|
|
any("/.git/" in p for p in all_paths),
|
|
f".git not pruned in {all_paths}",
|
|
)
|
|
self.assertFalse(any("/__pycache__/" in p for p in all_paths))
|
|
self.assertFalse(any("/node_modules/" in p for p in all_paths))
|
|
self.assertFalse(any("/vendor/" in p for p in all_paths))
|
|
|
|
def test_extensions_filter(self) -> None:
|
|
ymls = list(iter_project_files(extensions=(".yml",)))
|
|
self.assertEqual(len(ymls), 1)
|
|
self.assertTrue(ymls[0].endswith("services.yml"))
|
|
|
|
markdown = list(iter_project_files(extensions=(".md",)))
|
|
self.assertEqual(len(markdown), 1)
|
|
self.assertTrue(markdown[0].endswith("readme.md"))
|
|
|
|
def test_exclude_tests_skips_tests_subtree(self) -> None:
|
|
with_tests = list(iter_project_files(extensions=(".py",)))
|
|
self.assertEqual(len(with_tests), 1)
|
|
|
|
without_tests = list(
|
|
iter_project_files(extensions=(".py",), exclude_tests=True)
|
|
)
|
|
self.assertEqual(without_tests, [])
|
|
|
|
def test_exclude_dirs_path_segment_match(self) -> None:
|
|
kept = list(iter_project_files(exclude_dirs=("docs",)))
|
|
self.assertFalse(any(p.endswith("readme.md") for p in kept))
|
|
|
|
def test_walk_is_cached_across_calls(self) -> None:
|
|
first = set(iter_project_files())
|
|
_touch(self.root / "after.txt", "late")
|
|
second = set(iter_project_files())
|
|
self.assertEqual(first, second)
|
|
|
|
_reset()
|
|
third = set(iter_project_files())
|
|
self.assertIn(str(self.root / "after.txt"), third)
|
|
|
|
|
|
class TestIterProjectFilesWithContent(_ProjectRootFixture, unittest.TestCase):
|
|
def test_yields_path_and_content(self) -> None:
|
|
_touch(self.root / "a.txt", "alpha")
|
|
_touch(self.root / "b.txt", "beta")
|
|
result = sorted(iter_project_files_with_content(extensions=(".txt",)))
|
|
self.assertEqual(
|
|
result,
|
|
sorted(
|
|
[
|
|
(str(self.root / "a.txt"), "alpha"),
|
|
(str(self.root / "b.txt"), "beta"),
|
|
]
|
|
),
|
|
)
|
|
|
|
def test_non_utf8_files_silently_skipped(self) -> None:
|
|
good = self.root / "good.bin"
|
|
bad = self.root / "bad.bin"
|
|
good.write_text("ok", encoding="utf-8")
|
|
bad.write_bytes(b"\xff\xfe\x00invalid")
|
|
result = list(iter_project_files_with_content(extensions=(".bin",)))
|
|
self.assertEqual(result, [(str(good), "ok")])
|
|
|
|
|
|
class TestReset(_ProjectRootFixture, unittest.TestCase):
|
|
def test_reset_clears_both_caches(self) -> None:
|
|
_touch(self.root / "x.txt", "v1")
|
|
list(iter_project_files())
|
|
read_text(str(self.root / "x.txt"))
|
|
_touch(self.root / "y.txt", "v2")
|
|
(self.root / "x.txt").write_text("v1-changed", encoding="utf-8")
|
|
self.assertNotIn(str(self.root / "y.txt"), set(iter_project_files()))
|
|
self.assertEqual(read_text(str(self.root / "x.txt")), "v1")
|
|
_reset()
|
|
self.assertIn(str(self.root / "y.txt"), set(iter_project_files()))
|
|
self.assertEqual(read_text(str(self.root / "x.txt")), "v1-changed")
|
|
|
|
|
|
class TestWalkIsKeyedOnTheRoot(_ProjectRootFixture, unittest.TestCase):
|
|
def test_a_second_root_is_walked_rather_than_answered_from_the_first(
|
|
self,
|
|
) -> None:
|
|
_touch(self.root / "first.txt", "a")
|
|
self.assertEqual(
|
|
{str(self.root / "first.txt")},
|
|
set(iter_project_files()),
|
|
)
|
|
|
|
with TemporaryDirectory() as other:
|
|
other_root = Path(other)
|
|
_touch(other_root / "second.txt", "b")
|
|
with patch.object(files_module, "PROJECT_ROOT", other_root):
|
|
self.assertEqual(
|
|
{str(other_root / "second.txt")},
|
|
set(iter_project_files()),
|
|
"the walk answered for the previous root, so a test that "
|
|
"points PROJECT_ROOT at a fixture silently sweeps the real "
|
|
"repository instead",
|
|
)
|
|
|
|
self.assertEqual(
|
|
{str(self.root / "first.txt")},
|
|
set(iter_project_files()),
|
|
)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
unittest.main()
|