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