Files
computer-playbook/tests/unit/utils/roles/validation/test_invokable.py
T
kevinveenbirkenbachandClaude Opus 4.7 f6c8c85b1b test(lint): role-file-spot guard + 139-file mapping migration
Add tests/lint/repository/test_role_file_spot.py: AST-walks every .py
file under the production trees and tests/, flags any reference to a
role file (standalone literal, f-string segment, pathlib `/` chain,
`+` concat chain, os.path.join / Path.joinpath / posixpath.join /
ntpath.join call) whose value or joined trailing operands match a
ROLE_FILE_* constant in utils.roles.mapping. The flag set is
auto-discovered from the module so adding a new constant extends
coverage without further edits. ROLE_FILE_README is exempt as too
generic. Per-line `# nocheck: role-file-spot` (same-or-above) is the
documented opt-out, catalog row added in
docs/contributing/actions/testing/suppression.md.

Migrate 139 .py files (377 occurrences) to use the constants:

- 135 files via direct constant import (`from utils.roles.mapping
  import ROLE_FILE_*`), with the symbol used in pathlib chains, in
  os.path.join / Path.joinpath calls, in f-string interpolation, and
  in glob patterns.
- 4 files via `# nocheck: role-file-spot` where the literal lives in
  argparse help text or in an allowlist tuple meant to declare the
  literal path.

Adjust utils/roles/dependency_resolver.py to use lazy-formatting
positional args in the logger.exception call (G003-clean) instead of
string concatenation against the constant.

Drop a redundant `if cond: return True; return False` guard in the
new test for `return cond` (SIM103-clean).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 13:21:22 +02:00

126 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.roles.validation.invokable as inv
from utils.cache.yaml import dump_yaml
from utils.roles.mapping import ROLE_FILE_VARS_MAIN
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 / ROLE_FILE_VARS_MAIN, {"application_id": application_id})
else:
dump_yaml(rd / ROLE_FILE_VARS_MAIN, {})
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, "PROJECT_ROOT", self.tmp),
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, "PROJECT_ROOT", self.tmp),
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 / ROLE_FILE_VARS_MAIN, {})
with (
patch.object(inv, "PROJECT_ROOT", self.tmp),
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, "PROJECT_ROOT", self.tmp),
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()