Navegador

navegador / tests / test_analysis.py
Source Blame History 803 lines
1ceb8b0… lmata 1 # Copyright CONFLICT LLC 2026 (weareconflict.com)
1ceb8b0… lmata 2 """Tests for navegador.analysis — structural analysis tools."""
1ceb8b0… lmata 3
1ceb8b0… lmata 4 import json
1ceb8b0… lmata 5 from unittest.mock import MagicMock, patch
1ceb8b0… lmata 6
1ceb8b0… lmata 7 from click.testing import CliRunner
1ceb8b0… lmata 8
1ceb8b0… lmata 9 from navegador.cli.commands import main
1ceb8b0… lmata 10
1ceb8b0… lmata 11 # ── Shared helpers ─────────────────────────────────────────────────────────────
1ceb8b0… lmata 12
1ceb8b0… lmata 13
1ceb8b0… lmata 14 def _mock_store(result_set=None):
1ceb8b0… lmata 15 """Return a mock GraphStore whose .query() returns the given result_set."""
1ceb8b0… lmata 16 store = MagicMock()
1ceb8b0… lmata 17 result = MagicMock()
1ceb8b0… lmata 18 result.result_set = result_set or []
1ceb8b0… lmata 19 store.query.return_value = result
1ceb8b0… lmata 20 return store
1ceb8b0… lmata 21
1ceb8b0… lmata 22
1ceb8b0… lmata 23 def _multi_mock_store(*result_sets):
1ceb8b0… lmata 24 """
1ceb8b0… lmata 25 Return a mock GraphStore whose .query() returns successive result_sets.
1ceb8b0… lmata 26 Each call to .query() gets the next item from the list.
1ceb8b0… lmata 27 """
1ceb8b0… lmata 28 store = MagicMock()
1ceb8b0… lmata 29 results = []
1ceb8b0… lmata 30 for rs in result_sets:
1ceb8b0… lmata 31 r = MagicMock()
1ceb8b0… lmata 32 r.result_set = rs
1ceb8b0… lmata 33 results.append(r)
1ceb8b0… lmata 34 store.query.side_effect = results
1ceb8b0… lmata 35 return store
1ceb8b0… lmata 36
1ceb8b0… lmata 37
1ceb8b0… lmata 38 # ── #3: ImpactAnalyzer ────────────────────────────────────────────────────────
1ceb8b0… lmata 39
1ceb8b0… lmata 40
1ceb8b0… lmata 41 class TestImpactAnalyzer:
1ceb8b0… lmata 42 def test_returns_impact_result_structure(self):
1ceb8b0… lmata 43 from navegador.analysis.impact import ImpactAnalyzer, ImpactResult
1ceb8b0… lmata 44
1ceb8b0… lmata 45 store = _multi_mock_store(
1ceb8b0… lmata 46 # blast radius query
1ceb8b0… lmata 47 [
1ceb8b0… lmata 48 ["Function", "callee_a", "src/a.py", 10],
1ceb8b0… lmata 49 ["Class", "ClassB", "src/b.py", 20],
1ceb8b0… lmata 50 ],
1ceb8b0… lmata 51 # knowledge query
1ceb8b0… lmata 52 [],
1ceb8b0… lmata 53 )
1ceb8b0… lmata 54 analyzer = ImpactAnalyzer(store)
1ceb8b0… lmata 55 result = analyzer.blast_radius("my_func")
1ceb8b0… lmata 56
1ceb8b0… lmata 57 assert isinstance(result, ImpactResult)
1ceb8b0… lmata 58 assert result.name == "my_func"
1ceb8b0… lmata 59 assert result.depth == 3
1ceb8b0… lmata 60 assert len(result.affected_nodes) == 2
1ceb8b0… lmata 61 assert "src/a.py" in result.affected_files
1ceb8b0… lmata 62 assert "src/b.py" in result.affected_files
1ceb8b0… lmata 63
1ceb8b0… lmata 64 def test_affected_nodes_have_correct_keys(self):
1ceb8b0… lmata 65 from navegador.analysis.impact import ImpactAnalyzer
1ceb8b0… lmata 66
1ceb8b0… lmata 67 store = _multi_mock_store(
1ceb8b0… lmata 68 [["Function", "do_thing", "utils.py", 5]],
1ceb8b0… lmata 69 [],
1ceb8b0… lmata 70 )
1ceb8b0… lmata 71 result = ImpactAnalyzer(store).blast_radius("entry")
1ceb8b0… lmata 72 node = result.affected_nodes[0]
1ceb8b0… lmata 73 assert "type" in node
1ceb8b0… lmata 74 assert "name" in node
1ceb8b0… lmata 75 assert "file_path" in node
1ceb8b0… lmata 76 assert "line_start" in node
1ceb8b0… lmata 77
1ceb8b0… lmata 78 def test_empty_graph_returns_empty_result(self):
1ceb8b0… lmata 79 from navegador.analysis.impact import ImpactAnalyzer
1ceb8b0… lmata 80
1ceb8b0… lmata 81 store = _multi_mock_store([], [])
1ceb8b0… lmata 82 result = ImpactAnalyzer(store).blast_radius("nothing")
1ceb8b0… lmata 83
1ceb8b0… lmata 84 assert result.affected_nodes == []
1ceb8b0… lmata 85 assert result.affected_files == []
1ceb8b0… lmata 86 assert result.affected_knowledge == []
1ceb8b0… lmata 87 assert result.depth_reached == 0
1ceb8b0… lmata 88
1ceb8b0… lmata 89 def test_with_file_path_narrowing(self):
1ceb8b0… lmata 90 from navegador.analysis.impact import ImpactAnalyzer
1ceb8b0… lmata 91
1ceb8b0… lmata 92 store = _multi_mock_store([], [])
1ceb8b0… lmata 93 result = ImpactAnalyzer(store).blast_radius("func", file_path="src/auth.py", depth=2)
1ceb8b0… lmata 94
1ceb8b0… lmata 95 assert result.file_path == "src/auth.py"
1ceb8b0… lmata 96 assert result.depth == 2
1ceb8b0… lmata 97
1ceb8b0… lmata 98 def test_knowledge_layer_populated(self):
1ceb8b0… lmata 99 from navegador.analysis.impact import ImpactAnalyzer
1ceb8b0… lmata 100
1ceb8b0… lmata 101 store = _multi_mock_store(
1ceb8b0… lmata 102 [["Function", "impl", "src/impl.py", 1]],
1ceb8b0… lmata 103 [["Concept", "AuthToken"]],
1ceb8b0… lmata 104 )
1ceb8b0… lmata 105 result = ImpactAnalyzer(store).blast_radius("validate")
1ceb8b0… lmata 106 assert len(result.affected_knowledge) == 1
1ceb8b0… lmata 107 assert result.affected_knowledge[0]["name"] == "AuthToken"
1ceb8b0… lmata 108
1ceb8b0… lmata 109 def test_to_dict_keys(self):
1ceb8b0… lmata 110 from navegador.analysis.impact import ImpactAnalyzer
1ceb8b0… lmata 111
1ceb8b0… lmata 112 store = _multi_mock_store([], [])
1ceb8b0… lmata 113 d = ImpactAnalyzer(store).blast_radius("fn").to_dict()
1ceb8b0… lmata 114 for key in ("name", "file_path", "depth", "depth_reached",
1ceb8b0… lmata 115 "affected_nodes", "affected_files", "affected_knowledge"):
1ceb8b0… lmata 116 assert key in d
1ceb8b0… lmata 117
1ceb8b0… lmata 118 def test_query_exception_returns_empty(self):
1ceb8b0… lmata 119 from navegador.analysis.impact import ImpactAnalyzer
1ceb8b0… lmata 120
1ceb8b0… lmata 121 store = MagicMock()
1ceb8b0… lmata 122 store.query.side_effect = RuntimeError("db error")
1ceb8b0… lmata 123 result = ImpactAnalyzer(store).blast_radius("x")
1ceb8b0… lmata 124 assert result.affected_nodes == []
1ceb8b0… lmata 125
1ceb8b0… lmata 126 def test_affected_files_sorted(self):
1ceb8b0… lmata 127 from navegador.analysis.impact import ImpactAnalyzer
1ceb8b0… lmata 128
1ceb8b0… lmata 129 store = _multi_mock_store(
1ceb8b0… lmata 130 [
1ceb8b0… lmata 131 ["Function", "b", "zzz.py", 1],
1ceb8b0… lmata 132 ["Function", "a", "aaa.py", 2],
1ceb8b0… lmata 133 ],
1ceb8b0… lmata 134 [],
1ceb8b0… lmata 135 )
1ceb8b0… lmata 136 result = ImpactAnalyzer(store).blast_radius("root")
1ceb8b0… lmata 137 assert result.affected_files == ["aaa.py", "zzz.py"]
1ceb8b0… lmata 138
1ceb8b0… lmata 139
1ceb8b0… lmata 140 # ── #4: FlowTracer ────────────────────────────────────────────────────────────
1ceb8b0… lmata 141
1ceb8b0… lmata 142
1ceb8b0… lmata 143 class TestFlowTracer:
1ceb8b0… lmata 144 def test_returns_list_of_call_chains(self):
1ceb8b0… lmata 145 from navegador.analysis.flow import CallChain, FlowTracer
1ceb8b0… lmata 146
1ceb8b0… lmata 147 # entry resolve → one result; CALLS query → one callee; next CALLS → empty
1ceb8b0… lmata 148 store = _multi_mock_store(
1ceb8b0… lmata 149 [["entry", "src/main.py"]], # _RESOLVE_ENTRY
1ceb8b0… lmata 150 [["entry", "helper", "src/util.py"]], # _CALLS_FROM (depth 0)
1ceb8b0… lmata 151 [], # _CALLS_FROM (depth 1, no more)
1ceb8b0… lmata 152 )
1ceb8b0… lmata 153 tracer = FlowTracer(store)
1ceb8b0… lmata 154 chains = tracer.trace("entry")
1ceb8b0… lmata 155
1ceb8b0… lmata 156 assert isinstance(chains, list)
1ceb8b0… lmata 157 # At least one chain should have been produced
1ceb8b0… lmata 158 assert len(chains) >= 1
1ceb8b0… lmata 159 assert all(isinstance(c, CallChain) for c in chains)
1ceb8b0… lmata 160
1ceb8b0… lmata 161 def test_entry_not_found_returns_empty(self):
1ceb8b0… lmata 162 from navegador.analysis.flow import FlowTracer
1ceb8b0… lmata 163
1ceb8b0… lmata 164 store = _mock_store(result_set=[])
1ceb8b0… lmata 165 chains = FlowTracer(store).trace("nonexistent")
1ceb8b0… lmata 166 assert chains == []
1ceb8b0… lmata 167
1ceb8b0… lmata 168 def test_call_chain_to_list_format(self):
1ceb8b0… lmata 169 from navegador.analysis.flow import CallChain
1ceb8b0… lmata 170
1ceb8b0… lmata 171 chain = CallChain(steps=[("a", "b", "src/b.py"), ("b", "c", "src/c.py")])
1ceb8b0… lmata 172 lst = chain.to_list()
1ceb8b0… lmata 173 assert lst[0] == {"caller": "a", "callee": "b", "file_path": "src/b.py"}
1ceb8b0… lmata 174 assert lst[1] == {"caller": "b", "callee": "c", "file_path": "src/c.py"}
1ceb8b0… lmata 175
1ceb8b0… lmata 176 def test_empty_chain_length(self):
1ceb8b0… lmata 177 from navegador.analysis.flow import CallChain
1ceb8b0… lmata 178
1ceb8b0… lmata 179 chain = CallChain(steps=[])
1ceb8b0… lmata 180 assert len(chain) == 0
1ceb8b0… lmata 181
1ceb8b0… lmata 182 def test_chain_length(self):
1ceb8b0… lmata 183 from navegador.analysis.flow import CallChain
1ceb8b0… lmata 184
1ceb8b0… lmata 185 chain = CallChain(steps=[("a", "b", ""), ("b", "c", "")])
1ceb8b0… lmata 186 assert len(chain) == 2
1ceb8b0… lmata 187
1ceb8b0… lmata 188 def test_max_depth_respected(self):
1ceb8b0… lmata 189 """With max_depth=1 the tracer should not go beyond one level."""
1ceb8b0… lmata 190 from navegador.analysis.flow import FlowTracer
1ceb8b0… lmata 191
1ceb8b0… lmata 192 store = _multi_mock_store(
1ceb8b0… lmata 193 [["entry", ""]], # _RESOLVE_ENTRY
1ceb8b0… lmata 194 [["entry", "level1", "a.py"]], # depth 0 CALLS
1ceb8b0… lmata 195 # No further calls needed since max_depth=1
1ceb8b0… lmata 196 )
1ceb8b0… lmata 197 chains = FlowTracer(store).trace("entry", max_depth=1)
1ceb8b0… lmata 198 # All chains should have at most 1 step
1ceb8b0… lmata 199 for chain in chains:
1ceb8b0… lmata 200 assert len(chain) <= 1
1ceb8b0… lmata 201
1ceb8b0… lmata 202 def test_cycle_does_not_loop_forever(self):
1ceb8b0… lmata 203 """A cycle (a→b→a) should not produce an infinite loop."""
1ceb8b0… lmata 204 from navegador.analysis.flow import FlowTracer
1ceb8b0… lmata 205
1ceb8b0… lmata 206 call_results = [
1ceb8b0… lmata 207 [["entry", ""]], # resolve entry
1ceb8b0… lmata 208 [["entry", "entry", "src.py"]], # entry calls itself (cycle)
1ceb8b0… lmata 209 ]
1ceb8b0… lmata 210 store = MagicMock()
1ceb8b0… lmata 211 results = []
1ceb8b0… lmata 212 for rs in call_results:
1ceb8b0… lmata 213 r = MagicMock()
1ceb8b0… lmata 214 r.result_set = rs
1ceb8b0… lmata 215 results.append(r)
1ceb8b0… lmata 216 store.query.side_effect = results + [MagicMock(result_set=[])] * 20
1ceb8b0… lmata 217
1ceb8b0… lmata 218 chains = FlowTracer(store).trace("entry", max_depth=5)
1ceb8b0… lmata 219 # Must terminate and return something (or empty)
1ceb8b0… lmata 220 assert isinstance(chains, list)
1ceb8b0… lmata 221
1ceb8b0… lmata 222 def test_no_calls_from_entry(self):
1ceb8b0… lmata 223 """Entry exists but calls nothing — should return empty chains list."""
1ceb8b0… lmata 224 from navegador.analysis.flow import FlowTracer
1ceb8b0… lmata 225
1ceb8b0… lmata 226 store = _multi_mock_store(
1ceb8b0… lmata 227 [["entry", "src/main.py"]], # resolve entry
1ceb8b0… lmata 228 [], # no CALLS edges
1ceb8b0… lmata 229 )
1ceb8b0… lmata 230 chains = FlowTracer(store).trace("entry")
1ceb8b0… lmata 231 assert chains == []
1ceb8b0… lmata 232
1ceb8b0… lmata 233
1ceb8b0… lmata 234 # ── #35: DeadCodeDetector ─────────────────────────────────────────────────────
1ceb8b0… lmata 235
1ceb8b0… lmata 236
1ceb8b0… lmata 237 class TestDeadCodeDetector:
1ceb8b0… lmata 238 def test_returns_dead_code_report(self):
1ceb8b0… lmata 239 from navegador.analysis.deadcode import DeadCodeDetector, DeadCodeReport
1ceb8b0… lmata 240
1ceb8b0… lmata 241 store = _multi_mock_store(
1ceb8b0… lmata 242 [["Function", "orphan_fn", "src/util.py", 5]], # dead functions
1ceb8b0… lmata 243 [["UnusedClass", "src/models.py", 10]], # dead classes
1ceb8b0… lmata 244 [["src/unused.py"]], # orphan files
1ceb8b0… lmata 245 )
1ceb8b0… lmata 246 report = DeadCodeDetector(store).detect()
1ceb8b0… lmata 247 assert isinstance(report, DeadCodeReport)
1ceb8b0… lmata 248 assert len(report.unreachable_functions) == 1
1ceb8b0… lmata 249 assert len(report.unreachable_classes) == 1
1ceb8b0… lmata 250 assert len(report.orphan_files) == 1
1ceb8b0… lmata 251
1ceb8b0… lmata 252 def test_empty_graph_all_empty(self):
1ceb8b0… lmata 253 from navegador.analysis.deadcode import DeadCodeDetector
1ceb8b0… lmata 254
1ceb8b0… lmata 255 store = _multi_mock_store([], [], [])
1ceb8b0… lmata 256 report = DeadCodeDetector(store).detect()
1ceb8b0… lmata 257 assert report.unreachable_functions == []
1ceb8b0… lmata 258 assert report.unreachable_classes == []
1ceb8b0… lmata 259 assert report.orphan_files == []
1ceb8b0… lmata 260
1ceb8b0… lmata 261 def test_to_dict_contains_summary(self):
1ceb8b0… lmata 262 from navegador.analysis.deadcode import DeadCodeDetector
1ceb8b0… lmata 263
1ceb8b0… lmata 264 store = _multi_mock_store(
1ceb8b0… lmata 265 [["Function", "dead_fn", "a.py", 1]],
1ceb8b0… lmata 266 [],
1ceb8b0… lmata 267 [],
1ceb8b0… lmata 268 )
1ceb8b0… lmata 269 d = DeadCodeDetector(store).detect().to_dict()
1ceb8b0… lmata 270 assert "summary" in d
1ceb8b0… lmata 271 assert d["summary"]["unreachable_functions"] == 1
1ceb8b0… lmata 272 assert d["summary"]["unreachable_classes"] == 0
1ceb8b0… lmata 273 assert d["summary"]["orphan_files"] == 0
1ceb8b0… lmata 274
1ceb8b0… lmata 275 def test_function_node_structure(self):
1ceb8b0… lmata 276 from navegador.analysis.deadcode import DeadCodeDetector
1ceb8b0… lmata 277
1ceb8b0… lmata 278 store = _multi_mock_store(
1ceb8b0… lmata 279 [["Method", "stale_method", "service.py", 88]],
1ceb8b0… lmata 280 [],
1ceb8b0… lmata 281 [],
1ceb8b0… lmata 282 )
1ceb8b0… lmata 283 report = DeadCodeDetector(store).detect()
1ceb8b0… lmata 284 fn = report.unreachable_functions[0]
1ceb8b0… lmata 285 assert fn["type"] == "Method"
1ceb8b0… lmata 286 assert fn["name"] == "stale_method"
1ceb8b0… lmata 287 assert fn["file_path"] == "service.py"
1ceb8b0… lmata 288 assert fn["line_start"] == 88
1ceb8b0… lmata 289
1ceb8b0… lmata 290 def test_class_node_structure(self):
1ceb8b0… lmata 291 from navegador.analysis.deadcode import DeadCodeDetector
1ceb8b0… lmata 292
1ceb8b0… lmata 293 store = _multi_mock_store(
1ceb8b0… lmata 294 [],
1ceb8b0… lmata 295 [["LegacyWidget", "widgets.py", 20]],
1ceb8b0… lmata 296 [],
1ceb8b0… lmata 297 )
1ceb8b0… lmata 298 report = DeadCodeDetector(store).detect()
1ceb8b0… lmata 299 cls = report.unreachable_classes[0]
1ceb8b0… lmata 300 assert cls["name"] == "LegacyWidget"
1ceb8b0… lmata 301 assert cls["file_path"] == "widgets.py"
1ceb8b0… lmata 302
1ceb8b0… lmata 303 def test_orphan_files_as_strings(self):
1ceb8b0… lmata 304 from navegador.analysis.deadcode import DeadCodeDetector
1ceb8b0… lmata 305
1ceb8b0… lmata 306 store = _multi_mock_store(
1ceb8b0… lmata 307 [],
1ceb8b0… lmata 308 [],
1ceb8b0… lmata 309 [["legacy/old.py"], ["legacy/dead.py"]],
1ceb8b0… lmata 310 )
1ceb8b0… lmata 311 report = DeadCodeDetector(store).detect()
1ceb8b0… lmata 312 assert "legacy/old.py" in report.orphan_files
1ceb8b0… lmata 313 assert "legacy/dead.py" in report.orphan_files
1ceb8b0… lmata 314
1ceb8b0… lmata 315 def test_query_exception_returns_empty_report(self):
1ceb8b0… lmata 316 from navegador.analysis.deadcode import DeadCodeDetector
1ceb8b0… lmata 317
1ceb8b0… lmata 318 store = MagicMock()
1ceb8b0… lmata 319 store.query.side_effect = RuntimeError("db down")
1ceb8b0… lmata 320 report = DeadCodeDetector(store).detect()
1ceb8b0… lmata 321 assert report.unreachable_functions == []
1ceb8b0… lmata 322 assert report.unreachable_classes == []
1ceb8b0… lmata 323 assert report.orphan_files == []
1ceb8b0… lmata 324
1ceb8b0… lmata 325 def test_multiple_dead_functions(self):
1ceb8b0… lmata 326 from navegador.analysis.deadcode import DeadCodeDetector
1ceb8b0… lmata 327
1ceb8b0… lmata 328 store = _multi_mock_store(
1ceb8b0… lmata 329 [
1ceb8b0… lmata 330 ["Function", "fn_a", "a.py", 1],
1ceb8b0… lmata 331 ["Function", "fn_b", "b.py", 2],
1ceb8b0… lmata 332 ["Method", "meth_c", "c.py", 3],
1ceb8b0… lmata 333 ],
1ceb8b0… lmata 334 [],
1ceb8b0… lmata 335 [],
1ceb8b0… lmata 336 )
1ceb8b0… lmata 337 report = DeadCodeDetector(store).detect()
1ceb8b0… lmata 338 assert len(report.unreachable_functions) == 3
1ceb8b0… lmata 339
1ceb8b0… lmata 340
1ceb8b0… lmata 341 # ── #36: TestMapper ───────────────────────────────────────────────────────────
1ceb8b0… lmata 342
1ceb8b0… lmata 343
1ceb8b0… lmata 344 class TestTestMapper:
1ceb8b0… lmata 345 def test_returns_test_map_result(self):
1ceb8b0… lmata 346 from navegador.analysis.testmap import TestMapper, TestMapResult
1ceb8b0… lmata 347
1ceb8b0… lmata 348 # Query calls: _TEST_FUNCTIONS_QUERY, then for each test:
1ceb8b0… lmata 349 # _CALLS_FROM_TEST, _CALLS_FROM_TEST (again for source detection), _CREATE_TESTS_EDGE
1ceb8b0… lmata 350 store = _multi_mock_store(
1ceb8b0… lmata 351 [["test_validate", "tests/test_auth.py", 10]], # test functions
1ceb8b0… lmata 352 [["Function", "validate", "auth.py"]], # CALLS_FROM_TEST
1ceb8b0… lmata 353 [["Function", "validate", "auth.py"]], # CALLS_FROM_TEST (source)
1ceb8b0… lmata 354 [], # CREATE_TESTS_EDGE
1ceb8b0… lmata 355 )
1ceb8b0… lmata 356 result = TestMapper(store).map_tests()
1ceb8b0… lmata 357 assert isinstance(result, TestMapResult)
1ceb8b0… lmata 358
1ceb8b0… lmata 359 def test_no_test_functions_returns_empty(self):
1ceb8b0… lmata 360 from navegador.analysis.testmap import TestMapper
1ceb8b0… lmata 361
1ceb8b0… lmata 362 store = _mock_store(result_set=[])
1ceb8b0… lmata 363 result = TestMapper(store).map_tests()
1ceb8b0… lmata 364 assert result.links == []
1ceb8b0… lmata 365 assert result.unmatched_tests == []
1ceb8b0… lmata 366 assert result.edges_created == 0
1ceb8b0… lmata 367
1ceb8b0… lmata 368 def test_link_via_calls_edge(self):
1ceb8b0… lmata 369 from navegador.analysis.testmap import TestMapper
1ceb8b0… lmata 370
1ceb8b0… lmata 371 store = _multi_mock_store(
1ceb8b0… lmata 372 [["test_process", "tests/test_core.py", 5]], # test functions
1ceb8b0… lmata 373 [["Function", "process", "core.py"]], # CALLS_FROM_TEST
1ceb8b0… lmata 374 [["Function", "process", "core.py"]], # CALLS_FROM_TEST (source)
1ceb8b0… lmata 375 [], # CREATE edge
1ceb8b0… lmata 376 )
1ceb8b0… lmata 377 result = TestMapper(store).map_tests()
1ceb8b0… lmata 378 assert len(result.links) == 1
1ceb8b0… lmata 379 link = result.links[0]
1ceb8b0… lmata 380 assert link.test_name == "test_process"
1ceb8b0… lmata 381 assert link.prod_name == "process"
1ceb8b0… lmata 382 assert link.prod_file == "core.py"
1ceb8b0… lmata 383
1ceb8b0… lmata 384 def test_link_via_heuristic(self):
1ceb8b0… lmata 385 """When no CALLS edge exists, fall back to name heuristic."""
1ceb8b0… lmata 386 from navegador.analysis.testmap import TestMapper
1ceb8b0… lmata 387
1ceb8b0… lmata 388 store = _multi_mock_store(
1ceb8b0… lmata 389 [["test_render_output", "tests/test_renderer.py", 1]], # test fns
1ceb8b0… lmata 390 [], # no CALLS
1ceb8b0… lmata 391 [["Function", "render_output", "renderer.py"]], # heuristic
1ceb8b0… lmata 392 [["Function", "render_output", "renderer.py"]], # verify calls
1ceb8b0… lmata 393 [], # CREATE edge
1ceb8b0… lmata 394 )
1ceb8b0… lmata 395 result = TestMapper(store).map_tests()
1ceb8b0… lmata 396 assert len(result.links) == 1
1ceb8b0… lmata 397 assert result.links[0].prod_name == "render_output"
1ceb8b0… lmata 398
1ceb8b0… lmata 399 def test_unmatched_test_recorded(self):
1ceb8b0… lmata 400 """A test with no call and no matching heuristic goes to unmatched."""
1ceb8b0… lmata 401 from navegador.analysis.testmap import TestMapper
1ceb8b0… lmata 402
1ceb8b0… lmata 403 # Test functions: one test. Then all queries return empty.
1ceb8b0… lmata 404 store = MagicMock()
1ceb8b0… lmata 405 results_iter = [
1ceb8b0… lmata 406 MagicMock(result_set=[["test_xyzzy", "tests/t.py", 1]]),
1ceb8b0… lmata 407 MagicMock(result_set=[]), # no CALLS
1ceb8b0… lmata 408 MagicMock(result_set=[]), # heuristic: test_xyzzy
1ceb8b0… lmata 409 MagicMock(result_set=[]), # heuristic: test_xyz (truncated)
1ceb8b0… lmata 410 MagicMock(result_set=[]), # heuristic: test_x
1ceb8b0… lmata 411 ] + [MagicMock(result_set=[])] * 10
1ceb8b0… lmata 412 store.query.side_effect = results_iter
1ceb8b0… lmata 413
1ceb8b0… lmata 414 result = TestMapper(store).map_tests()
1ceb8b0… lmata 415 assert len(result.unmatched_tests) == 1
1ceb8b0… lmata 416 assert result.unmatched_tests[0]["name"] == "test_xyzzy"
1ceb8b0… lmata 417
1ceb8b0… lmata 418 def test_to_dict_structure(self):
1ceb8b0… lmata 419 from navegador.analysis.testmap import TestMapper
1ceb8b0… lmata 420
1ceb8b0… lmata 421 store = _mock_store(result_set=[])
1ceb8b0… lmata 422 d = TestMapper(store).map_tests().to_dict()
1ceb8b0… lmata 423 for key in ("links", "unmatched_tests", "edges_created", "summary"):
1ceb8b0… lmata 424 assert key in d
1ceb8b0… lmata 425 assert "matched" in d["summary"]
1ceb8b0… lmata 426 assert "unmatched" in d["summary"]
1ceb8b0… lmata 427 assert "edges_created" in d["summary"]
1ceb8b0… lmata 428
1ceb8b0… lmata 429 def test_edges_created_count(self):
1ceb8b0… lmata 430 from navegador.analysis.testmap import TestMapper
1ceb8b0… lmata 431
1ceb8b0… lmata 432 store = _multi_mock_store(
1ceb8b0… lmata 433 [["test_foo", "tests/t.py", 1]], # test fns
1ceb8b0… lmata 434 [["Function", "foo", "app.py"]], # CALLS_FROM_TEST
1ceb8b0… lmata 435 [["Function", "foo", "app.py"]], # source verify
1ceb8b0… lmata 436 [], # CREATE edge (no error = success)
1ceb8b0… lmata 437 )
1ceb8b0… lmata 438 result = TestMapper(store).map_tests()
1ceb8b0… lmata 439 assert result.edges_created == 1
1ceb8b0… lmata 440
1ceb8b0… lmata 441
1ceb8b0… lmata 442 # ── #37: CycleDetector ────────────────────────────────────────────────────────
1ceb8b0… lmata 443
1ceb8b0… lmata 444
1ceb8b0… lmata 445 class TestCycleDetector:
1ceb8b0… lmata 446 def test_no_import_cycles(self):
1ceb8b0… lmata 447 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 448
1ceb8b0… lmata 449 # Linear imports: a → b → c, no cycle
1ceb8b0… lmata 450 store = _mock_store(
1ceb8b0… lmata 451 result_set=[
1ceb8b0… lmata 452 ["a", "a.py", "b", "b.py"],
1ceb8b0… lmata 453 ["b", "b.py", "c", "c.py"],
1ceb8b0… lmata 454 ]
1ceb8b0… lmata 455 )
1ceb8b0… lmata 456 cycles = CycleDetector(store).detect_import_cycles()
1ceb8b0… lmata 457 assert cycles == []
1ceb8b0… lmata 458
1ceb8b0… lmata 459 def test_detects_simple_import_cycle(self):
1ceb8b0… lmata 460 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 461
1ceb8b0… lmata 462 # a → b → a (cycle)
1ceb8b0… lmata 463 store = _mock_store(
1ceb8b0… lmata 464 result_set=[
1ceb8b0… lmata 465 ["a", "a.py", "b", "b.py"],
1ceb8b0… lmata 466 ["b", "b.py", "a", "a.py"],
1ceb8b0… lmata 467 ]
1ceb8b0… lmata 468 )
1ceb8b0… lmata 469 cycles = CycleDetector(store).detect_import_cycles()
1ceb8b0… lmata 470 assert len(cycles) == 1
1ceb8b0… lmata 471 cycle = cycles[0]
1ceb8b0… lmata 472 assert "a.py" in cycle
1ceb8b0… lmata 473 assert "b.py" in cycle
1ceb8b0… lmata 474
1ceb8b0… lmata 475 def test_detects_three_node_cycle(self):
1ceb8b0… lmata 476 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 477
1ceb8b0… lmata 478 store = _mock_store(
1ceb8b0… lmata 479 result_set=[
1ceb8b0… lmata 480 ["a", "a.py", "b", "b.py"],
1ceb8b0… lmata 481 ["b", "b.py", "c", "c.py"],
1ceb8b0… lmata 482 ["c", "c.py", "a", "a.py"],
1ceb8b0… lmata 483 ]
1ceb8b0… lmata 484 )
1ceb8b0… lmata 485 cycles = CycleDetector(store).detect_import_cycles()
1ceb8b0… lmata 486 assert len(cycles) >= 1
1ceb8b0… lmata 487 cycle = cycles[0]
1ceb8b0… lmata 488 assert len(cycle) == 3
1ceb8b0… lmata 489
1ceb8b0… lmata 490 def test_no_call_cycles(self):
1ceb8b0… lmata 491 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 492
1ceb8b0… lmata 493 store = _mock_store(
1ceb8b0… lmata 494 result_set=[
1ceb8b0… lmata 495 ["fn_a", "fn_b"],
1ceb8b0… lmata 496 ["fn_b", "fn_c"],
1ceb8b0… lmata 497 ]
1ceb8b0… lmata 498 )
1ceb8b0… lmata 499 cycles = CycleDetector(store).detect_call_cycles()
1ceb8b0… lmata 500 assert cycles == []
1ceb8b0… lmata 501
1ceb8b0… lmata 502 def test_detects_call_cycle(self):
1ceb8b0… lmata 503 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 504
1ceb8b0… lmata 505 # fn_a → fn_b → fn_a
1ceb8b0… lmata 506 store = _mock_store(
1ceb8b0… lmata 507 result_set=[
1ceb8b0… lmata 508 ["fn_a", "fn_b"],
1ceb8b0… lmata 509 ["fn_b", "fn_a"],
1ceb8b0… lmata 510 ]
1ceb8b0… lmata 511 )
1ceb8b0… lmata 512 cycles = CycleDetector(store).detect_call_cycles()
1ceb8b0… lmata 513 assert len(cycles) == 1
1ceb8b0… lmata 514 assert "fn_a" in cycles[0]
1ceb8b0… lmata 515 assert "fn_b" in cycles[0]
1ceb8b0… lmata 516
1ceb8b0… lmata 517 def test_empty_graph_no_cycles(self):
1ceb8b0… lmata 518 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 519
1ceb8b0… lmata 520 store = _mock_store(result_set=[])
1ceb8b0… lmata 521 assert CycleDetector(store).detect_import_cycles() == []
1ceb8b0… lmata 522 assert CycleDetector(store).detect_call_cycles() == []
1ceb8b0… lmata 523
1ceb8b0… lmata 524 def test_self_loop_not_included(self):
1ceb8b0… lmata 525 """A self-loop (a → a) should be skipped by the adjacency builder."""
1ceb8b0… lmata 526 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 527
1ceb8b0… lmata 528 store = _mock_store(result_set=[["a", "a.py", "a", "a.py"]])
1ceb8b0… lmata 529 cycles = CycleDetector(store).detect_import_cycles()
1ceb8b0… lmata 530 # Self-loops filtered out in _build_import_adjacency
1ceb8b0… lmata 531 assert cycles == []
1ceb8b0… lmata 532
1ceb8b0… lmata 533 def test_cycle_normalised_no_duplicates(self):
1ceb8b0… lmata 534 """The same cycle reported from different start points should appear once."""
1ceb8b0… lmata 535 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 536
1ceb8b0… lmata 537 store = _mock_store(
1ceb8b0… lmata 538 result_set=[
1ceb8b0… lmata 539 ["fn_b", "fn_a"],
1ceb8b0… lmata 540 ["fn_a", "fn_b"],
1ceb8b0… lmata 541 ]
1ceb8b0… lmata 542 )
1ceb8b0… lmata 543 cycles = CycleDetector(store).detect_call_cycles()
1ceb8b0… lmata 544 assert len(cycles) == 1
1ceb8b0… lmata 545
1ceb8b0… lmata 546 def test_query_exception_returns_empty(self):
1ceb8b0… lmata 547 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 548
1ceb8b0… lmata 549 store = MagicMock()
1ceb8b0… lmata 550 store.query.side_effect = RuntimeError("connection refused")
1ceb8b0… lmata 551 assert CycleDetector(store).detect_import_cycles() == []
1ceb8b0… lmata 552 assert CycleDetector(store).detect_call_cycles() == []
1ceb8b0… lmata 553
1ceb8b0… lmata 554 def test_multiple_independent_cycles(self):
1ceb8b0… lmata 555 """Two independent cycles (a↔b and c↔d) should both be found."""
1ceb8b0… lmata 556 from navegador.analysis.cycles import CycleDetector
1ceb8b0… lmata 557
1ceb8b0… lmata 558 store = _mock_store(
1ceb8b0… lmata 559 result_set=[
1ceb8b0… lmata 560 ["fn_a", "fn_b"],
1ceb8b0… lmata 561 ["fn_b", "fn_a"],
1ceb8b0… lmata 562 ["fn_c", "fn_d"],
1ceb8b0… lmata 563 ["fn_d", "fn_c"],
1ceb8b0… lmata 564 ]
1ceb8b0… lmata 565 )
1ceb8b0… lmata 566 cycles = CycleDetector(store).detect_call_cycles()
1ceb8b0… lmata 567 assert len(cycles) == 2
1ceb8b0… lmata 568
1ceb8b0… lmata 569
1ceb8b0… lmata 570 # ── CLI command tests ──────────────────────────────────────────────────────────
1ceb8b0… lmata 571
1ceb8b0… lmata 572
1ceb8b0… lmata 573 class TestImpactCLI:
1ceb8b0… lmata 574 def _make_result(self):
1ceb8b0… lmata 575 from navegador.analysis.impact import ImpactResult
1ceb8b0… lmata 576 return ImpactResult(
1ceb8b0… lmata 577 name="fn",
1ceb8b0… lmata 578 file_path="",
1ceb8b0… lmata 579 depth=3,
1ceb8b0… lmata 580 affected_nodes=[
1ceb8b0… lmata 581 {"type": "Function", "name": "callee", "file_path": "b.py", "line_start": 5}
1ceb8b0… lmata 582 ],
1ceb8b0… lmata 583 affected_files=["b.py"],
1ceb8b0… lmata 584 affected_knowledge=[],
1ceb8b0… lmata 585 depth_reached=3,
1ceb8b0… lmata 586 )
1ceb8b0… lmata 587
1ceb8b0… lmata 588 _BR_PATH = "navegador.analysis.impact.ImpactAnalyzer.blast_radius"
1ceb8b0… lmata 589
1ceb8b0… lmata 590 def test_impact_json_output(self):
1ceb8b0… lmata 591 runner = CliRunner()
1ceb8b0… lmata 592 mock_result = self._make_result()
1ceb8b0… lmata 593 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 594 patch(self._BR_PATH, return_value=mock_result):
1ceb8b0… lmata 595 result = runner.invoke(main, ["impact", "fn", "--json"])
1ceb8b0… lmata 596 assert result.exit_code == 0
1ceb8b0… lmata 597 data = json.loads(result.output)
1ceb8b0… lmata 598 assert data["name"] == "fn"
1ceb8b0… lmata 599 assert len(data["affected_nodes"]) == 1
1ceb8b0… lmata 600
1ceb8b0… lmata 601 def test_impact_markdown_output(self):
1ceb8b0… lmata 602 runner = CliRunner()
1ceb8b0… lmata 603 mock_result = self._make_result()
1ceb8b0… lmata 604 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 605 patch(self._BR_PATH, return_value=mock_result):
1ceb8b0… lmata 606 result = runner.invoke(main, ["impact", "fn"])
1ceb8b0… lmata 607 assert result.exit_code == 0
1ceb8b0… lmata 608 assert "Blast radius" in result.output
1ceb8b0… lmata 609
1ceb8b0… lmata 610 def test_impact_no_affected_nodes(self):
1ceb8b0… lmata 611 from navegador.analysis.impact import ImpactResult
1ceb8b0… lmata 612 runner = CliRunner()
1ceb8b0… lmata 613 empty_result = ImpactResult(name="x", file_path="", depth=3)
1ceb8b0… lmata 614 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 615 patch(self._BR_PATH, return_value=empty_result):
1ceb8b0… lmata 616 result = runner.invoke(main, ["impact", "x"])
1ceb8b0… lmata 617 assert result.exit_code == 0
1ceb8b0… lmata 618 assert "No affected nodes" in result.output
1ceb8b0… lmata 619
1ceb8b0… lmata 620 def test_impact_depth_option(self):
1ceb8b0… lmata 621 from navegador.analysis.impact import ImpactResult
1ceb8b0… lmata 622 runner = CliRunner()
1ceb8b0… lmata 623 empty_result = ImpactResult(name="x", file_path="", depth=5)
1ceb8b0… lmata 624 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 625 patch(self._BR_PATH, return_value=empty_result) as mock_br:
1ceb8b0… lmata 626 result = runner.invoke(main, ["impact", "x", "--depth", "5"])
1ceb8b0… lmata 627 assert result.exit_code == 0
1ceb8b0… lmata 628 mock_br.assert_called_once()
1ceb8b0… lmata 629 call_kwargs = mock_br.call_args
1ceb8b0… lmata 630 assert call_kwargs[1]["depth"] == 5 or call_kwargs[0][1] == 5
1ceb8b0… lmata 631
1ceb8b0… lmata 632
1ceb8b0… lmata 633 class TestTraceCLI:
1ceb8b0… lmata 634 def test_trace_json_output(self):
1ceb8b0… lmata 635 from navegador.analysis.flow import CallChain
1ceb8b0… lmata 636 runner = CliRunner()
1ceb8b0… lmata 637 chains = [CallChain(steps=[("a", "b", "b.py")])]
1ceb8b0… lmata 638 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 639 patch("navegador.analysis.flow.FlowTracer.trace", return_value=chains):
1ceb8b0… lmata 640 result = runner.invoke(main, ["trace", "a", "--json"])
1ceb8b0… lmata 641 assert result.exit_code == 0
1ceb8b0… lmata 642 data = json.loads(result.output)
1ceb8b0… lmata 643 assert len(data) == 1
1ceb8b0… lmata 644 assert data[0][0]["caller"] == "a"
1ceb8b0… lmata 645
1ceb8b0… lmata 646 def test_trace_no_chains(self):
1ceb8b0… lmata 647 runner = CliRunner()
1ceb8b0… lmata 648 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 649 patch("navegador.analysis.flow.FlowTracer.trace", return_value=[]):
1ceb8b0… lmata 650 result = runner.invoke(main, ["trace", "entry"])
1ceb8b0… lmata 651 assert result.exit_code == 0
1ceb8b0… lmata 652 assert "No call chains" in result.output
1ceb8b0… lmata 653
1ceb8b0… lmata 654 def test_trace_markdown_shows_path(self):
1ceb8b0… lmata 655 from navegador.analysis.flow import CallChain
1ceb8b0… lmata 656 runner = CliRunner()
1ceb8b0… lmata 657 chains = [CallChain(steps=[("entry", "helper", "util.py")])]
1ceb8b0… lmata 658 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 659 patch("navegador.analysis.flow.FlowTracer.trace", return_value=chains):
1ceb8b0… lmata 660 result = runner.invoke(main, ["trace", "entry"])
1ceb8b0… lmata 661 assert result.exit_code == 0
1ceb8b0… lmata 662 assert "entry" in result.output
1ceb8b0… lmata 663 assert "helper" in result.output
1ceb8b0… lmata 664
1ceb8b0… lmata 665
1ceb8b0… lmata 666 class TestDeadcodeCLI:
1ceb8b0… lmata 667 def test_deadcode_json_output(self):
1ceb8b0… lmata 668 from navegador.analysis.deadcode import DeadCodeReport
1ceb8b0… lmata 669 runner = CliRunner()
1ceb8b0… lmata 670 report = DeadCodeReport(
1ceb8b0… lmata 671 unreachable_functions=[
1ceb8b0… lmata 672 {"type": "Function", "name": "dead_fn", "file_path": "a.py", "line_start": 1}
1ceb8b0… lmata 673 ],
1ceb8b0… lmata 674 unreachable_classes=[],
1ceb8b0… lmata 675 orphan_files=[],
1ceb8b0… lmata 676 )
1ceb8b0… lmata 677 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 678 patch("navegador.analysis.deadcode.DeadCodeDetector.detect", return_value=report):
1ceb8b0… lmata 679 result = runner.invoke(main, ["deadcode", "--json"])
1ceb8b0… lmata 680 assert result.exit_code == 0
1ceb8b0… lmata 681 data = json.loads(result.output)
1ceb8b0… lmata 682 assert len(data["unreachable_functions"]) == 1
1ceb8b0… lmata 683
1ceb8b0… lmata 684 def test_deadcode_no_dead_code(self):
1ceb8b0… lmata 685 from navegador.analysis.deadcode import DeadCodeReport
1ceb8b0… lmata 686 runner = CliRunner()
1ceb8b0… lmata 687 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 688 patch("navegador.analysis.deadcode.DeadCodeDetector.detect",
1ceb8b0… lmata 689 return_value=DeadCodeReport()):
1ceb8b0… lmata 690 result = runner.invoke(main, ["deadcode"])
1ceb8b0… lmata 691 assert result.exit_code == 0
1ceb8b0… lmata 692 assert "No dead code" in result.output
1ceb8b0… lmata 693
1ceb8b0… lmata 694 def test_deadcode_shows_summary_line(self):
1ceb8b0… lmata 695 from navegador.analysis.deadcode import DeadCodeReport
1ceb8b0… lmata 696 runner = CliRunner()
1ceb8b0… lmata 697 report = DeadCodeReport(
1ceb8b0… lmata 698 unreachable_functions=[
1ceb8b0… lmata 699 {"type": "Function", "name": "fn", "file_path": "", "line_start": None}
1ceb8b0… lmata 700 ],
1ceb8b0… lmata 701 unreachable_classes=[],
1ceb8b0… lmata 702 orphan_files=["old.py"],
1ceb8b0… lmata 703 )
1ceb8b0… lmata 704 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 705 patch("navegador.analysis.deadcode.DeadCodeDetector.detect", return_value=report):
1ceb8b0… lmata 706 result = runner.invoke(main, ["deadcode"])
1ceb8b0… lmata 707 assert result.exit_code == 0
1ceb8b0… lmata 708 assert "dead functions" in result.output
1ceb8b0… lmata 709 assert "orphan files" in result.output
1ceb8b0… lmata 710
1ceb8b0… lmata 711
1ceb8b0… lmata 712 class TestTestmapCLI:
1ceb8b0… lmata 713 def test_testmap_json_output(self):
1ceb8b0… lmata 714 from navegador.analysis.testmap import TestLink, TestMapResult
1ceb8b0… lmata 715 runner = CliRunner()
1ceb8b0… lmata 716 link = TestLink(
1ceb8b0… lmata 717 test_name="test_foo", test_file="tests/t.py",
1ceb8b0… lmata 718 prod_name="foo", prod_file="app.py", prod_type="Function", source="calls"
1ceb8b0… lmata 719 )
1ceb8b0… lmata 720 mock_result = TestMapResult(links=[link], unmatched_tests=[], edges_created=1)
1ceb8b0… lmata 721 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 722 patch("navegador.analysis.testmap.TestMapper.map_tests", return_value=mock_result):
1ceb8b0… lmata 723 result = runner.invoke(main, ["testmap", "--json"])
1ceb8b0… lmata 724 assert result.exit_code == 0
1ceb8b0… lmata 725 data = json.loads(result.output)
1ceb8b0… lmata 726 assert data["edges_created"] == 1
1ceb8b0… lmata 727 assert len(data["links"]) == 1
1ceb8b0… lmata 728
1ceb8b0… lmata 729 def test_testmap_no_tests(self):
1ceb8b0… lmata 730 from navegador.analysis.testmap import TestMapResult
1ceb8b0… lmata 731 runner = CliRunner()
1ceb8b0… lmata 732 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 733 patch("navegador.analysis.testmap.TestMapper.map_tests",
1ceb8b0… lmata 734 return_value=TestMapResult()):
1ceb8b0… lmata 735 result = runner.invoke(main, ["testmap"])
1ceb8b0… lmata 736 assert result.exit_code == 0
1ceb8b0… lmata 737 assert "0 linked" in result.output
1ceb8b0… lmata 738
1ceb8b0… lmata 739 def test_testmap_unmatched_shown(self):
1ceb8b0… lmata 740 from navegador.analysis.testmap import TestMapResult
1ceb8b0… lmata 741 runner = CliRunner()
1ceb8b0… lmata 742 mock_result = TestMapResult(
1ceb8b0… lmata 743 links=[],
1ceb8b0… lmata 744 unmatched_tests=[{"name": "test_mystery", "file_path": "t.py"}],
1ceb8b0… lmata 745 edges_created=0,
1ceb8b0… lmata 746 )
1ceb8b0… lmata 747 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 748 patch("navegador.analysis.testmap.TestMapper.map_tests", return_value=mock_result):
1ceb8b0… lmata 749 result = runner.invoke(main, ["testmap"])
1ceb8b0… lmata 750 assert result.exit_code == 0
1ceb8b0… lmata 751 assert "test_mystery" in result.output
1ceb8b0… lmata 752
1ceb8b0… lmata 753
1ceb8b0… lmata 754 class TestCyclesCLI:
1ceb8b0… lmata 755 def test_cycles_json_output(self):
1ceb8b0… lmata 756 runner = CliRunner()
1ceb8b0… lmata 757 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 758 patch("navegador.analysis.cycles.CycleDetector.detect_import_cycles",
1ceb8b0… lmata 759 return_value=[["a.py", "b.py"]]), \
1ceb8b0… lmata 760 patch("navegador.analysis.cycles.CycleDetector.detect_call_cycles",
1ceb8b0… lmata 761 return_value=[]):
1ceb8b0… lmata 762 result = runner.invoke(main, ["cycles", "--json"])
1ceb8b0… lmata 763 assert result.exit_code == 0
1ceb8b0… lmata 764 data = json.loads(result.output)
1ceb8b0… lmata 765 assert "import_cycles" in data
1ceb8b0… lmata 766 assert "call_cycles" in data
1ceb8b0… lmata 767 assert len(data["import_cycles"]) == 1
1ceb8b0… lmata 768
1ceb8b0… lmata 769 def test_no_cycles_message(self):
1ceb8b0… lmata 770 runner = CliRunner()
1ceb8b0… lmata 771 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 772 patch("navegador.analysis.cycles.CycleDetector.detect_import_cycles",
1ceb8b0… lmata 773 return_value=[]), \
1ceb8b0… lmata 774 patch("navegador.analysis.cycles.CycleDetector.detect_call_cycles",
1ceb8b0… lmata 775 return_value=[]):
1ceb8b0… lmata 776 result = runner.invoke(main, ["cycles"])
1ceb8b0… lmata 777 assert result.exit_code == 0
1ceb8b0… lmata 778 assert "No circular dependencies" in result.output
1ceb8b0… lmata 779
1ceb8b0… lmata 780 def test_imports_only_flag(self):
1ceb8b0… lmata 781 runner = CliRunner()
1ceb8b0… lmata 782 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 783 patch("navegador.analysis.cycles.CycleDetector.detect_import_cycles",
1ceb8b0… lmata 784 return_value=[["x.py", "y.py"]]) as mock_imp, \
1ceb8b0… lmata 785 patch("navegador.analysis.cycles.CycleDetector.detect_call_cycles",
1ceb8b0… lmata 786 return_value=[]) as mock_call:
1ceb8b0… lmata 787 result = runner.invoke(main, ["cycles", "--imports"])
1ceb8b0… lmata 788 assert result.exit_code == 0
1ceb8b0… lmata 789 # --imports restricts to import cycle detection only
1ceb8b0… lmata 790 mock_imp.assert_called_once()
1ceb8b0… lmata 791 mock_call.assert_not_called()
1ceb8b0… lmata 792
1ceb8b0… lmata 793 def test_cycles_with_call_cycles_shown(self):
1ceb8b0… lmata 794 runner = CliRunner()
1ceb8b0… lmata 795 with patch("navegador.cli.commands._get_store"), \
1ceb8b0… lmata 796 patch("navegador.analysis.cycles.CycleDetector.detect_import_cycles",
1ceb8b0… lmata 797 return_value=[]), \
1ceb8b0… lmata 798 patch("navegador.analysis.cycles.CycleDetector.detect_call_cycles",
1ceb8b0… lmata 799 return_value=[["fn_a", "fn_b"]]):
1ceb8b0… lmata 800 result = runner.invoke(main, ["cycles"])
1ceb8b0… lmata 801 assert result.exit_code == 0
1ceb8b0… lmata 802 assert "fn_a" in result.output
1ceb8b0… lmata 803 assert "fn_b" in result.output

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button