gabriel / muse public
test_test_selection_speedup.py python
281 lines 11.1 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 140 days ago
1 """TDD tests for muse code test speedup.
2
3 Two root causes of slowness being fixed:
4
5 1. ``select_tests`` never passed ``callgraph_cache`` to ``build_forward_graph``.
6 Every invocation re-read every blob, re-parsed every AST, and re-walked
7 every function body. Fix: load ``CallGraphCache``, pass it, save it.
8
9 2. ``SymbolCache`` was loaded from disk twice per ``muse code test`` run:
10 once in ``changed_symbols_from_diff`` and once in ``select_tests``.
11 Fix: load once in ``test_cmd.run()``, pass to both callers.
12
13 Coverage
14 --------
15 - ``select_tests`` accepts ``callgraph_cache`` keyword parameter.
16 - After ``select_tests`` runs, ``.muse/callgraph_cache.msgpack`` exists.
17 - Second ``select_tests`` call with pre-populated cache skips ``parse_symbols``
18 (warm-cache path).
19 - Results are identical whether callgraph_cache is cold, warm, or not passed.
20 - ``select_tests`` accepts a shared ``SymbolCache`` (``cache=``) without
21 double-loading from disk.
22 - ``changed_symbols_from_diff`` and ``select_tests`` can share one
23 ``SymbolCache`` instance.
24 - ``load_symbol_cache`` is called at most once when a cache is pre-supplied.
25 """
26
27 from __future__ import annotations
28
29 import pathlib
30 from unittest.mock import patch, call
31
32 import pytest
33
34 from muse.core._types import blob_id, Manifest
35 from muse.core.object_store import write_object
36 from muse.core.test_selection import (
37 ChangedSymbol,
38 changed_symbols_from_diff,
39 select_tests,
40 )
41
42
43 # ---------------------------------------------------------------------------
44 # Shared fixture — minimal repo with prod.py + tests/test_prod.py
45 # ---------------------------------------------------------------------------
46
47
48 def _make_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, Manifest]:
49 """Return (root, manifest) for a tiny repo with .muse/ initialised."""
50 muse_dir = tmp_path / ".muse"
51 muse_dir.mkdir(exist_ok=True)
52
53 prod_src = b"""\
54 def compute(x: int) -> int:
55 return x * 2
56
57 def helper() -> int:
58 return 42
59 """
60 test_src = b"""\
61 from prod import compute
62
63 def test_compute() -> None:
64 assert compute(2) == 4
65 """
66
67 prod_oid = blob_id(prod_src)
68 test_oid = blob_id(test_src)
69
70 write_object(tmp_path, prod_oid, prod_src)
71 write_object(tmp_path, test_oid, test_src)
72
73 (tmp_path / "prod.py").write_bytes(prod_src)
74 tests_dir = tmp_path / "tests"
75 tests_dir.mkdir(exist_ok=True)
76 (tests_dir / "test_prod.py").write_bytes(test_src)
77
78 manifest: Manifest = {
79 "prod.py": prod_oid,
80 "tests/test_prod.py": test_oid,
81 }
82 return tmp_path, manifest
83
84
85 # ---------------------------------------------------------------------------
86 # 1. select_tests accepts callgraph_cache keyword
87 # ---------------------------------------------------------------------------
88
89
90 class TestSelectTestsAcceptsCallgraphCache:
91 def test_accepts_callgraph_cache_none(self, tmp_path: pathlib.Path) -> None:
92 root, manifest = _make_repo(tmp_path)
93 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
94 # Must not raise TypeError regardless of whether the parameter exists.
95 result = select_tests(root, changed, manifest, callgraph_cache=None)
96 assert isinstance(result, dict)
97
98 def test_accepts_callgraph_cache_instance(self, tmp_path: pathlib.Path) -> None:
99 from muse.core.callgraph_cache import CallGraphCache
100 root, manifest = _make_repo(tmp_path)
101 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
102 cg_cache = CallGraphCache.empty()
103 result = select_tests(root, changed, manifest, callgraph_cache=cg_cache)
104 assert isinstance(result, dict)
105
106 def test_result_unchanged_with_or_without_cache(
107 self, tmp_path: pathlib.Path
108 ) -> None:
109 """Passing callgraph_cache does not change the selection result."""
110 from muse.core.callgraph_cache import CallGraphCache
111 root, manifest = _make_repo(tmp_path)
112 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
113
114 result_no_cache = select_tests(root, changed, manifest)
115 result_with_cache = select_tests(
116 root, changed, manifest, callgraph_cache=CallGraphCache.empty()
117 )
118
119 assert result_no_cache["changed_addresses"] == result_with_cache["changed_addresses"]
120 assert result_no_cache["fallback_used"] == result_with_cache["fallback_used"]
121
122
123 # ---------------------------------------------------------------------------
124 # 2. select_tests populates the callgraph cache on disk
125 # ---------------------------------------------------------------------------
126
127
128 class TestSelectTestsPersistsCallgraphCache:
129 def test_callgraph_cache_file_created_after_run(
130 self, tmp_path: pathlib.Path
131 ) -> None:
132 root, manifest = _make_repo(tmp_path)
133 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
134
135 select_tests(root, changed, manifest)
136
137 cache_file = root / ".muse" / "callgraph_cache.msgpack"
138 assert cache_file.exists(), (
139 "select_tests should create .muse/callgraph_cache.msgpack on first run"
140 )
141
142 def test_callgraph_cache_nonempty_after_run(self, tmp_path: pathlib.Path) -> None:
143 from muse.core.callgraph_cache import CallGraphCache
144 root, manifest = _make_repo(tmp_path)
145 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
146
147 select_tests(root, changed, manifest)
148
149 loaded = CallGraphCache.load(root / ".muse")
150 assert loaded.size > 0, (
151 "callgraph_cache should have at least one entry after select_tests"
152 )
153
154 def test_explicit_cache_is_populated_after_run(
155 self, tmp_path: pathlib.Path
156 ) -> None:
157 from muse.core.callgraph_cache import CallGraphCache
158 root, manifest = _make_repo(tmp_path)
159 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
160 cg_cache = CallGraphCache.empty()
161
162 select_tests(root, changed, manifest, callgraph_cache=cg_cache)
163
164 assert cg_cache.size > 0, (
165 "select_tests should populate the passed callgraph_cache instance"
166 )
167
168
169 # ---------------------------------------------------------------------------
170 # 3. Warm callgraph cache skips parse_symbols on second call
171 # ---------------------------------------------------------------------------
172
173
174 class TestWarmCallgraphCacheSkipsParse:
175 def test_second_call_skips_parse_symbols(self, tmp_path: pathlib.Path) -> None:
176 """On warm callgraph_cache, parse_symbols is never called."""
177 from muse.core.callgraph_cache import CallGraphCache, load_callgraph_cache
178 root, manifest = _make_repo(tmp_path)
179 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
180
181 # First call — cold cache, populates it.
182 select_tests(root, changed, manifest)
183
184 # Second call — warm cache loaded from disk.
185 with patch("muse.plugins.code.ast_parser.parse_symbols") as mock_parse:
186 select_tests(root, changed, manifest)
187 mock_parse.assert_not_called()
188
189 def test_warm_cache_result_matches_cold(self, tmp_path: pathlib.Path) -> None:
190 """Results are identical on cold and warm callgraph_cache."""
191 root, manifest = _make_repo(tmp_path)
192 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
193
194 result_cold = select_tests(root, changed, manifest)
195 result_warm = select_tests(root, changed, manifest)
196
197 assert result_cold["changed_addresses"] == result_warm["changed_addresses"]
198 assert result_cold["fallback_used"] == result_warm["fallback_used"]
199 assert set(t["node_id"] for t in result_cold["test_targets"]) == set(
200 t["node_id"] for t in result_warm["test_targets"]
201 )
202
203
204 # ---------------------------------------------------------------------------
205 # 4. Shared SymbolCache — load_symbol_cache not called when cache= is supplied
206 # ---------------------------------------------------------------------------
207
208
209 class TestSelectTestsSharedSymbolCache:
210 def test_load_symbol_cache_not_called_when_cache_supplied(
211 self, tmp_path: pathlib.Path
212 ) -> None:
213 """When ``cache=`` is passed, select_tests must not load it from disk again."""
214 from muse.core.symbol_cache import SymbolCache
215 root, manifest = _make_repo(tmp_path)
216 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
217 shared_cache = SymbolCache.load(root / ".muse")
218
219 with patch("muse.core.test_selection.load_symbol_cache") as mock_load:
220 select_tests(root, changed, manifest, cache=shared_cache)
221 mock_load.assert_not_called()
222
223 def test_load_symbol_cache_called_once_when_none(
224 self, tmp_path: pathlib.Path
225 ) -> None:
226 """Without cache=, select_tests loads from disk exactly once."""
227 root, manifest = _make_repo(tmp_path)
228 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
229 from muse.core.symbol_cache import SymbolCache
230
231 real_load = __import__(
232 "muse.core.symbol_cache", fromlist=["load_symbol_cache"]
233 ).load_symbol_cache
234 call_count = []
235
236 def counting_load(root_arg):
237 call_count.append(1)
238 return real_load(root_arg)
239
240 with patch("muse.core.test_selection.load_symbol_cache", side_effect=counting_load):
241 select_tests(root, changed, manifest)
242
243 assert sum(call_count) == 1, (
244 f"load_symbol_cache called {sum(call_count)} times — expected 1"
245 )
246
247
248 # ---------------------------------------------------------------------------
249 # 5. changed_symbols_from_diff + select_tests can share one SymbolCache
250 # ---------------------------------------------------------------------------
251
252
253 class TestSharedCacheAcrossBothCalls:
254 def test_shared_cache_produces_same_diff(self, tmp_path: pathlib.Path) -> None:
255 """changed_symbols_from_diff with a shared SymbolCache returns same result."""
256 from muse.core.symbol_cache import load_symbol_cache
257 root, manifest = _make_repo(tmp_path)
258
259 # Edit a file on disk so there's something to diff.
260 (root / "prod.py").write_bytes(b"def compute(x: int) -> int:\n return x * 3\n")
261
262 shared_cache = load_symbol_cache(root)
263 result_shared = changed_symbols_from_diff(root, manifest, cache=shared_cache)
264 result_own = changed_symbols_from_diff(root, manifest)
265
266 assert {c["address"] for c in result_shared} == {c["address"] for c in result_own}
267
268 def test_select_tests_with_prepopulated_cache(
269 self, tmp_path: pathlib.Path
270 ) -> None:
271 """select_tests with a pre-warmed SymbolCache returns correct results."""
272 from muse.core.symbol_cache import load_symbol_cache
273 root, manifest = _make_repo(tmp_path)
274 changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")]
275
276 # Warm the symbol cache by running select_tests once.
277 select_tests(root, changed, manifest)
278 shared_cache = load_symbol_cache(root)
279
280 result = select_tests(root, changed, manifest, cache=shared_cache)
281 assert "prod.py::compute" in result["changed_addresses"]
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 140 days ago