gabriel / muse public
test_coverage_supercharge.py python
357 lines 13.7 KB
Raw
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 6 days ago
1 """Supercharge tests for ``muse code coverage`` — agent-usability gaps.
2
3 The existing TestCoverage suite in test_code_commands.py covers correctness,
4 JSON schema, --exclude-dunder, --exclude-private, --min-callers, --exclude-self,
5 --compare diff schema, --count, and --no-show-callers.
6
7 This file targets only the gaps those tests leave open:
8
9 Coverage matrix
10 ---------------
11 - --json / -j: -j alias works identically to --json
12 - exit_code: JSON output includes exit_code = 0 on success
13 - duration_ms: JSON output includes non-negative float duration_ms
14 - TypedDicts: _CoveragePayload gains exit_code/duration_ms annotations
15 - Docstrings: run() docstring mentions exit_code and duration_ms
16 - ANSI: JSON output never contains terminal escape sequences
17 - Performance: duration_ms stays under 2000 ms for a small repo
18 """
19
20 from __future__ import annotations
21 from collections.abc import Mapping
22
23 import argparse
24
25 import json
26 import os
27 import pathlib
28 import textwrap
29
30 import pytest
31
32 from tests.cli_test_helper import CliRunner, InvokeResult
33
34 runner = CliRunner()
35
36 _ADDR = "models.py::User"
37
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43
44 def _env(root: pathlib.Path) -> Mapping[str, str]:
45 return {"MUSE_REPO_ROOT": str(root)}
46
47
48 def _run(root: pathlib.Path, *args: str) -> InvokeResult:
49 return runner.invoke(None, list(args), env=_env(root))
50
51
52 # ---------------------------------------------------------------------------
53 # Fixture — class with mixed covered/uncovered methods
54 # ---------------------------------------------------------------------------
55
56
57 @pytest.fixture()
58 def coverage_repo(
59 tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
60 ) -> pathlib.Path:
61 """Repo with a User class where some methods are called, some are not.
62
63 Layout::
64
65 models.py — class User with __init__, save, delete, to_dict
66 api.py — calls User.__init__ and save (not delete or to_dict)
67
68 Two commits so history analysis works correctly.
69 """
70 monkeypatch.chdir(tmp_path)
71 r = _run(tmp_path, "init", "--domain", "code")
72 assert r.exit_code == 0, r.output
73
74 # commit 1 — User class
75 (tmp_path / "models.py").write_text(textwrap.dedent("""\
76 class User:
77 def __init__(self, name):
78 self.name = name
79
80 def save(self):
81 return True
82
83 def delete(self):
84 return False
85
86 def to_dict(self):
87 return {"name": self.name}
88 """))
89 r = _run(tmp_path, "code", "add", ".")
90 assert r.exit_code == 0, r.output
91 r = _run(tmp_path, "commit", "-m", "feat: add User class")
92 assert r.exit_code == 0, r.output
93
94 # commit 2 — callers (init + save used; delete + to_dict not used)
95 (tmp_path / "api.py").write_text(textwrap.dedent("""\
96 from models import User
97
98 def create_user(name):
99 user = User(name)
100 user.save()
101 return user
102
103 def update_user(name):
104 user = User(name)
105 user.save()
106 return user
107 """))
108 r = _run(tmp_path, "code", "add", ".")
109 assert r.exit_code == 0, r.output
110 r = _run(tmp_path, "commit", "-m", "feat: add api callers")
111 assert r.exit_code == 0, r.output
112
113 return tmp_path
114
115
116 # ---------------------------------------------------------------------------
117 # TestJsonAlias — -j works identically to --json
118 # ---------------------------------------------------------------------------
119
120
121 class TestJsonAlias:
122 """-j shorthand must behave identically to --json."""
123
124 def test_j_alias_exits_zero(self, coverage_repo: pathlib.Path) -> None:
125 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
126 assert r.exit_code == 0, r.output
127
128 def test_j_alias_valid_json(self, coverage_repo: pathlib.Path) -> None:
129 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
130 json.loads(r.output) # must not raise
131
132 def test_j_alias_has_methods_key(self, coverage_repo: pathlib.Path) -> None:
133 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
134 assert "methods" in json.loads(r.output)
135
136 def test_j_alias_has_percent_key(self, coverage_repo: pathlib.Path) -> None:
137 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
138 assert "percent" in json.loads(r.output)
139
140 def test_j_alias_same_top_level_keys_as_json_flag(
141 self, coverage_repo: pathlib.Path
142 ) -> None:
143 r1 = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
144 r2 = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
145 d1 = json.loads(r1.output)
146 d2 = json.loads(r2.output)
147 d1.pop("duration_ms", None)
148 d2.pop("duration_ms", None)
149 assert set(d1.keys()) == set(d2.keys())
150
151 def test_j_alias_method_count_matches_json_flag(
152 self, coverage_repo: pathlib.Path
153 ) -> None:
154 r1 = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
155 r2 = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
156 assert len(json.loads(r1.output)["methods"]) == len(json.loads(r2.output)["methods"])
157
158 def test_j_alias_with_exclude_dunder(self, coverage_repo: pathlib.Path) -> None:
159 r = _run(coverage_repo, "code", "coverage", "-j", "--exclude-dunder", _ADDR)
160 assert r.exit_code == 0, r.output
161 assert json.loads(r.output)["filters"]["exclude_dunder"] is True
162
163 def test_j_alias_address_reflected(self, coverage_repo: pathlib.Path) -> None:
164 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
165 assert json.loads(r.output)["address"] == _ADDR
166
167
168 # ---------------------------------------------------------------------------
169 # TestDurationMs — JSON output must include duration_ms
170 # ---------------------------------------------------------------------------
171
172
173 class TestDurationMs:
174 """JSON output must include a non-negative float duration_ms."""
175
176 def test_json_has_duration_ms(self, coverage_repo: pathlib.Path) -> None:
177 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
178 assert "duration_ms" in json.loads(r.output)
179
180 def test_json_duration_ms_nonnegative(self, coverage_repo: pathlib.Path) -> None:
181 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
182 assert json.loads(r.output)["duration_ms"] >= 0
183
184 def test_json_duration_ms_is_float(self, coverage_repo: pathlib.Path) -> None:
185 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
186 assert isinstance(json.loads(r.output)["duration_ms"], float)
187
188 def test_j_alias_duration_ms_present(self, coverage_repo: pathlib.Path) -> None:
189 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
190 assert "duration_ms" in json.loads(r.output)
191
192 def test_duration_ms_with_exclude_dunder(self, coverage_repo: pathlib.Path) -> None:
193 r = _run(coverage_repo, "code", "coverage", "--json", "--exclude-dunder", _ADDR)
194 data = json.loads(r.output)
195 assert "duration_ms" in data
196 assert data["duration_ms"] >= 0
197
198 def test_duration_ms_with_compare(self, coverage_repo: pathlib.Path) -> None:
199 """duration_ms present even when --compare diff analysis runs."""
200 r = _run(coverage_repo, "code", "coverage", "--json", "--compare", "HEAD", _ADDR)
201 data = json.loads(r.output)
202 assert "duration_ms" in data
203 assert isinstance(data["duration_ms"], float)
204
205
206 # ---------------------------------------------------------------------------
207 # TestExitCode — JSON includes exit_code = 0 on success
208 # ---------------------------------------------------------------------------
209
210
211 class TestExitCode:
212 """JSON exit_code must be 0 on success."""
213
214 def test_json_has_exit_code(self, coverage_repo: pathlib.Path) -> None:
215 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
216 assert "exit_code" in json.loads(r.output)
217
218 def test_json_exit_code_zero(self, coverage_repo: pathlib.Path) -> None:
219 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
220 assert r.exit_code == 0
221 assert json.loads(r.output)["exit_code"] == 0
222
223 def test_json_exit_code_is_int(self, coverage_repo: pathlib.Path) -> None:
224 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
225 assert isinstance(json.loads(r.output)["exit_code"], int)
226
227 def test_j_alias_exit_code_present(self, coverage_repo: pathlib.Path) -> None:
228 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
229 assert "exit_code" in json.loads(r.output)
230
231 def test_exit_code_mirrors_process_exit(self, coverage_repo: pathlib.Path) -> None:
232 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
233 assert json.loads(r.output)["exit_code"] == r.exit_code
234
235 def test_exit_code_zero_with_exclude_dunder(
236 self, coverage_repo: pathlib.Path
237 ) -> None:
238 r = _run(coverage_repo, "code", "coverage", "--json", "--exclude-dunder", _ADDR)
239 assert r.exit_code == 0
240 assert json.loads(r.output)["exit_code"] == 0
241
242 def test_exit_code_zero_with_compare(self, coverage_repo: pathlib.Path) -> None:
243 r = _run(coverage_repo, "code", "coverage", "--json", "--compare", "HEAD", _ADDR)
244 assert r.exit_code == 0
245 assert json.loads(r.output)["exit_code"] == 0
246
247
248 # ---------------------------------------------------------------------------
249 # TestTypedDicts — _CoveragePayload carries the new fields
250 # ---------------------------------------------------------------------------
251
252
253 class TestTypedDicts:
254 """_CoveragePayload must carry exit_code and duration_ms annotations."""
255
256 def test_coverage_payload_typeddict_exists(self) -> None:
257 from muse.cli.commands.coverage import _CoveragePayload # noqa: F401
258
259 def test_has_exit_code_annotation(self) -> None:
260 from muse.cli.commands.coverage import _CoveragePayload
261 assert "exit_code" in _CoveragePayload.__annotations__
262
263 def test_has_duration_ms_annotation(self) -> None:
264 from muse.cli.commands.coverage import _CoveragePayload
265 assert "duration_ms" in _CoveragePayload.__annotations__
266
267 def test_retains_address_annotation(self) -> None:
268 from muse.cli.commands.coverage import _CoveragePayload
269 assert "address" in _CoveragePayload.__annotations__
270
271 def test_retains_methods_annotation(self) -> None:
272 from muse.cli.commands.coverage import _CoveragePayload
273 assert "methods" in _CoveragePayload.__annotations__
274
275 def test_retains_percent_annotation(self) -> None:
276 from muse.cli.commands.coverage import _CoveragePayload
277 assert "percent" in _CoveragePayload.__annotations__
278
279 def test_retains_filters_annotation(self) -> None:
280 from muse.cli.commands.coverage import _CoveragePayload
281 assert "filters" in _CoveragePayload.__annotations__
282
283
284 # ---------------------------------------------------------------------------
285 # TestAnsiSanitization — no escape codes in JSON output
286 # ---------------------------------------------------------------------------
287
288
289 class TestAnsiSanitization:
290 """No ANSI escape sequences anywhere in the JSON output."""
291
292 def test_json_output_no_ansi(self, coverage_repo: pathlib.Path) -> None:
293 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
294 assert "\x1b" not in r.output
295
296 def test_j_alias_output_no_ansi(self, coverage_repo: pathlib.Path) -> None:
297 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
298 assert "\x1b" not in r.output
299
300 def test_json_output_no_ansi_with_exclude_dunder(
301 self, coverage_repo: pathlib.Path
302 ) -> None:
303 r = _run(coverage_repo, "code", "coverage", "--json", "--exclude-dunder", _ADDR)
304 assert "\x1b" not in r.output
305
306
307 # ---------------------------------------------------------------------------
308 # TestPerformance — duration_ms under 2000 ms for a small repo
309 # ---------------------------------------------------------------------------
310
311
312 class TestPerformance:
313 """duration_ms must stay under 2000 ms for small repos."""
314
315 def test_json_duration_under_2000ms(self, coverage_repo: pathlib.Path) -> None:
316 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
317 assert json.loads(r.output)["duration_ms"] < 2000
318
319 def test_j_alias_duration_under_2000ms(self, coverage_repo: pathlib.Path) -> None:
320 r = _run(coverage_repo, "code", "coverage", "-j", _ADDR)
321 assert json.loads(r.output)["duration_ms"] < 2000
322
323 def test_duration_ms_is_float_not_int(self, coverage_repo: pathlib.Path) -> None:
324 r = _run(coverage_repo, "code", "coverage", "--json", _ADDR)
325 assert isinstance(json.loads(r.output)["duration_ms"], float)
326
327
328 # ---------------------------------------------------------------------------
329 # TestRegisterFlags — --json / -j normalized at argparse level
330 # ---------------------------------------------------------------------------
331
332
333 class TestRegisterFlags:
334 """register() must expose --json with -j shorthand and dest=json_out."""
335
336 def _make_parser(self) -> argparse.ArgumentParser:
337 import argparse as ap
338 from muse.cli.commands.coverage import register
339 root = ap.ArgumentParser()
340 subs = root.add_subparsers()
341 register(subs)
342 return root
343
344 def test_json_out_default_false(self) -> None:
345 p = self._make_parser()
346 ns = p.parse_args(['coverage', 'billing.py::Invoice'])
347 assert ns.json_out is False
348
349 def test_json_out_true_with_json_flag(self) -> None:
350 p = self._make_parser()
351 ns = p.parse_args(['coverage', 'billing.py::Invoice', '--json'])
352 assert ns.json_out is True
353
354 def test_json_out_true_with_j_flag(self) -> None:
355 p = self._make_parser()
356 ns = p.parse_args(['coverage', 'billing.py::Invoice', '-j'])
357 assert ns.json_out is True
File History 1 commit
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b fix: try fetch/presign before fetch/mpack to avoid Cloudfla… Sonnet 4.6 patch 6 days ago