|
b853fc0…
|
lmata
|
1 |
"""Tests for navegador.diff — DiffAnalyzer and the CLI 'diff' command.""" |
|
b853fc0…
|
lmata
|
2 |
|
|
b853fc0…
|
lmata
|
3 |
from __future__ import annotations |
|
b853fc0…
|
lmata
|
4 |
|
|
b853fc0…
|
lmata
|
5 |
import json |
|
b853fc0…
|
lmata
|
6 |
import subprocess |
|
b853fc0…
|
lmata
|
7 |
from pathlib import Path |
|
b853fc0…
|
lmata
|
8 |
from unittest.mock import MagicMock, patch |
|
b853fc0…
|
lmata
|
9 |
|
|
b853fc0…
|
lmata
|
10 |
import pytest |
|
b853fc0…
|
lmata
|
11 |
from click.testing import CliRunner |
|
b853fc0…
|
lmata
|
12 |
|
|
b853fc0…
|
lmata
|
13 |
from navegador.cli.commands import main |
|
b853fc0…
|
lmata
|
14 |
from navegador.diff import ( |
|
b853fc0…
|
lmata
|
15 |
DiffAnalyzer, |
|
b853fc0…
|
lmata
|
16 |
_lines_overlap, |
|
b853fc0…
|
lmata
|
17 |
_parse_unified_diff_hunks, |
|
b853fc0…
|
lmata
|
18 |
) |
|
b853fc0…
|
lmata
|
19 |
|
|
b853fc0…
|
lmata
|
20 |
|
|
b853fc0…
|
lmata
|
21 |
# ── Helpers ──────────────────────────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
22 |
|
|
b853fc0…
|
lmata
|
23 |
|
|
b853fc0…
|
lmata
|
24 |
def _mock_store(result_set: list | None = None): |
|
b853fc0…
|
lmata
|
25 |
"""Return a MagicMock GraphStore whose .query() yields *result_set*.""" |
|
b853fc0…
|
lmata
|
26 |
store = MagicMock() |
|
b853fc0…
|
lmata
|
27 |
store.query.return_value = MagicMock(result_set=result_set or []) |
|
b853fc0…
|
lmata
|
28 |
return store |
|
b853fc0…
|
lmata
|
29 |
|
|
b853fc0…
|
lmata
|
30 |
|
|
b853fc0…
|
lmata
|
31 |
def _analyzer(store=None, repo_path: Path | None = None, changed: list[str] | None = None): |
|
b853fc0…
|
lmata
|
32 |
"""Build a DiffAnalyzer with the given store, patching GitAdapter.changed_files.""" |
|
b853fc0…
|
lmata
|
33 |
if store is None: |
|
b853fc0…
|
lmata
|
34 |
store = _mock_store() |
|
b853fc0…
|
lmata
|
35 |
if repo_path is None: |
|
b853fc0…
|
lmata
|
36 |
repo_path = Path("/fake/repo") |
|
b853fc0…
|
lmata
|
37 |
analyzer = DiffAnalyzer(store, repo_path) |
|
b853fc0…
|
lmata
|
38 |
if changed is not None: |
|
b853fc0…
|
lmata
|
39 |
analyzer._git = MagicMock() |
|
b853fc0…
|
lmata
|
40 |
analyzer._git.changed_files.return_value = changed |
|
b853fc0…
|
lmata
|
41 |
return analyzer |
|
b853fc0…
|
lmata
|
42 |
|
|
b853fc0…
|
lmata
|
43 |
|
|
b853fc0…
|
lmata
|
44 |
# ── _parse_unified_diff_hunks ───────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
45 |
|
|
b853fc0…
|
lmata
|
46 |
|
|
b853fc0…
|
lmata
|
47 |
class TestParseUnifiedDiffHunks: |
|
b853fc0…
|
lmata
|
48 |
SAMPLE_DIFF = """\ |
|
b853fc0…
|
lmata
|
49 |
diff --git a/foo.py b/foo.py |
|
b853fc0…
|
lmata
|
50 |
index 0000000..1111111 100644 |
|
b853fc0…
|
lmata
|
51 |
--- a/foo.py |
|
b853fc0…
|
lmata
|
52 |
+++ b/foo.py |
|
b853fc0…
|
lmata
|
53 |
@@ -10,3 +10,5 @@ |
|
b853fc0…
|
lmata
|
54 |
unchanged |
|
b853fc0…
|
lmata
|
55 |
+added line 1 |
|
b853fc0…
|
lmata
|
56 |
+added line 2 |
|
b853fc0…
|
lmata
|
57 |
diff --git a/bar.py b/bar.py |
|
b853fc0…
|
lmata
|
58 |
index 0000000..2222222 100644 |
|
b853fc0…
|
lmata
|
59 |
--- a/bar.py |
|
b853fc0…
|
lmata
|
60 |
+++ b/bar.py |
|
b853fc0…
|
lmata
|
61 |
@@ -5 +5,2 @@ |
|
b853fc0…
|
lmata
|
62 |
-old line |
|
b853fc0…
|
lmata
|
63 |
+new line A |
|
b853fc0…
|
lmata
|
64 |
+new line B |
|
b853fc0…
|
lmata
|
65 |
""" |
|
b853fc0…
|
lmata
|
66 |
|
|
b853fc0…
|
lmata
|
67 |
def test_returns_dict(self): |
|
b853fc0…
|
lmata
|
68 |
result = _parse_unified_diff_hunks(self.SAMPLE_DIFF) |
|
b853fc0…
|
lmata
|
69 |
assert isinstance(result, dict) |
|
b853fc0…
|
lmata
|
70 |
|
|
b853fc0…
|
lmata
|
71 |
def test_detects_both_files(self): |
|
b853fc0…
|
lmata
|
72 |
result = _parse_unified_diff_hunks(self.SAMPLE_DIFF) |
|
b853fc0…
|
lmata
|
73 |
assert "foo.py" in result |
|
b853fc0…
|
lmata
|
74 |
assert "bar.py" in result |
|
b853fc0…
|
lmata
|
75 |
|
|
b853fc0…
|
lmata
|
76 |
def test_correct_range_for_foo(self): |
|
b853fc0…
|
lmata
|
77 |
result = _parse_unified_diff_hunks(self.SAMPLE_DIFF) |
|
b853fc0…
|
lmata
|
78 |
# hunk: +10,5 → start=10, end=14 |
|
b853fc0…
|
lmata
|
79 |
ranges = result["foo.py"] |
|
b853fc0…
|
lmata
|
80 |
assert len(ranges) == 1 |
|
b853fc0…
|
lmata
|
81 |
start, end = ranges[0] |
|
b853fc0…
|
lmata
|
82 |
assert start == 10 |
|
b853fc0…
|
lmata
|
83 |
assert end == 14 # 10 + 5 - 1 |
|
b853fc0…
|
lmata
|
84 |
|
|
b853fc0…
|
lmata
|
85 |
def test_correct_range_for_bar(self): |
|
b853fc0…
|
lmata
|
86 |
result = _parse_unified_diff_hunks(self.SAMPLE_DIFF) |
|
b853fc0…
|
lmata
|
87 |
# hunk: +5,2 → start=5, end=6 |
|
b853fc0…
|
lmata
|
88 |
ranges = result["bar.py"] |
|
b853fc0…
|
lmata
|
89 |
assert len(ranges) == 1 |
|
b853fc0…
|
lmata
|
90 |
start, end = ranges[0] |
|
b853fc0…
|
lmata
|
91 |
assert start == 5 |
|
b853fc0…
|
lmata
|
92 |
assert end == 6 |
|
b853fc0…
|
lmata
|
93 |
|
|
b853fc0…
|
lmata
|
94 |
def test_empty_diff_returns_empty_dict(self): |
|
b853fc0…
|
lmata
|
95 |
result = _parse_unified_diff_hunks("") |
|
b853fc0…
|
lmata
|
96 |
assert result == {} |
|
b853fc0…
|
lmata
|
97 |
|
|
b853fc0…
|
lmata
|
98 |
def test_deleted_file_not_included(self): |
|
b853fc0…
|
lmata
|
99 |
diff = """\ |
|
b853fc0…
|
lmata
|
100 |
--- a/deleted.py |
|
b853fc0…
|
lmata
|
101 |
+++ /dev/null |
|
b853fc0…
|
lmata
|
102 |
@@ -1 +0,0 @@ |
|
b853fc0…
|
lmata
|
103 |
-old |
|
b853fc0…
|
lmata
|
104 |
""" |
|
b853fc0…
|
lmata
|
105 |
result = _parse_unified_diff_hunks(diff) |
|
b853fc0…
|
lmata
|
106 |
assert "deleted.py" not in result |
|
b853fc0…
|
lmata
|
107 |
|
|
b853fc0…
|
lmata
|
108 |
def test_multiple_hunks_same_file(self): |
|
b853fc0…
|
lmata
|
109 |
diff = """\ |
|
b853fc0…
|
lmata
|
110 |
diff --git a/multi.py b/multi.py |
|
b853fc0…
|
lmata
|
111 |
--- a/multi.py |
|
b853fc0…
|
lmata
|
112 |
+++ b/multi.py |
|
b853fc0…
|
lmata
|
113 |
@@ -1,2 +1,3 @@ |
|
b853fc0…
|
lmata
|
114 |
+first |
|
b853fc0…
|
lmata
|
115 |
unchanged |
|
b853fc0…
|
lmata
|
116 |
+second |
|
b853fc0…
|
lmata
|
117 |
@@ -20 +21,2 @@ |
|
b853fc0…
|
lmata
|
118 |
-old |
|
b853fc0…
|
lmata
|
119 |
+new1 |
|
b853fc0…
|
lmata
|
120 |
+new2 |
|
b853fc0…
|
lmata
|
121 |
""" |
|
b853fc0…
|
lmata
|
122 |
result = _parse_unified_diff_hunks(diff) |
|
b853fc0…
|
lmata
|
123 |
assert "multi.py" in result |
|
b853fc0…
|
lmata
|
124 |
assert len(result["multi.py"]) == 2 |
|
b853fc0…
|
lmata
|
125 |
|
|
b853fc0…
|
lmata
|
126 |
|
|
b853fc0…
|
lmata
|
127 |
# ── _lines_overlap ───────────────────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
128 |
|
|
b853fc0…
|
lmata
|
129 |
|
|
b853fc0…
|
lmata
|
130 |
class TestLinesOverlap: |
|
b853fc0…
|
lmata
|
131 |
def test_exact_overlap(self): |
|
b853fc0…
|
lmata
|
132 |
assert _lines_overlap([(10, 20)], 10, 20) is True |
|
b853fc0…
|
lmata
|
133 |
|
|
b853fc0…
|
lmata
|
134 |
def test_symbol_inside_range(self): |
|
b853fc0…
|
lmata
|
135 |
assert _lines_overlap([(5, 30)], 10, 15) is True |
|
b853fc0…
|
lmata
|
136 |
|
|
b853fc0…
|
lmata
|
137 |
def test_range_inside_symbol(self): |
|
b853fc0…
|
lmata
|
138 |
assert _lines_overlap([(12, 14)], 10, 20) is True |
|
b853fc0…
|
lmata
|
139 |
|
|
b853fc0…
|
lmata
|
140 |
def test_no_overlap_before(self): |
|
b853fc0…
|
lmata
|
141 |
assert _lines_overlap([(20, 30)], 5, 10) is False |
|
b853fc0…
|
lmata
|
142 |
|
|
b853fc0…
|
lmata
|
143 |
def test_no_overlap_after(self): |
|
b853fc0…
|
lmata
|
144 |
assert _lines_overlap([(1, 5)], 10, 20) is False |
|
b853fc0…
|
lmata
|
145 |
|
|
b853fc0…
|
lmata
|
146 |
def test_adjacent_not_overlapping(self): |
|
b853fc0…
|
lmata
|
147 |
assert _lines_overlap([(1, 9)], 10, 20) is False |
|
b853fc0…
|
lmata
|
148 |
|
|
b853fc0…
|
lmata
|
149 |
def test_none_line_start_returns_false(self): |
|
b853fc0…
|
lmata
|
150 |
assert _lines_overlap([(1, 100)], None, None) is False |
|
b853fc0…
|
lmata
|
151 |
|
|
b853fc0…
|
lmata
|
152 |
def test_no_line_end_uses_start(self): |
|
b853fc0…
|
lmata
|
153 |
# line_end=None → treated as single-line symbol |
|
b853fc0…
|
lmata
|
154 |
assert _lines_overlap([(10, 20)], 15, None) is True |
|
b853fc0…
|
lmata
|
155 |
|
|
b853fc0…
|
lmata
|
156 |
def test_empty_ranges_returns_false(self): |
|
b853fc0…
|
lmata
|
157 |
assert _lines_overlap([], 10, 20) is False |
|
b853fc0…
|
lmata
|
158 |
|
|
b853fc0…
|
lmata
|
159 |
def test_multiple_ranges_one_hits(self): |
|
b853fc0…
|
lmata
|
160 |
assert _lines_overlap([(1, 5), (50, 60)], 52, 55) is True |
|
b853fc0…
|
lmata
|
161 |
|
|
b853fc0…
|
lmata
|
162 |
|
|
b853fc0…
|
lmata
|
163 |
# ── DiffAnalyzer.changed_files ──────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
164 |
|
|
b853fc0…
|
lmata
|
165 |
|
|
b853fc0…
|
lmata
|
166 |
class TestDiffAnalyzerChangedFiles: |
|
b853fc0…
|
lmata
|
167 |
def test_delegates_to_git_adapter(self): |
|
b853fc0…
|
lmata
|
168 |
analyzer = _analyzer(changed=["a.py", "b.py"]) |
|
b853fc0…
|
lmata
|
169 |
assert analyzer.changed_files() == ["a.py", "b.py"] |
|
b853fc0…
|
lmata
|
170 |
|
|
b853fc0…
|
lmata
|
171 |
def test_empty_when_no_changes(self): |
|
b853fc0…
|
lmata
|
172 |
analyzer = _analyzer(changed=[]) |
|
b853fc0…
|
lmata
|
173 |
assert analyzer.changed_files() == [] |
|
b853fc0…
|
lmata
|
174 |
|
|
b853fc0…
|
lmata
|
175 |
def test_returns_list(self): |
|
b853fc0…
|
lmata
|
176 |
analyzer = _analyzer(changed=["x.py"]) |
|
b853fc0…
|
lmata
|
177 |
assert isinstance(analyzer.changed_files(), list) |
|
b853fc0…
|
lmata
|
178 |
|
|
b853fc0…
|
lmata
|
179 |
def test_uses_subprocess_via_git_adapter(self, tmp_path): |
|
b853fc0…
|
lmata
|
180 |
"""Verify changed_files() relies on subprocess (through GitAdapter._run).""" |
|
b853fc0…
|
lmata
|
181 |
repo = tmp_path / "repo" |
|
b853fc0…
|
lmata
|
182 |
repo.mkdir() |
|
b853fc0…
|
lmata
|
183 |
store = _mock_store() |
|
b853fc0…
|
lmata
|
184 |
analyzer = DiffAnalyzer(store, repo) |
|
b853fc0…
|
lmata
|
185 |
|
|
b853fc0…
|
lmata
|
186 |
fake_result = MagicMock() |
|
b853fc0…
|
lmata
|
187 |
fake_result.stdout = "changed.py\n" |
|
b853fc0…
|
lmata
|
188 |
fake_result.returncode = 0 |
|
b853fc0…
|
lmata
|
189 |
|
|
b853fc0…
|
lmata
|
190 |
with patch("subprocess.run", return_value=fake_result): |
|
b853fc0…
|
lmata
|
191 |
files = analyzer.changed_files() |
|
b853fc0…
|
lmata
|
192 |
|
|
b853fc0…
|
lmata
|
193 |
assert "changed.py" in files |
|
b853fc0…
|
lmata
|
194 |
|
|
b853fc0…
|
lmata
|
195 |
|
|
b853fc0…
|
lmata
|
196 |
# ── DiffAnalyzer.changed_lines ──────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
197 |
|
|
b853fc0…
|
lmata
|
198 |
|
|
b853fc0…
|
lmata
|
199 |
class TestDiffAnalyzerChangedLines: |
|
b853fc0…
|
lmata
|
200 |
def test_returns_dict(self, tmp_path): |
|
b853fc0…
|
lmata
|
201 |
analyzer = _analyzer(changed=["f.py"]) |
|
b853fc0…
|
lmata
|
202 |
fake = MagicMock() |
|
b853fc0…
|
lmata
|
203 |
fake.returncode = 0 |
|
b853fc0…
|
lmata
|
204 |
fake.stdout = "+++ b/f.py\n@@ -1 +1,3 @@\n+a\n+b\n+c\n" |
|
b853fc0…
|
lmata
|
205 |
with patch("subprocess.run", return_value=fake): |
|
b853fc0…
|
lmata
|
206 |
result = analyzer.changed_lines() |
|
b853fc0…
|
lmata
|
207 |
assert isinstance(result, dict) |
|
b853fc0…
|
lmata
|
208 |
|
|
b853fc0…
|
lmata
|
209 |
def test_fallback_on_no_output(self): |
|
b853fc0…
|
lmata
|
210 |
"""No diff output → full-file sentinel range for each changed file.""" |
|
b853fc0…
|
lmata
|
211 |
analyzer = _analyzer(changed=["x.py", "y.py"]) |
|
b853fc0…
|
lmata
|
212 |
fake = MagicMock() |
|
b853fc0…
|
lmata
|
213 |
fake.returncode = 0 |
|
b853fc0…
|
lmata
|
214 |
fake.stdout = "" |
|
b853fc0…
|
lmata
|
215 |
with patch("subprocess.run", return_value=fake): |
|
b853fc0…
|
lmata
|
216 |
result = analyzer.changed_lines() |
|
b853fc0…
|
lmata
|
217 |
assert "x.py" in result |
|
b853fc0…
|
lmata
|
218 |
assert "y.py" in result |
|
b853fc0…
|
lmata
|
219 |
assert result["x.py"] == [(1, 999_999)] |
|
b853fc0…
|
lmata
|
220 |
|
|
b853fc0…
|
lmata
|
221 |
def test_fallback_on_nonzero_exit(self): |
|
b853fc0…
|
lmata
|
222 |
"""Non-zero exit (e.g. no HEAD) → full-file sentinel for all changed files.""" |
|
b853fc0…
|
lmata
|
223 |
analyzer = _analyzer(changed=["z.py"]) |
|
b853fc0…
|
lmata
|
224 |
fake = MagicMock() |
|
b853fc0…
|
lmata
|
225 |
fake.returncode = 128 |
|
b853fc0…
|
lmata
|
226 |
fake.stdout = "" |
|
b853fc0…
|
lmata
|
227 |
with patch("subprocess.run", return_value=fake): |
|
b853fc0…
|
lmata
|
228 |
result = analyzer.changed_lines() |
|
b853fc0…
|
lmata
|
229 |
assert result["z.py"] == [(1, 999_999)] |
|
b853fc0…
|
lmata
|
230 |
|
|
b853fc0…
|
lmata
|
231 |
def test_missing_files_get_sentinel(self): |
|
b853fc0…
|
lmata
|
232 |
"""Files in changed_files() but absent from diff get sentinel range.""" |
|
b853fc0…
|
lmata
|
233 |
analyzer = _analyzer(changed=["in_diff.py", "not_in_diff.py"]) |
|
b853fc0…
|
lmata
|
234 |
fake = MagicMock() |
|
b853fc0…
|
lmata
|
235 |
fake.returncode = 0 |
|
b853fc0…
|
lmata
|
236 |
fake.stdout = "+++ b/in_diff.py\n@@ -5 +5,2 @@ \n+x\n+y\n" |
|
b853fc0…
|
lmata
|
237 |
with patch("subprocess.run", return_value=fake): |
|
b853fc0…
|
lmata
|
238 |
result = analyzer.changed_lines() |
|
b853fc0…
|
lmata
|
239 |
assert "not_in_diff.py" in result |
|
b853fc0…
|
lmata
|
240 |
assert result["not_in_diff.py"] == [(1, 999_999)] |
|
b853fc0…
|
lmata
|
241 |
|
|
b853fc0…
|
lmata
|
242 |
|
|
b853fc0…
|
lmata
|
243 |
# ── DiffAnalyzer.affected_symbols ───────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
244 |
|
|
b853fc0…
|
lmata
|
245 |
|
|
b853fc0…
|
lmata
|
246 |
class TestDiffAnalyzerAffectedSymbols: |
|
b853fc0…
|
lmata
|
247 |
def _sym_rows(self): |
|
b853fc0…
|
lmata
|
248 |
"""Return fake graph rows: (type, name, file_path, line_start, line_end).""" |
|
b853fc0…
|
lmata
|
249 |
return [ |
|
b853fc0…
|
lmata
|
250 |
("Function", "do_thing", "app.py", 10, 25), |
|
b853fc0…
|
lmata
|
251 |
("Class", "MyClass", "app.py", 30, 80), |
|
b853fc0…
|
lmata
|
252 |
("Method", "helper", "utils.py", 5, 15), |
|
b853fc0…
|
lmata
|
253 |
] |
|
b853fc0…
|
lmata
|
254 |
|
|
b853fc0…
|
lmata
|
255 |
def test_returns_list(self): |
|
b853fc0…
|
lmata
|
256 |
store = _mock_store(result_set=self._sym_rows()) |
|
b853fc0…
|
lmata
|
257 |
analyzer = _analyzer(store=store, changed=["app.py"]) |
|
b853fc0…
|
lmata
|
258 |
with patch.object(analyzer, "changed_lines", return_value={"app.py": [(1, 999_999)]}): |
|
b853fc0…
|
lmata
|
259 |
result = analyzer.affected_symbols() |
|
b853fc0…
|
lmata
|
260 |
assert isinstance(result, list) |
|
b853fc0…
|
lmata
|
261 |
|
|
b853fc0…
|
lmata
|
262 |
def test_symbols_overlap_returned(self): |
|
b853fc0…
|
lmata
|
263 |
store = _mock_store(result_set=self._sym_rows()) |
|
b853fc0…
|
lmata
|
264 |
analyzer = _analyzer(store=store, changed=["app.py"]) |
|
b853fc0…
|
lmata
|
265 |
# Changed lines 15-20 overlap do_thing (10-25) |
|
b853fc0…
|
lmata
|
266 |
with patch.object(analyzer, "changed_lines", return_value={"app.py": [(15, 20)]}): |
|
b853fc0…
|
lmata
|
267 |
result = analyzer.affected_symbols() |
|
b853fc0…
|
lmata
|
268 |
names = [s["name"] for s in result] |
|
b853fc0…
|
lmata
|
269 |
assert "do_thing" in names |
|
b853fc0…
|
lmata
|
270 |
|
|
b853fc0…
|
lmata
|
271 |
def test_non_overlapping_symbols_excluded(self): |
|
b853fc0…
|
lmata
|
272 |
store = _mock_store(result_set=self._sym_rows()) |
|
b853fc0…
|
lmata
|
273 |
analyzer = _analyzer(store=store, changed=["app.py"]) |
|
b853fc0…
|
lmata
|
274 |
# Changed lines 50-60 overlap MyClass (30-80) but not do_thing (10-25) |
|
b853fc0…
|
lmata
|
275 |
with patch.object(analyzer, "changed_lines", return_value={"app.py": [(50, 60)]}): |
|
b853fc0…
|
lmata
|
276 |
result = analyzer.affected_symbols() |
|
b853fc0…
|
lmata
|
277 |
names = [s["name"] for s in result] |
|
b853fc0…
|
lmata
|
278 |
assert "MyClass" in names |
|
b853fc0…
|
lmata
|
279 |
assert "do_thing" not in names |
|
b853fc0…
|
lmata
|
280 |
|
|
b853fc0…
|
lmata
|
281 |
def test_empty_when_no_graph_nodes(self): |
|
b853fc0…
|
lmata
|
282 |
store = _mock_store(result_set=[]) |
|
b853fc0…
|
lmata
|
283 |
analyzer = _analyzer(store=store, changed=["app.py"]) |
|
b853fc0…
|
lmata
|
284 |
with patch.object(analyzer, "changed_lines", return_value={"app.py": [(1, 50)]}): |
|
b853fc0…
|
lmata
|
285 |
result = analyzer.affected_symbols() |
|
b853fc0…
|
lmata
|
286 |
assert result == [] |
|
b853fc0…
|
lmata
|
287 |
|
|
b853fc0…
|
lmata
|
288 |
def test_empty_when_no_changed_files(self): |
|
b853fc0…
|
lmata
|
289 |
store = _mock_store(result_set=self._sym_rows()) |
|
b853fc0…
|
lmata
|
290 |
analyzer = _analyzer(store=store, changed=[]) |
|
b853fc0…
|
lmata
|
291 |
with patch.object(analyzer, "changed_lines", return_value={}): |
|
b853fc0…
|
lmata
|
292 |
result = analyzer.affected_symbols() |
|
b853fc0…
|
lmata
|
293 |
assert result == [] |
|
b853fc0…
|
lmata
|
294 |
|
|
b853fc0…
|
lmata
|
295 |
def test_symbol_dict_has_required_keys(self): |
|
b853fc0…
|
lmata
|
296 |
store = _mock_store(result_set=[("Function", "foo", "a.py", 1, 10)]) |
|
b853fc0…
|
lmata
|
297 |
analyzer = _analyzer(store=store, changed=["a.py"]) |
|
b853fc0…
|
lmata
|
298 |
with patch.object(analyzer, "changed_lines", return_value={"a.py": [(1, 10)]}): |
|
b853fc0…
|
lmata
|
299 |
result = analyzer.affected_symbols() |
|
b853fc0…
|
lmata
|
300 |
assert len(result) == 1 |
|
b853fc0…
|
lmata
|
301 |
sym = result[0] |
|
b853fc0…
|
lmata
|
302 |
assert "type" in sym |
|
b853fc0…
|
lmata
|
303 |
assert "name" in sym |
|
b853fc0…
|
lmata
|
304 |
assert "file_path" in sym |
|
b853fc0…
|
lmata
|
305 |
assert "line_start" in sym |
|
b853fc0…
|
lmata
|
306 |
assert "line_end" in sym |
|
b853fc0…
|
lmata
|
307 |
|
|
b853fc0…
|
lmata
|
308 |
def test_no_duplicate_symbols(self): |
|
b853fc0…
|
lmata
|
309 |
"""Same symbol matched by two hunk ranges must appear only once.""" |
|
b853fc0…
|
lmata
|
310 |
rows = [("Function", "foo", "a.py", 5, 20)] |
|
b853fc0…
|
lmata
|
311 |
store = _mock_store(result_set=rows) |
|
b853fc0…
|
lmata
|
312 |
analyzer = _analyzer(store=store, changed=["a.py"]) |
|
b853fc0…
|
lmata
|
313 |
with patch.object(analyzer, "changed_lines", return_value={"a.py": [(5, 10), (15, 20)]}): |
|
b853fc0…
|
lmata
|
314 |
result = analyzer.affected_symbols() |
|
b853fc0…
|
lmata
|
315 |
assert len(result) == 1 |
|
b853fc0…
|
lmata
|
316 |
|
|
b853fc0…
|
lmata
|
317 |
|
|
b853fc0…
|
lmata
|
318 |
# ── DiffAnalyzer.affected_knowledge ─────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
319 |
|
|
b853fc0…
|
lmata
|
320 |
|
|
b853fc0…
|
lmata
|
321 |
class TestDiffAnalyzerAffectedKnowledge: |
|
b853fc0…
|
lmata
|
322 |
def _k_rows(self): |
|
b853fc0…
|
lmata
|
323 |
"""Fake knowledge rows: (type, name, description, domain, status).""" |
|
b853fc0…
|
lmata
|
324 |
return [ |
|
b853fc0…
|
lmata
|
325 |
("Concept", "Billing", "Handles money", "finance", "stable"), |
|
b853fc0…
|
lmata
|
326 |
("Rule", "no_refund_after_30d", "30 day rule", "finance", "active"), |
|
b853fc0…
|
lmata
|
327 |
] |
|
b853fc0…
|
lmata
|
328 |
|
|
b853fc0…
|
lmata
|
329 |
def test_returns_list(self): |
|
b853fc0…
|
lmata
|
330 |
store = _mock_store(result_set=self._k_rows()) |
|
b853fc0…
|
lmata
|
331 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
332 |
sym = [{"name": "charge", "file_path": "billing.py"}] |
|
b853fc0…
|
lmata
|
333 |
with patch.object(analyzer, "affected_symbols", return_value=sym): |
|
b853fc0…
|
lmata
|
334 |
result = analyzer.affected_knowledge() |
|
b853fc0…
|
lmata
|
335 |
assert isinstance(result, list) |
|
b853fc0…
|
lmata
|
336 |
|
|
b853fc0…
|
lmata
|
337 |
def test_knowledge_nodes_returned(self): |
|
b853fc0…
|
lmata
|
338 |
store = _mock_store(result_set=self._k_rows()) |
|
b853fc0…
|
lmata
|
339 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
340 |
sym = [{"name": "charge", "file_path": "billing.py"}] |
|
b853fc0…
|
lmata
|
341 |
with patch.object(analyzer, "affected_symbols", return_value=sym): |
|
b853fc0…
|
lmata
|
342 |
result = analyzer.affected_knowledge() |
|
b853fc0…
|
lmata
|
343 |
names = [k["name"] for k in result] |
|
b853fc0…
|
lmata
|
344 |
assert "Billing" in names |
|
b853fc0…
|
lmata
|
345 |
assert "no_refund_after_30d" in names |
|
b853fc0…
|
lmata
|
346 |
|
|
b853fc0…
|
lmata
|
347 |
def test_empty_when_no_symbols(self): |
|
b853fc0…
|
lmata
|
348 |
store = _mock_store(result_set=self._k_rows()) |
|
b853fc0…
|
lmata
|
349 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
350 |
with patch.object(analyzer, "affected_symbols", return_value=[]): |
|
b853fc0…
|
lmata
|
351 |
result = analyzer.affected_knowledge() |
|
b853fc0…
|
lmata
|
352 |
assert result == [] |
|
b853fc0…
|
lmata
|
353 |
|
|
b853fc0…
|
lmata
|
354 |
def test_empty_when_no_graph_knowledge(self): |
|
b853fc0…
|
lmata
|
355 |
store = _mock_store(result_set=[]) |
|
b853fc0…
|
lmata
|
356 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
357 |
sym = [{"name": "foo", "file_path": "a.py"}] |
|
b853fc0…
|
lmata
|
358 |
with patch.object(analyzer, "affected_symbols", return_value=sym): |
|
b853fc0…
|
lmata
|
359 |
result = analyzer.affected_knowledge() |
|
b853fc0…
|
lmata
|
360 |
assert result == [] |
|
b853fc0…
|
lmata
|
361 |
|
|
b853fc0…
|
lmata
|
362 |
def test_no_duplicate_knowledge_nodes(self): |
|
b853fc0…
|
lmata
|
363 |
"""Two symbols linking to the same knowledge node → deduplicated.""" |
|
b853fc0…
|
lmata
|
364 |
rows = [("Concept", "SharedConcept", "desc", "core", "stable")] |
|
b853fc0…
|
lmata
|
365 |
store = _mock_store(result_set=rows) |
|
b853fc0…
|
lmata
|
366 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
367 |
syms = [ |
|
b853fc0…
|
lmata
|
368 |
{"name": "alpha", "file_path": "a.py"}, |
|
b853fc0…
|
lmata
|
369 |
{"name": "beta", "file_path": "b.py"}, |
|
b853fc0…
|
lmata
|
370 |
] |
|
b853fc0…
|
lmata
|
371 |
with patch.object(analyzer, "affected_symbols", return_value=syms): |
|
b853fc0…
|
lmata
|
372 |
result = analyzer.affected_knowledge() |
|
b853fc0…
|
lmata
|
373 |
assert len([k for k in result if k["name"] == "SharedConcept"]) == 1 |
|
b853fc0…
|
lmata
|
374 |
|
|
b853fc0…
|
lmata
|
375 |
def test_knowledge_dict_has_required_keys(self): |
|
b853fc0…
|
lmata
|
376 |
rows = [("Rule", "my_rule", "some desc", "payments", "")] |
|
b853fc0…
|
lmata
|
377 |
store = _mock_store(result_set=rows) |
|
b853fc0…
|
lmata
|
378 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
379 |
sym = [{"name": "process", "file_path": "pay.py"}] |
|
b853fc0…
|
lmata
|
380 |
with patch.object(analyzer, "affected_symbols", return_value=sym): |
|
b853fc0…
|
lmata
|
381 |
result = analyzer.affected_knowledge() |
|
b853fc0…
|
lmata
|
382 |
assert len(result) == 1 |
|
b853fc0…
|
lmata
|
383 |
k = result[0] |
|
b853fc0…
|
lmata
|
384 |
assert "type" in k |
|
b853fc0…
|
lmata
|
385 |
assert "name" in k |
|
b853fc0…
|
lmata
|
386 |
assert "description" in k |
|
b853fc0…
|
lmata
|
387 |
assert "domain" in k |
|
b853fc0…
|
lmata
|
388 |
assert "status" in k |
|
b853fc0…
|
lmata
|
389 |
|
|
b853fc0…
|
lmata
|
390 |
|
|
b853fc0…
|
lmata
|
391 |
# ── DiffAnalyzer.impact_summary ─────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
392 |
|
|
b853fc0…
|
lmata
|
393 |
|
|
b853fc0…
|
lmata
|
394 |
class TestDiffAnalyzerImpactSummary: |
|
b853fc0…
|
lmata
|
395 |
def _build(self, files=None, symbols=None, knowledge=None): |
|
b853fc0…
|
lmata
|
396 |
store = _mock_store() |
|
b853fc0…
|
lmata
|
397 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
398 |
with ( |
|
b853fc0…
|
lmata
|
399 |
patch.object(analyzer, "changed_files", return_value=files or []), |
|
b853fc0…
|
lmata
|
400 |
patch.object(analyzer, "affected_symbols", return_value=symbols or []), |
|
b853fc0…
|
lmata
|
401 |
patch.object(analyzer, "affected_knowledge", return_value=knowledge or []), |
|
b853fc0…
|
lmata
|
402 |
): |
|
b853fc0…
|
lmata
|
403 |
return analyzer.impact_summary() |
|
b853fc0…
|
lmata
|
404 |
|
|
b853fc0…
|
lmata
|
405 |
def test_returns_dict(self): |
|
b853fc0…
|
lmata
|
406 |
result = self._build() |
|
b853fc0…
|
lmata
|
407 |
assert isinstance(result, dict) |
|
b853fc0…
|
lmata
|
408 |
|
|
b853fc0…
|
lmata
|
409 |
def test_has_all_top_level_keys(self): |
|
b853fc0…
|
lmata
|
410 |
result = self._build() |
|
b853fc0…
|
lmata
|
411 |
assert "files" in result |
|
b853fc0…
|
lmata
|
412 |
assert "symbols" in result |
|
b853fc0…
|
lmata
|
413 |
assert "knowledge" in result |
|
b853fc0…
|
lmata
|
414 |
assert "counts" in result |
|
b853fc0…
|
lmata
|
415 |
|
|
b853fc0…
|
lmata
|
416 |
def test_counts_match_lengths(self): |
|
b853fc0…
|
lmata
|
417 |
files = ["a.py", "b.py"] |
|
b853fc0…
|
lmata
|
418 |
symbols = [{"type": "Function", "name": "f", "file_path": "a.py", |
|
b853fc0…
|
lmata
|
419 |
"line_start": 1, "line_end": 5}] |
|
b853fc0…
|
lmata
|
420 |
knowledge = [{"type": "Concept", "name": "X", "description": "", |
|
b853fc0…
|
lmata
|
421 |
"domain": "", "status": ""}] |
|
b853fc0…
|
lmata
|
422 |
result = self._build(files=files, symbols=symbols, knowledge=knowledge) |
|
b853fc0…
|
lmata
|
423 |
assert result["counts"]["files"] == 2 |
|
b853fc0…
|
lmata
|
424 |
assert result["counts"]["symbols"] == 1 |
|
b853fc0…
|
lmata
|
425 |
assert result["counts"]["knowledge"] == 1 |
|
b853fc0…
|
lmata
|
426 |
|
|
b853fc0…
|
lmata
|
427 |
def test_empty_summary_all_zeros(self): |
|
b853fc0…
|
lmata
|
428 |
result = self._build() |
|
b853fc0…
|
lmata
|
429 |
assert result["counts"]["files"] == 0 |
|
b853fc0…
|
lmata
|
430 |
assert result["counts"]["symbols"] == 0 |
|
b853fc0…
|
lmata
|
431 |
assert result["counts"]["knowledge"] == 0 |
|
b853fc0…
|
lmata
|
432 |
|
|
b853fc0…
|
lmata
|
433 |
def test_files_list_propagated(self): |
|
b853fc0…
|
lmata
|
434 |
result = self._build(files=["x.py", "y.py"]) |
|
b853fc0…
|
lmata
|
435 |
assert result["files"] == ["x.py", "y.py"] |
|
b853fc0…
|
lmata
|
436 |
|
|
b853fc0…
|
lmata
|
437 |
|
|
b853fc0…
|
lmata
|
438 |
# ── DiffAnalyzer.to_json ────────────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
439 |
|
|
b853fc0…
|
lmata
|
440 |
|
|
b853fc0…
|
lmata
|
441 |
class TestDiffAnalyzerToJson: |
|
b853fc0…
|
lmata
|
442 |
def test_returns_valid_json(self): |
|
b853fc0…
|
lmata
|
443 |
store = _mock_store() |
|
b853fc0…
|
lmata
|
444 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
445 |
summary = {"files": [], "symbols": [], "knowledge": [], "counts": {"files": 0}} |
|
b853fc0…
|
lmata
|
446 |
with patch.object(analyzer, "impact_summary", return_value=summary): |
|
b853fc0…
|
lmata
|
447 |
output = analyzer.to_json() |
|
b853fc0…
|
lmata
|
448 |
parsed = json.loads(output) |
|
b853fc0…
|
lmata
|
449 |
assert isinstance(parsed, dict) |
|
b853fc0…
|
lmata
|
450 |
|
|
b853fc0…
|
lmata
|
451 |
def test_json_contains_summary_keys(self): |
|
b853fc0…
|
lmata
|
452 |
store = _mock_store() |
|
b853fc0…
|
lmata
|
453 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
454 |
summary = {"files": ["f.py"], "symbols": [], "knowledge": [], "counts": {"files": 1}} |
|
b853fc0…
|
lmata
|
455 |
with patch.object(analyzer, "impact_summary", return_value=summary): |
|
b853fc0…
|
lmata
|
456 |
output = analyzer.to_json() |
|
b853fc0…
|
lmata
|
457 |
parsed = json.loads(output) |
|
b853fc0…
|
lmata
|
458 |
assert "files" in parsed |
|
b853fc0…
|
lmata
|
459 |
|
|
b853fc0…
|
lmata
|
460 |
|
|
b853fc0…
|
lmata
|
461 |
# ── DiffAnalyzer.to_markdown ────────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
462 |
|
|
b853fc0…
|
lmata
|
463 |
|
|
b853fc0…
|
lmata
|
464 |
class TestDiffAnalyzerToMarkdown: |
|
b853fc0…
|
lmata
|
465 |
def _md(self, files=None, symbols=None, knowledge=None): |
|
b853fc0…
|
lmata
|
466 |
store = _mock_store() |
|
b853fc0…
|
lmata
|
467 |
analyzer = _analyzer(store=store) |
|
b853fc0…
|
lmata
|
468 |
summary = { |
|
b853fc0…
|
lmata
|
469 |
"files": files or [], |
|
b853fc0…
|
lmata
|
470 |
"symbols": symbols or [], |
|
b853fc0…
|
lmata
|
471 |
"knowledge": knowledge or [], |
|
b853fc0…
|
lmata
|
472 |
"counts": { |
|
b853fc0…
|
lmata
|
473 |
"files": len(files or []), |
|
b853fc0…
|
lmata
|
474 |
"symbols": len(symbols or []), |
|
b853fc0…
|
lmata
|
475 |
"knowledge": len(knowledge or []), |
|
b853fc0…
|
lmata
|
476 |
}, |
|
b853fc0…
|
lmata
|
477 |
} |
|
b853fc0…
|
lmata
|
478 |
with patch.object(analyzer, "impact_summary", return_value=summary): |
|
b853fc0…
|
lmata
|
479 |
return analyzer.to_markdown() |
|
b853fc0…
|
lmata
|
480 |
|
|
b853fc0…
|
lmata
|
481 |
def test_returns_string(self): |
|
b853fc0…
|
lmata
|
482 |
assert isinstance(self._md(), str) |
|
b853fc0…
|
lmata
|
483 |
|
|
b853fc0…
|
lmata
|
484 |
def test_contains_heading(self): |
|
b853fc0…
|
lmata
|
485 |
assert "Diff Impact Summary" in self._md() |
|
b853fc0…
|
lmata
|
486 |
|
|
b853fc0…
|
lmata
|
487 |
def test_lists_changed_file(self): |
|
b853fc0…
|
lmata
|
488 |
md = self._md(files=["src/main.py"]) |
|
b853fc0…
|
lmata
|
489 |
assert "src/main.py" in md |
|
b853fc0…
|
lmata
|
490 |
|
|
b853fc0…
|
lmata
|
491 |
def test_lists_affected_symbol(self): |
|
b853fc0…
|
lmata
|
492 |
syms = [{"type": "Function", "name": "pay", "file_path": "billing.py", |
|
b853fc0…
|
lmata
|
493 |
"line_start": 10, "line_end": 20}] |
|
b853fc0…
|
lmata
|
494 |
md = self._md(symbols=syms) |
|
b853fc0…
|
lmata
|
495 |
assert "pay" in md |
|
b853fc0…
|
lmata
|
496 |
|
|
b853fc0…
|
lmata
|
497 |
def test_lists_knowledge_node(self): |
|
b853fc0…
|
lmata
|
498 |
know = [{"type": "Rule", "name": "no_double_charge", |
|
b853fc0…
|
lmata
|
499 |
"description": "desc", "domain": "", "status": ""}] |
|
b853fc0…
|
lmata
|
500 |
md = self._md(knowledge=know) |
|
b853fc0…
|
lmata
|
501 |
assert "no_double_charge" in md |
|
b853fc0…
|
lmata
|
502 |
|
|
b853fc0…
|
lmata
|
503 |
def test_empty_sections_show_placeholder(self): |
|
b853fc0…
|
lmata
|
504 |
md = self._md() |
|
b853fc0…
|
lmata
|
505 |
assert "No changed files" in md |
|
b853fc0…
|
lmata
|
506 |
assert "No affected symbols" in md |
|
b853fc0…
|
lmata
|
507 |
assert "No linked knowledge" in md |
|
b853fc0…
|
lmata
|
508 |
|
|
b853fc0…
|
lmata
|
509 |
|
|
b853fc0…
|
lmata
|
510 |
# ── CLI: navegador diff ──────────────────────────────────────────────────────── |
|
b853fc0…
|
lmata
|
511 |
|
|
b853fc0…
|
lmata
|
512 |
|
|
b853fc0…
|
lmata
|
513 |
class TestCLIDiffCommand: |
|
b853fc0…
|
lmata
|
514 |
def _runner(self): |
|
b853fc0…
|
lmata
|
515 |
return CliRunner() |
|
b853fc0…
|
lmata
|
516 |
|
|
b853fc0…
|
lmata
|
517 |
def _mock_analyzer(self, summary=None): |
|
b853fc0…
|
lmata
|
518 |
"""Patch DiffAnalyzer so it never touches git or the graph.""" |
|
b853fc0…
|
lmata
|
519 |
if summary is None: |
|
b853fc0…
|
lmata
|
520 |
summary = { |
|
b853fc0…
|
lmata
|
521 |
"files": ["app.py"], |
|
b853fc0…
|
lmata
|
522 |
"symbols": [{"type": "Function", "name": "run", |
|
b853fc0…
|
lmata
|
523 |
"file_path": "app.py", "line_start": 1, "line_end": 10}], |
|
b853fc0…
|
lmata
|
524 |
"knowledge": [], |
|
b853fc0…
|
lmata
|
525 |
"counts": {"files": 1, "symbols": 1, "knowledge": 0}, |
|
b853fc0…
|
lmata
|
526 |
} |
|
b853fc0…
|
lmata
|
527 |
mock_inst = MagicMock() |
|
b853fc0…
|
lmata
|
528 |
mock_inst.impact_summary.return_value = summary |
|
b853fc0…
|
lmata
|
529 |
mock_inst.to_json.return_value = json.dumps(summary, indent=2) |
|
b853fc0…
|
lmata
|
530 |
mock_inst.to_markdown.return_value = "# Diff Impact Summary\n\n## Changed Files (1)" |
|
b853fc0…
|
lmata
|
531 |
return mock_inst |
|
b853fc0…
|
lmata
|
532 |
|
|
b853fc0…
|
lmata
|
533 |
def test_command_exists(self): |
|
b853fc0…
|
lmata
|
534 |
runner = self._runner() |
|
b853fc0…
|
lmata
|
535 |
result = runner.invoke(main, ["diff", "--help"]) |
|
b853fc0…
|
lmata
|
536 |
assert result.exit_code == 0 |
|
b853fc0…
|
lmata
|
537 |
|
|
b853fc0…
|
lmata
|
538 |
def test_markdown_output_by_default(self, tmp_path): |
|
b853fc0…
|
lmata
|
539 |
runner = self._runner() |
|
b853fc0…
|
lmata
|
540 |
mock_inst = self._mock_analyzer() |
|
b853fc0…
|
lmata
|
541 |
with ( |
|
b853fc0…
|
lmata
|
542 |
runner.isolated_filesystem(), |
|
b853fc0…
|
lmata
|
543 |
patch("navegador.cli.commands._get_store", return_value=_mock_store()), |
|
b853fc0…
|
lmata
|
544 |
patch("navegador.diff.DiffAnalyzer", return_value=mock_inst), |
|
b853fc0…
|
lmata
|
545 |
): |
|
b853fc0…
|
lmata
|
546 |
result = runner.invoke(main, ["diff", "--repo", str(tmp_path)]) |
|
b853fc0…
|
lmata
|
547 |
assert result.exit_code == 0 |
|
b853fc0…
|
lmata
|
548 |
assert "Diff Impact Summary" in result.output |
|
b853fc0…
|
lmata
|
549 |
|
|
b853fc0…
|
lmata
|
550 |
def test_json_output_flag(self, tmp_path): |
|
b853fc0…
|
lmata
|
551 |
runner = self._runner() |
|
b853fc0…
|
lmata
|
552 |
mock_inst = self._mock_analyzer() |
|
b853fc0…
|
lmata
|
553 |
with ( |
|
b853fc0…
|
lmata
|
554 |
runner.isolated_filesystem(), |
|
b853fc0…
|
lmata
|
555 |
patch("navegador.cli.commands._get_store", return_value=_mock_store()), |
|
b853fc0…
|
lmata
|
556 |
patch("navegador.diff.DiffAnalyzer", return_value=mock_inst), |
|
b853fc0…
|
lmata
|
557 |
): |
|
b853fc0…
|
lmata
|
558 |
result = runner.invoke(main, ["diff", "--format", "json", "--repo", str(tmp_path)]) |
|
b853fc0…
|
lmata
|
559 |
assert result.exit_code == 0 |
|
b853fc0…
|
lmata
|
560 |
parsed = json.loads(result.output) |
|
b853fc0…
|
lmata
|
561 |
assert "files" in parsed |
|
b853fc0…
|
lmata
|
562 |
|
|
b853fc0…
|
lmata
|
563 |
def test_json_is_valid(self, tmp_path): |
|
b853fc0…
|
lmata
|
564 |
runner = self._runner() |
|
b853fc0…
|
lmata
|
565 |
summary = { |
|
b853fc0…
|
lmata
|
566 |
"files": ["x.py"], |
|
b853fc0…
|
lmata
|
567 |
"symbols": [], |
|
b853fc0…
|
lmata
|
568 |
"knowledge": [], |
|
b853fc0…
|
lmata
|
569 |
"counts": {"files": 1, "symbols": 0, "knowledge": 0}, |
|
b853fc0…
|
lmata
|
570 |
} |
|
b853fc0…
|
lmata
|
571 |
mock_inst = self._mock_analyzer(summary=summary) |
|
b853fc0…
|
lmata
|
572 |
with ( |
|
b853fc0…
|
lmata
|
573 |
runner.isolated_filesystem(), |
|
b853fc0…
|
lmata
|
574 |
patch("navegador.cli.commands._get_store", return_value=_mock_store()), |
|
b853fc0…
|
lmata
|
575 |
patch("navegador.diff.DiffAnalyzer", return_value=mock_inst), |
|
b853fc0…
|
lmata
|
576 |
): |
|
b853fc0…
|
lmata
|
577 |
result = runner.invoke(main, ["diff", "--format", "json", "--repo", str(tmp_path)]) |
|
b853fc0…
|
lmata
|
578 |
assert result.exit_code == 0 |
|
b853fc0…
|
lmata
|
579 |
data = json.loads(result.output) |
|
b853fc0…
|
lmata
|
580 |
assert data["files"] == ["x.py"] |
|
b853fc0…
|
lmata
|
581 |
assert data["counts"]["files"] == 1 |