Option C Tools
Loading...
Searching...
No Matches
test_branch_policy.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_branch_policy.py
4
5"""
6Purpose
7-------
8Tests for branch naming enforcement and auto-profile selection
9(Phase 4E — G-E3, G-E4, G-E10).
10
11Responsibilities
12----------------
13- Validate ``validate_branch_name`` against default and custom patterns.
14- Validate ``_resolve_auto_profile`` branch-to-profile mapping.
15- Validate ``resolve_effective_profile`` with ``"auto"`` config value.
16
17Diagnostics
18-----------
19Domain: GIT-TESTS
20Levels:
21 L2 — test lifecycle
22 L3 — test details
23 L4 — deep tracing
24
25Contracts
26---------
27- All tests are pure unit tests (no git repo needed).
28"""
29
30import pytest
31
32from oct.git.policy import (
33 validate_branch_name,
34 _resolve_auto_profile,
35 resolve_effective_profile,
36 generate_branch_name_from_message,
37 DEFAULT_BRANCH_PATTERNS,
38 DEFAULT_PROFILE,
39)
40
41
42# =====================================================================
43# TestBranchNaming
44# =====================================================================
45
46
48 """Tests for validate_branch_name()."""
49
50 def test_main_valid(self) -> None:
51 valid, msg = validate_branch_name("main", None)
52 assert valid is True
53 assert msg == ""
54
55 def test_feature_branch_valid(self) -> None:
56 valid, msg = validate_branch_name("feature/add-auth", None)
57 assert valid is True
58
59 def test_release_branch_valid(self) -> None:
60 valid, msg = validate_branch_name("release/v1.0", None)
61 assert valid is True
62
63 def test_hotfix_branch_valid(self) -> None:
64 valid, msg = validate_branch_name("hotfix/fix-crash", None)
65 assert valid is True
66
67 def test_invalid_random_name(self) -> None:
68 valid, msg = validate_branch_name("my-random-branch", None)
69 assert valid is False
70 assert "does not match" in msg
71
73 """Custom patterns from octrc override defaults."""
74 octrc = {
75 "git": {
76 "branch_naming": {
77 "enabled": True,
78 "patterns": [r"^custom/.+$"],
79 },
80 },
81 }
82 valid, _ = validate_branch_name("custom/my-branch", octrc)
83 assert valid is True
84 # Default pattern should not match.
85 valid2, _ = validate_branch_name("feature/something", octrc)
86 assert valid2 is False
87
88 def test_disabled_always_valid(self) -> None:
89 """When branch_naming.enabled is false, everything is valid."""
90 octrc = {
91 "git": {
92 "branch_naming": {"enabled": False},
93 },
94 }
95 valid, msg = validate_branch_name("anything-goes", octrc)
96 assert valid is True
97 assert msg == ""
98
99 def test_none_branch_valid(self) -> None:
100 """None branch (detached HEAD) is always valid."""
101 valid, msg = validate_branch_name(None, None)
102 assert valid is True
103
104 def test_head_branch_valid(self) -> None:
105 """Literal 'HEAD' (detached) is always valid."""
106 valid, msg = validate_branch_name("HEAD", None)
107 assert valid is True
108
110 """Empty patterns list means everything is valid."""
111 octrc = {
112 "git": {
113 "branch_naming": {
114 "enabled": True,
115 "patterns": [],
116 },
117 },
118 }
119 valid, msg = validate_branch_name("whatever", octrc)
120 assert valid is True
121
122 def test_develop_valid(self) -> None:
123 valid, _ = validate_branch_name("develop", None)
124 assert valid is True
125
126 def test_docs_branch_valid(self) -> None:
127 valid, _ = validate_branch_name("docs/update-readme", None)
128 assert valid is True
129
130 def test_bugfix_branch_valid(self) -> None:
131 valid, _ = validate_branch_name("bugfix/null-pointer", None)
132 assert valid is True
133
134 def test_chore_branch_valid(self) -> None:
135 valid, _ = validate_branch_name("chore/deps-update", None)
136 assert valid is True
137
138
139# =====================================================================
140# TestAutoProfile
141# =====================================================================
142
143
145 """Tests for _resolve_auto_profile()."""
146
147 def test_main_strict(self) -> None:
148 assert _resolve_auto_profile("main") == "strict"
149
150 def test_master_strict(self) -> None:
151 assert _resolve_auto_profile("master") == "strict"
152
153 def test_feature_compact(self) -> None:
154 assert _resolve_auto_profile("feature/something") == "compact"
155
156 def test_release_strict(self) -> None:
157 assert _resolve_auto_profile("release/v1.0") == "strict"
158
159 def test_hotfix_strict(self) -> None:
160 assert _resolve_auto_profile("hotfix/urgent") == "strict"
161
162 def test_docs_compact(self) -> None:
163 assert _resolve_auto_profile("docs/update") == "compact"
164
165 def test_chore_compact(self) -> None:
166 assert _resolve_auto_profile("chore/cleanup") == "compact"
167
168 def test_unrecognized_default(self) -> None:
169 assert _resolve_auto_profile("random-branch") == DEFAULT_PROFILE
170
171 def test_none_branch_default(self) -> None:
172 assert _resolve_auto_profile(None) == DEFAULT_PROFILE
173
174 def test_head_default(self) -> None:
175 assert _resolve_auto_profile("HEAD") == DEFAULT_PROFILE
176
177 def test_bugfix_compact(self) -> None:
178 assert _resolve_auto_profile("bugfix/fix-it") == "compact"
179
180
181# =====================================================================
182# TestResolveEffectiveProfileAuto
183# =====================================================================
184
185
187 """Tests for resolve_effective_profile with auto mode."""
188
189 def test_auto_on_main(self) -> None:
190 """Auto on main resolves to strict (protected branch takes precedence)."""
191 octrc = {"git": {"pre_commit_profile": "auto", "protected_branches": ["main"]}}
192 # Protected branch override kicks in before auto.
193 result = resolve_effective_profile("main", octrc, None)
194 assert result == "strict"
195
196 def test_auto_on_feature(self) -> None:
197 """Auto on feature branch resolves to compact."""
198 octrc = {"git": {"pre_commit_profile": "auto"}}
199 result = resolve_effective_profile("feature/thing", octrc, None)
200 assert result == "compact"
201
202 def test_cli_overrides_auto(self) -> None:
203 """CLI --profile overrides auto."""
204 octrc = {"git": {"pre_commit_profile": "auto"}}
205 result = resolve_effective_profile("feature/thing", octrc, "proto")
206 assert result == "proto"
207
208 def test_auto_on_release(self) -> None:
209 """Auto on release branch resolves to strict."""
210 octrc = {"git": {"pre_commit_profile": "auto", "protected_branches": []}}
211 result = resolve_effective_profile("release/v1.0", octrc, None)
212 assert result == "strict"
213
214
215# =====================================================================
216# TestGenerateBranchName (Phase 4F — G-F2)
217# =====================================================================
218
219
221 """Tests for generate_branch_name_from_message()."""
222
224 name = generate_branch_name_from_message("feat: add login page")
225 assert name is not None
226 assert name.startswith("feature/")
227 assert "add-login-page" in name
228
229 def test_feat_with_scope(self) -> None:
230 name = generate_branch_name_from_message("feat(auth): add JWT tokens")
231 assert name == "feature/auth-add-jwt-tokens"
232
234 name = generate_branch_name_from_message("fix: null pointer in parser")
235 assert name is not None
236 assert name.startswith("bugfix/")
237
239 name = generate_branch_name_from_message("sec: patch CVE-2024-1234")
240 assert name is not None
241 assert name.startswith("hotfix/")
242
244 name = generate_branch_name_from_message("docs: update changelog")
245 assert name == "docs/update-changelog"
246
248 name = generate_branch_name_from_message("chore: cleanup deps")
249 assert name is not None
250 assert name.startswith("chore/")
251
253 name = generate_branch_name_from_message("refactor: simplify parser")
254 assert name is not None
255 assert name.startswith("chore/")
256
258 name = generate_branch_name_from_message("feat: add API v2 (beta)")
259 assert name is not None
260 # No parentheses, spaces, or special chars.
261 assert "(" not in name
262 assert " " not in name
263
265 long_msg = "feat: " + "a" * 100
266 name = generate_branch_name_from_message(long_msg)
267 assert name is not None
268 # Slug portion should not exceed 50 chars.
269 slug = name.split("/", 1)[1]
270 assert len(slug) <= 50
271
273 assert generate_branch_name_from_message("not a valid commit") is None
274 assert generate_branch_name_from_message("") is None
275
277 """All commit types produce names that pass validate_branch_name."""
278 messages = [
279 "feat: add feature",
280 "fix: fix bug",
281 "docs: update docs",
282 "style: fix formatting",
283 "refactor: simplify",
284 "perf: optimise query",
285 "test: add tests",
286 "chore: bump deps",
287 "sec: patch vuln",
288 ]
289 for msg in messages:
290 name = generate_branch_name_from_message(msg)
291 assert name is not None, f"failed for {msg!r}"
292 valid, val_msg = validate_branch_name(name, None)
293 assert valid, f"{name!r} from {msg!r}: {val_msg}"