Files
computer-playbook/tests/unit/utils/test_invokable.py
T
kevinveenbirkenbachandClaude Opus 4.7 623035618c refactor(cache): route every yaml call in tests/ through utils.cache.yaml
Stops every direct `yaml.safe_load`/`safe_load_all`/`safe_dump`/`dump`
call across the test corpus from re-parsing files the production cache
already holds. Each migrated test loads role meta, group_vars, inventory
fixtures, etc. through the shared cache, so a single `make test` run
parses every YAML file at most once across all suites.

Cache module changes (utils/cache/yaml.py):
  - Unify single- and multi-doc cache: one `_CACHE: Dict[sig, list]`
    holding the `safe_load_all` payload. Single-doc helpers
    (load_yaml / load_yaml_any) unwrap docs[0] and raise on >1
    document, preserving the old strict semantics. A file consumed
    both as single-doc and multi-doc is now parsed exactly once.
  - Add load_yaml_all (path-cached) and load_yaml_all_str (string,
    no cache) so multi-doc consumers no longer need a direct
    `yaml.safe_load_all` plus a separate file read.
  - dump_yaml now accepts any safe-dumpable structure (dict, list,
    scalar) so test fixtures writing list-of-tasks YAML can use it.

Lint guard (tests/lint/repository/test_no_direct_yaml_calls.py):
  - Extend the scanned tree to include `tests/` so future test code
    is forced through the cache, with the existing
    `# noqa: direct-yaml` per-call opt-out for genuine raw-yaml
    fixture needs.

Test corpus:
  - 89 test files migrated to load_yaml / load_yaml_any /
    load_yaml_str / load_yaml_all / load_yaml_all_str / dump_yaml /
    dump_yaml_str. No `# noqa` shortcuts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:16:20 +02:00

118 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# tests/unit/utils/test_invokable.py
from __future__ import annotations
import shutil
import tempfile
from pathlib import Path
from unittest import TestCase, main
from unittest.mock import patch
import utils.invokable as inv
from utils.cache.yaml import dump_yaml
class TestInvokable(TestCase):
def setUp(self) -> None:
self.tmp = Path(tempfile.mkdtemp(prefix="invokable_modutils_test_"))
self.roles_dir = self.tmp / "roles"
self.roles_dir.mkdir(parents=True, exist_ok=True)
self.categories_file = self.roles_dir / "categories.yml"
# This structure triggers the YAML fallback in _get_invokable_paths()
# because it expects "categories", but we will patch _get_invokable_paths() anyway
dump_yaml(
self.categories_file,
{"categories": [{"invokable_paths": ["web-app", "update", "util-desk"]}]},
)
def mk_role(name: str, application_id: str | None = None) -> None:
rd = self.roles_dir / name
(rd / "vars").mkdir(parents=True, exist_ok=True)
if application_id is not None:
dump_yaml(rd / "vars" / "main.yml", {"application_id": application_id})
else:
dump_yaml(rd / "vars" / "main.yml", {})
mk_role("web-app-nextcloud", "web-app-nextcloud")
mk_role("web-app-matomo", "matomo-app")
mk_role("web-svc-nginx", None) # not invokable by our patched paths
mk_role("update", None) # exact match
mk_role("util-desk-custom", None) # prefix match
def tearDown(self) -> None:
shutil.rmtree(self.tmp)
def test_list_invokable_app_ids(self) -> None:
with patch.object(inv, "_repo_root", return_value=self.tmp):
with patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-desk"],
):
got = inv.list_invokable_app_ids()
self.assertEqual(
got,
sorted(["web-app-nextcloud", "matomo-app", "update", "util-desk-custom"]),
)
def test_list_invokables_by_type(self) -> None:
with patch.object(inv, "_repo_root", return_value=self.tmp):
with patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-desk"],
):
grouped = inv.list_invokables_by_type()
# server: web-app-* minus excluded oauth2 proxy (not present in test)
self.assertIn("server", grouped)
self.assertIn("workstation", grouped)
self.assertIn("universal", grouped)
self.assertEqual(grouped["server"], sorted(["web-app-nextcloud", "matomo-app"]))
self.assertEqual(grouped["workstation"], sorted(["util-desk-custom"]))
# universal = invokable - (server ∪ workstation) -> update remains
self.assertEqual(grouped["universal"], ["update"])
def test_list_invokables_by_type_exclude(self) -> None:
# Ensure exclude regex is honored
rd = self.roles_dir / "web-app-oauth2-proxy"
(rd / "vars").mkdir(parents=True, exist_ok=True)
dump_yaml(rd / "vars" / "main.yml", {})
with patch.object(inv, "_repo_root", return_value=self.tmp):
with patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-desk"],
):
grouped = inv.list_invokables_by_type()
self.assertNotIn("web-app-oauth2-proxy", grouped["server"])
def test_types_from_group_names(self) -> None:
with patch.object(inv, "_repo_root", return_value=self.tmp):
with patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-desk"],
):
got = inv.types_from_group_names(
[
"all", # not invokable -> ignored
"web-app-nextcloud", # invokable + matches server rule
"util-desk-custom", # invokable + matches workstation rule
"update", # invokable leftover -> universal
"foo", # not invokable -> ignored
]
)
self.assertEqual(got, ["server", "universal", "workstation"])
if __name__ == "__main__":
main()