mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
200 lines
7.4 KiB
Python
200 lines
7.4 KiB
Python
"""Integration guard: every ``services.yml`` flag that resolves via
|
|
the dynamic ``"{{ '<role>' in group_names }}"`` form MUST be exercised
|
|
by ``meta/variants.yml`` — at least one variant entry pinning the
|
|
flag to literal ``true``, AND at least one pinning it to literal
|
|
``false``.
|
|
|
|
Rationale
|
|
---------
|
|
|
|
The matrix-deploy CLI iterates the per-role variant list (see
|
|
``docs/contributing/artefact/files/role/variants.md``) and produces
|
|
one inventory folder per entry. A flag declared as dynamic in
|
|
``services.yml`` only takes its boolean shape once the inventory
|
|
templar resolves it against the host's ``group_names``. Without
|
|
explicit variant overrides on both sides, the matrix only ever
|
|
exercises one branch — the role can ship for years with the
|
|
``false`` (or ``true``) path effectively dead.
|
|
|
|
This test makes that coverage explicit: the role's own
|
|
``meta/variants.yml`` MUST contain, for each dynamic
|
|
``(service_key, flag)`` pair, at least one variant overriding the
|
|
flag to ``true`` and one overriding it to ``false``. Pairs may share
|
|
variants — a single entry may pin multiple unrelated services true
|
|
while pinning others false; the test only checks that the union of
|
|
all variants covers both polarities for every dynamic flag.
|
|
|
|
For databases the same rule reduces to coverage of ``shared``: the
|
|
DB-consumer ``enabled`` flag stays literal ``true`` (with
|
|
``# nocheck: dynamic-flag``), so only ``shared`` is dynamic and needs
|
|
the two-polarity coverage.
|
|
|
|
Exempt a service with ``# nocheck: variants-coverage`` on (or directly
|
|
above) its ``meta/services.yml`` key — for flags whose group-derived
|
|
value is a hard invariant that variants must never override (e.g. a
|
|
browser frontend that must stay off on onion nodes).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.annotations.suppress import line_has_rule
|
|
from utils.cache.files import read_text
|
|
from utils.cache.yaml import load_yaml_any
|
|
from utils.roles.mapping import (
|
|
ROLE_FILE_META_SERVICES,
|
|
ROLE_FILE_META_VARIANTS,
|
|
ROLE_TYPE_APPLICATION,
|
|
)
|
|
from utils.roles.type import get_role_types
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
ROLES_DIR = PROJECT_ROOT / "roles"
|
|
|
|
|
|
_RULES = ("dynamic-flag", "variants-coverage")
|
|
|
|
|
|
def _is_dynamic_flag(value) -> bool:
|
|
return isinstance(value, str) and "in group_names" in value
|
|
|
|
|
|
def _suppressed_top_level_keys(file_path: Path) -> set[str]:
|
|
"""Return the set of top-level service keys whose preceding comment
|
|
block carries a ``# nocheck: dynamic-flag`` or
|
|
``# nocheck: variants-coverage`` marker (mirrors
|
|
``tests.integration.roles.meta.services.test_dynamic_flags``)."""
|
|
exceptions: set[str] = set()
|
|
pending = False
|
|
for raw_line in read_text(str(file_path)).splitlines():
|
|
stripped = raw_line.strip()
|
|
if stripped.startswith("#"):
|
|
if any(line_has_rule(raw_line, rule) for rule in _RULES):
|
|
pending = True
|
|
continue
|
|
if not stripped:
|
|
pending = False
|
|
continue
|
|
is_top_level = not raw_line.startswith((" ", "\t"))
|
|
if pending and is_top_level and ":" in stripped:
|
|
key = stripped.split(":", 1)[0].strip()
|
|
if key:
|
|
exceptions.add(key)
|
|
pending = False
|
|
return exceptions
|
|
|
|
|
|
def _dynamic_pairs(services: dict, suppressed: set[str]) -> list[tuple[str, str]]:
|
|
"""Return ``[(service_key, flag_name), ...]`` for every flag whose
|
|
value is a Jinja string carrying ``in group_names``."""
|
|
pairs: list[tuple[str, str]] = []
|
|
for key, entry in services.items():
|
|
if not isinstance(entry, dict) or key in suppressed:
|
|
continue
|
|
pairs.extend(
|
|
(key, flag)
|
|
for flag in ("enabled", "shared")
|
|
if _is_dynamic_flag(entry.get(flag))
|
|
)
|
|
return pairs
|
|
|
|
|
|
def _load_yaml(path: Path) -> object:
|
|
if not path.is_file():
|
|
return None
|
|
try:
|
|
return load_yaml_any(str(path), default_if_missing=None)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _variant_overrides_for(variant: dict, service_key: str, flag: str) -> object:
|
|
"""Return the literal override value for ``services.<key>.<flag>``
|
|
in ``variant``, or a sentinel ``MISSING`` if not overridden."""
|
|
services = variant.get("services") if isinstance(variant, dict) else None
|
|
if not isinstance(services, dict):
|
|
return _MISSING
|
|
entry = services.get(service_key)
|
|
if not isinstance(entry, dict):
|
|
return _MISSING
|
|
if flag not in entry:
|
|
return _MISSING
|
|
return entry[flag]
|
|
|
|
|
|
_MISSING = object()
|
|
|
|
|
|
class TestVariantsCoverage(unittest.TestCase):
|
|
def test_every_dynamic_flag_has_true_and_false_variant(self):
|
|
offenders: list[str] = []
|
|
|
|
for role_dir in sorted(p for p in ROLES_DIR.iterdir() if p.is_dir()):
|
|
role_name = role_dir.name
|
|
if ROLE_TYPE_APPLICATION not in get_role_types(role_dir):
|
|
continue
|
|
services_path = role_dir / ROLE_FILE_META_SERVICES
|
|
services = _load_yaml(services_path)
|
|
if not isinstance(services, dict):
|
|
continue
|
|
|
|
pairs = _dynamic_pairs(services, _suppressed_top_level_keys(services_path))
|
|
if not pairs:
|
|
continue
|
|
|
|
variants_path = role_dir / ROLE_FILE_META_VARIANTS
|
|
variants = _load_yaml(variants_path)
|
|
if not isinstance(variants, list):
|
|
offenders.append(
|
|
f"{role_name}: services.yml declares {len(pairs)} dynamic "
|
|
f"flag(s) but {variants_path.relative_to(PROJECT_ROOT)} "
|
|
f"is missing or not a YAML list. Add a list with at least "
|
|
f"two entries that pin each dynamic flag to ``true`` and "
|
|
f"``false`` respectively."
|
|
)
|
|
continue
|
|
|
|
normalised_variants = [v if isinstance(v, dict) else {} for v in variants]
|
|
|
|
for service_key, flag in sorted(pairs):
|
|
seen_true = False
|
|
seen_false = False
|
|
for variant in normalised_variants:
|
|
override = _variant_overrides_for(variant, service_key, flag)
|
|
if override is True:
|
|
seen_true = True
|
|
elif override is False:
|
|
seen_false = True
|
|
if not seen_true:
|
|
offenders.append(
|
|
f"{role_name}: services.{service_key}.{flag} is "
|
|
f"dynamic but no variant in meta/variants.yml pins "
|
|
f"it to ``true``. Add an entry with "
|
|
f"``services.{service_key}.{flag}: true``."
|
|
)
|
|
if not seen_false:
|
|
offenders.append(
|
|
f"{role_name}: services.{service_key}.{flag} is "
|
|
f"dynamic but no variant in meta/variants.yml pins "
|
|
f"it to ``false``. Add an entry with "
|
|
f"``services.{service_key}.{flag}: false``."
|
|
)
|
|
|
|
if offenders:
|
|
self.fail(
|
|
"Dynamic-flag variant coverage is incomplete (every "
|
|
"``in group_names`` flag needs a true-pinning AND a "
|
|
"false-pinning variant entry):\n"
|
|
+ "\n".join(f" - {o}" for o in offenders)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
unittest.main()
|