|
1 | 1 | """Unit tests for scripts/diff_to_mermaid.py — diff logic + Mermaid rendering.""" |
2 | 2 |
|
| 3 | +import json |
3 | 4 | import re |
4 | 5 | import sys |
| 6 | +import tempfile |
5 | 7 | import unittest |
6 | 8 | from pathlib import Path |
| 9 | +from types import ModuleType, SimpleNamespace |
| 10 | +from unittest.mock import patch |
7 | 11 |
|
8 | 12 | sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts")) |
9 | 13 | import diff_to_mermaid as dm # noqa: E402 |
@@ -33,6 +37,159 @@ def linkstyle_indices_in_range(text): |
33 | 37 |
|
34 | 38 |
|
35 | 39 | class TestDiff(unittest.TestCase): |
| 40 | + def test_core_loader_projects_global_relation_into_rendered_mermaid(self): |
| 41 | + root_a = SimpleNamespace(component_id="1") |
| 42 | + root_b = SimpleNamespace(component_id="2") |
| 43 | + child_a = SimpleNamespace(component_id="1.1") |
| 44 | + child_b = SimpleNamespace(component_id="2.1") |
| 45 | + |
| 46 | + def parse_unified_analysis(data): |
| 47 | + relations = [SimpleNamespace(**relation) for relation in data.get("components_relations", [])] |
| 48 | + root = SimpleNamespace(components=[root_a, root_b], components_relations=relations) |
| 49 | + return root, { |
| 50 | + "1": SimpleNamespace(components=[child_a]), |
| 51 | + "2": SimpleNamespace(components=[child_b]), |
| 52 | + } |
| 53 | + |
| 54 | + def build_id_to_name_map(_root, _subs): |
| 55 | + return {"1": "Root A", "2": "Root B", "1.1": "Child A", "2.1": "Child B"} |
| 56 | + |
| 57 | + def project_relations_to_level(relations, level_ids, id_to_name): |
| 58 | + projected = [] |
| 59 | + for relation in relations: |
| 60 | + src_id = next( |
| 61 | + (cid for cid in level_ids if relation.src_id == cid or relation.src_id.startswith(cid + ".")), None |
| 62 | + ) |
| 63 | + dst_id = next( |
| 64 | + (cid for cid in level_ids if relation.dst_id == cid or relation.dst_id.startswith(cid + ".")), None |
| 65 | + ) |
| 66 | + if src_id is None or dst_id is None or src_id == dst_id: |
| 67 | + continue |
| 68 | + projected.append( |
| 69 | + SimpleNamespace( |
| 70 | + relation=relation.relation, |
| 71 | + src_name=id_to_name[src_id], |
| 72 | + dst_name=id_to_name[dst_id], |
| 73 | + src_id=src_id, |
| 74 | + dst_id=dst_id, |
| 75 | + ) |
| 76 | + ) |
| 77 | + return projected |
| 78 | + |
| 79 | + rendering = ModuleType("codeboarding_workflows.rendering") |
| 80 | + rendering.project_relations_to_level = project_relations_to_level |
| 81 | + analysis_json = ModuleType("diagram_analysis.analysis_json") |
| 82 | + analysis_json.parse_unified_analysis = parse_unified_analysis |
| 83 | + analysis_json.build_id_to_name_map = build_id_to_name_map |
| 84 | + modules = { |
| 85 | + "codeboarding_workflows": ModuleType("codeboarding_workflows"), |
| 86 | + "codeboarding_workflows.rendering": rendering, |
| 87 | + "diagram_analysis": ModuleType("diagram_analysis"), |
| 88 | + "diagram_analysis.analysis_json": analysis_json, |
| 89 | + } |
| 90 | + analysis = { |
| 91 | + "components": [ |
| 92 | + {"name": "Root A", "component_id": "1", "components": [{"name": "Child A", "component_id": "1.1"}]}, |
| 93 | + {"name": "Root B", "component_id": "2", "components": [{"name": "Child B", "component_id": "2.1"}]}, |
| 94 | + ], |
| 95 | + "components_relations": [], |
| 96 | + } |
| 97 | + |
| 98 | + with tempfile.TemporaryDirectory() as tmp, patch.dict(sys.modules, modules): |
| 99 | + base_path = Path(tmp) / "base.json" |
| 100 | + head_path = Path(tmp) / "head.json" |
| 101 | + base_path.write_text(json.dumps(analysis)) |
| 102 | + head_path.write_text( |
| 103 | + json.dumps( |
| 104 | + { |
| 105 | + **analysis, |
| 106 | + "components_relations": [ |
| 107 | + { |
| 108 | + "relation": "calls", |
| 109 | + "src_name": "Child A", |
| 110 | + "dst_name": "Child B", |
| 111 | + "src_id": "1.1", |
| 112 | + "dst_id": "2.1", |
| 113 | + } |
| 114 | + ], |
| 115 | + } |
| 116 | + ) |
| 117 | + ) |
| 118 | + diff = dm.build_diff(dm.load_analysis(base_path), dm.load_analysis(head_path)) |
| 119 | + |
| 120 | + text, meta = dm.render_mermaid(diff) |
| 121 | + self.assertIn('n_Root_A -- "calls" --> n_Root_B', text) |
| 122 | + self.assertEqual(meta["n_edges"], 1) |
| 123 | + self.assertTrue(meta["changed"]) |
| 124 | + |
| 125 | + def test_projects_global_relations_at_every_component_level(self): |
| 126 | + child = SimpleNamespace(component_id="1.1") |
| 127 | + peer = SimpleNamespace(component_id="1.2") |
| 128 | + root_a = SimpleNamespace(component_id="1") |
| 129 | + root_b = SimpleNamespace(component_id="2") |
| 130 | + global_relation = SimpleNamespace( |
| 131 | + relation="calls", |
| 132 | + src_name="Child", |
| 133 | + dst_name="Root B", |
| 134 | + src_id="1.1", |
| 135 | + dst_id="2", |
| 136 | + ) |
| 137 | + sibling_relation = SimpleNamespace( |
| 138 | + relation="delegates", |
| 139 | + src_name="Child", |
| 140 | + dst_name="Peer", |
| 141 | + src_id="1.1", |
| 142 | + dst_id="1.2", |
| 143 | + ) |
| 144 | + root_analysis = SimpleNamespace( |
| 145 | + components=[root_a, root_b], components_relations=[global_relation, sibling_relation] |
| 146 | + ) |
| 147 | + sub_analysis = SimpleNamespace(components=[child, peer]) |
| 148 | + data = { |
| 149 | + "components": [ |
| 150 | + { |
| 151 | + "name": "Root A", |
| 152 | + "component_id": "1", |
| 153 | + "components": [ |
| 154 | + {"name": "Child", "component_id": "1.1"}, |
| 155 | + {"name": "Peer", "component_id": "1.2"}, |
| 156 | + ], |
| 157 | + "components_relations": [{"relation": "stale"}], |
| 158 | + }, |
| 159 | + {"name": "Root B", "component_id": "2"}, |
| 160 | + ], |
| 161 | + "components_relations": [], |
| 162 | + } |
| 163 | + |
| 164 | + def projector(relations, level_ids, _id_to_name): |
| 165 | + self.assertEqual(relations, [global_relation, sibling_relation]) |
| 166 | + if level_ids == {"1", "2"}: |
| 167 | + return [ |
| 168 | + SimpleNamespace( |
| 169 | + relation="calls", |
| 170 | + src_name="Root A", |
| 171 | + dst_name="Root B", |
| 172 | + src_id="1", |
| 173 | + dst_id="2", |
| 174 | + ) |
| 175 | + ] |
| 176 | + if level_ids == {"1.1", "1.2"}: |
| 177 | + return [sibling_relation] |
| 178 | + self.fail(f"unexpected component level: {level_ids}") |
| 179 | + |
| 180 | + projected = dm._project_analysis( |
| 181 | + data, |
| 182 | + root_analysis, |
| 183 | + {"1": sub_analysis}, |
| 184 | + {"1": "Root A", "2": "Root B", "1.1": "Child", "1.2": "Peer"}, |
| 185 | + projector, |
| 186 | + ) |
| 187 | + |
| 188 | + self.assertEqual(projected["components_relations"][0]["src_id"], "1") |
| 189 | + nested_relations = projected["components"][0]["components_relations"] |
| 190 | + self.assertEqual(nested_relations[0]["src_id"], "1.1") |
| 191 | + self.assertEqual(nested_relations[0]["dst_id"], "1.2") |
| 192 | + |
36 | 193 | def test_added_modified_deleted_unchanged(self): |
37 | 194 | base = {"components": [comp("A", {"a.py": ["f"]}), comp("B"), comp("D")], "components_relations": []} |
38 | 195 | head = {"components": [comp("A", {"a.py": ["f", "g"]}), comp("B"), comp("C")], "components_relations": []} |
|
0 commit comments