Option C Tools
Loading...
Searching...
No Matches
test_conventional.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_conventional.py
4
5"""
6Purpose
7-------
8Unit tests for ``oct.git.conventional`` (Phase 4D — G-D1, G-D2, G-D5).
9
10Responsibilities
11----------------
12- Verify Conventional Commits parsing (type, scope, subject, body, footer).
13- Verify commit message validation rules.
14- Verify validation error formatting.
15- Verify diagnostics impact analysis on diffs.
16
17Diagnostics
18-----------
19Domain: GIT-TESTS
20Levels:
21 L2 — test lifecycle
22 L3 — assertion details
23 L4 — deep tracing
24
25Contracts
26---------
27- All tests are pure unit tests (no filesystem or git needed).
28"""
29
30import textwrap
31
32import pytest
33
34from oct.git.conventional import (
35 COMMIT_TYPES,
36 MAX_SUBJECT_LENGTH,
37 ConventionalCommit,
38 analyse_diagnostics_impact,
39 format_validation_errors,
40 parse_commit_message,
41 validate_commit_message,
42)
43
44
45# =====================================================================
46# Parsing
47# =====================================================================
48
49
51
53 c = parse_commit_message("feat: add login page")
54 assert c is not None
55 assert c.type == "feat"
56 assert c.scope is None
57 assert c.subject == "add login page"
58 assert c.breaking is False
59
60 def test_with_scope(self):
61 c = parse_commit_message("fix(auth): patch null pointer")
62 assert c is not None
63 assert c.type == "fix"
64 assert c.scope == "auth"
65 assert c.subject == "patch null pointer"
66
68 c = parse_commit_message("docs(readme): update install section")
69 assert c is not None
70 assert c.type == "docs"
71 assert c.scope == "readme"
72 assert c.subject == "update install section"
73
75 c = parse_commit_message("feat!: breaking change")
76 assert c is not None
77 assert c.breaking is True
78
80 msg = "feat: add thing\n\nBREAKING CHANGE: old API removed"
81 c = parse_commit_message(msg)
82 assert c is not None
83 assert c.breaking is True
84
86 assert parse_commit_message("random garbage") is None
87
89 assert parse_commit_message("") is None
90
92 assert parse_commit_message(" \n ") is None
93
95 msg = "fix: patch bug\n\nThis fixes the null pointer\nin the auth module."
96 c = parse_commit_message(msg)
97 assert c is not None
98 assert c.subject == "patch bug"
99 assert "null pointer" in c.body
100
102 for t in COMMIT_TYPES:
103 c = parse_commit_message(f"{t}: do something")
104 assert c is not None, f"Failed to parse type '{t}'"
105 assert c.type == t
106
108 c = parse_commit_message("feat(core-git): add function")
109 assert c is not None
110 assert c.scope == "core-git"
111
113 c = parse_commit_message("fix:patch bug")
114 assert c is not None
115 assert c.subject == "patch bug"
116
118 c = parse_commit_message("fix: lots of spaces")
119 assert c is not None
120 assert c.subject == "lots of spaces"
121
122
123# =====================================================================
124# Validation
125# =====================================================================
126
127
129
131 errors = validate_commit_message("feat(auth): add token validation")
132 assert errors == []
133
135 errors = validate_commit_message("fix: patch bug")
136 assert errors == []
137
139 errors = validate_commit_message("update: change stuff")
140 assert any("update" in e and "not valid" in e for e in errors)
141 # Should mention available types.
142 assert any("feat" in e for e in errors)
143
145 long_subject = "a" * 80
146 errors = validate_commit_message(f"feat: {long_subject}")
147 assert any("exceeds" in e or "72" in e for e in errors)
148
150 errors = validate_commit_message("feat: Add something")
151 assert any("lowercase" in e for e in errors)
152
154 errors = validate_commit_message("feat: add something.")
155 assert any("period" in e for e in errors)
156
158 errors = validate_commit_message("")
159 assert len(errors) > 0
160 assert any("empty" in e.lower() for e in errors)
161
163 errors = validate_commit_message("just a plain message")
164 assert len(errors) > 0
165 assert any("Conventional Commits" in e for e in errors)
166
168 errors = validate_commit_message("update: Add something.")
169 # Should have at least: unknown type, uppercase, trailing period.
170 assert len(errors) >= 2
171
173 # "feat: " is 6 chars, so subject can be 66 chars for total 72.
174 subject = "a" * 66
175 errors = validate_commit_message(f"feat: {subject}")
176 assert not any("exceeds" in e or "72" in e for e in errors)
177
179 errors = validate_commit_message("sec: patch vulnerability")
180 assert errors == []
181
183 errors = validate_commit_message("feat!: breaking api change")
184 assert errors == []
185
186
187# =====================================================================
188# Error formatting
189# =====================================================================
190
191
193
195 result = format_validation_errors(["Type is invalid."])
196 assert "Commit message validation failed:" in result
197 assert "- Type is invalid." in result
198 assert "Example:" in result
199
201 errors = ["Error one.", "Error two."]
202 result = format_validation_errors(errors)
203 assert "- Error one." in result
204 assert "- Error two." in result
205
207 result = format_validation_errors(["bad"])
208 assert "fix(auth):" in result
209
210
211# =====================================================================
212# Diagnostics impact analysis
213# =====================================================================
214
215
217
219 diff = textwrap.dedent("""\
220 --- a/src/module.py
221 +++ b/src/module.py
222 @@ -1,3 +1,4 @@
223 + _dbg("GIT", func, "starting", 2)
224 """)
225 impact = analyse_diagnostics_impact(diff)
226 assert any("+ Added: GIT" in line for line in impact)
227
229 diff = textwrap.dedent("""\
230 --- a/src/module.py
231 +++ b/src/module.py
232 @@ -1,4 +1,3 @@
233 - _dbg("AUTH", func, "checking", 3)
234 """)
235 impact = analyse_diagnostics_impact(diff)
236 assert any("- Removed: AUTH" in line for line in impact)
237
239 diff = textwrap.dedent("""\
240 --- a/src/module.py
241 +++ b/src/module.py
242 @@ -1,4 +1,4 @@
243 - _dbg("AUTH", func, "old", 2)
244 + _dbg("AUTH", func, "new", 3)
245 """)
246 impact = analyse_diagnostics_impact(diff)
247 assert any("~ Modified: AUTH" in line for line in impact)
248
250 diff = textwrap.dedent("""\
251 --- a/src/module.py
252 +++ b/src/module.py
253 @@ -1,3 +1,3 @@
254 - x = 1
255 + x = 2
256 """)
257 impact = analyse_diagnostics_impact(diff)
258 assert impact == []
259
261 assert analyse_diagnostics_impact("") == []
262
264 diff = textwrap.dedent("""\
265 + _dbg("GIT", func, "new", 2)
266 - _dbg("CACHE", func, "old", 1)
267 """)
268 impact = analyse_diagnostics_impact(diff)
269 assert any("+ Added: GIT" in line for line in impact)
270 assert any("- Removed: CACHE" in line for line in impact)
271
273 impact = analyse_diagnostics_impact("not a real diff\ngarbage\n+++---")
274 assert isinstance(impact, list)
275
276
277# =====================================================================
278# OI-526 — _FOOTER_RE stability (regex compiled once at module scope)
279# =====================================================================
280
281
283 """Verify _FOOTER_RE is defined at module scope and not recompiled per call."""
284
286 from oct.git import conventional
287 assert hasattr(conventional, "_FOOTER_RE")
288 import re as _re
289 assert isinstance(conventional._FOOTER_RE, _re.Pattern)
290
292 from oct.git import conventional
293 before = conventional._FOOTER_RE
294 parse_commit_message("feat: first call\n\nbody\n\nRefs: #1")
295 parse_commit_message("fix: second call\n\nSigned-off-by: x <x@y>")
296 after = conventional._FOOTER_RE
297 assert before is after