Navegador

navegador / tests / test_churn.py
Source Blame History 461 lines
33fd0c0… lmata 1 """Tests for navegador.churn — ChurnAnalyzer and the `churn` CLI command."""
33fd0c0… lmata 2
33fd0c0… lmata 3 from __future__ import annotations
33fd0c0… lmata 4
33fd0c0… lmata 5 import json
33fd0c0… lmata 6 from pathlib import Path
33fd0c0… lmata 7 from unittest.mock import MagicMock, patch
33fd0c0… lmata 8
33fd0c0… lmata 9 import pytest
33fd0c0… lmata 10 from click.testing import CliRunner
33fd0c0… lmata 11
33fd0c0… lmata 12 from navegador.churn import ChurnAnalyzer, ChurnEntry, CouplingPair
33fd0c0… lmata 13 from navegador.cli.commands import main
33fd0c0… lmata 14
33fd0c0… lmata 15
33fd0c0… lmata 16 # ── Helpers ───────────────────────────────────────────────────────────────────
33fd0c0… lmata 17
33fd0c0… lmata 18 # Fake git log --format=%H --name-only output
33fd0c0… lmata 19 # Three commits (all-hex 40-char hashes):
33fd0c0… lmata 20 # aaaa... touches a.py, b.py
33fd0c0… lmata 21 # bbbb... touches b.py, c.py
33fd0c0… lmata 22 # cccc... touches a.py, b.py, c.py
33fd0c0… lmata 23 GIT_LOG_NAME_ONLY = """\
33fd0c0… lmata 24 aaaa111111111111111111111111111111111111
33fd0c0… lmata 25
33fd0c0… lmata 26 a.py
33fd0c0… lmata 27 b.py
33fd0c0… lmata 28 bbbb222222222222222222222222222222222222
33fd0c0… lmata 29
33fd0c0… lmata 30 b.py
33fd0c0… lmata 31 c.py
33fd0c0… lmata 32 cccc333333333333333333333333333333333333
33fd0c0… lmata 33
33fd0c0… lmata 34 a.py
33fd0c0… lmata 35 b.py
33fd0c0… lmata 36 c.py
33fd0c0… lmata 37 """
33fd0c0… lmata 38
33fd0c0… lmata 39 # Fake git log --numstat --format= output
33fd0c0… lmata 40 GIT_LOG_NUMSTAT = """\
33fd0c0… lmata 41 10\t2\ta.py
33fd0c0… lmata 42 5\t1\tb.py
33fd0c0… lmata 43 3\t0\tb.py
33fd0c0… lmata 44 2\t2\tc.py
33fd0c0… lmata 45 8\t1\ta.py
33fd0c0… lmata 46 4\t1\tb.py
33fd0c0… lmata 47 1\t1\tc.py
33fd0c0… lmata 48 """
33fd0c0… lmata 49
33fd0c0… lmata 50
33fd0c0… lmata 51 def _make_analyzer(tmp_path: Path) -> ChurnAnalyzer:
33fd0c0… lmata 52 """Return a ChurnAnalyzer pointed at a temp dir (git not required)."""
33fd0c0… lmata 53 return ChurnAnalyzer(tmp_path, limit=500)
33fd0c0… lmata 54
33fd0c0… lmata 55
33fd0c0… lmata 56 def _mock_run(name_only_output: str = GIT_LOG_NAME_ONLY,
33fd0c0… lmata 57 numstat_output: str = GIT_LOG_NUMSTAT):
33fd0c0… lmata 58 """Return a side_effect function for ChurnAnalyzer._run that dispatches
33fd0c0… lmata 59 on the git args list."""
33fd0c0… lmata 60
33fd0c0… lmata 61 def _side_effect(args: list[str]) -> str:
33fd0c0… lmata 62 if "--name-only" in args:
33fd0c0… lmata 63 return name_only_output
33fd0c0… lmata 64 if "--numstat" in args:
33fd0c0… lmata 65 return numstat_output
33fd0c0… lmata 66 return ""
33fd0c0… lmata 67
33fd0c0… lmata 68 return _side_effect
33fd0c0… lmata 69
33fd0c0… lmata 70
33fd0c0… lmata 71 # ── ChurnEntry / CouplingPair dataclasses ─────────────────────────────────────
33fd0c0… lmata 72
33fd0c0… lmata 73
33fd0c0… lmata 74 class TestDataclasses:
33fd0c0… lmata 75 def test_churn_entry_fields(self):
33fd0c0… lmata 76 e = ChurnEntry(file_path="foo.py", commit_count=5, lines_changed=100)
33fd0c0… lmata 77 assert e.file_path == "foo.py"
33fd0c0… lmata 78 assert e.commit_count == 5
33fd0c0… lmata 79 assert e.lines_changed == 100
33fd0c0… lmata 80
33fd0c0… lmata 81 def test_coupling_pair_fields(self):
33fd0c0… lmata 82 p = CouplingPair(file_a="a.py", file_b="b.py", co_change_count=3, confidence=0.75)
33fd0c0… lmata 83 assert p.file_a == "a.py"
33fd0c0… lmata 84 assert p.file_b == "b.py"
33fd0c0… lmata 85 assert p.co_change_count == 3
33fd0c0… lmata 86 assert p.confidence == 0.75
33fd0c0… lmata 87
33fd0c0… lmata 88
33fd0c0… lmata 89 # ── file_churn ────────────────────────────────────────────────────────────────
33fd0c0… lmata 90
33fd0c0… lmata 91
33fd0c0… lmata 92 class TestFileChurn:
33fd0c0… lmata 93 def test_returns_list_of_churn_entries(self, tmp_path):
33fd0c0… lmata 94 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 95 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 96 result = analyzer.file_churn()
33fd0c0… lmata 97 assert isinstance(result, list)
33fd0c0… lmata 98 assert all(isinstance(e, ChurnEntry) for e in result)
33fd0c0… lmata 99
33fd0c0… lmata 100 def test_commit_counts_are_correct(self, tmp_path):
33fd0c0… lmata 101 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 102 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 103 result = analyzer.file_churn()
33fd0c0… lmata 104
33fd0c0… lmata 105 counts = {e.file_path: e.commit_count for e in result}
33fd0c0… lmata 106 # a.py: commits abc + ghi = 2
33fd0c0… lmata 107 assert counts["a.py"] == 2
33fd0c0… lmata 108 # b.py: commits abc + def + ghi = 3
33fd0c0… lmata 109 assert counts["b.py"] == 3
33fd0c0… lmata 110 # c.py: commits def + ghi = 2
33fd0c0… lmata 111 assert counts["c.py"] == 2
33fd0c0… lmata 112
33fd0c0… lmata 113 def test_sorted_by_commit_count_descending(self, tmp_path):
33fd0c0… lmata 114 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 115 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 116 result = analyzer.file_churn()
33fd0c0… lmata 117 counts = [e.commit_count for e in result]
33fd0c0… lmata 118 assert counts == sorted(counts, reverse=True)
33fd0c0… lmata 119
33fd0c0… lmata 120 def test_lines_changed_aggregated(self, tmp_path):
33fd0c0… lmata 121 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 122 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 123 result = analyzer.file_churn()
33fd0c0… lmata 124 by_file = {e.file_path: e.lines_changed for e in result}
33fd0c0… lmata 125 # a.py: (10+2) + (8+1) = 21
33fd0c0… lmata 126 assert by_file["a.py"] == 21
33fd0c0… lmata 127 # b.py: (5+1) + (3+0) + (4+1) = 14
33fd0c0… lmata 128 assert by_file["b.py"] == 14
33fd0c0… lmata 129 # c.py: (2+2) + (1+1) = 6
33fd0c0… lmata 130 assert by_file["c.py"] == 6
33fd0c0… lmata 131
33fd0c0… lmata 132 def test_empty_git_output_returns_empty_list(self, tmp_path):
33fd0c0… lmata 133 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 134 with patch.object(analyzer, "_run", return_value=""):
33fd0c0… lmata 135 result = analyzer.file_churn()
33fd0c0… lmata 136 assert result == []
33fd0c0… lmata 137
33fd0c0… lmata 138 def test_binary_files_skipped_in_lines_changed(self, tmp_path):
33fd0c0… lmata 139 numstat_with_binary = "-\t-\timage.png\n10\t2\ta.py\n"
33fd0c0… lmata 140 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 141 with patch.object(
33fd0c0… lmata 142 analyzer, "_run",
33fd0c0… lmata 143 side_effect=_mock_run(numstat_output=numstat_with_binary)
33fd0c0… lmata 144 ):
33fd0c0… lmata 145 result = analyzer.file_churn()
33fd0c0… lmata 146 by_file = {e.file_path: e.lines_changed for e in result}
33fd0c0… lmata 147 # Binary file should not cause a crash; a.py lines should still be counted
33fd0c0… lmata 148 assert by_file.get("a.py", 0) == 12
33fd0c0… lmata 149
33fd0c0… lmata 150
33fd0c0… lmata 151 # ── coupling_pairs ────────────────────────────────────────────────────────────
33fd0c0… lmata 152
33fd0c0… lmata 153
33fd0c0… lmata 154 class TestCouplingPairs:
33fd0c0… lmata 155 def test_returns_list_of_coupling_pairs(self, tmp_path):
33fd0c0… lmata 156 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 157 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 158 result = analyzer.coupling_pairs(min_co_changes=1, min_confidence=0.0)
33fd0c0… lmata 159 assert isinstance(result, list)
33fd0c0… lmata 160 assert all(isinstance(p, CouplingPair) for p in result)
33fd0c0… lmata 161
33fd0c0… lmata 162 def test_ab_pair_co_change_count(self, tmp_path):
33fd0c0… lmata 163 """a.py and b.py appear together in commits abc and ghi → co_change=2."""
33fd0c0… lmata 164 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 165 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 166 result = analyzer.coupling_pairs(min_co_changes=1, min_confidence=0.0)
33fd0c0… lmata 167 pairs_by_key = {(p.file_a, p.file_b): p for p in result}
33fd0c0… lmata 168 ab = pairs_by_key.get(("a.py", "b.py"))
33fd0c0… lmata 169 assert ab is not None
33fd0c0… lmata 170 assert ab.co_change_count == 2
33fd0c0… lmata 171
33fd0c0… lmata 172 def test_bc_pair_co_change_count(self, tmp_path):
33fd0c0… lmata 173 """b.py and c.py appear together in commits def and ghi → co_change=2."""
33fd0c0… lmata 174 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 175 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 176 result = analyzer.coupling_pairs(min_co_changes=1, min_confidence=0.0)
33fd0c0… lmata 177 pairs_by_key = {(p.file_a, p.file_b): p for p in result}
33fd0c0… lmata 178 bc = pairs_by_key.get(("b.py", "c.py"))
33fd0c0… lmata 179 assert bc is not None
33fd0c0… lmata 180 assert bc.co_change_count == 2
33fd0c0… lmata 181
33fd0c0… lmata 182 def test_confidence_formula(self, tmp_path):
33fd0c0… lmata 183 """confidence = co_change_count / max(changes_a, changes_b)."""
33fd0c0… lmata 184 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 185 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 186 result = analyzer.coupling_pairs(min_co_changes=1, min_confidence=0.0)
33fd0c0… lmata 187 pairs_by_key = {(p.file_a, p.file_b): p for p in result}
33fd0c0… lmata 188 # a.py: 2 commits, b.py: 3 commits, co=2 → 2/3 ≈ 0.6667
33fd0c0… lmata 189 ab = pairs_by_key[("a.py", "b.py")]
33fd0c0… lmata 190 assert abs(ab.confidence - round(2 / 3, 4)) < 0.001
33fd0c0… lmata 191
33fd0c0… lmata 192 def test_min_co_changes_filter(self, tmp_path):
33fd0c0… lmata 193 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 194 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 195 # All pairs have co_change ≤ 2, so requesting ≥ 3 returns nothing
33fd0c0… lmata 196 result = analyzer.coupling_pairs(min_co_changes=3, min_confidence=0.0)
33fd0c0… lmata 197 assert result == []
33fd0c0… lmata 198
33fd0c0… lmata 199 def test_min_confidence_filter(self, tmp_path):
33fd0c0… lmata 200 # Commit breakdown:
33fd0c0… lmata 201 # aaaa: a.py, b.py
33fd0c0… lmata 202 # bbbb: b.py, c.py
33fd0c0… lmata 203 # cccc: a.py, b.py, c.py
33fd0c0… lmata 204 #
33fd0c0… lmata 205 # commit counts: a=2, b=3, c=2
33fd0c0… lmata 206 # (a,b): co=2 → confidence=2/3≈0.667
33fd0c0… lmata 207 # (a,c): co=1 → confidence=1/2=0.5
33fd0c0… lmata 208 # (b,c): co=2 → confidence=2/3≈0.667
33fd0c0… lmata 209 #
33fd0c0… lmata 210 # At min_confidence=0.6: a/b and b/c pass; a/c does not.
33fd0c0… lmata 211 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 212 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 213 result = analyzer.coupling_pairs(min_co_changes=1, min_confidence=0.6)
33fd0c0… lmata 214 pairs_by_key = {(p.file_a, p.file_b): p for p in result}
33fd0c0… lmata 215 assert ("a.py", "b.py") in pairs_by_key
33fd0c0… lmata 216 assert ("b.py", "c.py") in pairs_by_key
33fd0c0… lmata 217 # a/c has confidence=0.5, below threshold
33fd0c0… lmata 218 assert ("a.py", "c.py") not in pairs_by_key
33fd0c0… lmata 219
33fd0c0… lmata 220 def test_sorted_by_co_change_count_descending(self, tmp_path):
33fd0c0… lmata 221 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 222 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 223 result = analyzer.coupling_pairs(min_co_changes=1, min_confidence=0.0)
33fd0c0… lmata 224 counts = [p.co_change_count for p in result]
33fd0c0… lmata 225 assert counts == sorted(counts, reverse=True)
33fd0c0… lmata 226
33fd0c0… lmata 227 def test_empty_history_returns_empty_list(self, tmp_path):
33fd0c0… lmata 228 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 229 with patch.object(analyzer, "_run", return_value=""):
33fd0c0… lmata 230 result = analyzer.coupling_pairs()
33fd0c0… lmata 231 assert result == []
33fd0c0… lmata 232
33fd0c0… lmata 233 def test_single_file_per_commit_no_pairs(self, tmp_path):
33fd0c0… lmata 234 """Commits touching only one file produce no coupling pairs."""
33fd0c0… lmata 235 log = (
33fd0c0… lmata 236 "abc1111111111111111111111111111111111111\n\na.py\n"
33fd0c0… lmata 237 "def2222222222222222222222222222222222222\n\nb.py\n"
33fd0c0… lmata 238 )
33fd0c0… lmata 239 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 240 with patch.object(analyzer, "_run", side_effect=_mock_run(name_only_output=log)):
33fd0c0… lmata 241 result = analyzer.coupling_pairs(min_co_changes=1, min_confidence=0.0)
33fd0c0… lmata 242 assert result == []
33fd0c0… lmata 243
33fd0c0… lmata 244
33fd0c0… lmata 245 # ── store_churn ───────────────────────────────────────────────────────────────
33fd0c0… lmata 246
33fd0c0… lmata 247
33fd0c0… lmata 248 class TestStoreChurn:
33fd0c0… lmata 249 def _make_store(self):
33fd0c0… lmata 250 store = MagicMock()
33fd0c0… lmata 251 store.query.return_value = MagicMock(
33fd0c0… lmata 252 nodes_modified=1, properties_set=2
33fd0c0… lmata 253 )
33fd0c0… lmata 254 return store
33fd0c0… lmata 255
33fd0c0… lmata 256 def test_returns_dict_with_expected_keys(self, tmp_path):
33fd0c0… lmata 257 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 258 store = self._make_store()
33fd0c0… lmata 259 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 260 result = analyzer.store_churn(store)
33fd0c0… lmata 261 assert "churn_updated" in result
33fd0c0… lmata 262 assert "couplings_written" in result
33fd0c0… lmata 263
33fd0c0… lmata 264 def test_churn_updated_count(self, tmp_path):
33fd0c0… lmata 265 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 266 store = self._make_store()
33fd0c0… lmata 267 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 268 result = analyzer.store_churn(store)
33fd0c0… lmata 269 # Three unique files → 3 churn updates
33fd0c0… lmata 270 assert result["churn_updated"] == 3
33fd0c0… lmata 271
33fd0c0… lmata 272 def test_store_query_called_for_each_file(self, tmp_path):
33fd0c0… lmata 273 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 274 store = self._make_store()
33fd0c0… lmata 275 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 276 analyzer.store_churn(store)
33fd0c0… lmata 277 # store.query must have been called at least 3 times (one per file)
33fd0c0… lmata 278 assert store.query.call_count >= 3
33fd0c0… lmata 279
33fd0c0… lmata 280 def test_coupled_with_edges_written(self, tmp_path):
33fd0c0… lmata 281 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 282 store = self._make_store()
33fd0c0… lmata 283 with patch.object(analyzer, "_run", side_effect=_mock_run()):
33fd0c0… lmata 284 result = analyzer.store_churn(store)
33fd0c0… lmata 285 # Default thresholds: min_co_changes=3, min_confidence=0.5
33fd0c0… lmata 286 # In our fixture all pairs have co_change ≤ 2, so couplings_written == 0
33fd0c0… lmata 287 assert isinstance(result["couplings_written"], int)
33fd0c0… lmata 288
33fd0c0… lmata 289 def test_coupled_with_edges_written_low_threshold(self, tmp_path):
33fd0c0… lmata 290 """With relaxed thresholds coupling edges should be written."""
33fd0c0… lmata 291 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 292 store = self._make_store()
33fd0c0… lmata 293 # Override coupling_pairs to always return pairs
33fd0c0… lmata 294 fake_pairs = [
33fd0c0… lmata 295 CouplingPair("a.py", "b.py", co_change_count=2, confidence=0.67),
33fd0c0… lmata 296 ]
33fd0c0… lmata 297 with patch.object(analyzer, "_run", side_effect=_mock_run()), \
33fd0c0… lmata 298 patch.object(analyzer, "coupling_pairs", return_value=fake_pairs):
33fd0c0… lmata 299 result = analyzer.store_churn(store)
33fd0c0… lmata 300 assert result["couplings_written"] == 1
33fd0c0… lmata 301
33fd0c0… lmata 302 def test_cypher_contains_coupled_with(self, tmp_path):
33fd0c0… lmata 303 """Verify the Cypher for edges references COUPLED_WITH."""
33fd0c0… lmata 304 analyzer = _make_analyzer(tmp_path)
33fd0c0… lmata 305 store = self._make_store()
33fd0c0… lmata 306 fake_pairs = [CouplingPair("a.py", "b.py", co_change_count=5, confidence=0.8)]
33fd0c0… lmata 307 with patch.object(analyzer, "_run", side_effect=_mock_run()), \
33fd0c0… lmata 308 patch.object(analyzer, "coupling_pairs", return_value=fake_pairs):
33fd0c0… lmata 309 analyzer.store_churn(store)
33fd0c0… lmata 310
33fd0c0… lmata 311 all_cypher_calls = [call[0][0] for call in store.query.call_args_list]
33fd0c0… lmata 312 edge_cyphers = [c for c in all_cypher_calls if "COUPLED_WITH" in c]
33fd0c0… lmata 313 assert len(edge_cyphers) == 1
33fd0c0… lmata 314
33fd0c0… lmata 315
33fd0c0… lmata 316 # ── CLI command ───────────────────────────────────────────────────────────────
33fd0c0… lmata 317
33fd0c0… lmata 318
33fd0c0… lmata 319 class TestChurnCLI:
33fd0c0… lmata 320 def _analyzer_patch(self, churn_entries=None, pairs=None):
33fd0c0… lmata 321 """Return a context manager that patches ChurnAnalyzer in the CLI module."""
33fd0c0… lmata 322 if churn_entries is None:
33fd0c0… lmata 323 churn_entries = [
33fd0c0… lmata 324 ChurnEntry("foo.py", commit_count=5, lines_changed=100),
33fd0c0… lmata 325 ChurnEntry("bar.py", commit_count=3, lines_changed=40),
33fd0c0… lmata 326 ]
33fd0c0… lmata 327 if pairs is None:
33fd0c0… lmata 328 pairs = [
33fd0c0… lmata 329 CouplingPair("bar.py", "foo.py", co_change_count=3, confidence=0.6),
33fd0c0… lmata 330 ]
33fd0c0… lmata 331
33fd0c0… lmata 332 mock_analyzer = MagicMock()
33fd0c0… lmata 333 mock_analyzer.file_churn.return_value = churn_entries
33fd0c0… lmata 334 mock_analyzer.coupling_pairs.return_value = pairs
33fd0c0… lmata 335
33fd0c0… lmata 336 return patch("navegador.churn.ChurnAnalyzer", return_value=mock_analyzer)
33fd0c0… lmata 337
33fd0c0… lmata 338 def test_basic_invocation_exits_zero(self, tmp_path):
33fd0c0… lmata 339 runner = CliRunner()
33fd0c0… lmata 340 with runner.isolated_filesystem():
33fd0c0… lmata 341 with self._analyzer_patch():
33fd0c0… lmata 342 result = runner.invoke(main, ["churn", str(tmp_path)])
33fd0c0… lmata 343 assert result.exit_code == 0, result.output
33fd0c0… lmata 344
33fd0c0… lmata 345 def test_json_output_has_expected_keys(self, tmp_path):
33fd0c0… lmata 346 runner = CliRunner()
33fd0c0… lmata 347 with runner.isolated_filesystem():
33fd0c0… lmata 348 with self._analyzer_patch():
33fd0c0… lmata 349 result = runner.invoke(main, ["churn", str(tmp_path), "--json"])
33fd0c0… lmata 350 assert result.exit_code == 0, result.output
33fd0c0… lmata 351 data = json.loads(result.output)
33fd0c0… lmata 352 assert "churn" in data
33fd0c0… lmata 353 assert "coupling_pairs" in data
33fd0c0… lmata 354
33fd0c0… lmata 355 def test_json_churn_entry_shape(self, tmp_path):
33fd0c0… lmata 356 runner = CliRunner()
33fd0c0… lmata 357 with runner.isolated_filesystem():
33fd0c0… lmata 358 with self._analyzer_patch():
33fd0c0… lmata 359 result = runner.invoke(main, ["churn", str(tmp_path), "--json"])
33fd0c0… lmata 360 data = json.loads(result.output)
33fd0c0… lmata 361 entry = data["churn"][0]
33fd0c0… lmata 362 assert "file_path" in entry
33fd0c0… lmata 363 assert "commit_count" in entry
33fd0c0… lmata 364 assert "lines_changed" in entry
33fd0c0… lmata 365
33fd0c0… lmata 366 def test_json_coupling_pair_shape(self, tmp_path):
33fd0c0… lmata 367 runner = CliRunner()
33fd0c0… lmata 368 with runner.isolated_filesystem():
33fd0c0… lmata 369 with self._analyzer_patch():
33fd0c0… lmata 370 result = runner.invoke(main, ["churn", str(tmp_path), "--json"])
33fd0c0… lmata 371 data = json.loads(result.output)
33fd0c0… lmata 372 pair = data["coupling_pairs"][0]
33fd0c0… lmata 373 assert "file_a" in pair
33fd0c0… lmata 374 assert "file_b" in pair
33fd0c0… lmata 375 assert "co_change_count" in pair
33fd0c0… lmata 376 assert "confidence" in pair
33fd0c0… lmata 377
33fd0c0… lmata 378 def test_limit_option_passed_to_analyzer(self, tmp_path):
33fd0c0… lmata 379 runner = CliRunner()
33fd0c0… lmata 380 mock_cls = MagicMock()
33fd0c0… lmata 381 mock_instance = MagicMock()
33fd0c0… lmata 382 mock_instance.file_churn.return_value = []
33fd0c0… lmata 383 mock_instance.coupling_pairs.return_value = []
33fd0c0… lmata 384 mock_cls.return_value = mock_instance
33fd0c0… lmata 385
33fd0c0… lmata 386 with runner.isolated_filesystem():
33fd0c0… lmata 387 with patch("navegador.churn.ChurnAnalyzer", mock_cls):
33fd0c0… lmata 388 runner.invoke(main, ["churn", str(tmp_path), "--limit", "100"])
33fd0c0… lmata 389
33fd0c0… lmata 390 _, kwargs = mock_cls.call_args
33fd0c0… lmata 391 assert kwargs.get("limit") == 100 or mock_cls.call_args[0][1] == 100
33fd0c0… lmata 392
33fd0c0… lmata 393 def test_min_confidence_passed_to_coupling_pairs(self, tmp_path):
33fd0c0… lmata 394 runner = CliRunner()
33fd0c0… lmata 395 mock_cls = MagicMock()
33fd0c0… lmata 396 mock_instance = MagicMock()
33fd0c0… lmata 397 mock_instance.file_churn.return_value = []
33fd0c0… lmata 398 mock_instance.coupling_pairs.return_value = []
33fd0c0… lmata 399 mock_cls.return_value = mock_instance
33fd0c0… lmata 400
33fd0c0… lmata 401 with runner.isolated_filesystem():
33fd0c0… lmata 402 with patch("navegador.churn.ChurnAnalyzer", mock_cls):
33fd0c0… lmata 403 runner.invoke(main, ["churn", str(tmp_path), "--min-confidence", "0.8"])
33fd0c0… lmata 404
33fd0c0… lmata 405 mock_instance.coupling_pairs.assert_called_once()
33fd0c0… lmata 406 _, kwargs = mock_instance.coupling_pairs.call_args
33fd0c0… lmata 407 assert kwargs.get("min_confidence") == 0.8
33fd0c0… lmata 408
33fd0c0… lmata 409 def test_store_flag_calls_store_churn(self, tmp_path):
33fd0c0… lmata 410 runner = CliRunner()
33fd0c0… lmata 411 mock_cls = MagicMock()
33fd0c0… lmata 412 mock_instance = MagicMock()
33fd0c0… lmata 413 mock_instance.store_churn.return_value = {
33fd0c0… lmata 414 "churn_updated": 2,
33fd0c0… lmata 415 "couplings_written": 1,
33fd0c0… lmata 416 }
33fd0c0… lmata 417 mock_cls.return_value = mock_instance
33fd0c0… lmata 418
33fd0c0… lmata 419 with runner.isolated_filesystem():
33fd0c0… lmata 420 with patch("navegador.churn.ChurnAnalyzer", mock_cls), \
33fd0c0… lmata 421 patch("navegador.cli.commands._get_store", return_value=MagicMock()):
33fd0c0… lmata 422 result = runner.invoke(main, ["churn", str(tmp_path), "--store"])
33fd0c0… lmata 423
33fd0c0… lmata 424 assert result.exit_code == 0, result.output
33fd0c0… lmata 425 mock_instance.store_churn.assert_called_once()
33fd0c0… lmata 426
33fd0c0… lmata 427 def test_store_json_flag_outputs_stats(self, tmp_path):
33fd0c0… lmata 428 runner = CliRunner()
33fd0c0… lmata 429 mock_cls = MagicMock()
33fd0c0… lmata 430 mock_instance = MagicMock()
33fd0c0… lmata 431 mock_instance.store_churn.return_value = {
33fd0c0… lmata 432 "churn_updated": 5,
33fd0c0… lmata 433 "couplings_written": 2,
33fd0c0… lmata 434 }
33fd0c0… lmata 435 mock_cls.return_value = mock_instance
33fd0c0… lmata 436
33fd0c0… lmata 437 with runner.isolated_filesystem():
33fd0c0… lmata 438 with patch("navegador.churn.ChurnAnalyzer", mock_cls), \
33fd0c0… lmata 439 patch("navegador.cli.commands._get_store", return_value=MagicMock()):
33fd0c0… lmata 440 result = runner.invoke(main, ["churn", str(tmp_path), "--store", "--json"])
33fd0c0… lmata 441
33fd0c0… lmata 442 assert result.exit_code == 0, result.output
33fd0c0… lmata 443 data = json.loads(result.output)
33fd0c0… lmata 444 assert data["churn_updated"] == 5
33fd0c0… lmata 445 assert data["couplings_written"] == 2
33fd0c0… lmata 446
33fd0c0… lmata 447 def test_no_pairs_shows_message(self, tmp_path):
33fd0c0… lmata 448 runner = CliRunner()
33fd0c0… lmata 449 with runner.isolated_filesystem():
33fd0c0… lmata 450 with self._analyzer_patch(pairs=[]):
33fd0c0… lmata 451 result = runner.invoke(main, ["churn", str(tmp_path)])
33fd0c0… lmata 452 assert result.exit_code == 0
33fd0c0… lmata 453 assert "No coupling pairs found" in result.output
33fd0c0… lmata 454
33fd0c0… lmata 455 def test_table_output_contains_file_names(self, tmp_path):
33fd0c0… lmata 456 runner = CliRunner()
33fd0c0… lmata 457 with runner.isolated_filesystem():
33fd0c0… lmata 458 with self._analyzer_patch():
33fd0c0… lmata 459 result = runner.invoke(main, ["churn", str(tmp_path)])
33fd0c0… lmata 460 assert "foo.py" in result.output
33fd0c0… lmata 461 assert "bar.py" in result.output

Keyboard Shortcuts

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