mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
Fourteen real conflicts plus the 66 generated READMEs.
utils/cache/files.py and its test: both sides added a skip-dir rule, resolved
as a union so the root `build/` prune and the per-root walk key coexist.
plugins/lookup/database.py and postgres_libraries.py: main's added lookups kept,
with get_entity_name renamed to entity_name as this branch renamed it.
roles/web-app-{mailu,matrix}/meta/services.yml: main's `covers: [email]` and its
MAIL_PROVIDER-gated email entry, plus this branch's gitlab/jira/n8n/jitsi
consumers, which the merge base carried on neither side.
group_vars/all/17_resource.yml: this branch's lookup('resource', ...) refactor;
main's single added literal is already covered by _POSTGRES_MAX_LOCKS_PER_TRANSACTION.
docs/requirements/035-mcp-proxy-expansion.md: this branch moved the design into
ADR 0003, so the requirement keeps only the user story and the criteria. Main's
delta inside the moved block was two new roles, carried into the ADR audit:
web-app-stalwart as adapter_candidate, svc-net-tor-smtp as privileged_control_plane.
The no_surface total said 99 while its own subcategories summed to 112.
inventories/development/default.yml: biber keeps no explicit email. Both sides fixed
the same CI failure; this branch fixed it in _apply_account_defaults, which renders
biber@{{ DOMAIN_PRIMARY }} (utils/cache/users/__init__.py:68) — byte-identical to
the address main hardcoded, so the inventory line is a second copy of one fact.
roles/web-app-{gitlab,keycloak}/files/playwright/playwright.spec.js and
roles/web-app-gitlab/templates/playwright.env.j2: union of both import sets, with
requireDotenvValue kept over decodeDotenvQuotedValue for CANONICAL_DOMAIN because
an empty value must fail rather than match every substring.
The 66 role READMEs take this side: aceb5295ef moved Cosmos, Quick Setup and
Credits to the documentation build, and test_readme.py now rejects them in a
committed README. Main's prose edits inside them are carried over by hand (the
Mailu deprecation banner, Mailu -> Stalwart in erpnext and zammad), and the two
READMEs main added fresh are stripped of the three generated sections.
roles/web-app-sphinx/README.md stays deleted; 6eb9a59d14 merged that role into
web-app-docs. The root README's generated role table was regenerated from the
merged metadata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
224 lines
8.6 KiB
Python
224 lines
8.6 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", "")
|
|
_touch(self.root / "build/ruff-cache-0/0.16.7/entry.json", "{}")
|
|
_touch(self.root / "cli/build/docs/readme/overview.py", "x = 1")
|
|
|
|
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_build_is_pruned_at_the_root_only(self) -> None:
|
|
all_paths = set(iter_project_files())
|
|
self.assertFalse(
|
|
any(p.startswith(f"{self.root}/build/") for p in all_paths),
|
|
f"root build/ artefacts reached the walk: {all_paths}",
|
|
)
|
|
self.assertTrue(
|
|
any(p.endswith("cli/build/docs/readme/overview.py") for p in all_paths),
|
|
f"cli/build is source and must survive the prune: {all_paths}",
|
|
)
|
|
|
|
def test_git_local_exclude_dirs_pruned(self) -> None:
|
|
"""Plain directory entries in .git/info/exclude are pruned; file
|
|
patterns and globs are left to git."""
|
|
_touch(
|
|
self.root / ".git/info/exclude",
|
|
"# comment\n/local-tool-out/\nsettings.local.json\n*.tmp\n",
|
|
)
|
|
_touch(self.root / "local-tool-out/report.md", "generated")
|
|
_reset()
|
|
all_paths = set(iter_project_files())
|
|
self.assertFalse(any("/local-tool-out/" in p for p in all_paths))
|
|
self.assertTrue(any(p.endswith("readme.md") 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), 2)
|
|
|
|
without_tests = list(
|
|
iter_project_files(extensions=(".py",), exclude_tests=True)
|
|
)
|
|
self.assertEqual(len(without_tests), 1)
|
|
self.assertTrue(without_tests[0].endswith("cli/build/docs/readme/overview.py"))
|
|
|
|
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()
|