mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
New 'image' lookup is the SPOT for resolving <registry>/<image>:<version> (swarm registry prefix in swarm mode, bare in compose; custom= accepts a string base for a second distinct custom image inside one app). container_image becomes a thin wrapper that forwards terms+kwargs and adds the compose image: "<ref>" line, so all existing template call sites keep their output byte-identical. The lookup-usage integration test now counts lookup_loader.get() delegation as production usage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
164 lines
6.1 KiB
Python
164 lines
6.1 KiB
Python
"""Integration test: every defined lookup plugin must be referenced somewhere.
|
|
|
|
For every ``plugins/lookup/<name>.py`` that defines a ``LookupModule`` class,
|
|
verify that the lookup is actually consumed. Three invocation styles count
|
|
as real usage:
|
|
|
|
- ``lookup('<name>', ...)`` — the canonical Jinja/YAML form.
|
|
- ``query('<name>', ...)`` — the list-returning variant; equivalent to
|
|
``lookup(..., wantlist=True)``.
|
|
- ``from plugins.lookup.<name> import ...`` — direct Python import of the
|
|
plugin's internals (e.g. ``utils/runtime_data.py`` embeds several
|
|
``LookupModule`` classes as regular helpers instead of dispatching
|
|
through Ansible).
|
|
- ``lookup_loader.get('<name>')`` — one lookup delegating to another
|
|
(e.g. ``container_image`` wraps the ``image`` lookup).
|
|
|
|
Mirrors ``tests/integration/filters/test_usage.py`` in spirit and uses the
|
|
same inverted-scan approach (one pass over all project files, a single
|
|
alternation regex matching every defined lookup name).
|
|
|
|
"Used in tests only" counts as failure: lookups shipped in ``plugins/lookup/``
|
|
must be called from production code (roles, tasks, group_vars, templates,
|
|
playbooks, or other plugins) — not just from test fixtures.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import os
|
|
import re
|
|
import unittest
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.cache.files import iter_project_files, read_text
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
LOOKUP_PLUGINS_DIR = PROJECT_ROOT / "plugins" / "lookup"
|
|
|
|
USAGE_EXTS = (".yml", ".yaml", ".j2", ".jinja2", ".tmpl", ".py")
|
|
|
|
|
|
def _defines_lookup_module(path: Path) -> bool:
|
|
"""Return True if the module defines ``class LookupModule`` at top level.
|
|
|
|
Ansible discovers every lookup plugin by filename, but only files that
|
|
declare a ``LookupModule`` class are actual lookups. Helper modules
|
|
without that class (should any appear) must not be treated as lookups.
|
|
"""
|
|
try:
|
|
tree = ast.parse(read_text(str(path)), filename=str(path))
|
|
except (SyntaxError, OSError, UnicodeDecodeError):
|
|
return False
|
|
for node in tree.body:
|
|
if isinstance(node, ast.ClassDef) and node.name == "LookupModule":
|
|
return True
|
|
return False
|
|
|
|
|
|
def collect_defined_lookups() -> dict[str, Path]:
|
|
"""Return mapping ``{lookup_name: plugin_path}`` for all discovered plugins."""
|
|
defined: dict[str, Path] = {}
|
|
if not LOOKUP_PLUGINS_DIR.is_dir():
|
|
return defined
|
|
for entry in sorted(LOOKUP_PLUGINS_DIR.iterdir()):
|
|
if entry.suffix != ".py" or entry.name == "__init__.py":
|
|
continue
|
|
if not _defines_lookup_module(entry):
|
|
continue
|
|
defined[entry.stem] = entry.resolve()
|
|
return defined
|
|
|
|
|
|
def _scan_lookup_usage(
|
|
defined: dict[str, Path],
|
|
) -> dict[str, dict[str, bool]]:
|
|
"""Single-pass inverted scan for every counted invocation form.
|
|
|
|
Complexity: O(M_files * 1_master_regex) rather than O(N_lookups * M_files).
|
|
The combined regexes carry alternation over every defined lookup name and
|
|
cover ``lookup(...)``, ``query(...)`` (arbitrary whitespace inside the
|
|
call, both quote styles), the ``from plugins.lookup.<name> import`` form
|
|
that ``utils/runtime_data.py`` uses to embed a LookupModule directly, and
|
|
the ``lookup_loader.get('<name>')`` delegation form.
|
|
|
|
A plugin's own definition file is skipped: docstrings contain example
|
|
invocations of the same lookup and would otherwise register as self-usage.
|
|
"""
|
|
state: dict[str, dict[str, bool]] = {
|
|
name: {"used_any": False, "used_outside": False} for name in defined
|
|
}
|
|
if not defined:
|
|
return state
|
|
|
|
escaped = [re.escape(name) for name in defined]
|
|
name_alt = "(" + "|".join(escaped) + ")"
|
|
invoke_pat = re.compile(r"(?:lookup|query)\(\s*['\"]" + name_alt + r"['\"]")
|
|
import_pat = re.compile(r"from\s+plugins\.lookup\." + name_alt + r"\s+import\b")
|
|
loader_pat = re.compile(r"lookup_loader\.get\(\s*['\"]" + name_alt + r"['\"]")
|
|
|
|
tests_prefix = str(PROJECT_ROOT / "tests") + os.sep
|
|
defined_paths: set[str] = {str(p) for p in defined.values()}
|
|
|
|
for path in iter_project_files(extensions=USAGE_EXTS):
|
|
resolved = os.path.realpath(path)
|
|
if resolved in defined_paths:
|
|
continue
|
|
|
|
try:
|
|
content = read_text(path)
|
|
except (OSError, UnicodeDecodeError):
|
|
continue
|
|
|
|
is_in_tests = path.startswith(tests_prefix)
|
|
for pat in (invoke_pat, import_pat, loader_pat):
|
|
for match in pat.finditer(content):
|
|
name = match.group(1)
|
|
s = state[name]
|
|
s["used_any"] = True
|
|
if not is_in_tests:
|
|
s["used_outside"] = True
|
|
|
|
return state
|
|
|
|
|
|
class TestLookupDefinitionsAreUsed(unittest.TestCase):
|
|
"""Every lookup plugin under ``plugins/lookup/`` must be invoked by name
|
|
via ``lookup('<name>', ...)`` or ``query('<name>', ...)`` somewhere outside
|
|
the tests directory. Shipping an unused lookup is a maintenance hazard —
|
|
it stays in the module search path, increases cognitive load when
|
|
browsing plugins, and rots silently when the rest of the codebase evolves.
|
|
"""
|
|
|
|
def test_every_defined_lookup_is_used(self) -> None:
|
|
defined = collect_defined_lookups()
|
|
self.assertTrue(
|
|
defined,
|
|
"No lookup plugins found under plugins/lookup/; check PROJECT_ROOT.",
|
|
)
|
|
|
|
state = _scan_lookup_usage(defined)
|
|
|
|
unused: list[tuple[str, Path, str]] = []
|
|
for name in sorted(defined):
|
|
s = state[name]
|
|
if not s["used_any"]:
|
|
unused.append((name, defined[name], "not used anywhere"))
|
|
elif not s["used_outside"]:
|
|
unused.append((name, defined[name], "only used in tests"))
|
|
|
|
if unused:
|
|
lines = ["The following lookup plugins are defined but unused:"]
|
|
for name, path, reason in unused:
|
|
rel = path.relative_to(PROJECT_ROOT).as_posix()
|
|
lines.append(f"- '{name}' defined in {rel} → {reason}")
|
|
self.fail("\n".join(lines))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|