Navegador

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

Keyboard Shortcuts

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