gabriel / muse public
test_cmd_clones.py python
864 lines 32.5 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for ``muse code clones``.
2
3 Coverage layers
4 ---------------
5 Unit
6 find_clones — exact tier, near tier, both, kind_filter, language_filter,
7 file_filter, exclude_same_file, min_cluster, empty manifest.
8 _all_same_file — single file, multi-file.
9 _file_hotspots — ranking, top-N cap, empty input.
10 _CloneCluster — to_dict count is int (not str), member fields present.
11
12 Integration (live repo via CliRunner)
13 Exits zero for all valid tier values.
14 JSON schema: all required top-level keys, correct types.
15 JSON: count field is int (type-regression guard).
16 JSON: branch field present and non-empty.
17 JSON: total_symbols_involved matches sum of cluster member counts.
18 JSON: file_hotspots is a ranked list of dicts.
19 --tier exact, near, both.
20 --kind restricts output symbols.
21 --language restricts to that language.
22 --file restricts to path prefix.
23 --exclude-same-file removes same-file clusters.
24 --min-cluster < 2 rejected.
25 --min-cluster 3 raises minimum size.
26 --commit HEAD analyses specific snapshot.
27 --commit invalid ref exits non-zero.
28 Text output contains all section headers.
29 No-repo exits non-zero.
30 Empty repo (no commits) exits non-zero.
31
32 E2E (real duplicate symbols in a live repo)
33 Exact clone detected when two files contain identical function bodies.
34 Near-clone detected when two files share a signature but differ in body.
35 No false-positive clones in a repo with unique symbols only.
36 --exclude-same-file removes a same-file cluster but keeps cross-file ones.
37 file_hotspots ranks the file with the most clones first.
38
39 Stress
40 10 000 symbols, 1 000 exact-clone pairs: correct count, fast.
41 Large near-clone group: all members present, no duplicates.
42 Repeated runs: identical deterministic output.
43 """
44
45 from __future__ import annotations
46
47 import json
48 import pathlib
49 import textwrap
50 import time
51 from typing import TypedDict
52
53 import pytest
54 from tests.cli_test_helper import CliRunner
55
56 from muse.cli.commands.clones import (
57 CloneTier,
58 _CloneCluster,
59 _all_same_file,
60 _file_hotspots,
61 find_clones,
62 )
63 from muse.plugins.code.ast_parser import SymbolKind, SymbolRecord, SymbolTree
64
65 cli = None # argparse migration — CliRunner ignores this arg
66 runner = CliRunner()
67
68 type _SymMap = dict[str, SymbolTree]
69 type _SymMapInput = dict[str, list[tuple[str, SymbolRecord]]]
70
71
72 # ---------------------------------------------------------------------------
73 # Typed payload for JSON assertions
74 # ---------------------------------------------------------------------------
75
76
77 class _MemberEntry(TypedDict):
78 address: str
79 kind: str
80 language: str
81 body_hash: str
82 signature_id: str
83 content_id: str
84
85
86 class _ClusterEntry(TypedDict):
87 tier: str
88 hash: str
89 count: int
90 members: list[_MemberEntry]
91
92
93 class _HotspotEntry(TypedDict):
94 file: str
95 clone_symbols: int
96
97
98 class _ClonesPayload(TypedDict):
99 schema_version: str
100 commit: str
101 branch: str
102 tier: str
103 min_cluster: int
104 kind_filter: str | None
105 language_filter: str | None
106 file_filter: str | None
107 exclude_same_file: bool
108 exact_clone_clusters: int
109 near_clone_clusters: int
110 total_symbols_involved: int
111 file_hotspots: list[_HotspotEntry]
112 clusters: list[_ClusterEntry]
113
114
115 # ---------------------------------------------------------------------------
116 # Test helpers
117 # ---------------------------------------------------------------------------
118
119
120 def _make_record(
121 kind: SymbolKind = "function",
122 body_hash: str = "aabbccdd",
123 sig_id: str = "11223344",
124 content_id: str = "deadbeef",
125 ) -> SymbolRecord:
126 return SymbolRecord(
127 kind=kind,
128 name="fn",
129 qualified_name="fn",
130 lineno=1,
131 end_lineno=5,
132 content_id=content_id * 8,
133 body_hash=body_hash * 8,
134 signature_id=sig_id * 8,
135 metadata_id="",
136 canonical_key="",
137 )
138
139
140 def _make_sym_map(
141 files: _SymMapInput,
142 ) -> _SymMap:
143 """Build a sym_map from a {file_path: [(addr, record), ...]} dict."""
144 result: _SymMap = {}
145 for fp, entries in files.items():
146 tree: SymbolTree = {addr: rec for addr, rec in entries}
147 result[fp] = tree
148 return result
149
150
151 def _clones_json(args: list[str] | None = None) -> _ClonesPayload:
152 cmd = ["code", "clones", "--json"] + (args or [])
153 result = runner.invoke(cli, cmd)
154 assert result.exit_code == 0, result.output
155 raw: _ClonesPayload = json.loads(result.output)
156 return raw
157
158
159 # ---------------------------------------------------------------------------
160 # Fixtures
161 # ---------------------------------------------------------------------------
162
163
164 @pytest.fixture
165 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
166 monkeypatch.chdir(tmp_path)
167 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
168 result = runner.invoke(cli, ["init", "--domain", "code"])
169 assert result.exit_code == 0, result.output
170 return tmp_path
171
172
173 @pytest.fixture
174 def code_repo(repo: pathlib.Path) -> pathlib.Path:
175 """Repo with a single committed Python file — no duplicates."""
176 (repo / "billing.py").write_text(textwrap.dedent("""\
177 def compute_total(items):
178 return sum(items)
179
180 def apply_discount(total, pct):
181 return total * (1 - pct)
182 """))
183 r = runner.invoke(cli, ["commit", "-m", "Initial"])
184 assert r.exit_code == 0, r.output
185 return repo
186
187
188 @pytest.fixture
189 def exact_clone_repo(repo: pathlib.Path) -> pathlib.Path:
190 """Two files with identical content — exact clone.
191
192 Uses genuinely byte-for-byte identical files to exercise the
193 SymbolCache re-key path (_rekey_tree) that was fixed to handle
194 same-SHA-256 files without conflating their addresses.
195 """
196 body = textwrap.dedent("""\
197 def helper(x):
198 return x * 2
199 """)
200 (repo / "a.py").write_text(body)
201 (repo / "b.py").write_text(body)
202 r = runner.invoke(cli, ["commit", "-m", "Exact clone"])
203 assert r.exit_code == 0, r.output
204 return repo
205
206
207 @pytest.fixture
208 def near_clone_repo(repo: pathlib.Path) -> pathlib.Path:
209 """Two files with the same function signature but different bodies — near-clone."""
210 (repo / "a.py").write_text(textwrap.dedent("""\
211 def transform(x: int) -> int:
212 return x * 2
213 """))
214 (repo / "b.py").write_text(textwrap.dedent("""\
215 def transform(x: int) -> int:
216 return x + 10
217 """))
218 r = runner.invoke(cli, ["commit", "-m", "Near clone"])
219 assert r.exit_code == 0, r.output
220 return repo
221
222
223 @pytest.fixture
224 def mixed_clone_repo(repo: pathlib.Path) -> pathlib.Path:
225 """Repo with both exact and near clones plus an isolated file."""
226 identical_body = textwrap.dedent("""\
227 def shared(x):
228 return x
229 """)
230 (repo / "alpha.py").write_text(identical_body)
231 (repo / "beta.py").write_text(identical_body)
232 (repo / "gamma.py").write_text(textwrap.dedent("""\
233 def shared(x):
234 return x + 1
235 """))
236 (repo / "unique.py").write_text(textwrap.dedent("""\
237 def one_of_a_kind():
238 return 42
239 """))
240 r = runner.invoke(cli, ["commit", "-m", "Mixed clones"])
241 assert r.exit_code == 0, r.output
242 return repo
243
244
245 @pytest.fixture
246 def same_file_clone_repo(repo: pathlib.Path) -> pathlib.Path:
247 """One file with two identical helper functions (same-file clone) plus
248 a second file that also shares the same body (cross-file clone).
249
250 utils.py: _helper_a and _helper_b are same-file clones of each other,
251 AND of _helper_c in other.py.
252 other.py: _helper_c is a cross-file clone of utils.py's helpers.
253 """
254 (repo / "utils.py").write_text(textwrap.dedent("""\
255 def _helper_a(x):
256 return x * 2
257
258 def _helper_b(x):
259 return x * 2
260 """))
261 (repo / "other.py").write_text(textwrap.dedent("""\
262 def _helper_c(x):
263 return x * 2
264 """))
265 r = runner.invoke(cli, ["commit", "-m", "Same-file clone"])
266 assert r.exit_code == 0, r.output
267 return repo
268
269
270 # ---------------------------------------------------------------------------
271 # Unit — _all_same_file
272 # ---------------------------------------------------------------------------
273
274
275 class TestAllSameFile:
276 def test_single_member_same_file(self) -> None:
277 members = [("src/a.py::fn", _make_record())]
278 assert _all_same_file(members) is True
279
280 def test_two_members_same_file(self) -> None:
281 rec = _make_record()
282 members = [("src/a.py::fn1", rec), ("src/a.py::fn2", rec)]
283 assert _all_same_file(members) is True
284
285 def test_two_members_different_files(self) -> None:
286 rec = _make_record()
287 members = [("src/a.py::fn", rec), ("src/b.py::fn", rec)]
288 assert _all_same_file(members) is False
289
290 def test_three_members_one_different(self) -> None:
291 rec = _make_record()
292 members = [
293 ("src/a.py::fn", rec),
294 ("src/a.py::gn", rec),
295 ("src/b.py::fn", rec),
296 ]
297 assert _all_same_file(members) is False
298
299
300 # ---------------------------------------------------------------------------
301 # Unit — _file_hotspots
302 # ---------------------------------------------------------------------------
303
304
305 class TestFileHotspots:
306 def _cluster(self, addresses: list[str]) -> _CloneCluster:
307 rec = _make_record()
308 return _CloneCluster("exact", "aabb", [(a, rec) for a in addresses])
309
310 def test_empty_clusters_returns_empty(self) -> None:
311 assert _file_hotspots([]) == []
312
313 def test_single_cluster_single_file(self) -> None:
314 cluster = self._cluster(["a.py::fn1", "a.py::fn2"])
315 result = _file_hotspots([cluster])
316 assert len(result) == 1
317 assert result[0]["file"] == "a.py"
318 assert result[0]["clone_symbols"] == 2
319
320 def test_ranked_descending(self) -> None:
321 c1 = self._cluster(["a.py::f1", "a.py::f2", "a.py::f3"])
322 c2 = self._cluster(["b.py::f1"])
323 result = _file_hotspots([c1, c2])
324 assert result[0]["file"] == "a.py"
325 assert result[0]["clone_symbols"] == 3
326
327 def test_top_cap_respected(self) -> None:
328 clusters = [self._cluster([f"file_{i}.py::fn"]) for i in range(20)]
329 result = _file_hotspots(clusters, top=5)
330 assert len(result) == 5
331
332 def test_cross_cluster_accumulation(self) -> None:
333 c1 = self._cluster(["shared.py::fn1", "other.py::fn2"])
334 c2 = self._cluster(["shared.py::fn3", "another.py::fn4"])
335 result = _file_hotspots([c1, c2])
336 shared = next(h for h in result if h["file"] == "shared.py")
337 assert shared["clone_symbols"] == 2
338
339
340 # ---------------------------------------------------------------------------
341 # Unit — _CloneCluster.to_dict
342 # ---------------------------------------------------------------------------
343
344
345 class TestCloneClusterToDict:
346 def _cluster(self, n: int = 2) -> _CloneCluster:
347 rec = _make_record()
348 members = [(f"src/file_{i}.py::fn", rec) for i in range(n)]
349 return _CloneCluster("exact", "aabbccdd" * 8, members)
350
351 def test_count_is_int_not_str(self) -> None:
352 d = self._cluster(3).to_dict()
353 assert isinstance(d["count"], int), "count must be int — not str"
354 assert d["count"] == 3
355
356 def test_tier_field(self) -> None:
357 assert self._cluster().to_dict()["tier"] == "exact"
358
359 def test_hash_truncated_to_8_chars(self) -> None:
360 d = self._cluster().to_dict()
361 assert len(d["hash"]) == 8
362
363 def test_member_has_all_required_fields(self) -> None:
364 d = self._cluster().to_dict()
365 member = d["members"][0]
366 for field in ("address", "kind", "language", "body_hash", "signature_id", "content_id"):
367 assert field in member
368
369 def test_member_hashes_truncated(self) -> None:
370 d = self._cluster().to_dict()
371 m = d["members"][0]
372 assert len(m["body_hash"]) == 8
373 assert len(m["signature_id"]) == 8
374 assert len(m["content_id"]) == 8
375
376
377 # ---------------------------------------------------------------------------
378 # Unit — find_clones (pure logic via sym_map injection)
379 # ---------------------------------------------------------------------------
380
381
382 class TestFindClonesUnit:
383 """Tests that bypass the object store by mocking symbols_for_snapshot."""
384
385 def test_empty_manifest_returns_no_clusters(
386 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
387 ) -> None:
388 from muse.cli.commands import clones as clones_mod
389
390 monkeypatch.setattr(
391 clones_mod, "symbols_for_snapshot",
392 lambda *a, **kw: {},
393 )
394 result = find_clones(tmp_path, {}, "both", None, 2)
395 assert result == []
396
397 def test_exact_clone_detected(
398 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
399 ) -> None:
400 from muse.cli.commands import clones as clones_mod
401
402 rec = _make_record(body_hash="deadbeef")
403 sym_map = _make_sym_map({
404 "a.py": [("a.py::fn", rec)],
405 "b.py": [("b.py::fn", rec)],
406 })
407 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
408 result = find_clones(tmp_path, {}, "exact", None, 2)
409 assert len(result) == 1
410 assert result[0].tier == "exact"
411 assert len(result[0].members) == 2
412
413 def test_near_clone_detected(
414 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
415 ) -> None:
416 from muse.cli.commands import clones as clones_mod
417
418 rec_a = _make_record(body_hash="aaaaaaaa", sig_id="shared123")
419 rec_b = _make_record(body_hash="bbbbbbbb", sig_id="shared123")
420 sym_map = _make_sym_map({
421 "a.py": [("a.py::fn", rec_a)],
422 "b.py": [("b.py::fn", rec_b)],
423 })
424 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
425 result = find_clones(tmp_path, {}, "near", None, 2)
426 assert len(result) == 1
427 assert result[0].tier == "near"
428
429 def test_exact_not_reported_in_near_tier(
430 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
431 ) -> None:
432 from muse.cli.commands import clones as clones_mod
433
434 rec = _make_record(body_hash="identical", sig_id="same_sig")
435 sym_map = _make_sym_map({
436 "a.py": [("a.py::fn", rec)],
437 "b.py": [("b.py::fn", rec)],
438 })
439 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
440 # Same body AND same signature — should not appear in near tier
441 # because unique_bodies has only 1 element.
442 result = find_clones(tmp_path, {}, "near", None, 2)
443 assert result == []
444
445 def test_min_cluster_filters_small_groups(
446 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
447 ) -> None:
448 from muse.cli.commands import clones as clones_mod
449
450 rec = _make_record(body_hash="pair")
451 sym_map = _make_sym_map({
452 "a.py": [("a.py::fn", rec)],
453 "b.py": [("b.py::fn", rec)],
454 })
455 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
456 # Require at least 3 — pair of 2 should be excluded.
457 result = find_clones(tmp_path, {}, "exact", None, 3)
458 assert result == []
459
460 def test_exclude_same_file_skips_same_file_cluster(
461 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
462 ) -> None:
463 from muse.cli.commands import clones as clones_mod
464
465 rec = _make_record(body_hash="twin")
466 sym_map = _make_sym_map({
467 "a.py": [("a.py::fn1", rec), ("a.py::fn2", rec)],
468 })
469 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
470 result = find_clones(tmp_path, {}, "exact", None, 2, exclude_same_file=True)
471 assert result == []
472
473 def test_exclude_same_file_keeps_cross_file_cluster(
474 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
475 ) -> None:
476 from muse.cli.commands import clones as clones_mod
477
478 rec = _make_record(body_hash="cross")
479 sym_map = _make_sym_map({
480 "a.py": [("a.py::fn", rec)],
481 "b.py": [("b.py::fn", rec)],
482 })
483 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
484 result = find_clones(tmp_path, {}, "exact", None, 2, exclude_same_file=True)
485 assert len(result) == 1
486
487 def test_file_filter_restricts_by_prefix(
488 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
489 ) -> None:
490 from muse.cli.commands import clones as clones_mod
491
492 rec = _make_record(body_hash="filtered")
493 sym_map = _make_sym_map({
494 "src/a.py": [("src/a.py::fn", rec)],
495 "tests/a.py": [("tests/a.py::fn", rec)],
496 })
497 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
498 result = find_clones(tmp_path, {}, "exact", None, 2, file_filter="src/")
499 # Only src/ symbols — cluster disappears (only 1 member after filter).
500 assert result == []
501
502 def test_clusters_sorted_largest_first(
503 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
504 ) -> None:
505 from muse.cli.commands import clones as clones_mod
506
507 rec_big = _make_record(body_hash="bigclone")
508 rec_small = _make_record(body_hash="smllone")
509 sym_map = _make_sym_map({
510 "a.py": [("a.py::fn", rec_small)],
511 "b.py": [("b.py::fn", rec_small)],
512 "c.py": [("c.py::fn", rec_big)],
513 "d.py": [("d.py::fn", rec_big)],
514 "e.py": [("e.py::fn", rec_big)],
515 })
516 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
517 result = find_clones(tmp_path, {}, "exact", None, 2)
518 assert len(result[0].members) >= len(result[-1].members)
519
520
521 # ---------------------------------------------------------------------------
522 # Integration — basic CLI
523 # ---------------------------------------------------------------------------
524
525
526 class TestClonesCLIBasic:
527 def test_exits_zero(self, code_repo: pathlib.Path) -> None:
528 result = runner.invoke(cli, ["code", "clones"])
529 assert result.exit_code == 0, result.output
530
531 def test_tier_exact_exits_zero(self, code_repo: pathlib.Path) -> None:
532 result = runner.invoke(cli, ["code", "clones", "--tier", "exact"])
533 assert result.exit_code == 0
534
535 def test_tier_near_exits_zero(self, code_repo: pathlib.Path) -> None:
536 result = runner.invoke(cli, ["code", "clones", "--tier", "near"])
537 assert result.exit_code == 0
538
539 def test_tier_invalid_exits_nonzero(self, code_repo: pathlib.Path) -> None:
540 result = runner.invoke(cli, ["code", "clones", "--tier", "bogus"])
541 assert result.exit_code != 0
542
543 def test_no_repo_exits_nonzero(
544 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
545 ) -> None:
546 monkeypatch.chdir(tmp_path)
547 monkeypatch.delenv("MUSE_REPO_ROOT", raising=False)
548 result = runner.invoke(cli, ["code", "clones"])
549 assert result.exit_code != 0
550
551 def test_text_output_no_crash(self, code_repo: pathlib.Path) -> None:
552 result = runner.invoke(cli, ["code", "clones"])
553 assert result.exit_code == 0
554 assert "Clone analysis" in result.output
555
556 def test_min_cluster_1_exits_nonzero(self, code_repo: pathlib.Path) -> None:
557 result = runner.invoke(cli, ["code", "clones", "--min-cluster", "1"])
558 assert result.exit_code != 0
559
560 def test_empty_repo_exits_nonzero(self, repo: pathlib.Path) -> None:
561 result = runner.invoke(cli, ["code", "clones"])
562 assert result.exit_code != 0
563
564
565 # ---------------------------------------------------------------------------
566 # Integration — JSON schema
567 # ---------------------------------------------------------------------------
568
569
570 class TestClonesJSONSchema:
571 def test_json_is_valid(self, code_repo: pathlib.Path) -> None:
572 data = _clones_json()
573 assert isinstance(data, dict)
574
575 def test_json_required_top_level_keys(self, code_repo: pathlib.Path) -> None:
576 data = _clones_json()
577 required = {
578 "schema_version", "commit", "branch", "tier", "min_cluster",
579 "kind_filter", "language_filter", "file_filter", "exclude_same_file",
580 "exact_clone_clusters", "near_clone_clusters",
581 "total_symbols_involved", "file_hotspots", "clusters",
582 }
583 assert required <= data.keys()
584
585 def test_json_count_is_int(self, exact_clone_repo: pathlib.Path) -> None:
586 data = _clones_json(["--tier", "exact"])
587 for cluster in data["clusters"]:
588 assert isinstance(cluster["count"], int), (
589 f"count must be int, got {type(cluster['count']).__name__}"
590 )
591
592 def test_json_branch_is_nonempty_string(self, code_repo: pathlib.Path) -> None:
593 data = _clones_json()
594 assert isinstance(data["branch"], str)
595 assert data["branch"]
596
597 def test_json_total_symbols_matches_cluster_sums(
598 self, exact_clone_repo: pathlib.Path
599 ) -> None:
600 data = _clones_json()
601 expected = sum(c["count"] for c in data["clusters"])
602 assert data["total_symbols_involved"] == expected
603
604 def test_json_file_hotspots_is_list(self, code_repo: pathlib.Path) -> None:
605 data = _clones_json()
606 assert isinstance(data["file_hotspots"], list)
607
608 def test_json_file_hotspots_entry_fields(
609 self, exact_clone_repo: pathlib.Path
610 ) -> None:
611 data = _clones_json()
612 for h in data["file_hotspots"]:
613 assert "file" in h
614 assert "clone_symbols" in h
615 assert isinstance(h["clone_symbols"], int)
616
617 def test_json_exclude_same_file_flag_reflected(
618 self, code_repo: pathlib.Path
619 ) -> None:
620 data = _clones_json(["--exclude-same-file"])
621 assert data["exclude_same_file"] is True
622
623 def test_json_language_filter_reflected(self, code_repo: pathlib.Path) -> None:
624 data = _clones_json(["--language", "Python"])
625 assert data["language_filter"] == "Python"
626
627 def test_json_file_filter_reflected(self, code_repo: pathlib.Path) -> None:
628 data = _clones_json(["--file", "src/"])
629 assert data["file_filter"] == "src/"
630
631 def test_json_commit_is_8_char_hex(self, code_repo: pathlib.Path) -> None:
632 data = _clones_json()
633 assert len(data["commit"]) == 8
634 assert all(c in "0123456789abcdef" for c in data["commit"])
635
636 def test_json_cluster_member_has_all_fields(
637 self, exact_clone_repo: pathlib.Path
638 ) -> None:
639 data = _clones_json(["--tier", "exact"])
640 for cluster in data["clusters"]:
641 for member in cluster["members"]:
642 for field in ("address", "kind", "language", "body_hash",
643 "signature_id", "content_id"):
644 assert field in member
645
646
647 # ---------------------------------------------------------------------------
648 # Integration — flags
649 # ---------------------------------------------------------------------------
650
651
652 class TestClonesFlags:
653 def test_min_cluster_3_excludes_pairs(
654 self, exact_clone_repo: pathlib.Path
655 ) -> None:
656 data_2 = _clones_json(["--tier", "exact"])
657 data_3 = _clones_json(["--tier", "exact", "--min-cluster", "3"])
658 # The exact_clone_repo has only a 2-member cluster — disappears at min 3.
659 assert data_2["exact_clone_clusters"] >= 1
660 assert data_3["exact_clone_clusters"] == 0
661
662 def test_language_filter_restricts(self, code_repo: pathlib.Path) -> None:
663 data_py = _clones_json(["--language", "Python"])
664 data_all = _clones_json()
665 # Python-filtered should have ≤ as many clusters as unfiltered.
666 total_py = data_py["exact_clone_clusters"] + data_py["near_clone_clusters"]
667 total_all = data_all["exact_clone_clusters"] + data_all["near_clone_clusters"]
668 assert total_py <= total_all
669
670 def test_file_filter_restricts(self, mixed_clone_repo: pathlib.Path) -> None:
671 data_all = _clones_json()
672 data_filtered = _clones_json(["--file", "unique.py"])
673 # unique.py has no clones — filtering to it yields 0 clusters.
674 assert data_filtered["exact_clone_clusters"] == 0
675 assert data_filtered["near_clone_clusters"] == 0
676
677 def test_commit_head_flag(self, code_repo: pathlib.Path) -> None:
678 data = _clones_json(["--commit", "HEAD"])
679 assert data["commit"]
680
681 def test_commit_invalid_exits_nonzero(self, code_repo: pathlib.Path) -> None:
682 result = runner.invoke(cli, ["code", "clones", "--commit", "no_such_ref_xyz"])
683 assert result.exit_code != 0
684
685 def test_kind_filter_in_json(self, code_repo: pathlib.Path) -> None:
686 data = _clones_json(["--kind", "function"])
687 assert data["kind_filter"] == "function"
688
689
690 # ---------------------------------------------------------------------------
691 # E2E — real clone detection
692 # ---------------------------------------------------------------------------
693
694
695 class TestClonesE2E:
696 def test_exact_clone_detected(self, exact_clone_repo: pathlib.Path) -> None:
697 data = _clones_json(["--tier", "exact"])
698 assert data["exact_clone_clusters"] >= 1
699 # Each exact cluster must have ≥ 2 distinct members.
700 for cluster in data["clusters"]:
701 if cluster["tier"] == "exact":
702 assert cluster["count"] >= 2
703 addresses = {m["address"] for m in cluster["members"]}
704 # Members must live in different files.
705 files = {addr.split("::")[0] for addr in addresses}
706 assert len(files) >= 2, f"Exact clone cluster should span files, got: {files}"
707
708 def test_exact_clone_count_is_2(self, exact_clone_repo: pathlib.Path) -> None:
709 data = _clones_json(["--tier", "exact"])
710 # The helper function is the only clone; count = 2.
711 clone_clusters = [c for c in data["clusters"] if c["tier"] == "exact"]
712 assert any(c["count"] == 2 for c in clone_clusters)
713
714 def test_near_clone_detected(self, near_clone_repo: pathlib.Path) -> None:
715 data = _clones_json(["--tier", "near"])
716 assert data["near_clone_clusters"] >= 1
717
718 def test_near_clone_members_differ_in_body(
719 self, near_clone_repo: pathlib.Path
720 ) -> None:
721 data = _clones_json(["--tier", "near"])
722 for cluster in data["clusters"]:
723 if cluster["tier"] == "near":
724 bodies = {m["body_hash"] for m in cluster["members"]}
725 assert len(bodies) > 1, "near-clone members must have different body hashes"
726
727 def test_no_false_positive_clones(self, code_repo: pathlib.Path) -> None:
728 """Unique repo (no real clones) should detect zero cross-file clones."""
729 data = _clones_json(["--exclude-same-file"])
730 # With --exclude-same-file, all same-file duplicates are removed.
731 # The code_repo has only one file with unique functions.
732 assert data["exact_clone_clusters"] == 0
733
734 def test_exclude_same_file_removes_same_file_cluster(
735 self, same_file_clone_repo: pathlib.Path
736 ) -> None:
737 data_incl = _clones_json(["--tier", "exact"])
738 data_excl = _clones_json(["--tier", "exact", "--exclude-same-file"])
739 # The same-file cluster (utils.py::_helper_a + utils.py::_helper_b)
740 # should disappear. The cross-file clone (utils.py + other.py) stays.
741 assert data_excl["exact_clone_clusters"] <= data_incl["exact_clone_clusters"]
742
743 def test_file_hotspots_ranks_busiest_file_first(
744 self, mixed_clone_repo: pathlib.Path
745 ) -> None:
746 data = _clones_json()
747 if data["file_hotspots"]:
748 counts = [h["clone_symbols"] for h in data["file_hotspots"]]
749 assert counts == sorted(counts, reverse=True)
750
751 def test_mixed_repo_has_both_tiers(self, mixed_clone_repo: pathlib.Path) -> None:
752 data = _clones_json(["--tier", "both"])
753 # alpha.py and beta.py are exact clones; gamma.py is near-clone of both.
754 assert data["exact_clone_clusters"] >= 1
755
756 def test_total_symbols_nonzero_when_clones_exist(
757 self, exact_clone_repo: pathlib.Path
758 ) -> None:
759 data = _clones_json()
760 assert data["total_symbols_involved"] >= 2
761
762 def test_text_output_exact_section(self, exact_clone_repo: pathlib.Path) -> None:
763 result = runner.invoke(cli, ["code", "clones", "--tier", "exact"])
764 assert result.exit_code == 0
765 assert "Exact clones" in result.output
766
767 def test_identical_file_content_reports_distinct_addresses(
768 self, exact_clone_repo: pathlib.Path
769 ) -> None:
770 """Regression: SymbolCache re-key bug.
771
772 When a.py and b.py have byte-for-byte identical content they share the
773 same SHA-256 cache key. Before the fix, b.py's tree was served with
774 a.py's addresses, collapsing both members into the same address and
775 making the cluster look like a same-file duplicate. After the fix,
776 each file gets correctly addressed symbols.
777 """
778 data = _clones_json(["--tier", "exact"])
779 for cluster in data["clusters"]:
780 if cluster["tier"] == "exact" and cluster["count"] >= 2:
781 files = {m["address"].split("::")[0] for m in cluster["members"]}
782 assert len(files) >= 2, (
783 f"Cache re-key bug: cluster members collapsed to one file: {files}"
784 )
785
786 def test_text_output_no_clones_message(self, code_repo: pathlib.Path) -> None:
787 result = runner.invoke(
788 cli, ["code", "clones", "--tier", "exact", "--exclude-same-file"]
789 )
790 assert result.exit_code == 0
791 assert "No clones detected" in result.output or "0 clone cluster" in result.output
792
793
794 # ---------------------------------------------------------------------------
795 # Stress — performance and determinism
796 # ---------------------------------------------------------------------------
797
798
799 class TestClonesStress:
800 def test_large_exact_clone_group(
801 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
802 ) -> None:
803 """1 000 files all containing the same function body — one big cluster."""
804 from muse.cli.commands import clones as clones_mod
805
806 rec = _make_record(body_hash="bigclone")
807 sym_map = _make_sym_map(
808 {f"src/file_{i}.py": [(f"src/file_{i}.py::fn", rec)] for i in range(1000)}
809 )
810 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
811 result = find_clones(tmp_path, {}, "exact", None, 2)
812 assert len(result) == 1
813 assert len(result[0].members) == 1000
814
815 def test_many_distinct_clone_pairs_performance(
816 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
817 ) -> None:
818 """500 clone pairs (1 000 unique body hashes, 2 files each)."""
819 from muse.cli.commands import clones as clones_mod
820
821 sym_map: _SymMap = {}
822 for i in range(500):
823 rec = _make_record(body_hash=f"hash_{i:04d}")
824 sym_map[f"a_{i}.py"] = {f"a_{i}.py::fn": rec}
825 sym_map[f"b_{i}.py"] = {f"b_{i}.py::fn": rec}
826
827 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
828 start = time.monotonic()
829 result = find_clones(tmp_path, {}, "exact", None, 2)
830 elapsed = time.monotonic() - start
831 assert len(result) == 500
832 assert elapsed < 5.0, f"find_clones took {elapsed:.1f}s on 1000 symbols — too slow"
833
834 def test_near_clone_large_group(
835 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
836 ) -> None:
837 """200 symbols sharing the same signature but each with a unique body."""
838 from muse.cli.commands import clones as clones_mod
839
840 sym_map: _SymMap = {}
841 for i in range(200):
842 rec = _make_record(body_hash=f"body_{i:04d}", sig_id="shared_sig")
843 sym_map[f"f_{i}.py"] = {f"f_{i}.py::fn": rec}
844
845 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
846 result = find_clones(tmp_path, {}, "near", None, 2)
847 assert len(result) == 1
848 assert len(result[0].members) == 200
849
850 def test_repeated_runs_deterministic(self, exact_clone_repo: pathlib.Path) -> None:
851 result_a = runner.invoke(cli, ["code", "clones", "--json"])
852 result_b = runner.invoke(cli, ["code", "clones", "--json"])
853 assert result_a.exit_code == 0
854 assert result_b.exit_code == 0
855 assert json.loads(result_a.output) == json.loads(result_b.output)
856
857 def test_clones_completes_within_time_bound(
858 self, exact_clone_repo: pathlib.Path
859 ) -> None:
860 start = time.monotonic()
861 result = runner.invoke(cli, ["code", "clones", "--json"])
862 elapsed = time.monotonic() - start
863 assert result.exit_code == 0
864 assert elapsed < 10.0, f"clones took {elapsed:.1f}s — too slow"
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago