mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
Enhance tree builder: detect include_role dependencies from tasks/*.yml
- Added logic to scan each role’s tasks/*.yml files for include_role usage - Supports: * loop/with_items with literal strings → adds each role * patterns with variables inside literals (e.g. svc-db-{{database_type}}) → expanded to glob and matched * pure variable-only names ({{var}}) → ignored * pure literal names → added directly - Merges discovered dependencies under graphs["dependencies"]["include_role"] - Added dedicated unit test covering looped includes, glob patterns, pure literals, and ignoring pure variables See ChatGPT conversation (https://chatgpt.com/share/68a4ace0-7268-800f-bd32-b475c5c9ba1d) for context.
This commit is contained in:
143
tests/unit/cli/build/test_tree_include_role_dependencies.py
Normal file
143
tests/unit/cli/build/test_tree_include_role_dependencies.py
Normal file
@@ -0,0 +1,143 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import tempfile
|
||||
import shutil
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Absoluter Pfad zum tree.py Script (wie im vorhandenen Test)
|
||||
SCRIPT_PATH = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "../../../../cli/build/tree.py")
|
||||
)
|
||||
|
||||
class TestTreeIncludeRoleDependencies(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Temp roles root
|
||||
self.roles_dir = tempfile.mkdtemp()
|
||||
|
||||
# Producer-Role (die wir scannen) + Zielrollen für Matches
|
||||
self.producer = "producer"
|
||||
self.producer_path = os.path.join(self.roles_dir, self.producer)
|
||||
os.makedirs(os.path.join(self.producer_path, "tasks"))
|
||||
os.makedirs(os.path.join(self.producer_path, "meta"))
|
||||
|
||||
# Rollen, die durch Pattern/Loops gematcht werden sollen
|
||||
self.roles_to_create = [
|
||||
"sys-ctl-hlth-webserver",
|
||||
"sys-ctl-hlth-csp",
|
||||
"svc-db-postgres",
|
||||
"svc-db-mysql",
|
||||
"axb", # für a{{database_type}}b → a*b
|
||||
"ayyb", # für a{{database_type}}b → a*b
|
||||
"literal-role", # für reinen Literalnamen
|
||||
]
|
||||
for r in self.roles_to_create:
|
||||
os.makedirs(os.path.join(self.roles_dir, r, "meta"), exist_ok=True)
|
||||
|
||||
# tasks/main.yml mit allen geforderten Varianten
|
||||
tasks_yaml = """
|
||||
- name: Include health dependencies
|
||||
include_role:
|
||||
name: "{{ item }}"
|
||||
loop:
|
||||
- sys-ctl-hlth-webserver
|
||||
- sys-ctl-hlth-csp
|
||||
|
||||
- name: Pattern with literal + var suffix
|
||||
include_role:
|
||||
name: "svc-db-{{database_type}}"
|
||||
|
||||
- name: Pattern with literal prefix/suffix around var
|
||||
include_role:
|
||||
name: "a{{database_type}}b"
|
||||
|
||||
- name: Pure variable only (should be ignored)
|
||||
include_role:
|
||||
name: "{{database_type}}"
|
||||
|
||||
- name: Pure literal include
|
||||
include_role:
|
||||
name: "literal-role"
|
||||
"""
|
||||
with open(os.path.join(self.producer_path, "tasks", "main.yml"), "w", encoding="utf-8") as f:
|
||||
f.write(tasks_yaml)
|
||||
|
||||
# shadow folder
|
||||
self.shadow_dir = tempfile.mkdtemp()
|
||||
|
||||
# Patch argv
|
||||
self.orig_argv = sys.argv[:]
|
||||
sys.argv = [
|
||||
SCRIPT_PATH,
|
||||
"-d", self.roles_dir,
|
||||
"-s", self.shadow_dir,
|
||||
"-o", "json",
|
||||
]
|
||||
|
||||
# Ensure project root on sys.path
|
||||
project_root = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "../../../../")
|
||||
)
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
def tearDown(self):
|
||||
sys.argv = self.orig_argv
|
||||
shutil.rmtree(self.roles_dir)
|
||||
shutil.rmtree(self.shadow_dir)
|
||||
|
||||
@patch("cli.build.tree.output_graph")
|
||||
@patch("cli.build.tree.build_mappings")
|
||||
def test_include_role_dependencies_detected(self, mock_build_mappings, mock_output_graph):
|
||||
# Basis-Graph leer, damit nur unsere Dependencies sichtbar sind
|
||||
mock_build_mappings.return_value = {}
|
||||
|
||||
# Import und Ausführen
|
||||
import importlib
|
||||
tree_mod = importlib.import_module("cli.build.tree")
|
||||
tree_mod.main()
|
||||
|
||||
# Erwarteter Pfad im Shadow-Folder
|
||||
expected_tree_path = os.path.join(
|
||||
self.shadow_dir, self.producer, "meta", "tree.json"
|
||||
)
|
||||
self.assertTrue(
|
||||
os.path.isfile(expected_tree_path),
|
||||
f"tree.json not found at {expected_tree_path}"
|
||||
)
|
||||
|
||||
# JSON laden und Abhängigkeiten prüfen
|
||||
with open(expected_tree_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Erwartete include_role-Dependenzen:
|
||||
expected = sorted([
|
||||
"sys-ctl-hlth-webserver", # aus loop
|
||||
"sys-ctl-hlth-csp", # aus loop
|
||||
"svc-db-postgres", # aus svc-db-{{database_type}}
|
||||
"svc-db-mysql", # aus svc-db-{{database_type}}
|
||||
"axb", # aus a{{database_type}}b
|
||||
"ayyb", # aus a{{database_type}}b
|
||||
"literal-role", # reiner Literalname
|
||||
])
|
||||
|
||||
deps = (
|
||||
data
|
||||
.get("dependencies", {})
|
||||
.get("include_role", [])
|
||||
)
|
||||
self.assertEqual(deps, expected, "include_role dependencies mismatch")
|
||||
|
||||
# Sicherstellen, dass der pure Variable-Name "{{database_type}}" NICHT aufgenommen wurde
|
||||
self.assertNotIn("{{database_type}}", deps, "pure variable include should be ignored")
|
||||
|
||||
# Sicherstellen, dass im Original-meta der Producer-Role nichts geschrieben wurde
|
||||
original_tree_path = os.path.join(self.producer_path, "meta", "tree.json")
|
||||
self.assertFalse(
|
||||
os.path.exists(original_tree_path),
|
||||
"tree.json should NOT be written to the real meta/ folder"
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@@ -17,7 +17,7 @@ class TestGetServiceName(unittest.TestCase):
|
||||
def test_explicit_custom_suffix(self):
|
||||
self.assertEqual(
|
||||
get_service_name.get_service_name("sys-ctl-bkp@", "postgres", "special"),
|
||||
"sys-ctl-bkp.postgres@.special"
|
||||
"sys-ctl-bkp.postgres@special"
|
||||
)
|
||||
|
||||
def test_explicit_false_suffix(self):
|
||||
@@ -32,7 +32,7 @@ class TestGetServiceName(unittest.TestCase):
|
||||
|
||||
def test_case_is_lowered(self):
|
||||
self.assertEqual(
|
||||
get_service_name.get_service_name("Sys-CTL-BKP@", "Postgres", "SeRviCe"),
|
||||
get_service_name.get_service_name("Sys-CTL-BKP@", "Postgres", ".SeRviCe"),
|
||||
"sys-ctl-bkp.postgres@.service"
|
||||
)
|
||||
|
||||
|
Reference in New Issue
Block a user