"""TDD tests for muse code test speedup. Two root causes of slowness being fixed: 1. ``select_tests`` never passed ``callgraph_cache`` to ``build_forward_graph``. Every invocation re-read every blob, re-parsed every AST, and re-walked every function body. Fix: load ``CallGraphCache``, pass it, save it. 2. ``SymbolCache`` was loaded from disk twice per ``muse code test`` run: once in ``changed_symbols_from_diff`` and once in ``select_tests``. Fix: load once in ``test_cmd.run()``, pass to both callers. Coverage -------- - ``select_tests`` accepts ``callgraph_cache`` keyword parameter. - After ``select_tests`` runs, ``.muse/cache/callgraph.json`` exists. - Second ``select_tests`` call with pre-populated cache skips ``parse_symbols`` (warm-cache path). - Results are identical whether callgraph_cache is cold, warm, or not passed. - ``select_tests`` accepts a shared ``SymbolCache`` (``cache=``) without double-loading from disk. - ``changed_symbols_from_diff`` and ``select_tests`` can share one ``SymbolCache`` instance. - ``load_symbol_cache`` is called at most once when a cache is pre-supplied. """ from __future__ import annotations from typing import TYPE_CHECKING import pathlib from unittest.mock import patch, call import pytest if TYPE_CHECKING: from muse.core.symbol_cache import SymbolCache from muse.core.types import blob_id, Manifest from muse.core.object_store import write_object from muse.core.test_selection import ( ChangedSymbol, changed_symbols_from_diff, select_tests, ) from muse.core.paths import muse_dir # --------------------------------------------------------------------------- # Shared fixture — minimal repo with prod.py + tests/test_prod.py # --------------------------------------------------------------------------- def _make_repo(tmp_path: pathlib.Path) -> tuple[pathlib.Path, Manifest]: """Return (root, manifest) for a tiny repo with .muse/ initialised.""" dot_muse = muse_dir(tmp_path) dot_muse.mkdir(exist_ok=True) prod_src = b"""\ def compute(x: int) -> int: return x * 2 def helper() -> int: return 42 """ test_src = b"""\ from prod import compute def test_compute() -> None: assert compute(2) == 4 """ prod_oid = blob_id(prod_src) test_oid = blob_id(test_src) write_object(tmp_path, prod_oid, prod_src) write_object(tmp_path, test_oid, test_src) (tmp_path / "prod.py").write_bytes(prod_src) tests_dir = tmp_path / "tests" tests_dir.mkdir(exist_ok=True) (tests_dir / "test_prod.py").write_bytes(test_src) manifest: Manifest = { "prod.py": prod_oid, "tests/test_prod.py": test_oid, } return tmp_path, manifest # --------------------------------------------------------------------------- # 1. select_tests accepts callgraph_cache keyword # --------------------------------------------------------------------------- class TestSelectTestsAcceptsCallgraphCache: def test_accepts_callgraph_cache_none(self, tmp_path: pathlib.Path) -> None: root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] # Must not raise TypeError regardless of whether the parameter exists. result = select_tests(root, changed, manifest, callgraph_cache=None) assert isinstance(result, dict) def test_accepts_callgraph_cache_instance(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] cg_cache = CallGraphCache.empty() result = select_tests(root, changed, manifest, callgraph_cache=cg_cache) assert isinstance(result, dict) def test_result_unchanged_with_or_without_cache( self, tmp_path: pathlib.Path ) -> None: """Passing callgraph_cache does not change the selection result.""" from muse.core.callgraph_cache import CallGraphCache root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] result_no_cache = select_tests(root, changed, manifest) result_with_cache = select_tests( root, changed, manifest, callgraph_cache=CallGraphCache.empty() ) assert result_no_cache["changed_addresses"] == result_with_cache["changed_addresses"] assert result_no_cache["fallback_used"] == result_with_cache["fallback_used"] # --------------------------------------------------------------------------- # 2. select_tests populates the callgraph cache on disk # --------------------------------------------------------------------------- class TestSelectTestsPersistsCallgraphCache: def test_callgraph_cache_file_created_after_run( self, tmp_path: pathlib.Path ) -> None: root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] select_tests(root, changed, manifest) from muse.core.paths import callgraph_cache_path assert callgraph_cache_path(root).exists(), ( "select_tests should create .muse/cache/callgraph.json on first run" ) def test_callgraph_cache_nonempty_after_run(self, tmp_path: pathlib.Path) -> None: from muse.core.callgraph_cache import CallGraphCache root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] select_tests(root, changed, manifest) loaded = CallGraphCache.load(muse_dir(root)) assert loaded.size > 0, ( "callgraph_cache should have at least one entry after select_tests" ) def test_explicit_cache_is_populated_after_run( self, tmp_path: pathlib.Path ) -> None: from muse.core.callgraph_cache import CallGraphCache root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] cg_cache = CallGraphCache.empty() select_tests(root, changed, manifest, callgraph_cache=cg_cache) assert cg_cache.size > 0, ( "select_tests should populate the passed callgraph_cache instance" ) # --------------------------------------------------------------------------- # 3. Warm callgraph cache skips parse_symbols on second call # --------------------------------------------------------------------------- class TestWarmCallgraphCacheSkipsParse: def test_second_call_skips_parse_symbols(self, tmp_path: pathlib.Path) -> None: """On warm callgraph_cache, parse_symbols is never called.""" from muse.core.callgraph_cache import CallGraphCache, load_callgraph_cache root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] # First call — cold cache, populates it. select_tests(root, changed, manifest) # Second call — warm cache loaded from disk. with patch("muse.plugins.code.ast_parser.parse_symbols") as mock_parse: select_tests(root, changed, manifest) mock_parse.assert_not_called() def test_warm_cache_result_matches_cold(self, tmp_path: pathlib.Path) -> None: """Results are identical on cold and warm callgraph_cache.""" root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] result_cold = select_tests(root, changed, manifest) result_warm = select_tests(root, changed, manifest) assert result_cold["changed_addresses"] == result_warm["changed_addresses"] assert result_cold["fallback_used"] == result_warm["fallback_used"] assert set(t["node_id"] for t in result_cold["test_targets"]) == set( t["node_id"] for t in result_warm["test_targets"] ) # --------------------------------------------------------------------------- # 4. Shared SymbolCache — load_symbol_cache not called when cache= is supplied # --------------------------------------------------------------------------- class TestSelectTestsSharedSymbolCache: def test_load_symbol_cache_not_called_when_cache_supplied( self, tmp_path: pathlib.Path ) -> None: """When ``cache=`` is passed, select_tests must not load it from disk again.""" from muse.core.symbol_cache import SymbolCache root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] shared_cache = SymbolCache.load(muse_dir(root)) with patch("muse.core.test_selection.load_symbol_cache") as mock_load: select_tests(root, changed, manifest, cache=shared_cache) mock_load.assert_not_called() def test_load_symbol_cache_called_once_when_none( self, tmp_path: pathlib.Path ) -> None: """Without cache=, select_tests loads from disk exactly once.""" root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] from muse.core.symbol_cache import SymbolCache real_load = __import__( "muse.core.symbol_cache", fromlist=["load_symbol_cache"] ).load_symbol_cache call_count = [] def counting_load(root_arg: pathlib.Path) -> "SymbolCache": call_count.append(1) return real_load(root_arg) with patch("muse.core.test_selection.load_symbol_cache", side_effect=counting_load): select_tests(root, changed, manifest) assert sum(call_count) == 1, ( f"load_symbol_cache called {sum(call_count)} times — expected 1" ) # --------------------------------------------------------------------------- # 5. changed_symbols_from_diff + select_tests can share one SymbolCache # --------------------------------------------------------------------------- class TestSharedCacheAcrossBothCalls: def test_shared_cache_produces_same_diff(self, tmp_path: pathlib.Path) -> None: """changed_symbols_from_diff with a shared SymbolCache returns same result.""" from muse.core.symbol_cache import load_symbol_cache root, manifest = _make_repo(tmp_path) # Edit a file on disk so there's something to diff. (root / "prod.py").write_bytes(b"def compute(x: int) -> int:\n return x * 3\n") shared_cache = load_symbol_cache(root) result_shared = changed_symbols_from_diff(root, manifest, cache=shared_cache) result_own = changed_symbols_from_diff(root, manifest) assert {c["address"] for c in result_shared} == {c["address"] for c in result_own} def test_select_tests_with_prepopulated_cache( self, tmp_path: pathlib.Path ) -> None: """select_tests with a pre-warmed SymbolCache returns correct results.""" from muse.core.symbol_cache import load_symbol_cache root, manifest = _make_repo(tmp_path) changed = [ChangedSymbol(address="prod.py::compute", change_kind="modified")] # Warm the symbol cache by running select_tests once. select_tests(root, changed, manifest) shared_cache = load_symbol_cache(root) result = select_tests(root, changed, manifest, cache=shared_cache) assert "prod.py::compute" in result["changed_addresses"]