cgcardona / muse public
test_core_invariants.py python
180 lines 5.6 KB
766ee24d feat: code domain leverages core invariants, query engine, manifests, p… Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Tests for the generic invariants engine in muse/core/invariants.py."""
2 from __future__ import annotations
3
4 import pathlib
5 import tempfile
6
7 import pytest
8
9 from muse.core.invariants import (
10 BaseReport,
11 BaseViolation,
12 InvariantChecker,
13 InvariantSeverity,
14 format_report,
15 load_rules_toml,
16 make_report,
17 )
18
19
20 # ---------------------------------------------------------------------------
21 # make_report
22 # ---------------------------------------------------------------------------
23
24
25 def _make_violation(
26 rule_name: str = "test_rule",
27 severity: InvariantSeverity = "error",
28 address: str = "src/foo.py",
29 description: str = "test violation",
30 ) -> BaseViolation:
31 return BaseViolation(
32 rule_name=rule_name,
33 severity=severity,
34 address=address,
35 description=description,
36 )
37
38
39 class TestMakeReport:
40 def test_empty_violations(self) -> None:
41 report = make_report("abc123", "code", [], 3)
42 assert report["commit_id"] == "abc123"
43 assert report["domain"] == "code"
44 assert report["violations"] == []
45 assert report["rules_checked"] == 3
46 assert not report["has_errors"]
47 assert not report["has_warnings"]
48
49 def test_error_sets_has_errors(self) -> None:
50 v = _make_violation(severity="error")
51 report = make_report("abc", "code", [v], 1)
52 assert report["has_errors"] is True
53 assert report["has_warnings"] is False
54
55 def test_warning_sets_has_warnings(self) -> None:
56 v = _make_violation(severity="warning")
57 report = make_report("abc", "code", [v], 1)
58 assert report["has_errors"] is False
59 assert report["has_warnings"] is True
60
61 def test_violations_sorted_by_address(self) -> None:
62 v1 = _make_violation(address="z.py")
63 v2 = _make_violation(address="a.py")
64 report = make_report("abc", "code", [v1, v2], 2)
65 assert report["violations"][0]["address"] == "a.py"
66 assert report["violations"][1]["address"] == "z.py"
67
68 def test_info_does_not_set_flags(self) -> None:
69 v = _make_violation(severity="info")
70 report = make_report("abc", "code", [v], 1)
71 assert not report["has_errors"]
72 assert not report["has_warnings"]
73
74
75 # ---------------------------------------------------------------------------
76 # format_report
77 # ---------------------------------------------------------------------------
78
79
80 class TestFormatReport:
81 def test_no_violations_shows_green(self) -> None:
82 report = make_report("abc", "code", [], 5)
83 out = format_report(report)
84 assert "✅" in out
85 assert "5 rules" in out
86
87 def test_error_shows_red_cross(self) -> None:
88 v = _make_violation(severity="error", address="src/foo.py::bar")
89 report = make_report("abc", "code", [v], 1)
90 out = format_report(report)
91 assert "❌" in out
92 assert "src/foo.py::bar" in out
93
94 def test_warning_shows_warning_emoji(self) -> None:
95 v = _make_violation(severity="warning", address="src/baz.py")
96 report = make_report("abc", "code", [v], 1)
97 out = format_report(report)
98 assert "⚠️" in out
99
100 def test_no_color_mode(self) -> None:
101 v = _make_violation(severity="error")
102 report = make_report("abc", "code", [v], 1)
103 out = format_report(report, color=False)
104 assert "[error]" in out
105 assert "❌" not in out
106
107
108 # ---------------------------------------------------------------------------
109 # load_rules_toml
110 # ---------------------------------------------------------------------------
111
112
113 class TestLoadRulesToml:
114 def test_missing_file_returns_empty(self) -> None:
115 path = pathlib.Path("/nonexistent/path/rules.toml")
116 result = load_rules_toml(path)
117 assert result == []
118
119 def test_valid_toml_parsed(self) -> None:
120 toml_content = """
121 [[rule]]
122 name = "my_rule"
123 severity = "error"
124 scope = "file"
125 rule_type = "max_complexity"
126
127 [rule.params]
128 threshold = 10
129 """
130 with tempfile.NamedTemporaryFile(suffix=".toml", mode="w", delete=False) as f:
131 f.write(toml_content)
132 path = pathlib.Path(f.name)
133
134 try:
135 rules = load_rules_toml(path)
136 assert len(rules) == 1
137 assert rules[0]["name"] == "my_rule"
138 assert rules[0]["severity"] == "error"
139 finally:
140 path.unlink(missing_ok=True)
141
142 def test_empty_toml_returns_empty(self) -> None:
143 with tempfile.NamedTemporaryFile(suffix=".toml", mode="w", delete=False) as f:
144 f.write("")
145 path = pathlib.Path(f.name)
146 try:
147 result = load_rules_toml(path)
148 assert result == []
149 finally:
150 path.unlink(missing_ok=True)
151
152
153 # ---------------------------------------------------------------------------
154 # InvariantChecker protocol
155 # ---------------------------------------------------------------------------
156
157
158 class TestInvariantCheckerProtocol:
159 def test_concrete_checker_satisfies_protocol(self) -> None:
160 """A class with a check() method satisfies the InvariantChecker protocol."""
161
162 class MyChecker:
163 def check(
164 self,
165 repo_root: pathlib.Path,
166 commit_id: str,
167 *,
168 rules_file: pathlib.Path | None = None,
169 ) -> BaseReport:
170 return make_report(commit_id, "test", [], 0)
171
172 checker = MyChecker()
173 assert isinstance(checker, InvariantChecker)
174
175 def test_missing_check_method_fails_protocol(self) -> None:
176 class NotAChecker:
177 def run(self) -> None:
178 pass
179
180 assert not isinstance(NotAChecker(), InvariantChecker)