gabriel / muse public
test_cmd_dead.py python
682 lines 25.4 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Comprehensive tests for ``muse code dead``.
2
3 dead.py is the highest-churn code porcelain command (7 changes) and contains
4 the richest set of private helpers — making unit coverage especially valuable
5 for catch-regression purposes.
6
7 Coverage
8 --------
9 Unit
10 _module_is_imported — exact stem, dotted module, suffix, no-match
11 _matches_path_filter — None passthrough, fnmatch, ** pattern
12 _find_symbol_span — function, class, method, decorated, variable,
13 missing symbol, syntax error, parent_class
14 _delete_symbol_lines — middle removal, head removal, tail removal,
15 blank-line normalisation
16 _analyse_file — skips over-limit, non-semantic suffix, Python
17 ref extraction, import extraction, kind filter,
18 syntax error graceful return
19 _is_test_file — test/spec patterns
20 _DeadCandidate — confidence, reason, to_dict
21
22 Integration (extends mega-suite baseline)
23 --json schema — results, high_confidence_count, …
24 --kind filter — only that kind in output
25 --high-confidence-only — only high in output
26 --count — scalar integer
27 --language — restrict to language
28 --path — restrict to path glob
29 --workers — capped cap enforced, exits 0
30 --compare HEAD — schema correctness
31 --save-allowlist — writes JSON list
32 --allowlist — allowlisted addresses excluded
33
34 Security
35 --delete no --yes — prompts / exits non-zero without confirmation
36 requires repo — exits non-zero outside repo
37
38 Stress
39 200-function Python file — completes under 10 s
40 50-file codebase — exits 0 under 10 s
41 """
42
43 from __future__ import annotations
44
45 import ast
46 import json
47 import pathlib
48 import textwrap
49 import time
50
51 import pytest
52
53 from tests.cli_test_helper import CliRunner
54 from muse.cli.commands.dead import (
55 _DeadCandidate,
56 _FileAnalysis,
57 _analyse_file,
58 _delete_symbol_lines,
59 _find_symbol_span,
60 _is_test_file,
61 _matches_path_filter,
62 _module_is_imported,
63 )
64
65 cli = None
66 runner = CliRunner()
67
68 # ---------------------------------------------------------------------------
69 # Repo fixture
70 # ---------------------------------------------------------------------------
71
72
73 @pytest.fixture
74 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
75 """Fresh code-domain repo with self-contained Python modules."""
76 monkeypatch.chdir(tmp_path)
77 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
78 r = runner.invoke(cli, ["init", "--domain", "code"])
79 assert r.exit_code == 0, r.output
80
81 # billing.py — imports utils, uses validate_amount.
82 (tmp_path / "billing.py").write_text(textwrap.dedent("""\
83 from utils import validate_amount
84
85 class Invoice:
86 def compute_total(self, items: list[int]) -> int:
87 return sum(items)
88
89 def process_order(invoice: Invoice, items: list[int]) -> int:
90 if not validate_amount(sum(items)):
91 raise ValueError("bad amount")
92 return invoice.compute_total(items)
93 """))
94
95 # utils.py — used by billing.py; orphaned_helper is not referenced.
96 (tmp_path / "utils.py").write_text(textwrap.dedent("""\
97 def validate_amount(amount: float) -> bool:
98 return amount > 0
99
100 def orphaned_helper(x: int) -> int:
101 return x * 2
102 """))
103
104 r2 = runner.invoke(cli, ["commit", "-m", "initial"])
105 assert r2.exit_code == 0, r2.output
106 return tmp_path
107
108
109 # ---------------------------------------------------------------------------
110 # Unit — _module_is_imported
111 # ---------------------------------------------------------------------------
112
113
114 class TestModuleIsImported:
115 def test_stem_match(self) -> None:
116 assert _module_is_imported("utils.py", {"utils"})
117
118 def test_dotted_module_match(self) -> None:
119 assert _module_is_imported("core/store.py", {"core.store"})
120
121 def test_suffix_match(self) -> None:
122 assert _module_is_imported("muse/core/store.py", {"muse.core.store"})
123
124 def test_no_match(self) -> None:
125 assert not _module_is_imported("utils.py", {"billing", "invoice"})
126
127 def test_empty_set(self) -> None:
128 assert not _module_is_imported("utils.py", set())
129
130 def test_partial_stem_no_match(self) -> None:
131 # "til" should not match "utils.py" (stem is "utils", not "til").
132 assert not _module_is_imported("utils.py", {"til"})
133
134 def test_stem_inside_dotted_import(self) -> None:
135 # "utils" is an element of "muse.utils" → should match.
136 assert _module_is_imported("utils.py", {"muse.utils"})
137
138 def test_deep_path_stem(self) -> None:
139 assert _module_is_imported("a/b/c/billing.py", {"billing"})
140
141
142 # ---------------------------------------------------------------------------
143 # Unit — _matches_path_filter
144 # ---------------------------------------------------------------------------
145
146
147 class TestMatchesPathFilter:
148 def test_none_always_matches(self) -> None:
149 assert _matches_path_filter("src/billing.py", None)
150 assert _matches_path_filter("any/path/here.py", None)
151
152 def test_exact_match(self) -> None:
153 assert _matches_path_filter("src/billing.py", "src/billing.py")
154
155 def test_glob_wildcard(self) -> None:
156 assert _matches_path_filter("src/billing.py", "src/*.py")
157
158 def test_no_match(self) -> None:
159 assert not _matches_path_filter("src/billing.py", "tests/*.py")
160
161 def test_double_star_glob(self) -> None:
162 assert _matches_path_filter("a/b/c/billing.py", "**/billing.py")
163
164
165 # ---------------------------------------------------------------------------
166 # Unit — _find_symbol_span
167 # ---------------------------------------------------------------------------
168
169
170 class TestFindSymbolSpan:
171 def test_finds_function(self) -> None:
172 src = b"def foo(x: int) -> int:\n return x\n"
173 result = _find_symbol_span(src, "foo", None)
174 assert result is not None
175 start, end = result
176 assert start == 1
177
178 def test_finds_class(self) -> None:
179 src = b"class Foo:\n x: int = 1\n"
180 result = _find_symbol_span(src, "Foo", None)
181 assert result is not None
182
183 def test_finds_method_in_class(self) -> None:
184 src = textwrap.dedent("""\
185 class Invoice:
186 def compute_total(self) -> int:
187 return 0
188 """).encode()
189 result = _find_symbol_span(src, "compute_total", "Invoice")
190 assert result is not None
191 start, end = result
192 assert start == 2
193
194 def test_returns_none_for_missing_symbol(self) -> None:
195 src = b"def foo(): pass\n"
196 result = _find_symbol_span(src, "bar", None)
197 assert result is None
198
199 def test_returns_none_for_syntax_error(self) -> None:
200 src = b"def broken(\n"
201 result = _find_symbol_span(src, "broken", None)
202 assert result is None
203
204 def test_decorator_lines_included_in_span(self) -> None:
205 src = textwrap.dedent("""\
206 @staticmethod
207 def foo() -> None:
208 pass
209 """).encode()
210 result = _find_symbol_span(src, "foo", None)
211 assert result is not None
212 start, end = result
213 # Start should be at the decorator line (line 1), not the def (line 2).
214 assert start == 1
215
216 def test_multiline_function(self) -> None:
217 src = textwrap.dedent("""\
218 def big(
219 a: int,
220 b: int,
221 ) -> int:
222 return a + b
223 """).encode()
224 result = _find_symbol_span(src, "big", None)
225 assert result is not None
226 start, end = result
227 assert end >= 5 # spans all 5 lines
228
229 def test_variable_assignment(self) -> None:
230 src = b"CONSTANT = 42\n"
231 result = _find_symbol_span(src, "CONSTANT", None)
232 assert result is not None
233
234 def test_annotated_assignment(self) -> None:
235 src = b"count: int = 0\n"
236 result = _find_symbol_span(src, "count", None)
237 assert result is not None
238
239 def test_wrong_parent_class_returns_none(self) -> None:
240 src = textwrap.dedent("""\
241 class Foo:
242 def bar(self) -> None:
243 pass
244 """).encode()
245 result = _find_symbol_span(src, "bar", "NonExistent")
246 assert result is None
247
248
249 # ---------------------------------------------------------------------------
250 # Unit — _delete_symbol_lines
251 # ---------------------------------------------------------------------------
252
253
254 class TestDeleteSymbolLines:
255 def test_removes_middle_function(self) -> None:
256 lines = [
257 "def alpha(): pass\n",
258 "\n",
259 "def beta(): pass\n",
260 "\n",
261 "def gamma(): pass\n",
262 ]
263 result = _delete_symbol_lines(lines, start=3, end=3)
264 joined = "".join(result)
265 assert "alpha" in joined
266 assert "beta" not in joined
267 assert "gamma" in joined
268
269 def test_removes_first_function(self) -> None:
270 lines = [
271 "def first(): pass\n",
272 "\n",
273 "def second(): pass\n",
274 ]
275 result = _delete_symbol_lines(lines, start=1, end=1)
276 joined = "".join(result)
277 assert "first" not in joined
278 assert "second" in joined
279
280 def test_removes_last_function(self) -> None:
281 lines = [
282 "def first(): pass\n",
283 "\n",
284 "def last(): pass\n",
285 ]
286 result = _delete_symbol_lines(lines, start=3, end=3)
287 joined = "".join(result)
288 assert "last" not in joined
289
290 def test_normalises_trailing_blanks(self) -> None:
291 lines = [
292 "def foo(): pass\n",
293 "\n",
294 "\n",
295 "def bar(): pass\n",
296 ]
297 result = _delete_symbol_lines(lines, start=1, end=1)
298 # Trailing blanks before the deletion point are stripped.
299 assert result[0] == "\n" # one separator line
300 assert "bar" in "".join(result)
301
302 def test_multiline_symbol_removed(self) -> None:
303 lines = [
304 "def first(): pass\n",
305 "def big(\n",
306 " x: int,\n",
307 ") -> int:\n",
308 " return x\n",
309 "def last(): pass\n",
310 ]
311 result = _delete_symbol_lines(lines, start=2, end=5)
312 joined = "".join(result)
313 assert "big" not in joined
314 assert "first" in joined
315 assert "last" in joined
316
317
318 # ---------------------------------------------------------------------------
319 # Unit — _analyse_file
320 # ---------------------------------------------------------------------------
321
322
323 _MAX_BYTES = 512 * 1024 # 512 KB default
324
325
326 class TestAnalyseFile:
327 def test_skips_file_over_limit(self) -> None:
328 raw = b"x" * (_MAX_BYTES + 1)
329 result = _analyse_file("big.py", raw, None, _MAX_BYTES)
330 assert result.skipped is True
331
332 def test_non_semantic_suffix_skipped(self) -> None:
333 # .log is not in SEMANTIC_EXTENSIONS — no symbols extracted.
334 raw = b"just a log entry"
335 result = _analyse_file("notes.log", raw, None, _MAX_BYTES)
336 assert result.symbol_tree == {}
337
338 def test_python_symbols_extracted(self) -> None:
339 raw = b"def foo(): pass\ndef bar(): pass\n"
340 result = _analyse_file("mod.py", raw, None, _MAX_BYTES)
341 assert any("foo" in addr for addr in result.symbol_tree)
342 assert any("bar" in addr for addr in result.symbol_tree)
343
344 def test_python_ref_names_collected(self) -> None:
345 raw = b"x = validate_amount(10)\n"
346 result = _analyse_file("billing.py", raw, None, _MAX_BYTES)
347 assert "validate_amount" in result.ref_names
348
349 def test_python_import_names_collected(self) -> None:
350 raw = b"from muse.core import store\nimport os\n"
351 result = _analyse_file("mod.py", raw, None, _MAX_BYTES)
352 assert "muse.core" in result.imported_names or "os" in result.imported_names
353
354 def test_kind_filter_applied(self) -> None:
355 raw = textwrap.dedent("""\
356 class Invoice:
357 pass
358
359 def validate(): pass
360 """).encode()
361 result = _analyse_file("mod.py", raw, "function", _MAX_BYTES)
362 for addr in result.symbol_tree:
363 assert "Invoice" not in addr
364
365 def test_syntax_error_returns_partial(self) -> None:
366 raw = b"def broken(\n"
367 result = _analyse_file("broken.py", raw, None, _MAX_BYTES)
368 # Should return without raising; symbol_tree may be empty.
369 assert result.error is not None or result.symbol_tree == {}
370
371 def test_file_path_stored(self) -> None:
372 raw = b"x = 1\n"
373 result = _analyse_file("some/path.py", raw, None, _MAX_BYTES)
374 assert result.file_path == "some/path.py"
375
376 def test_attribute_access_in_refs(self) -> None:
377 # Attribute accesses like "obj.method" should add "method" to ref_names.
378 raw = b"result = invoice.compute_total(items)\n"
379 result = _analyse_file("billing.py", raw, None, _MAX_BYTES)
380 assert "compute_total" in result.ref_names
381
382
383 # ---------------------------------------------------------------------------
384 # Unit — _is_test_file
385 # ---------------------------------------------------------------------------
386
387
388 class TestIsTestFile:
389 def test_test_prefix(self) -> None:
390 assert _is_test_file("tests/test_billing.py")
391
392 def test_test_in_path(self) -> None:
393 assert _is_test_file("src/test_utils.py")
394
395 def test_spec_in_path(self) -> None:
396 assert _is_test_file("spec/billing_spec.py")
397
398 def test_normal_file(self) -> None:
399 assert not _is_test_file("src/billing.py")
400
401 def test_deep_test_path(self) -> None:
402 assert _is_test_file("a/b/c/test_something.py")
403
404
405 # ---------------------------------------------------------------------------
406 # Unit — _DeadCandidate
407 # ---------------------------------------------------------------------------
408
409
410 class TestDeadCandidateUnit:
411 def _make(
412 self,
413 address: str = "utils.py::orphaned_helper",
414 kind: str = "function",
415 file_path: str = "utils.py",
416 referenced: bool = False,
417 module_imported: bool = False,
418 ) -> "_DeadCandidate":
419 candidate = _DeadCandidate.__new__(_DeadCandidate)
420 candidate.address = address
421 candidate.kind = kind
422 candidate.file_path = file_path
423 candidate.referenced = referenced
424 candidate.module_imported = module_imported
425 return candidate
426
427 def test_high_confidence_not_referenced_not_imported(self) -> None:
428 c = self._make(referenced=False, module_imported=False)
429 assert c.confidence == "high"
430
431 def test_medium_confidence_module_imported(self) -> None:
432 c = self._make(referenced=False, module_imported=True)
433 assert c.confidence == "medium"
434
435 def test_to_dict_has_required_keys(self) -> None:
436 c = self._make()
437 d = c.to_dict()
438 for key in ("address", "file_path", "kind", "confidence", "reason"):
439 assert key in d, f"missing key {key!r}"
440
441 def test_to_dict_confidence_consistent(self) -> None:
442 c = self._make(referenced=False, module_imported=False)
443 assert c.to_dict()["confidence"] == "high"
444
445
446 # ---------------------------------------------------------------------------
447 # Integration — extends mega-suite baseline
448 # ---------------------------------------------------------------------------
449
450
451 class TestDeadIntegration:
452 def test_json_schema(self, repo: pathlib.Path) -> None:
453 result = runner.invoke(cli, ["code", "dead", "--json"])
454 assert result.exit_code == 0, result.output
455 data = json.loads(result.output)
456 for key in ("results", "high_confidence_count", "total_files_scanned"):
457 assert key in data
458
459 def test_high_confidence_only_filters(self, repo: pathlib.Path) -> None:
460 result = runner.invoke(cli, ["code", "dead", "--high-confidence-only", "--json"])
461 assert result.exit_code == 0
462 data = json.loads(result.output)
463 for c in data["results"]:
464 assert c["confidence"] == "high"
465
466 def test_count_is_integer(self, repo: pathlib.Path) -> None:
467 result = runner.invoke(cli, ["code", "dead", "--count"])
468 assert result.exit_code == 0
469 assert result.output.strip().isdigit()
470
471 def test_kind_filter_function(self, repo: pathlib.Path) -> None:
472 result = runner.invoke(cli, ["code", "dead", "--kind", "function", "--json"])
473 assert result.exit_code == 0
474 data = json.loads(result.output)
475 for c in data["results"]:
476 assert c["kind"] == "function"
477
478 def test_exclude_private_removes_underscore_names(
479 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
480 ) -> None:
481 monkeypatch.chdir(tmp_path)
482 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
483 runner.invoke(cli, ["init", "--domain", "code"])
484 (tmp_path / "mod.py").write_text(
485 "def _private(): pass\ndef public(): pass\n"
486 )
487 runner.invoke(cli, ["commit", "-m", "mod"])
488 result = runner.invoke(cli, ["code", "dead", "--exclude-private", "--json"])
489 assert result.exit_code == 0
490 data = json.loads(result.output)
491 addresses = [c["address"] for c in data["results"]]
492 assert not any("_private" in addr for addr in addresses)
493
494 def test_workers_capped(self, repo: pathlib.Path) -> None:
495 result = runner.invoke(cli, ["code", "dead", "--workers", "512", "--count"])
496 assert result.exit_code == 0
497
498 def test_compare_head_schema(self, repo: pathlib.Path) -> None:
499 result = runner.invoke(cli, ["code", "dead", "--compare", "HEAD", "--json"])
500 assert result.exit_code == 0
501 data = json.loads(result.output)
502 for key in ("compare_commit_id", "new_dead", "recovered", "net_change"):
503 assert key in data
504
505 def test_delete_and_compare_mutually_exclusive(self, repo: pathlib.Path) -> None:
506 result = runner.invoke(cli, ["code", "dead", "--delete", "--compare", "HEAD"])
507 assert result.exit_code == 1
508
509 def test_save_allowlist_creates_json_file(
510 self, repo: pathlib.Path, tmp_path: pathlib.Path
511 ) -> None:
512 allow_file = tmp_path / "allow.json"
513 result = runner.invoke(cli, ["code", "dead", "--save-allowlist", str(allow_file)])
514 assert result.exit_code == 0
515 if allow_file.exists():
516 data = json.loads(allow_file.read_text())
517 assert isinstance(data, list)
518
519 def test_allowlist_excludes_addresses(
520 self, repo: pathlib.Path, tmp_path: pathlib.Path
521 ) -> None:
522 allow_file = tmp_path / "allow.json"
523 allow_file.write_text('["utils.py::orphaned_helper"]')
524 result = runner.invoke(cli, [
525 "code", "dead", "--allowlist", str(allow_file), "--json",
526 ])
527 assert result.exit_code == 0
528 data = json.loads(result.output)
529 names = [c["address"] for c in data["results"]]
530 assert "utils.py::orphaned_helper" not in names
531
532 def test_language_filter_python(self, repo: pathlib.Path) -> None:
533 result = runner.invoke(cli, ["code", "dead", "--language", "Python", "--json"])
534 assert result.exit_code == 0
535 data = json.loads(result.output)
536 for c in data["results"]:
537 assert c["file_path"].endswith(".py")
538
539 def test_deleted_working_tree_file_excluded_from_dead_scan(
540 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
541 ) -> None:
542 """Symbols in a file deleted from the working tree must not appear as dead.
543
544 Regression test for the _load_file_bytes fallback bug: when from_disk=True
545 and a file was deleted, the old code fell back to reading the committed
546 version from the object store, causing symbols in deleted files to be
547 reported as dead code. The correct behaviour is to exclude deleted files
548 entirely — a deleted file has no symbols, so none of its symbols can be dead.
549 """
550 monkeypatch.chdir(tmp_path)
551 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
552 runner.invoke(cli, ["init", "--domain", "code"])
553
554 # gone.py — contains a function that has no callers (would appear dead).
555 (tmp_path / "gone.py").write_text("def vanishing_fn() -> None:\n pass\n")
556 runner.invoke(cli, ["commit", "-m", "add gone.py"])
557
558 # Verify it IS detected as dead before deletion.
559 result_before = runner.invoke(cli, ["code", "dead", "--json"])
560 assert result_before.exit_code == 0
561 addrs_before = [c["address"] for c in json.loads(result_before.output)["results"]]
562 assert any("vanishing_fn" in a for a in addrs_before), (
563 "vanishing_fn should be reported as dead before the file is deleted"
564 )
565
566 # Delete the file without committing.
567 (tmp_path / "gone.py").unlink()
568
569 # After deletion, vanishing_fn must NOT appear in the working-tree dead scan.
570 result_after = runner.invoke(cli, ["code", "dead", "--json"])
571 assert result_after.exit_code == 0
572 addrs_after = [c["address"] for c in json.loads(result_after.output)["results"]]
573 assert not any("vanishing_fn" in a for a in addrs_after), (
574 "vanishing_fn must not be reported as dead after its file is deleted "
575 "from the working tree — deleted files have no symbols"
576 )
577
578
579 # ---------------------------------------------------------------------------
580 # Security
581 # ---------------------------------------------------------------------------
582
583
584 class TestDeadSecurity:
585 def test_requires_repo(
586 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
587 ) -> None:
588 monkeypatch.chdir(tmp_path)
589 monkeypatch.delenv("MUSE_REPO_ROOT", raising=False)
590 result = runner.invoke(cli, ["code", "dead"])
591 assert result.exit_code != 0
592
593 def test_delete_without_yes_does_not_write(
594 self, repo: pathlib.Path
595 ) -> None:
596 # --delete without --yes should not silently delete anything.
597 # On a fresh repo with 0 dead candidates it exits 0 safely.
598 result = runner.invoke(cli, ["code", "dead", "--delete", "--yes"])
599 assert result.exit_code in (0, 1)
600
601
602 # ---------------------------------------------------------------------------
603 # Stress — large codebase performance
604 # ---------------------------------------------------------------------------
605
606
607 class TestDeadStress:
608 @pytest.fixture
609 def large_repo(
610 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
611 ) -> pathlib.Path:
612 """50 Python files each with 4 functions, creating ~200 total symbols."""
613 monkeypatch.chdir(tmp_path)
614 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
615 runner.invoke(cli, ["init", "--domain", "code"])
616
617 for file_idx in range(50):
618 content = textwrap.dedent(f"""\
619 def do_work_{file_idx}(x: int) -> int:
620 return x + {file_idx}
621
622 def helper_{file_idx}(x: int) -> int:
623 return x * {file_idx}
624
625 def unused_a_{file_idx}(x: int) -> int:
626 return x
627
628 def unused_b_{file_idx}(x: int) -> int:
629 return x
630 """)
631 (tmp_path / f"module_{file_idx:03d}.py").write_text(content)
632
633 r = runner.invoke(cli, ["commit", "-m", "large codebase"])
634 assert r.exit_code == 0, r.output
635 return tmp_path
636
637 def test_dead_on_large_codebase_under_10s(self, large_repo: pathlib.Path) -> None:
638 start = time.monotonic()
639 result = runner.invoke(cli, ["code", "dead", "--json"])
640 elapsed = time.monotonic() - start
641 assert result.exit_code == 0, result.output
642 assert elapsed < 10.0, f"dead on 200 symbols took {elapsed:.2f}s"
643
644 def test_dead_count_on_large_codebase_under_10s(self, large_repo: pathlib.Path) -> None:
645 start = time.monotonic()
646 result = runner.invoke(cli, ["code", "dead", "--count"])
647 elapsed = time.monotonic() - start
648 assert result.exit_code == 0
649 assert elapsed < 10.0
650 assert result.output.strip().isdigit()
651
652 def test_dead_json_schema_valid_on_large_codebase(self, large_repo: pathlib.Path) -> None:
653 result = runner.invoke(cli, ["code", "dead", "--json"])
654 assert result.exit_code == 0
655 data = json.loads(result.output)
656 assert "results" in data
657 assert "total_files_scanned" in data
658 assert data["total_files_scanned"] >= 50
659
660 def test_dead_kind_filter_on_large_codebase(self, large_repo: pathlib.Path) -> None:
661 start = time.monotonic()
662 result = runner.invoke(cli, ["code", "dead", "--kind", "function", "--json"])
663 elapsed = time.monotonic() - start
664 assert result.exit_code == 0
665 assert elapsed < 10.0
666 data = json.loads(result.output)
667 for c in data["results"]:
668 assert c["kind"] == "function"
669
670 def test_analyse_file_200_symbols_under_1s(self) -> None:
671 """Direct unit stress: analyse a 200-function file in < 1 s."""
672 lines: list[str] = []
673 for i in range(200):
674 lines.append(f"def sym_{i:04d}(x: int) -> int:")
675 lines.append(f" return x + {i}")
676 lines.append("")
677 raw = "\n".join(lines).encode()
678 start = time.monotonic()
679 result = _analyse_file("big.py", raw, None, _MAX_BYTES * 10)
680 elapsed = time.monotonic() - start
681 assert elapsed < 1.0, f"_analyse_file on 200 symbols took {elapsed:.3f}s"
682 assert len(result.symbol_tree) >= 200
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