Files
computer-playbook/tests/unit/utils/cache/test_data.py
T
kevinveenbirkenbach 12dba01156 refactor(utils): consolidate cache modules into utils/cache/ package
Both in-process cache layers — the cached YAML loader and the cached
runtime-data getters — now live next to each other under
`utils/cache/`. The package gains a README that documents the
"orthogonal layers, no third cache" rule so future contributors hit
the right primitive on the first look.

Moves:
- `utils/yaml_cache.py` -> `utils/cache/yaml.py`
- `utils/runtime_data.py` -> `utils/cache/data.py`
- `tests/unit/utils/test_yaml_cache.py` -> `tests/unit/utils/cache/test_yaml.py`
- `tests/unit/utils/test_runtime_data.py` -> `tests/unit/utils/cache/test_data.py`
- `tests/unit/utils/test_variants.py` -> `tests/unit/utils/cache/test_variants.py`

New:
- `utils/cache/__init__.py` (empty per project convention)
- `utils/cache/README.md` describing the two layers, when to reach for
  which, lifetime, test-only helpers, and design guidelines (RFC 2119
  keywords, emoji-after-headline, no self-SPOT declaration, no
  slash-in-link-text).
- `tests/unit/utils/cache/__init__.py`

Mechanical updates:
- All imports across cli/, plugins/lookup/, tests/, and roles/ rewritten
  from `utils.yaml_cache` -> `utils.cache.yaml` and
  `utils.runtime_data` -> `utils.cache.data` (40+ files).
- `REPO_ROOT` parents-index in the moved tests bumped from `parents[3]`
  to `parents[4]` to reflect the deeper nesting.
- Six broken markdown links in docs/ updated to the new module paths
  (test_markdown_links lint test stays green).
- Two test files that used the `from utils import runtime_data` form
  now use `from utils.cache import data as runtime_data` so the local
  alias remains.

Tests: 848 passed across unit + lint suites (1 pre-existing collection
error in test_host_vars.py unrelated to this change).
2026-04-25 17:17:14 +02:00

474 lines
17 KiB
Python

from __future__ import annotations
import tempfile
import textwrap
import unittest
from pathlib import Path
from utils.cache.data import (
_build_users,
_compute_reserved_usernames,
_deep_merge,
_fingerprint_mapping,
_hydrate_users_tokens,
_load_user_defs,
_load_yaml_mapping,
_materialize_builtin_user_aliases,
_merge_users,
_reset_cache_for_tests,
_stable_variables_signature,
get_application_defaults,
get_user_defaults,
)
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(textwrap.dedent(content), encoding="utf-8")
class TestLoadYamlMapping(unittest.TestCase):
def test_missing_file_returns_empty(self):
with tempfile.TemporaryDirectory() as tmp:
self.assertEqual(_load_yaml_mapping(Path(tmp) / "missing.yml"), {})
def test_empty_file_returns_empty(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "empty.yml"
path.write_text("", encoding="utf-8")
self.assertEqual(_load_yaml_mapping(path), {})
def test_non_mapping_raises(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "list.yml"
path.write_text("- a\n- b\n", encoding="utf-8")
with self.assertRaises(ValueError):
_load_yaml_mapping(path)
def test_mapping_returns_dict(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "ok.yml"
path.write_text("foo: 1\nbar: [1, 2]\n", encoding="utf-8")
self.assertEqual(_load_yaml_mapping(path), {"foo": 1, "bar": [1, 2]})
class TestDeepMerge(unittest.TestCase):
def test_nested_merge(self):
base = {"a": {"b": 1, "c": 2}, "keep": [1]}
override = {"a": {"c": 99, "d": 3}, "new": "x"}
merged = _deep_merge(base, override)
self.assertEqual(
merged,
{"a": {"b": 1, "c": 99, "d": 3}, "keep": [1], "new": "x"},
)
def test_scalar_override_replaces_mapping(self):
self.assertEqual(_deep_merge({"a": 1}, "x"), "x")
def test_override_deepcopies(self):
base = {}
override = {"a": {"list": [1, 2]}}
merged = _deep_merge(base, override)
merged["a"]["list"].append(3)
self.assertEqual(override["a"]["list"], [1, 2])
class TestMergeUsers(unittest.TestCase):
def test_none_overrides_returns_defaults_copy(self):
defaults = {"alice": {"uid": 1001}}
merged = _merge_users(defaults, None)
self.assertEqual(merged, defaults)
merged["alice"]["uid"] = 0
self.assertEqual(defaults["alice"]["uid"], 1001)
def test_override_merges_per_user(self):
defaults = {"alice": {"uid": 1001, "email": "a@x"}}
overrides = {"alice": {"email": "new@x"}, "bob": {"uid": 2000}}
merged = _merge_users(defaults, overrides)
self.assertEqual(merged["alice"], {"uid": 1001, "email": "new@x"})
self.assertEqual(merged["bob"], {"uid": 2000})
class TestComputeReservedUsernames(unittest.TestCase):
def test_extracts_suffix_after_last_dash(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
(roles / "web-app-mailu").mkdir()
(roles / "svc-db-postgres").mkdir()
(roles / "sys-ctl-alm-email").mkdir()
(roles / "not-a-dir.txt").write_text("", encoding="utf-8")
self.assertEqual(
_compute_reserved_usernames(roles),
["email", "mailu", "postgres"],
)
def test_ignores_non_alnum_or_non_lower(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
(roles / "web-app-Foo").mkdir()
(roles / "weird-name_with_underscore").mkdir()
(roles / "svc-db-pg").mkdir()
self.assertEqual(_compute_reserved_usernames(roles), ["pg"])
class TestFingerprintMapping(unittest.TestCase):
def setUp(self) -> None:
_reset_cache_for_tests()
def test_none_is_zero(self):
self.assertEqual(_fingerprint_mapping(None), "0")
def test_same_content_same_fingerprint(self):
a = {"x": 1, "y": 2}
b = {"y": 2, "x": 1}
self.assertEqual(_fingerprint_mapping(a), _fingerprint_mapping(b))
def test_different_content_different_fingerprint(self):
a = {"x": 1}
b = {"x": 2}
self.assertNotEqual(_fingerprint_mapping(a), _fingerprint_mapping(b))
def test_id_cache_short_circuits_recompute(self):
obj = {"x": 1}
first = _fingerprint_mapping(obj)
obj["x"] = 999
cached = _fingerprint_mapping(obj)
self.assertEqual(first, cached)
_reset_cache_for_tests()
self.assertNotEqual(first, _fingerprint_mapping(obj))
class TestStableVariablesSignature(unittest.TestCase):
def setUp(self) -> None:
_reset_cache_for_tests()
def test_empty_variables_returns_sentinel(self):
self.assertEqual(_stable_variables_signature(None), ("0", "0", "", ""))
self.assertEqual(_stable_variables_signature({}), ("0", "0", "", ""))
def test_captures_key_subset(self):
sig = _stable_variables_signature(
{
"applications": {"a": 1},
"users": {"u": 2},
"DOMAIN_PRIMARY": "example.com",
"SYSTEM_EMAIL_DOMAIN": "mail.example.com",
"irrelevant": "noise",
}
)
self.assertEqual(sig[2], "example.com")
self.assertEqual(sig[3], "mail.example.com")
self.assertNotEqual(sig[0], "0")
self.assertNotEqual(sig[1], "0")
def test_signature_stable_across_identical_content(self):
v1 = {"applications": {"a": 1}, "users": {"u": 2}}
v2 = {"applications": {"a": 1}, "users": {"u": 2}}
self.assertEqual(
_stable_variables_signature(v1), _stable_variables_signature(v2)
)
class TestLoadUserDefs(unittest.TestCase):
def test_merges_non_conflicting_across_roles(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
_write(
roles / "role-a/users/main.yml",
"""
users:
alice:
username: alice
""",
)
_write(
roles / "role-b/users/main.yml",
"""
users:
alice:
email: alice@x
bob:
username: bob
""",
)
defs = _load_user_defs(roles)
self.assertEqual(defs["alice"], {"username": "alice", "email": "alice@x"})
self.assertEqual(defs["bob"], {"username": "bob"})
def test_conflicting_field_raises(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
_write(
roles / "role-a/users/main.yml",
"""
users:
alice:
uid: 1001
""",
)
_write(
roles / "role-b/users/main.yml",
"""
users:
alice:
uid: 2002
""",
)
with self.assertRaisesRegex(ValueError, "Conflict for user 'alice'"):
_load_user_defs(roles)
def test_non_dict_user_entry_raises(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
_write(
roles / "role-a/users/main.yml",
"""
users:
alice: "not-a-dict"
""",
)
with self.assertRaisesRegex(ValueError, "Invalid definition"):
_load_user_defs(roles)
class TestBuildUsers(unittest.TestCase):
def _defs(self, mapping):
from collections import OrderedDict
return OrderedDict(mapping)
def test_allocates_uids_starting_at_start_id(self):
defs = self._defs([("alice", {}), ("bob", {})])
users = _build_users(defs, "example.com", start_id=1001, become_pwd="pw")
self.assertEqual(users["alice"]["uid"], 1001)
self.assertEqual(users["bob"]["uid"], 1002)
self.assertEqual(users["alice"]["gid"], 1001)
def test_explicit_uid_preserved_allocator_skips(self):
defs = self._defs(
[("alice", {"uid": 1001}), ("bob", {}), ("carol", {"uid": 1002})]
)
users = _build_users(defs, "example.com", start_id=1001, become_pwd="pw")
self.assertEqual(users["alice"]["uid"], 1001)
self.assertEqual(users["bob"]["uid"], 1003)
self.assertEqual(users["carol"]["uid"], 1002)
def test_duplicate_explicit_uid_raises(self):
defs = self._defs([("alice", {"uid": 1001}), ("bob", {"uid": 1001})])
with self.assertRaisesRegex(ValueError, "Duplicate uid 1001"):
_build_users(defs, "example.com", start_id=1001, become_pwd="pw")
def test_duplicate_username_raises(self):
defs = self._defs(
[("alice", {"username": "shared"}), ("bob", {"username": "shared"})]
)
with self.assertRaisesRegex(ValueError, "Duplicate username 'shared'"):
_build_users(defs, "example.com", start_id=1001, become_pwd="pw")
def test_duplicate_email_raises(self):
defs = self._defs(
[
("alice", {"email": "same@x"}),
("bob", {"email": "same@x"}),
]
)
with self.assertRaisesRegex(ValueError, "Duplicate email 'same@x'"):
_build_users(defs, "example.com", start_id=1001, become_pwd="pw")
def test_defaults_fill_missing_fields(self):
defs = self._defs([("alice", {})])
users = _build_users(defs, "example.com", start_id=1001, become_pwd="pw")
self.assertEqual(users["alice"]["username"], "alice")
self.assertEqual(users["alice"]["email"], "alice@example.com")
self.assertEqual(users["alice"]["password"], "pw")
self.assertEqual(users["alice"]["firstname"], "alice")
self.assertEqual(users["alice"]["lastname"], "example.com")
class TestHydrateUsersTokens(unittest.TestCase):
def test_fills_only_missing_tokens(self):
users = {
"alice": {"tokens": {"app-a": "keep", "app-b": ""}},
"bob": {"tokens": {}},
}
store = {
"alice": {"tokens": {"app-a": "overwrite-attempt", "app-b": "fill"}},
"bob": {"tokens": {"app-c": "fresh"}},
"carol": {"tokens": {"app-d": "orphan"}},
}
hydrated = _hydrate_users_tokens(users, store)
self.assertEqual(hydrated["alice"]["tokens"]["app-a"], "keep")
self.assertEqual(hydrated["alice"]["tokens"]["app-b"], "fill")
self.assertEqual(hydrated["bob"]["tokens"]["app-c"], "fresh")
self.assertIn("carol", hydrated)
self.assertEqual(hydrated["carol"]["tokens"]["app-d"], "orphan")
def test_empty_store_is_passthrough(self):
users = {"alice": {"tokens": {"a": "1"}}}
self.assertEqual(_hydrate_users_tokens(users, None), users)
self.assertEqual(_hydrate_users_tokens(users, {}), users)
def test_does_not_mutate_input(self):
users = {"alice": {"tokens": {}}}
store = {"alice": {"tokens": {"x": "y"}}}
_hydrate_users_tokens(users, store)
self.assertEqual(users["alice"]["tokens"], {})
class TestMaterializeBuiltinUserAliases(unittest.TestCase):
def test_rewrites_sld_and_tld_usernames(self):
users = {
"sld": {"username": "{{ DOMAIN_PRIMARY.split('.')[0] }}"},
"tld": {"username": "{{ DOMAIN_PRIMARY.split('.')[1] }}"},
}
result = _materialize_builtin_user_aliases(
users, {"DOMAIN_PRIMARY": "example.com"}
)
self.assertEqual(result["sld"]["username"], "example")
self.assertEqual(result["tld"]["username"], "com")
def test_no_domain_primary_is_passthrough(self):
users = {"sld": {"username": "{{ DOMAIN_PRIMARY.split('.')[0] }}"}}
result = _materialize_builtin_user_aliases(users, {})
self.assertEqual(result, users)
def test_non_placeholder_username_untouched(self):
users = {"sld": {"username": "literal"}}
result = _materialize_builtin_user_aliases(
users, {"DOMAIN_PRIMARY": "example.com"}
)
self.assertEqual(result["sld"]["username"], "literal")
class TestGetApplicationDefaults(unittest.TestCase):
def setUp(self) -> None:
_reset_cache_for_tests()
def tearDown(self) -> None:
_reset_cache_for_tests()
def test_reads_role_config_files(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
_write(
roles / "web-app-alpha/config/main.yml",
"""
server:
domains:
canonical:
- alpha.example
""",
)
_write(
roles / "web-app-beta/config/main.yml",
"""
server:
domains:
canonical:
- beta.example
""",
)
defaults = get_application_defaults(roles_dir=roles)
self.assertEqual(sorted(defaults.keys()), ["web-app-alpha", "web-app-beta"])
self.assertIn("group_id", defaults["web-app-alpha"])
self.assertEqual(
defaults["web-app-alpha"]["server"]["domains"]["canonical"],
["alpha.example"],
)
def test_wires_users_to_lookup_indirection(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
_write(
roles / "web-app-alpha/config/main.yml",
"""
server: {}
""",
)
_write(
roles / "web-app-alpha/users/main.yml",
"""
users:
administrator:
username: administrator
""",
)
defaults = get_application_defaults(roles_dir=roles)
self.assertEqual(
defaults["web-app-alpha"]["users"]["administrator"],
"{{ lookup('users', 'administrator') }}",
)
def test_cache_returns_deep_copy(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
_write(
roles / "web-app-alpha/config/main.yml",
"""
server:
domains:
canonical:
- alpha.example
""",
)
first = get_application_defaults(roles_dir=roles)
first["web-app-alpha"]["server"]["mutated"] = True
second = get_application_defaults(roles_dir=roles)
self.assertNotIn("mutated", second["web-app-alpha"]["server"])
class TestGetUserDefaults(unittest.TestCase):
def setUp(self) -> None:
_reset_cache_for_tests()
def tearDown(self) -> None:
_reset_cache_for_tests()
def test_includes_role_defined_users(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
_write(
roles / "web-app-alpha/users/main.yml",
"""
users:
administrator:
username: administrator
""",
)
users = get_user_defaults(roles_dir=roles)
self.assertIn("administrator", users)
self.assertEqual(users["administrator"]["uid"], 1001)
def test_adds_reserved_users_from_role_suffixes(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
(roles / "svc-db-postgres").mkdir()
(roles / "sys-ctl-alm-email").mkdir()
users = get_user_defaults(roles_dir=roles)
self.assertIn("postgres", users)
self.assertTrue(users["postgres"]["reserved"])
self.assertIn("email", users)
self.assertTrue(users["email"]["reserved"])
def test_cache_returns_deep_copy(self):
with tempfile.TemporaryDirectory() as tmp:
roles = Path(tmp)
_write(
roles / "web-app-alpha/users/main.yml",
"""
users:
administrator:
username: administrator
""",
)
first = get_user_defaults(roles_dir=roles)
first["administrator"]["mutated"] = True
second = get_user_defaults(roles_dir=roles)
self.assertNotIn("mutated", second["administrator"])
if __name__ == "__main__":
unittest.main()