gabriel / muse public
test_code_invariants.py python
305 lines 11.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """Tests for the code-domain invariants engine."""
2
3 import pathlib
4 import tempfile
5
6 import pytest
7
8 from muse.core.invariants import InvariantChecker
9 from muse.plugins.code._invariants import (
10 CodeChecker,
11 CodeInvariantRule,
12 check_max_complexity,
13 check_no_circular_imports,
14 check_no_dead_exports,
15 check_test_coverage_floor,
16 load_invariant_rules,
17 run_invariants,
18 )
19 from muse.core.object_store import object_path
20
21
22 # ---------------------------------------------------------------------------
23 # Helpers
24 # ---------------------------------------------------------------------------
25
26
27 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
28 """Set up a minimal .muse/ structure."""
29 muse = tmp_path / ".muse"
30 muse.mkdir()
31 (muse / "repo.json").write_text('{"repo_id":"test"}')
32 (muse / "HEAD").write_text("ref: refs/heads/main")
33 (muse / "commits").mkdir()
34 (muse / "snapshots").mkdir()
35 (muse / "refs" / "heads").mkdir(parents=True)
36 (muse / "objects").mkdir()
37 return tmp_path
38
39
40 def _write_object(root: pathlib.Path, content: bytes) -> str:
41 from muse.core._types import blob_id
42 from muse.core.object_store import write_object
43 oid = blob_id(content)
44 write_object(root, oid, content)
45 return oid
46
47
48 # ---------------------------------------------------------------------------
49 # _estimate_complexity (via check_max_complexity)
50 # ---------------------------------------------------------------------------
51
52
53 class TestMaxComplexity:
54 def test_simple_function_no_violation(self) -> None:
55 with tempfile.TemporaryDirectory() as tmp:
56 root = _make_repo(pathlib.Path(tmp))
57 src = b"def simple():\n return 1\n"
58 h = _write_object(root, src)
59 manifest = {"mod.py": h}
60 violations = check_max_complexity(manifest, root, "test", "error", threshold=10)
61 assert violations == []
62
63 def test_complex_function_triggers_violation(self) -> None:
64 # 15+ branches = definitely over threshold 5.
65 src = b"""
66 def complex():
67 if True:
68 pass
69 if True:
70 pass
71 if True:
72 pass
73 if True:
74 pass
75 if True:
76 pass
77 if True:
78 pass
79 if True:
80 pass
81 return 1
82 """
83 with tempfile.TemporaryDirectory() as tmp:
84 root = _make_repo(pathlib.Path(tmp))
85 h = _write_object(root, src)
86 manifest = {"mod.py": h}
87 violations = check_max_complexity(manifest, root, "gate", "error", threshold=5)
88 assert len(violations) >= 1
89 assert violations[0]["rule_name"] == "gate"
90 assert "complexity" in violations[0]["description"].lower()
91
92 def test_non_python_file_skipped(self) -> None:
93 with tempfile.TemporaryDirectory() as tmp:
94 root = _make_repo(pathlib.Path(tmp))
95 src = b"def hello() { return 1; }"
96 h = _write_object(root, src)
97 manifest = {"mod.js": h}
98 violations = check_max_complexity(manifest, root, "c", "error", threshold=1)
99 assert violations == []
100
101
102 # ---------------------------------------------------------------------------
103 # check_no_circular_imports
104 # ---------------------------------------------------------------------------
105
106
107 class TestNoCircularImports:
108 def test_no_cycle_returns_empty(self) -> None:
109 with tempfile.TemporaryDirectory() as tmp:
110 root = _make_repo(pathlib.Path(tmp))
111 a = b"import b\n"
112 b_src = b"x = 1\n"
113 ha = _write_object(root, a)
114 hb = _write_object(root, b_src)
115 manifest = {"a.py": ha, "b.py": hb}
116 violations = check_no_circular_imports(manifest, root, "no_cycles", "error")
117 assert violations == []
118
119 def test_cycle_detected(self) -> None:
120 with tempfile.TemporaryDirectory() as tmp:
121 root = _make_repo(pathlib.Path(tmp))
122 # a imports b, b imports a → cycle
123 a = b"import b\n"
124 b_src = b"import a\n"
125 ha = _write_object(root, a)
126 hb = _write_object(root, b_src)
127 manifest = {"a.py": ha, "b.py": hb}
128 violations = check_no_circular_imports(manifest, root, "no_cycles", "error")
129 assert len(violations) >= 1
130 assert "cycle" in violations[0]["description"].lower()
131
132 def test_three_file_cycle_detected(self) -> None:
133 with tempfile.TemporaryDirectory() as tmp:
134 root = _make_repo(pathlib.Path(tmp))
135 a = b"import b\n"
136 b_src = b"import c\n"
137 c_src = b"import a\n"
138 ha = _write_object(root, a)
139 hb = _write_object(root, b_src)
140 hc = _write_object(root, c_src)
141 manifest = {"a.py": ha, "b.py": hb, "c.py": hc}
142 violations = check_no_circular_imports(manifest, root, "cycles", "error")
143 assert len(violations) >= 1
144
145
146 # ---------------------------------------------------------------------------
147 # check_no_dead_exports
148 # ---------------------------------------------------------------------------
149
150
151 class TestNoDeadExports:
152 def test_used_function_not_reported(self) -> None:
153 with tempfile.TemporaryDirectory() as tmp:
154 root = _make_repo(pathlib.Path(tmp))
155 lib = b"def my_func():\n return 1\n"
156 main = b"from lib import my_func\n"
157 hl = _write_object(root, lib)
158 hm = _write_object(root, main)
159 manifest = {"lib.py": hl, "main.py": hm}
160 violations = check_no_dead_exports(manifest, root, "dead", "warning")
161 # lib.my_func is imported by main.py → should not be reported.
162 addresses = [v["address"] for v in violations]
163 assert "lib.py::my_func" not in addresses
164
165 def test_unused_function_reported(self) -> None:
166 with tempfile.TemporaryDirectory() as tmp:
167 root = _make_repo(pathlib.Path(tmp))
168 lib = b"def orphan_fn():\n return 1\n"
169 other = b"x = 1\n"
170 hl = _write_object(root, lib)
171 ho = _write_object(root, other)
172 manifest = {"lib.py": hl, "other.py": ho}
173 violations = check_no_dead_exports(manifest, root, "dead", "warning")
174 addresses = [v["address"] for v in violations]
175 assert "lib.py::orphan_fn" in addresses
176
177 def test_private_function_exempt(self) -> None:
178 with tempfile.TemporaryDirectory() as tmp:
179 root = _make_repo(pathlib.Path(tmp))
180 lib = b"def _private():\n return 1\n"
181 h = _write_object(root, lib)
182 manifest = {"lib.py": h}
183 violations = check_no_dead_exports(manifest, root, "dead", "warning")
184 # Private functions are exempt.
185 assert all("_private" not in v["address"] for v in violations)
186
187 def test_test_file_exempt(self) -> None:
188 with tempfile.TemporaryDirectory() as tmp:
189 root = _make_repo(pathlib.Path(tmp))
190 lib = b"def test_something():\n assert True\n"
191 h = _write_object(root, lib)
192 manifest = {"test_stuff.py": h}
193 violations = check_no_dead_exports(manifest, root, "dead", "warning")
194 assert violations == []
195
196
197 # ---------------------------------------------------------------------------
198 # check_test_coverage_floor
199 # ---------------------------------------------------------------------------
200
201
202 class TestTestCoverageFloor:
203 def test_well_covered_code_no_violation(self) -> None:
204 with tempfile.TemporaryDirectory() as tmp:
205 root = _make_repo(pathlib.Path(tmp))
206 src = b"def foo():\n return 1\n"
207 test_src = b"def test_foo():\n assert True\n"
208 hs = _write_object(root, src)
209 ht = _write_object(root, test_src)
210 manifest = {"src.py": hs, "test_src.py": ht}
211 violations = check_test_coverage_floor(manifest, root, "coverage", "warning", min_ratio=0.5)
212 assert violations == []
213
214 def test_uncovered_code_violates(self) -> None:
215 with tempfile.TemporaryDirectory() as tmp:
216 root = _make_repo(pathlib.Path(tmp))
217 src = b"def foo():\n pass\ndef bar():\n pass\ndef baz():\n pass\n"
218 h = _write_object(root, src)
219 manifest = {"src.py": h}
220 violations = check_test_coverage_floor(manifest, root, "coverage", "warning", min_ratio=0.5)
221 assert len(violations) == 1
222 assert "coverage floor" in violations[0]["description"].lower()
223
224 def test_no_functions_no_violation(self) -> None:
225 with tempfile.TemporaryDirectory() as tmp:
226 root = _make_repo(pathlib.Path(tmp))
227 src = b"X = 1\n"
228 h = _write_object(root, src)
229 manifest = {"config.py": h}
230 violations = check_test_coverage_floor(manifest, root, "coverage", "warning", min_ratio=0.5)
231 assert violations == []
232
233
234 # ---------------------------------------------------------------------------
235 # load_invariant_rules
236 # ---------------------------------------------------------------------------
237
238
239 class TestLoadInvariantRules:
240 def test_no_file_returns_defaults(self) -> None:
241 """load_invariant_rules() with no argument returns built-in defaults."""
242 rules = load_invariant_rules(None)
243 assert len(rules) >= 1
244 rule_types = {r["rule_type"] for r in rules}
245 assert "max_complexity" in rule_types
246
247 def test_explicit_missing_path_returns_empty(self) -> None:
248 """An explicit path that does not exist yields no rules (caller opts out of defaults)."""
249 rules = load_invariant_rules(pathlib.Path("/no/such/file.toml"))
250 assert rules == []
251
252 def test_toml_file_loaded(self) -> None:
253 import tempfile
254 toml = "[[rule]]\nname='r1'\nseverity='error'\nscope='function'\nrule_type='max_complexity'\n"
255 with tempfile.NamedTemporaryFile(suffix=".toml", mode="w", delete=False) as f:
256 f.write(toml)
257 path = pathlib.Path(f.name)
258 try:
259 rules = load_invariant_rules(path)
260 assert any(r["rule_type"] == "max_complexity" for r in rules)
261 finally:
262 path.unlink(missing_ok=True)
263
264
265 # ---------------------------------------------------------------------------
266 # CodeChecker (protocol)
267 # ---------------------------------------------------------------------------
268
269
270 class TestCodeChecker:
271 def test_satisfies_invariant_checker_protocol(self) -> None:
272 checker = CodeChecker()
273 assert isinstance(checker, InvariantChecker)
274
275 def test_check_returns_base_report(self) -> None:
276 with tempfile.TemporaryDirectory() as tmp:
277 root = _make_repo(pathlib.Path(tmp))
278 # No commits — check should return a report with 0 violations.
279 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
280 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
281 import datetime
282 snap_id = compute_snapshot_id({})
283 snap = SnapshotRecord(snapshot_id=snap_id, manifest={})
284 write_snapshot(root, snap)
285 ts = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
286 cid = compute_commit_id(
287 repo_id="test",
288 parent_ids=[],
289 snapshot_id=snap_id,
290 message="init",
291 committed_at_iso=ts.isoformat(),
292 )
293 commit = CommitRecord(
294 commit_id=cid,
295 repo_id="test",
296 created_on_branch="main",
297 snapshot_id=snap_id,
298 message="init",
299 committed_at=ts,
300 )
301 write_commit(root, commit)
302 report = CodeChecker().check(root, cid)
303 assert report["commit_id"] == cid
304 assert report["domain"] == "code"
305 assert isinstance(report["violations"], list)
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 141 days ago