|
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 |