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