gabriel / muse public
test_cmd_clones.py python
874 lines 33.0 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 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_is_short_id(self) -> None:
360 # short_id() returns the first 12 hex chars of a raw hash
361 d = self._cluster().to_dict()
362 assert len(d["hash"]) == 12
363 assert all(c in "0123456789abcdef" for c in d["hash"])
364
365 def test_member_has_all_required_fields(self) -> None:
366 d = self._cluster().to_dict()
367 member = d["members"][0]
368 for field in ("address", "kind", "language", "body_hash", "signature_id", "content_id"):
369 assert field in member
370
371 def test_member_hashes_are_short_ids(self) -> None:
372 # short_id() returns the first 12 hex chars of a raw hash
373 d = self._cluster().to_dict()
374 m = d["members"][0]
375 for field in ("body_hash", "signature_id", "content_id"):
376 assert len(m[field]) == 12
377 assert all(c in "0123456789abcdef" for c in m[field])
378
379
380 # ---------------------------------------------------------------------------
381 # Unit — find_clones (pure logic via sym_map injection)
382 # ---------------------------------------------------------------------------
383
384
385 class TestFindClonesUnit:
386 """Tests that bypass the object store by mocking symbols_for_snapshot."""
387
388 def test_empty_manifest_returns_no_clusters(
389 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
390 ) -> None:
391 from muse.cli.commands import clones as clones_mod
392
393 monkeypatch.setattr(
394 clones_mod, "symbols_for_snapshot",
395 lambda *a, **kw: {},
396 )
397 result = find_clones(tmp_path, {}, "both", None, 2)
398 assert result == []
399
400 def test_exact_clone_detected(
401 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
402 ) -> None:
403 from muse.cli.commands import clones as clones_mod
404
405 rec = _make_record(body_hash="deadbeef")
406 sym_map = _make_sym_map({
407 "a.py": [("a.py::fn", rec)],
408 "b.py": [("b.py::fn", rec)],
409 })
410 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
411 result = find_clones(tmp_path, {}, "exact", None, 2)
412 assert len(result) == 1
413 assert result[0].tier == "exact"
414 assert len(result[0].members) == 2
415
416 def test_near_clone_detected(
417 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
418 ) -> None:
419 from muse.cli.commands import clones as clones_mod
420
421 rec_a = _make_record(body_hash="aaaaaaaa", sig_id="shared123")
422 rec_b = _make_record(body_hash="bbbbbbbb", sig_id="shared123")
423 sym_map = _make_sym_map({
424 "a.py": [("a.py::fn", rec_a)],
425 "b.py": [("b.py::fn", rec_b)],
426 })
427 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
428 result = find_clones(tmp_path, {}, "near", None, 2)
429 assert len(result) == 1
430 assert result[0].tier == "near"
431
432 def test_exact_not_reported_in_near_tier(
433 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
434 ) -> None:
435 from muse.cli.commands import clones as clones_mod
436
437 rec = _make_record(body_hash="identical", sig_id="same_sig")
438 sym_map = _make_sym_map({
439 "a.py": [("a.py::fn", rec)],
440 "b.py": [("b.py::fn", rec)],
441 })
442 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
443 # Same body AND same signature — should not appear in near tier
444 # because unique_bodies has only 1 element.
445 result = find_clones(tmp_path, {}, "near", None, 2)
446 assert result == []
447
448 def test_min_cluster_filters_small_groups(
449 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
450 ) -> None:
451 from muse.cli.commands import clones as clones_mod
452
453 rec = _make_record(body_hash="pair")
454 sym_map = _make_sym_map({
455 "a.py": [("a.py::fn", rec)],
456 "b.py": [("b.py::fn", rec)],
457 })
458 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
459 # Require at least 3 — pair of 2 should be excluded.
460 result = find_clones(tmp_path, {}, "exact", None, 3)
461 assert result == []
462
463 def test_exclude_same_file_skips_same_file_cluster(
464 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
465 ) -> None:
466 from muse.cli.commands import clones as clones_mod
467
468 rec = _make_record(body_hash="twin")
469 sym_map = _make_sym_map({
470 "a.py": [("a.py::fn1", rec), ("a.py::fn2", rec)],
471 })
472 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
473 result = find_clones(tmp_path, {}, "exact", None, 2, exclude_same_file=True)
474 assert result == []
475
476 def test_exclude_same_file_keeps_cross_file_cluster(
477 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
478 ) -> None:
479 from muse.cli.commands import clones as clones_mod
480
481 rec = _make_record(body_hash="cross")
482 sym_map = _make_sym_map({
483 "a.py": [("a.py::fn", rec)],
484 "b.py": [("b.py::fn", rec)],
485 })
486 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
487 result = find_clones(tmp_path, {}, "exact", None, 2, exclude_same_file=True)
488 assert len(result) == 1
489
490 def test_file_filter_restricts_by_prefix(
491 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
492 ) -> None:
493 from muse.cli.commands import clones as clones_mod
494
495 rec = _make_record(body_hash="filtered")
496 sym_map = _make_sym_map({
497 "src/a.py": [("src/a.py::fn", rec)],
498 "tests/a.py": [("tests/a.py::fn", rec)],
499 })
500 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
501 result = find_clones(tmp_path, {}, "exact", None, 2, file_filter="src/")
502 # Only src/ symbols — cluster disappears (only 1 member after filter).
503 assert result == []
504
505 def test_clusters_sorted_largest_first(
506 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
507 ) -> None:
508 from muse.cli.commands import clones as clones_mod
509
510 rec_big = _make_record(body_hash="bigclone")
511 rec_small = _make_record(body_hash="smllone")
512 sym_map = _make_sym_map({
513 "a.py": [("a.py::fn", rec_small)],
514 "b.py": [("b.py::fn", rec_small)],
515 "c.py": [("c.py::fn", rec_big)],
516 "d.py": [("d.py::fn", rec_big)],
517 "e.py": [("e.py::fn", rec_big)],
518 })
519 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
520 result = find_clones(tmp_path, {}, "exact", None, 2)
521 assert len(result[0].members) >= len(result[-1].members)
522
523
524 # ---------------------------------------------------------------------------
525 # Integration — basic CLI
526 # ---------------------------------------------------------------------------
527
528
529 class TestClonesCLIBasic:
530 def test_exits_zero(self, code_repo: pathlib.Path) -> None:
531 result = runner.invoke(cli, ["code", "clones"])
532 assert result.exit_code == 0, result.output
533
534 def test_tier_exact_exits_zero(self, code_repo: pathlib.Path) -> None:
535 result = runner.invoke(cli, ["code", "clones", "--tier", "exact"])
536 assert result.exit_code == 0
537
538 def test_tier_near_exits_zero(self, code_repo: pathlib.Path) -> None:
539 result = runner.invoke(cli, ["code", "clones", "--tier", "near"])
540 assert result.exit_code == 0
541
542 def test_tier_invalid_exits_nonzero(self, code_repo: pathlib.Path) -> None:
543 result = runner.invoke(cli, ["code", "clones", "--tier", "bogus"])
544 assert result.exit_code != 0
545
546 def test_no_repo_exits_nonzero(
547 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
548 ) -> None:
549 monkeypatch.chdir(tmp_path)
550 monkeypatch.delenv("MUSE_REPO_ROOT", raising=False)
551 result = runner.invoke(cli, ["code", "clones"])
552 assert result.exit_code != 0
553
554 def test_text_output_no_crash(self, code_repo: pathlib.Path) -> None:
555 result = runner.invoke(cli, ["code", "clones"])
556 assert result.exit_code == 0
557 assert "Clone analysis" in result.output
558
559 def test_min_cluster_1_exits_nonzero(self, code_repo: pathlib.Path) -> None:
560 result = runner.invoke(cli, ["code", "clones", "--min-cluster", "1"])
561 assert result.exit_code != 0
562
563 def test_empty_repo_exits_nonzero(self, repo: pathlib.Path) -> None:
564 result = runner.invoke(cli, ["code", "clones"])
565 assert result.exit_code != 0
566
567
568 # ---------------------------------------------------------------------------
569 # Integration — JSON schema
570 # ---------------------------------------------------------------------------
571
572
573 class TestClonesJSONSchema:
574 def test_json_is_valid(self, code_repo: pathlib.Path) -> None:
575 data = _clones_json()
576 assert isinstance(data, dict)
577
578 def test_json_required_top_level_keys(self, code_repo: pathlib.Path) -> None:
579 data = _clones_json()
580 required = {
581 "schema_version", "commit", "branch", "tier", "min_cluster",
582 "kind_filter", "language_filter", "file_filter", "exclude_same_file",
583 "exact_clone_clusters", "near_clone_clusters",
584 "total_symbols_involved", "file_hotspots", "clusters",
585 }
586 assert required <= data.keys()
587
588 def test_json_count_is_int(self, exact_clone_repo: pathlib.Path) -> None:
589 data = _clones_json(["--tier", "exact"])
590 for cluster in data["clusters"]:
591 assert isinstance(cluster["count"], int), (
592 f"count must be int, got {type(cluster['count']).__name__}"
593 )
594
595 def test_json_branch_is_nonempty_string(self, code_repo: pathlib.Path) -> None:
596 data = _clones_json()
597 assert isinstance(data["branch"], str)
598 assert data["branch"]
599
600 def test_json_total_symbols_matches_cluster_sums(
601 self, exact_clone_repo: pathlib.Path
602 ) -> None:
603 data = _clones_json()
604 expected = sum(c["count"] for c in data["clusters"])
605 assert data["total_symbols_involved"] == expected
606
607 def test_json_file_hotspots_is_list(self, code_repo: pathlib.Path) -> None:
608 data = _clones_json()
609 assert isinstance(data["file_hotspots"], list)
610
611 def test_json_file_hotspots_entry_fields(
612 self, exact_clone_repo: pathlib.Path
613 ) -> None:
614 data = _clones_json()
615 for h in data["file_hotspots"]:
616 assert "file" in h
617 assert "clone_symbols" in h
618 assert isinstance(h["clone_symbols"], int)
619
620 def test_json_exclude_same_file_flag_reflected(
621 self, code_repo: pathlib.Path
622 ) -> None:
623 data = _clones_json(["--exclude-same-file"])
624 assert data["exclude_same_file"] is True
625
626 def test_json_language_filter_reflected(self, code_repo: pathlib.Path) -> None:
627 data = _clones_json(["--language", "Python"])
628 assert data["language_filter"] == "Python"
629
630 def test_json_file_filter_reflected(self, code_repo: pathlib.Path) -> None:
631 data = _clones_json(["--file", "src/"])
632 assert data["file_filter"] == "src/"
633
634 def test_json_commit_is_short_id(self, code_repo: pathlib.Path) -> None:
635 # short_id() returns "sha256:<12 hex chars>" for sha256-prefixed IDs
636 data = _clones_json()
637 assert isinstance(data["commit"], str)
638 assert data["commit"].startswith("sha256:")
639 hex_part = data["commit"][len("sha256:"):]
640 assert all(c in "0123456789abcdef" for c in hex_part)
641
642 def test_json_cluster_member_has_all_fields(
643 self, exact_clone_repo: pathlib.Path
644 ) -> None:
645 data = _clones_json(["--tier", "exact"])
646 for cluster in data["clusters"]:
647 for member in cluster["members"]:
648 for field in ("address", "kind", "language", "body_hash",
649 "signature_id", "content_id"):
650 assert field in member
651
652
653 # ---------------------------------------------------------------------------
654 # Integration — flags
655 # ---------------------------------------------------------------------------
656
657
658 class TestClonesFlags:
659 def test_min_cluster_3_excludes_pairs(
660 self, exact_clone_repo: pathlib.Path
661 ) -> None:
662 data_2 = _clones_json(["--tier", "exact"])
663 data_3 = _clones_json(["--tier", "exact", "--min-cluster", "3"])
664 # The exact_clone_repo has only a 2-member cluster — disappears at min 3.
665 assert data_2["exact_clone_clusters"] >= 1
666 assert data_3["exact_clone_clusters"] == 0
667
668 def test_language_filter_restricts(self, code_repo: pathlib.Path) -> None:
669 data_py = _clones_json(["--language", "Python"])
670 data_all = _clones_json()
671 # Python-filtered should have ≤ as many clusters as unfiltered.
672 total_py = data_py["exact_clone_clusters"] + data_py["near_clone_clusters"]
673 total_all = data_all["exact_clone_clusters"] + data_all["near_clone_clusters"]
674 assert total_py <= total_all
675
676 def test_file_filter_restricts(self, mixed_clone_repo: pathlib.Path) -> None:
677 data_all = _clones_json()
678 data_filtered = _clones_json(["--file", "unique.py"])
679 # unique.py has no clones — filtering to it yields 0 clusters.
680 assert data_filtered["exact_clone_clusters"] == 0
681 assert data_filtered["near_clone_clusters"] == 0
682
683 def test_commit_head_flag(self, code_repo: pathlib.Path) -> None:
684 data = _clones_json(["--commit", "HEAD"])
685 assert data["commit"]
686
687 def test_commit_invalid_exits_nonzero(self, code_repo: pathlib.Path) -> None:
688 result = runner.invoke(cli, ["code", "clones", "--commit", "no_such_ref_xyz"])
689 assert result.exit_code != 0
690
691 def test_kind_filter_in_json(self, code_repo: pathlib.Path) -> None:
692 data = _clones_json(["--kind", "function"])
693 assert data["kind_filter"] == "function"
694
695
696 # ---------------------------------------------------------------------------
697 # E2E — real clone detection
698 # ---------------------------------------------------------------------------
699
700
701 class TestClonesE2E:
702 def test_exact_clone_detected(self, exact_clone_repo: pathlib.Path) -> None:
703 data = _clones_json(["--tier", "exact"])
704 assert data["exact_clone_clusters"] >= 1
705 # Each exact cluster must have ≥ 2 distinct members.
706 for cluster in data["clusters"]:
707 if cluster["tier"] == "exact":
708 assert cluster["count"] >= 2
709 addresses = {m["address"] for m in cluster["members"]}
710 # Members must live in different files.
711 files = {addr.split("::")[0] for addr in addresses}
712 assert len(files) >= 2, f"Exact clone cluster should span files, got: {files}"
713
714 def test_exact_clone_count_is_2(self, exact_clone_repo: pathlib.Path) -> None:
715 data = _clones_json(["--tier", "exact"])
716 # The helper function is the only clone; count = 2.
717 clone_clusters = [c for c in data["clusters"] if c["tier"] == "exact"]
718 assert any(c["count"] == 2 for c in clone_clusters)
719
720 def test_near_clone_detected(self, near_clone_repo: pathlib.Path) -> None:
721 data = _clones_json(["--tier", "near"])
722 assert data["near_clone_clusters"] >= 1
723
724 def test_near_clone_members_differ_in_body(
725 self, near_clone_repo: pathlib.Path
726 ) -> None:
727 data = _clones_json(["--tier", "near"])
728 for cluster in data["clusters"]:
729 if cluster["tier"] == "near":
730 bodies = {m["body_hash"] for m in cluster["members"]}
731 assert len(bodies) > 1, "near-clone members must have different body hashes"
732
733 def test_no_false_positive_clones(self, code_repo: pathlib.Path) -> None:
734 """Unique repo (no real clones) should detect zero cross-file clones."""
735 data = _clones_json(["--exclude-same-file"])
736 # With --exclude-same-file, all same-file duplicates are removed.
737 # The code_repo has only one file with unique functions.
738 assert data["exact_clone_clusters"] == 0
739
740 def test_exclude_same_file_removes_same_file_cluster(
741 self, same_file_clone_repo: pathlib.Path
742 ) -> None:
743 data_incl = _clones_json(["--tier", "exact"])
744 data_excl = _clones_json(["--tier", "exact", "--exclude-same-file"])
745 # The same-file cluster (utils.py::_helper_a + utils.py::_helper_b)
746 # should disappear. The cross-file clone (utils.py + other.py) stays.
747 assert data_excl["exact_clone_clusters"] <= data_incl["exact_clone_clusters"]
748
749 def test_file_hotspots_ranks_busiest_file_first(
750 self, mixed_clone_repo: pathlib.Path
751 ) -> None:
752 data = _clones_json()
753 if data["file_hotspots"]:
754 counts = [h["clone_symbols"] for h in data["file_hotspots"]]
755 assert counts == sorted(counts, reverse=True)
756
757 def test_mixed_repo_has_both_tiers(self, mixed_clone_repo: pathlib.Path) -> None:
758 data = _clones_json(["--tier", "both"])
759 # alpha.py and beta.py are exact clones; gamma.py is near-clone of both.
760 assert data["exact_clone_clusters"] >= 1
761
762 def test_total_symbols_nonzero_when_clones_exist(
763 self, exact_clone_repo: pathlib.Path
764 ) -> None:
765 data = _clones_json()
766 assert data["total_symbols_involved"] >= 2
767
768 def test_text_output_exact_section(self, exact_clone_repo: pathlib.Path) -> None:
769 result = runner.invoke(cli, ["code", "clones", "--tier", "exact"])
770 assert result.exit_code == 0
771 assert "Exact clones" in result.output
772
773 def test_identical_file_content_reports_distinct_addresses(
774 self, exact_clone_repo: pathlib.Path
775 ) -> None:
776 """Regression: SymbolCache re-key bug.
777
778 When a.py and b.py have byte-for-byte identical content they share the
779 same SHA-256 cache key. Before the fix, b.py's tree was served with
780 a.py's addresses, collapsing both members into the same address and
781 making the cluster look like a same-file duplicate. After the fix,
782 each file gets correctly addressed symbols.
783 """
784 data = _clones_json(["--tier", "exact"])
785 for cluster in data["clusters"]:
786 if cluster["tier"] == "exact" and cluster["count"] >= 2:
787 files = {m["address"].split("::")[0] for m in cluster["members"]}
788 assert len(files) >= 2, (
789 f"Cache re-key bug: cluster members collapsed to one file: {files}"
790 )
791
792 def test_text_output_no_clones_message(self, code_repo: pathlib.Path) -> None:
793 result = runner.invoke(
794 cli, ["code", "clones", "--tier", "exact", "--exclude-same-file"]
795 )
796 assert result.exit_code == 0
797 assert "No clones detected" in result.output or "0 clone cluster" in result.output
798
799
800 # ---------------------------------------------------------------------------
801 # Stress — performance and determinism
802 # ---------------------------------------------------------------------------
803
804
805 class TestClonesStress:
806 def test_large_exact_clone_group(
807 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
808 ) -> None:
809 """1 000 files all containing the same function body — one big cluster."""
810 from muse.cli.commands import clones as clones_mod
811
812 rec = _make_record(body_hash="bigclone")
813 sym_map = _make_sym_map(
814 {f"src/file_{i}.py": [(f"src/file_{i}.py::fn", rec)] for i in range(1000)}
815 )
816 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
817 result = find_clones(tmp_path, {}, "exact", None, 2)
818 assert len(result) == 1
819 assert len(result[0].members) == 1000
820
821 def test_many_distinct_clone_pairs_performance(
822 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
823 ) -> None:
824 """500 clone pairs (1 000 unique body hashes, 2 files each)."""
825 from muse.cli.commands import clones as clones_mod
826
827 sym_map: _SymMap = {}
828 for i in range(500):
829 rec = _make_record(body_hash=f"hash_{i:04d}")
830 sym_map[f"a_{i}.py"] = {f"a_{i}.py::fn": rec}
831 sym_map[f"b_{i}.py"] = {f"b_{i}.py::fn": rec}
832
833 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
834 start = time.monotonic()
835 result = find_clones(tmp_path, {}, "exact", None, 2)
836 elapsed = time.monotonic() - start
837 assert len(result) == 500
838 assert elapsed < 5.0, f"find_clones took {elapsed:.1f}s on 1000 symbols — too slow"
839
840 def test_near_clone_large_group(
841 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
842 ) -> None:
843 """200 symbols sharing the same signature but each with a unique body."""
844 from muse.cli.commands import clones as clones_mod
845
846 sym_map: _SymMap = {}
847 for i in range(200):
848 rec = _make_record(body_hash=f"body_{i:04d}", sig_id="shared_sig")
849 sym_map[f"f_{i}.py"] = {f"f_{i}.py::fn": rec}
850
851 monkeypatch.setattr(clones_mod, "symbols_for_snapshot", lambda *a, **kw: sym_map)
852 result = find_clones(tmp_path, {}, "near", None, 2)
853 assert len(result) == 1
854 assert len(result[0].members) == 200
855
856 def test_repeated_runs_deterministic(self, exact_clone_repo: pathlib.Path) -> None:
857 result_a = runner.invoke(cli, ["code", "clones", "--json"])
858 result_b = runner.invoke(cli, ["code", "clones", "--json"])
859 assert result_a.exit_code == 0
860 assert result_b.exit_code == 0
861 da = json.loads(result_a.output)
862 db = json.loads(result_b.output)
863 da.pop("duration_ms", None)
864 db.pop("duration_ms", None)
865 assert da == db
866
867 def test_clones_completes_within_time_bound(
868 self, exact_clone_repo: pathlib.Path
869 ) -> None:
870 start = time.monotonic()
871 result = runner.invoke(cli, ["code", "clones", "--json"])
872 elapsed = time.monotonic() - start
873 assert result.exit_code == 0
874 assert elapsed < 10.0, f"clones took {elapsed:.1f}s — too slow"
File History 2 commits
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 142 days ago