mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-12-04 00:19:34 +00:00
- Refactor cli/build/graph.py to use cached metadata and dependency indices for faster graph generation and cleaner separation of concerns - Refactor cli/build/tree.py to delegate per-role processing to process_role() and support parallel execution via ProcessPoolExecutor - Add unit tests for graph helper functions and build_mappings() under tests/unit/cli/build/test_graph.py - Add unit tests for find_roles() and process_role() behaviour under tests/unit/cli/build/test_tree.py - Remove the old include_role dependency integration test which relied on the previous tree.json dependencies bucket For details see ChatGPT conversation: https://chatgpt.com/share/6926b805-28a0-800f-a075-e5250aab5c4a
110 lines
4.1 KiB
Python
110 lines
4.1 KiB
Python
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
from io import StringIO
|
|
from contextlib import redirect_stdout
|
|
from unittest.mock import patch
|
|
|
|
from cli.build import tree as tree_module
|
|
|
|
|
|
class TestFindRoles(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.roles_dir = tempfile.mkdtemp()
|
|
self.addCleanup(lambda: shutil.rmtree(self.roles_dir, ignore_errors=True))
|
|
|
|
def test_find_roles_returns_only_directories(self):
|
|
# Create some role directories and a non-directory entry
|
|
os.makedirs(os.path.join(self.roles_dir, "role_a"))
|
|
os.makedirs(os.path.join(self.roles_dir, "role_b"))
|
|
with open(os.path.join(self.roles_dir, "not_a_role.txt"), "w", encoding="utf-8") as f:
|
|
f.write("dummy")
|
|
|
|
roles = dict(tree_module.find_roles(self.roles_dir))
|
|
self.assertEqual(set(roles.keys()), {"role_a", "role_b"})
|
|
self.assertTrue(all(os.path.isdir(path) for path in roles.values()))
|
|
|
|
|
|
class TestProcessRole(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
# We use a temporary "roles" directory and a separate shadow folder.
|
|
self.roles_dir = tempfile.mkdtemp()
|
|
self.shadow_dir = tempfile.mkdtemp()
|
|
self.addCleanup(lambda: shutil.rmtree(self.roles_dir, ignore_errors=True))
|
|
self.addCleanup(lambda: shutil.rmtree(self.shadow_dir, ignore_errors=True))
|
|
|
|
# Create a minimal role directory
|
|
os.makedirs(os.path.join(self.roles_dir, "myrole"), exist_ok=True)
|
|
|
|
def test_process_role_writes_tree_json_and_does_not_mutate_graphs(self):
|
|
graphs = {
|
|
"include_role_to": {"nodes": [{"id": "myrole"}], "links": []},
|
|
"custom_key": {"value": 42}, # sentinel to ensure we do not modify the dict
|
|
}
|
|
|
|
with patch.object(tree_module, "build_mappings", return_value=graphs) as mocked_build:
|
|
tree_module.process_role(
|
|
role_name="myrole",
|
|
roles_dir=self.roles_dir,
|
|
depth=0,
|
|
shadow_folder=self.shadow_dir,
|
|
output="json",
|
|
preview=False,
|
|
verbose=False,
|
|
no_include_role=False,
|
|
no_import_role=False,
|
|
no_dependencies=False,
|
|
no_run_after=False,
|
|
)
|
|
|
|
mocked_build.assert_called_once()
|
|
|
|
tree_file = os.path.join(self.shadow_dir, "myrole", "meta", "tree.json")
|
|
self.assertTrue(os.path.exists(tree_file), "tree.json was not written")
|
|
|
|
with open(tree_file, "r", encoding="utf-8") as f:
|
|
written_graphs = json.load(f)
|
|
|
|
# The written file must be exactly what build_mappings returned
|
|
self.assertEqual(graphs, written_graphs)
|
|
# Especially: no extra top-level "dependencies" block is added
|
|
self.assertNotIn("dependencies", written_graphs)
|
|
|
|
def test_process_role_preview_calls_output_graph_and_does_not_write_file(self):
|
|
graphs = {
|
|
"graph_a": {"nodes": [{"id": "myrole"}], "links": []},
|
|
"graph_b": {"nodes": [], "links": []},
|
|
}
|
|
|
|
with patch.object(tree_module, "build_mappings", return_value=graphs), patch.object(
|
|
tree_module, "output_graph"
|
|
) as mocked_output:
|
|
buf = StringIO()
|
|
with redirect_stdout(buf):
|
|
tree_module.process_role(
|
|
role_name="myrole",
|
|
roles_dir=self.roles_dir,
|
|
depth=0,
|
|
shadow_folder=self.shadow_dir,
|
|
output="json",
|
|
preview=True,
|
|
verbose=True,
|
|
no_include_role=False,
|
|
no_import_role=False,
|
|
no_dependencies=False,
|
|
no_run_after=False,
|
|
)
|
|
|
|
# output_graph must be called once per graph entry
|
|
self.assertEqual(mocked_output.call_count, len(graphs))
|
|
|
|
# In preview mode, no tree.json should be written
|
|
tree_file = os.path.join(self.shadow_dir, "myrole", "meta", "tree.json")
|
|
self.assertFalse(os.path.exists(tree_file))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|