mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 04:06:42 +00:00
ruff format reflows the package batch, registry, plan and invocation code and tests and the Stalwart and Nextcloud tests, ansible-lint rewraps long Jinja lines in folded scalars of sys-service, Nextcloud and SeaweedFS, mbake restores the Makefile's final newline, and the Nextcloud OAuth test merges its nested with statements (SIM117). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
242 lines
8.0 KiB
Python
242 lines
8.0 KiB
Python
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from utils.cache.yaml import dump_yaml
|
|
from utils.roles.invocations import (
|
|
deploy_closure,
|
|
expand_roles,
|
|
role_invocations,
|
|
stage_files,
|
|
stage_invocations,
|
|
)
|
|
from utils.roles.mapping import (
|
|
ROLE_FILE_HANDLERS_MAIN,
|
|
ROLE_FILE_META_MAIN,
|
|
ROLE_FILE_TASKS_MAIN,
|
|
ROLE_FILE_VARS_MAIN,
|
|
)
|
|
|
|
ROLES = ("role-a", "role-b", "role-c", "sys-ctl-hlth-x", "sys-ctl-hlth-y")
|
|
|
|
|
|
def _role_file(role: str, role_file: str) -> str:
|
|
return f"roles/{role}/{role_file}"
|
|
|
|
|
|
class _TreeMixin:
|
|
def setUp(self) -> None:
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self._tmp.name)
|
|
self.addCleanup(self._tmp.cleanup)
|
|
for role in ROLES:
|
|
(self.root / "roles" / role / "tasks").mkdir(parents=True)
|
|
|
|
def _write(self, relative: str, payload) -> Path:
|
|
path = self.root / relative
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
dump_yaml(str(path), payload)
|
|
return path
|
|
|
|
def _invoked(self, tasks, role: str = "role-a") -> frozenset[str]:
|
|
self._write(_role_file(role, ROLE_FILE_TASKS_MAIN), tasks)
|
|
return role_invocations(str(self.root / "roles"), role)
|
|
|
|
|
|
class TestRoleInvocations(_TreeMixin, unittest.TestCase):
|
|
def test_a_literal_include_role_is_an_edge(self):
|
|
self.assertEqual(
|
|
self._invoked([{"include_role": {"name": "role-b"}}]), {"role-b"}
|
|
)
|
|
|
|
def test_an_import_role_is_an_edge(self):
|
|
self.assertEqual(
|
|
self._invoked([{"ansible.builtin.import_role": {"name": "role-b"}}]),
|
|
{"role-b"},
|
|
)
|
|
|
|
def test_a_literal_loop_names_every_item(self):
|
|
tasks = [
|
|
{
|
|
"include_role": {"name": "{{ dep }}"},
|
|
"loop": ["role-b", "role-c"],
|
|
"loop_control": {"loop_var": "dep"},
|
|
}
|
|
]
|
|
self.assertEqual(self._invoked(tasks), {"role-b", "role-c"})
|
|
|
|
def test_a_template_with_a_literal_part_matches_a_glob(self):
|
|
tasks = [{"include_role": {"name": "sys-ctl-hlth-{{ kind }}"}}]
|
|
self.assertEqual(self._invoked(tasks), {"sys-ctl-hlth-x", "sys-ctl-hlth-y"})
|
|
|
|
def test_a_role_name_handed_over_as_a_var_is_an_edge(self):
|
|
tasks = [
|
|
{
|
|
"include_tasks": "utils/load/app.yml",
|
|
"vars": {"load_app_id": "role-b"},
|
|
}
|
|
]
|
|
self.assertEqual(self._invoked(tasks), {"role-b"})
|
|
|
|
def test_including_a_task_file_of_another_role_is_an_edge(self):
|
|
tasks = [
|
|
{
|
|
"include_tasks": "{{ lookup('path_absolute', 'roles/role-c/tasks/x.yml') }}"
|
|
}
|
|
]
|
|
self.assertEqual(self._invoked(tasks), {"role-c"})
|
|
|
|
def test_includes_nested_in_blocks_count(self):
|
|
tasks = [
|
|
{
|
|
"block": [{"include_role": {"name": "role-b"}}],
|
|
"rescue": [{"include_role": {"name": "role-c"}}],
|
|
}
|
|
]
|
|
self.assertEqual(self._invoked(tasks), {"role-b", "role-c"})
|
|
|
|
def test_conditions_do_not_hide_an_edge(self):
|
|
tasks = [{"include_role": {"name": "role-b"}, "when": "false"}]
|
|
self.assertEqual(self._invoked(tasks), {"role-b"})
|
|
|
|
def test_a_bare_template_falls_back_to_quoted_role_names_in_vars(self):
|
|
self._write(
|
|
_role_file("role-a", ROLE_FILE_VARS_MAIN),
|
|
{"DEPS": "{{ ['role-b'] + (['role-c'] if flag else []) }}"},
|
|
)
|
|
tasks = [
|
|
{
|
|
"include_role": {"name": "{{ dep }}"},
|
|
"loop": "{{ DEPS }}",
|
|
"loop_control": {"loop_var": "dep"},
|
|
}
|
|
]
|
|
self.assertEqual(self._invoked(tasks), {"role-b", "role-c"})
|
|
|
|
def test_quoted_names_are_ignored_while_every_include_resolves(self):
|
|
self._write(
|
|
_role_file("role-a", ROLE_FILE_VARS_MAIN), {"OTHER": "{{ 'role-c' }}"}
|
|
)
|
|
self.assertEqual(
|
|
self._invoked([{"include_role": {"name": "role-b"}}]), {"role-b"}
|
|
)
|
|
|
|
def test_unknown_role_names_are_dropped(self):
|
|
self.assertEqual(
|
|
self._invoked([{"include_role": {"name": "role-missing"}}]), frozenset()
|
|
)
|
|
|
|
def test_a_role_does_not_invoke_itself(self):
|
|
self.assertEqual(
|
|
self._invoked([{"include_role": {"name": "role-a"}}]), frozenset()
|
|
)
|
|
|
|
def test_handlers_count(self):
|
|
self._write(
|
|
_role_file("role-a", ROLE_FILE_HANDLERS_MAIN),
|
|
[{"include_role": {"name": "role-c"}}],
|
|
)
|
|
self.assertEqual(
|
|
self._invoked([{"include_role": {"name": "role-b"}}]), {"role-b", "role-c"}
|
|
)
|
|
|
|
|
|
class TestStageInvocations(_TreeMixin, unittest.TestCase):
|
|
def _stage(self, tasks):
|
|
return self._write("tasks/stages/01_constructor.yml", tasks)
|
|
|
|
def test_a_group_membership_gate_closes_for_a_non_member(self):
|
|
path = self._stage(
|
|
[{"include_role": {"name": "role-b"}, "when": "'role-b' in group_names"}]
|
|
)
|
|
self.assertEqual(
|
|
stage_invocations(path, frozenset(ROLES), seeds=[], group_names=[]), set()
|
|
)
|
|
|
|
def test_a_group_membership_gate_opens_for_a_member(self):
|
|
path = self._stage(
|
|
[{"include_role": {"name": "role-b"}, "when": "'role-b' in group_names"}]
|
|
)
|
|
self.assertEqual(
|
|
stage_invocations(path, frozenset(ROLES), seeds=[], group_names=["role-b"]),
|
|
{"role-b"},
|
|
)
|
|
|
|
def test_an_application_allowed_gate_decides_on_the_seeds(self):
|
|
path = self._stage(
|
|
[
|
|
{
|
|
"when": "'role-b' | application_allowed(group_names, whitelist)",
|
|
"block": [{"include_role": {"name": "role-b"}}],
|
|
}
|
|
]
|
|
)
|
|
self.assertEqual(
|
|
stage_invocations(path, frozenset(ROLES), seeds=[], group_names=["role-b"]),
|
|
set(),
|
|
)
|
|
self.assertEqual(
|
|
stage_invocations(path, frozenset(ROLES), seeds=["role-b"], group_names=[]),
|
|
{"role-b"},
|
|
)
|
|
|
|
def test_any_other_condition_keeps_the_include(self):
|
|
path = self._stage(
|
|
[{"include_role": {"name": "role-c"}, "when": ["MODE_CLEANUP | bool"]}]
|
|
)
|
|
self.assertEqual(
|
|
stage_invocations(path, frozenset(ROLES), seeds=[], group_names=[]),
|
|
{"role-c"},
|
|
)
|
|
|
|
def test_stage_files_follow_the_host_types(self):
|
|
for name in ("01_constructor", "02_server", "02_workstation", "03_destructor"):
|
|
self._write(f"tasks/stages/{name}.yml", [])
|
|
self.assertEqual(
|
|
[path.name for path in stage_files(self.root, ["server"])],
|
|
["01_constructor.yml", "02_server.yml", "03_destructor.yml"],
|
|
)
|
|
|
|
|
|
class TestClosure(_TreeMixin, unittest.TestCase):
|
|
def test_invocations_are_followed_transitively(self):
|
|
self._write(
|
|
_role_file("role-b", ROLE_FILE_TASKS_MAIN),
|
|
[{"include_role": {"name": "role-c"}}],
|
|
)
|
|
self._write(
|
|
_role_file("role-a", ROLE_FILE_TASKS_MAIN),
|
|
[{"include_role": {"name": "role-b"}}],
|
|
)
|
|
self.assertEqual(
|
|
expand_roles({}, ["role-a"], project_root=self.root),
|
|
{"role-a", "role-b", "role-c"},
|
|
)
|
|
|
|
def test_meta_dependencies_are_followed(self):
|
|
self._write(
|
|
_role_file("role-a", ROLE_FILE_META_MAIN), {"dependencies": ["role-c"]}
|
|
)
|
|
self.assertEqual(
|
|
expand_roles({}, ["role-a"], project_root=self.root), {"role-a", "role-c"}
|
|
)
|
|
|
|
def test_the_deploy_closure_adds_the_stage_roles(self):
|
|
self._write(
|
|
"tasks/stages/01_constructor.yml", [{"include_role": {"name": "role-c"}}]
|
|
)
|
|
self.assertEqual(
|
|
deploy_closure(
|
|
{},
|
|
["role-a"],
|
|
project_root=self.root,
|
|
host_types=[],
|
|
group_names=["role-a"],
|
|
),
|
|
{"role-a", "role-c"},
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|