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