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