Files
computer-playbook/tests/unit/python/plugins/lookup/test_native_metrics_apps.py
T
kevinveenbirkenbachandClaude Opus 5 d88fe1d83a test(lint): let the legacy-path guard see the trees that still named the old layout
`config/main.yml` exists in no role: f65f4d60fb migrated the last one. A guard
against the string already exists, `TestNoLegacyPathReferences`, and it names
`config/main.yml` among its LEGACY_TOKENS. It scanned plugins, utils, cli,
roles, scripts, filter_plugins and lookup_plugins, which is every tree except
the two the stale references actually lived in.

So nine of them survived in `tests/`, and the guard reported green the whole
time. The code underneath was already correct everywhere: `test_csp_consistency`
and `test_role_wiring` iterate ROLE_FILE_META_SERVICES, and only their docstrings
and one assertion message still said `config/main.yml`. Had any of them globbed
the old path the test would have gone vacuous rather than red, which is the
failure mode worth naming here.

`tests` and `tasks` are now scanned. The targets come from
`utils/roles/mapping.py`, not from guesswork: `config/main.yml` is
`meta/services.yml`, `schema/main.yml` is `meta/secrets.yml` (confirmed at
`cli/administration/inventory/provision/reset.py:67`, which reads exactly that
file), and `users/main.yml` is `meta/users.yml`.

Scanning `tasks` exposed a second dead pointer. The migration tool became a
package on 2026-09-14, but EXEMPT_FILES still named `migrate_meta_layout.py`, a
file that no longer exists, so the exemption was inert and only invisible
because `tasks` went unscanned. Two dead pointers covering for each other. The
exemption is now a directory, so a new module in that package cannot fall out of
it. `test_no_direct_calls.py` named the same dead path in prose; that lint
allows through nocheck markers at the call site, so nothing was broken there.

`docs/administration/configuration/email.md` was the worst of them, because it
instructed operators rather than describing tests: it told them to write
`compose.services.email` into `roles/<application_id>/config/main.yml`. The
consumer, `plugins/lookup/email.py:255`, reads `entry["services"]["email"]` and
says in its own comment that there is no `compose.services` envelope. The role
snippet, the inventory snippet and the precedence list now match the reader.
`group_vars/all/TODO.md` pointed at the same dead file.

Verified by watching the guard fail, twice. Re-introducing `config/main.yml` in
a test file reports it; pointing the new directory exemption at a wrong path
reports `tasks/utils/migrate_meta_layout/credentials.py`, which proves both that
`tasks` is scanned and that the exemption is the only thing letting the
migration tool through. Restored, 11 passed.

The changelogs under `docs/changelog/` keep their mentions, and `.md` stays out
of the scanned suffixes for that reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 21:27:40 +02:00

230 lines
8.2 KiB
Python

from __future__ import annotations
import tempfile
import unittest
import unittest.mock as mock
from pathlib import Path
from unittest.mock import patch
from plugins.lookup.native_metrics_apps import LookupModule
def _run(
applications: dict,
roles_dir: Path,
group_names: list | None = None,
deployment_mode: str = "compose",
) -> list:
"""Helper: run the lookup with a temporary roles directory.
group_names defaults to all keys in applications so existing tests that
don't care about the deployment filter still pass without change.
"""
if group_names is None:
group_names = list(applications.keys())
lm = LookupModule()
lm._loader = mock.MagicMock()
lm._templar = mock.MagicMock(template=lambda v: v)
with (
patch("plugins.lookup.native_metrics_apps.ROLES_DIR", roles_dir),
patch("plugins.lookup.native_metrics_apps.lookup_loader") as loader_mock,
):
def _get(name, *a, **k):
if name == "applications":
return mock.MagicMock(run=lambda *_a, **_k: [applications])
return mock.MagicMock(run=lambda *_a, **_k: [{}])
loader_mock.get.side_effect = _get
return lm.run(
[],
variables={
"applications": applications,
"group_names": group_names,
"DEPLOYMENT_MODE": deployment_mode,
},
)[0]
def _make_roles(tmp: Path, specs: dict) -> dict:
"""Create minimal role fixtures under tmp/ and return an applications dict.
In real Ansible, role meta/services.yml defaults are merged into the
`applications` variable before any play runs. This helper replicates that
by building both the filesystem structure (for fragment discovery) and the
applications dict (for get_app_conf lookups) from the same spec.
specs: { app_id: {"native_metrics_enabled": bool, "has_fragment": bool,
"force_bridge": bool} }
Returns: applications dict ready to pass to the lookup.
"""
applications = {}
for app_id, cfg in specs.items():
if cfg.get("has_fragment"):
tpl_dir = tmp / app_id / "templates"
tpl_dir.mkdir(parents=True)
(tpl_dir / "prometheus.yml.j2").write_text(f' - job_name: "{app_id}"\n')
applications[app_id] = {
"networks": {"local": {"force_bridge": bool(cfg.get("force_bridge"))}},
"services": {
"prometheus": {
"native_metrics": {
"enabled": bool(cfg.get("native_metrics_enabled"))
}
}
},
}
return applications
class TestNativeMetricsApps(unittest.TestCase):
def setUp(self):
self._tmpdir = tempfile.TemporaryDirectory()
self.roles_dir = Path(self._tmpdir.name)
def tearDown(self):
self._tmpdir.cleanup()
# ── basic inclusion ────────────────────────────────────────────────────
def test_returns_app_with_enabled_and_fragment(self):
apps = _make_roles(
self.roles_dir,
{
"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True},
},
)
self.assertEqual(_run(apps, self.roles_dir), ["web-app-gitea"])
def test_excludes_app_without_native_metrics_enabled(self):
apps = _make_roles(
self.roles_dir,
{
"web-app-foo": {"native_metrics_enabled": False, "has_fragment": True},
},
)
self.assertEqual(_run(apps, self.roles_dir), [])
def test_excludes_app_without_scrape_fragment(self):
apps = _make_roles(
self.roles_dir,
{
"web-app-bar": {"native_metrics_enabled": True, "has_fragment": False},
},
)
self.assertEqual(_run(apps, self.roles_dir), [])
def test_excludes_app_not_in_applications(self):
"""Apps with a fragment but not deployed must be excluded."""
_make_roles(
self.roles_dir,
{
"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True},
},
)
self.assertEqual(_run({}, self.roles_dir), [])
# ── ordering ───────────────────────────────────────────────────────────
def test_result_is_sorted(self):
apps = _make_roles(
self.roles_dir,
{
"web-app-zzz": {"native_metrics_enabled": True, "has_fragment": True},
"web-app-aaa": {"native_metrics_enabled": True, "has_fragment": True},
"web-app-mmm": {"native_metrics_enabled": True, "has_fragment": True},
},
)
self.assertEqual(
_run(apps, self.roles_dir), ["web-app-aaa", "web-app-mmm", "web-app-zzz"]
)
# ── multiple apps mixed ────────────────────────────────────────────────
def test_returns_only_qualifying_apps(self):
apps = _make_roles(
self.roles_dir,
{
"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True},
"web-app-mattermost": {
"native_metrics_enabled": True,
"has_fragment": True,
},
"web-app-matrix": {
"native_metrics_enabled": True,
"has_fragment": True,
},
"web-app-nofrag": {
"native_metrics_enabled": True,
"has_fragment": False,
},
"web-app-disabled": {
"native_metrics_enabled": False,
"has_fragment": True,
},
},
)
self.assertEqual(
_run(apps, self.roles_dir),
["web-app-gitea", "web-app-matrix", "web-app-mattermost"],
)
# ── group_names deployment filter ──────────────────────────────────────
def test_excludes_app_not_in_group_names(self):
"""Apps not in group_names (not deployed on this host) must be excluded."""
apps = _make_roles(
self.roles_dir,
{
"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True},
"web-app-mattermost": {
"native_metrics_enabled": True,
"has_fragment": True,
},
},
)
result = _run(apps, self.roles_dir, group_names=["web-app-gitea"])
self.assertIn("web-app-gitea", result)
self.assertNotIn("web-app-mattermost", result)
def test_returns_empty_when_group_names_empty(self):
apps = _make_roles(
self.roles_dir,
{"web-app-gitea": {"native_metrics_enabled": True, "has_fragment": True}},
)
self.assertEqual(_run(apps, self.roles_dir, group_names=[]), [])
# ── force_bridge reachability (swarm only) ─────────────────────────────
def test_excludes_force_bridge_app_in_swarm(self):
apps = _make_roles(
self.roles_dir,
{
"web-app-matrix": {
"native_metrics_enabled": True,
"has_fragment": True,
"force_bridge": True,
},
},
)
self.assertEqual(_run(apps, self.roles_dir, deployment_mode="swarm"), [])
def test_includes_force_bridge_app_in_compose(self):
apps = _make_roles(
self.roles_dir,
{
"web-app-matrix": {
"native_metrics_enabled": True,
"has_fragment": True,
"force_bridge": True,
},
},
)
self.assertEqual(
_run(apps, self.roles_dir, deployment_mode="compose"), ["web-app-matrix"]
)
if __name__ == "__main__":
unittest.main()