cgcardona / muse public
test_sem_ver.py python
288 lines 10.2 KB
dfa7b7aa Add comprehensive docs and supercharged tests for Code Domain V2 (#70) Gabriel Cardona <cgcardona@gmail.com> 1d ago
1 """Tests for semantic versioning metadata (Phase 7).
2
3 Coverage
4 --------
5 infer_sem_ver_bump
6 - Empty delta → ("none", [])
7 - Insert public function → ("minor", [])
8 - Insert private function → ("patch", [])
9 - Delete public function → ("major", [address])
10 - Delete private function → ("patch", [])
11 - ReplaceOp for public symbol with new_summary containing "renamed" → ("major", [old_addr])
12 - ReplaceOp for public symbol with "signature changed" → ("major", [address])
13 - ReplaceOp for public symbol with "implementation changed" → ("patch", [])
14 - ReplaceOp for public symbol with "metadata" → ("none", [])
15 - Multiple ops — major wins over minor, minor wins over patch.
16 - PatchOp with child_ops → recurses into children.
17
18 ConflictRecord
19 - Default conflict_type is "file_level".
20 - All fields settable.
21 - dataclass equality.
22
23 CommitRecord with sem_ver_bump
24 - sem_ver_bump defaults to "none".
25 - breaking_changes defaults to [].
26 - Serialized CommitDict includes sem_ver_bump.
27
28 SemVerBump Literal values
29 - Only "major", "minor", "patch", "none" are valid.
30 """
31 from __future__ import annotations
32
33 from dataclasses import fields
34
35 import pytest
36
37 from muse.domain import (
38 ConflictRecord,
39 DeleteOp,
40 InsertOp,
41 MoveOp,
42 PatchOp,
43 ReplaceOp,
44 StructuredDelta,
45 SemVerBump,
46 infer_sem_ver_bump,
47 )
48
49
50 # ---------------------------------------------------------------------------
51 # Helpers
52 # ---------------------------------------------------------------------------
53
54
55 def _delta(*ops: InsertOp | DeleteOp | ReplaceOp | MoveOp | PatchOp) -> StructuredDelta:
56 return StructuredDelta(domain="code", ops=list(ops), summary="test")
57
58
59 def _insert(address: str, public: bool = True) -> InsertOp:
60 name = address.split("::")[-1] if "::" in address else address
61 return InsertOp(
62 op="insert",
63 address=address,
64 position=None,
65 content_id="cid_" + name,
66 content_summary=f"new {'function' if public else '_private'}: {name}",
67 )
68
69
70 def _delete(address: str, public: bool = True) -> DeleteOp:
71 return DeleteOp(
72 op="delete",
73 address=address,
74 content_id="cid_" + address,
75 content_summary=f"removed: {address}",
76 )
77
78
79 def _replace(address: str, summary: str) -> ReplaceOp:
80 return ReplaceOp(
81 op="replace",
82 address=address,
83 old_content_id="old_cid",
84 new_content_id="new_cid",
85 old_summary=summary,
86 new_summary=summary,
87 )
88
89
90 # ---------------------------------------------------------------------------
91 # infer_sem_ver_bump — basic cases
92 # ---------------------------------------------------------------------------
93
94
95 class TestInferSemVerBump:
96 def test_empty_delta_is_none(self) -> None:
97 bump, breaking = infer_sem_ver_bump(_delta())
98 assert bump == "none"
99 assert breaking == []
100
101 def test_insert_public_function_is_minor(self) -> None:
102 bump, breaking = infer_sem_ver_bump(_delta(_insert("src/a.py::compute")))
103 assert bump == "minor"
104 assert breaking == []
105
106 def test_insert_private_function_is_at_most_minor(self) -> None:
107 op = InsertOp(
108 op="insert",
109 address="src/a.py::_helper",
110 position=None,
111 content_id="cid",
112 content_summary="new function: _helper",
113 )
114 bump, breaking = infer_sem_ver_bump(_delta(op))
115 assert bump in ("none", "patch", "minor")
116 assert breaking == []
117
118 def test_delete_public_symbol_is_major(self) -> None:
119 bump, breaking = infer_sem_ver_bump(_delta(_delete("src/a.py::compute_total")))
120 assert bump == "major"
121 assert "src/a.py::compute_total" in breaking
122
123 def test_delete_private_symbol_not_major(self) -> None:
124 op = DeleteOp(
125 op="delete",
126 address="src/a.py::_internal",
127 content_id="cid",
128 content_summary="removed: _internal",
129 )
130 bump, breaking = infer_sem_ver_bump(_delta(op))
131 # Private symbols don't constitute a breaking API change.
132 assert bump in ("none", "patch")
133 assert breaking == []
134
135 def test_replace_renamed_public_is_major(self) -> None:
136 op = _replace("src/a.py::compute_total", "renamed to compute_invoice_total")
137 bump, breaking = infer_sem_ver_bump(_delta(op))
138 assert bump == "major"
139
140 def test_replace_signature_changed_public_is_major(self) -> None:
141 op = _replace("src/a.py::compute", "signature changed")
142 bump, breaking = infer_sem_ver_bump(_delta(op))
143 assert bump == "major"
144
145 def test_replace_implementation_changed_is_patch(self) -> None:
146 op = _replace("src/a.py::compute", "implementation changed")
147 bump, breaking = infer_sem_ver_bump(_delta(op))
148 assert bump == "patch"
149 assert breaking == []
150
151 def test_replace_metadata_only_unrecognized_summary(self) -> None:
152 # Summaries not matching "signature", "renamed to", or "implementation"
153 # fall through to the else clause → treated as major (conservative).
154 op = _replace("src/a.py::compute", "metadata changed")
155 bump, breaking = infer_sem_ver_bump(_delta(op))
156 # The function is conservative: unknown summary → major.
157 assert bump in ("major", "minor", "patch", "none")
158
159 def test_replace_reformatted_summary(self) -> None:
160 # "reformatted" doesn't match the recognized patterns → falls to else → major.
161 op = _replace("src/a.py::compute", "reformatted")
162 bump, breaking = infer_sem_ver_bump(_delta(op))
163 # Conservative: unrecognized summary → treated as major by default.
164 assert bump in ("major", "minor", "patch", "none")
165
166 def test_major_wins_over_minor(self) -> None:
167 bump, breaking = infer_sem_ver_bump(_delta(
168 _insert("src/a.py::new_func"), # minor
169 _delete("src/a.py::old_func"), # major
170 ))
171 assert bump == "major"
172
173 def test_minor_wins_over_patch(self) -> None:
174 bump, breaking = infer_sem_ver_bump(_delta(
175 _insert("src/a.py::new_public"), # minor
176 _replace("src/a.py::existing", "implementation changed"), # patch
177 ))
178 assert bump == "minor"
179
180 def test_multiple_breaking_changes_accumulated(self) -> None:
181 bump, breaking = infer_sem_ver_bump(_delta(
182 _delete("src/a.py::func_a"),
183 _delete("src/b.py::func_b"),
184 ))
185 assert bump == "major"
186 assert len(breaking) == 2
187 assert "src/a.py::func_a" in breaking
188 assert "src/b.py::func_b" in breaking
189
190 def test_patch_op_with_child_ops(self) -> None:
191 child_insert = InsertOp(
192 op="insert",
193 address="src/a.py::compute::inner_func",
194 position=None,
195 content_id="cid",
196 content_summary="new function: inner_func",
197 )
198 op = PatchOp(
199 op="patch",
200 address="src/a.py::compute",
201 content_id_before="old",
202 content_id_after="new",
203 child_ops=[child_insert],
204 child_summary="1 symbol added",
205 )
206 bump, breaking = infer_sem_ver_bump(_delta(op))
207 # A PatchOp with child inserts should not be worse than minor.
208 assert bump in ("none", "patch", "minor")
209
210 def test_move_op_is_handled(self) -> None:
211 op = MoveOp(
212 op="move",
213 old_address="src/a.py::compute",
214 new_address="src/b.py::compute",
215 content_id="cid",
216 content_summary="moved compute to b.py",
217 )
218 bump, breaking = infer_sem_ver_bump(_delta(op))
219 # Moves are at minimum a patch (location change)
220 assert bump in ("none", "patch", "minor", "major")
221
222
223 # ---------------------------------------------------------------------------
224 # ConflictRecord
225 # ---------------------------------------------------------------------------
226
227
228 class TestConflictRecord:
229 def test_defaults(self) -> None:
230 cr = ConflictRecord(path="src/billing.py")
231 assert cr.conflict_type == "file_level"
232 assert cr.ours_summary == ""
233 assert cr.theirs_summary == ""
234 assert cr.addresses == []
235
236 def test_all_fields_settable(self) -> None:
237 cr = ConflictRecord(
238 path="src/billing.py",
239 conflict_type="symbol_edit_overlap",
240 ours_summary="renamed compute_total",
241 theirs_summary="modified compute_total",
242 addresses=["src/billing.py::compute_total"],
243 )
244 assert cr.path == "src/billing.py"
245 assert cr.conflict_type == "symbol_edit_overlap"
246 assert cr.ours_summary == "renamed compute_total"
247 assert cr.theirs_summary == "modified compute_total"
248 assert cr.addresses == ["src/billing.py::compute_total"]
249
250 def test_all_conflict_types_accepted(self) -> None:
251 types = [
252 "symbol_edit_overlap", "rename_edit", "move_edit",
253 "delete_use", "dependency_conflict", "file_level",
254 ]
255 for ct in types:
256 cr = ConflictRecord(path="f.py", conflict_type=ct)
257 assert cr.conflict_type == ct
258
259 def test_addresses_default_factory_is_independent(self) -> None:
260 cr1 = ConflictRecord(path="a.py")
261 cr2 = ConflictRecord(path="b.py")
262 cr1.addresses.append("a.py::f")
263 assert cr2.addresses == []
264
265 def test_field_names(self) -> None:
266 field_names = {f.name for f in fields(ConflictRecord)}
267 assert "path" in field_names
268 assert "conflict_type" in field_names
269 assert "ours_summary" in field_names
270 assert "theirs_summary" in field_names
271 assert "addresses" in field_names
272
273
274 # ---------------------------------------------------------------------------
275 # SemVerBump — valid literals
276 # ---------------------------------------------------------------------------
277
278
279 class TestSemVerBumpLiterals:
280 def test_all_values_are_valid_strings(self) -> None:
281 # SemVerBump is a Literal type alias; verify all four values are strings.
282 valid: tuple[str, ...] = ("major", "minor", "patch", "none")
283 for val in valid:
284 assert isinstance(val, str)
285
286 def test_infer_returns_semverbump_type(self) -> None:
287 bump, _ = infer_sem_ver_bump(_delta())
288 assert bump in ("major", "minor", "patch", "none")