Option C Tools
Loading...
Searching...
No Matches
test_policy.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_git/test_policy.py
4
5"""
6Purpose
7-------
8Unit tests for :mod:`oct.git.policy` — the profile resolution and
9exit-code derivation layer for ``oct git check`` (Phase 4B / G-B4).
10
11Responsibilities
12----------------
13- Verify profile resolution precedence: protected branch > CLI >
14 ``.octrc.json`` > default.
15- Verify the :data:`~oct.git.policy.PROFILE_MATRIX` rows against the
16 blueprint §5.3 / §7.1 specification, byte for byte.
17- Verify the exit-code table 0..5 with every precedence permutation,
18 including the "secrets always blocks" invariant.
19
20Diagnostics
21-----------
22Domain: GIT (pure unit tests; logging exercised only incidentally)
23 Levels:
24 L2 — test lifecycle
25 L3 — assertion details
26 L4 — deep tracing
27
28Contracts
29---------
30- Every test is hermetic — no filesystem access, no subprocess, no git.
31- ``QualityGateResult`` stand-ins are built from :class:`types.SimpleNamespace`
32 so these tests do not depend on :mod:`oct.git.quality_gate` being
33 implemented yet (it lands later in the same phase).
34"""
35
36from types import SimpleNamespace
37
38import pytest
39
40from oct.git.policy import (
41 DEFAULT_PROFILE,
42 DEFAULT_PROTECTED_BRANCHES,
43 PROFILE_MATRIX,
44 SECRETS_ALWAYS_BLOCKS,
45 VALID_PROFILES,
46 apply_profile_policy,
47 is_protected_branch,
48 resolve_effective_profile,
49)
50
51
52# ---------------------------------------------------------------------
53# Helpers
54# ---------------------------------------------------------------------
55
56
58 *,
59 lint_violations: int = 0,
60 format_violations: int = 0,
61 secrets_findings: list | None = None,
62 test_failures: int = 0,
63) -> SimpleNamespace:
64 """Return a minimal stand-in for ``QualityGateResult``.
65
66 ``apply_profile_policy`` only reads the four attributes set here,
67 so a SimpleNamespace is sufficient. Using SimpleNamespace lets the
68 tests run before :mod:`oct.git.quality_gate` exists.
69 """
70 return SimpleNamespace(
71 lint_violations=lint_violations,
72 format_violations=format_violations,
73 secrets_findings=list(secrets_findings or []),
74 test_failures=test_failures,
75 )
76
77
78# ---------------------------------------------------------------------
79# PROFILE_MATRIX invariants
80# ---------------------------------------------------------------------
81
82
85 assert set(PROFILE_MATRIX.keys()) == {"proto", "compact", "strict"}
86
88 assert set(VALID_PROFILES) == set(PROFILE_MATRIX.keys())
89
91 assert SECRETS_ALWAYS_BLOCKS is True
92
94 # Blueprint §5.3 verbatim: "Secrets check is always blocking
95 # regardless of profile. Safety above all."
96 for profile, row in PROFILE_MATRIX.items():
97 assert row["secrets"] == "blocking", (
98 f"profile {profile} must have secrets=blocking"
99 )
100
102 # Blueprint §5.3: proto => lint warn, format skip, secrets blocking, tests skip
103 row = PROFILE_MATRIX["proto"]
104 assert row["lint"] == "warn"
105 assert row["format"] == "skip"
106 assert row["secrets"] == "blocking"
107 assert row["tests"] == "skip"
108
110 row = PROFILE_MATRIX["compact"]
111 assert row["lint"] == "blocking"
112 assert row["format"] == "blocking"
113 assert row["secrets"] == "blocking"
114 assert row["tests"] == "optional"
115
117 row = PROFILE_MATRIX["strict"]
118 assert row["lint"] == "blocking"
119 assert row["format"] == "blocking"
120 assert row["secrets"] == "blocking"
121 assert row["tests"] == "blocking"
122
124 # Blueprint §7: project default is "compact".
125 assert DEFAULT_PROFILE == "compact"
126
128 # Blueprint §7.2: default protected set is main + master.
129 assert DEFAULT_PROTECTED_BRANCHES == ("main", "master")
130
131
132# ---------------------------------------------------------------------
133# is_protected_branch
134# ---------------------------------------------------------------------
135
136
139 assert is_protected_branch("main", None) is True
140
142 assert is_protected_branch("master", None) is True
143
145 assert is_protected_branch("feature/xyz", None) is False
146
148 assert is_protected_branch(None, None) is False
149
151 assert is_protected_branch("", None) is False
152
154 # oct.core.git.current_branch returns "HEAD" when detached.
155 assert is_protected_branch("HEAD", None) is False
156
158 octrc = {"git": {"protected_branches": ["release/current"]}}
159 assert is_protected_branch("release/current", octrc) is True
160 # main is NOT in the custom list — not protected here
161 assert is_protected_branch("main", octrc) is False
162
164 # git.protected_branches is not a list -> ignored, use defaults
165 octrc = {"git": {"protected_branches": "not-a-list"}}
166 assert is_protected_branch("main", octrc) is True
167
169 octrc = {"git": {"protected_branches": ["main", 42, None, "dev"]}}
170 assert is_protected_branch("main", octrc) is True
171 assert is_protected_branch("dev", octrc) is True
172 assert is_protected_branch("feature/x", octrc) is False
173
174
175# ---------------------------------------------------------------------
176# resolve_effective_profile
177# ---------------------------------------------------------------------
178
179
182 # Even with CLI override asking for proto, main is strict
183 assert resolve_effective_profile("main", None, "proto") == "strict"
184
186 octrc = {"git": {"pre_commit_profile": "proto"}}
187 assert resolve_effective_profile("main", octrc, None) == "strict"
188
190 octrc = {"git": {"pre_commit_profile": "proto"}}
191 assert resolve_effective_profile("feature/x", octrc, "strict") == "strict"
192
194 octrc = {"git": {"pre_commit_profile": "proto"}}
195 # "banana" is not a valid profile -> fall through to octrc
196 assert resolve_effective_profile("feature/x", octrc, "banana") == "proto"
197
199 octrc = {"git": {"pre_commit_profile": "strict"}}
200 assert resolve_effective_profile("feature/x", octrc, None) == "strict"
201
203 octrc = {"linter": {"profile": "proto"}}
204 assert resolve_effective_profile("feature/x", octrc, None) == "proto"
205
207 octrc = {
208 "git": {"pre_commit_profile": "strict"},
209 "linter": {"profile": "proto"},
210 }
211 assert resolve_effective_profile("feature/x", octrc, None) == "strict"
212
214 assert resolve_effective_profile("feature/x", None, None) == DEFAULT_PROFILE
215
217 assert resolve_effective_profile("feature/x", {}, None) == DEFAULT_PROFILE
218
220 octrc = {"linter": {"profile": "not-a-profile"}}
221 assert resolve_effective_profile("feature/x", octrc, None) == DEFAULT_PROFILE
222
224 # Detached HEAD is not protected -> CLI override wins
225 assert resolve_effective_profile("HEAD", None, "strict") == "strict"
226
228 assert resolve_effective_profile(None, None, "proto") == "proto"
229
230
231# ---------------------------------------------------------------------
232# apply_profile_policy — exit code 0 (all green)
233# ---------------------------------------------------------------------
234
235
238 assert apply_profile_policy("compact", _gate_result()) == 0
239
241 for profile in VALID_PROFILES:
242 assert apply_profile_policy(profile, _gate_result()) == 0
243
245 assert apply_profile_policy("nonsense", _gate_result()) == 0
246
247
248# ---------------------------------------------------------------------
249# apply_profile_policy — exit code 1 (lint)
250# ---------------------------------------------------------------------
251
252
255 r = _gate_result(lint_violations=3)
256 assert apply_profile_policy("compact", r) == 1
257
259 r = _gate_result(lint_violations=1)
260 assert apply_profile_policy("strict", r) == 1
261
263 # proto: lint=warn -> not in exit code
264 r = _gate_result(lint_violations=5)
265 assert apply_profile_policy("proto", r) == 0
266
267
268# ---------------------------------------------------------------------
269# apply_profile_policy — exit code 2 (format)
270# ---------------------------------------------------------------------
271
272
275 r = _gate_result(format_violations=2)
276 assert apply_profile_policy("compact", r) == 2
277
279 r = _gate_result(format_violations=2)
280 assert apply_profile_policy("strict", r) == 2
281
283 # proto: format=skip -> never contributes to exit code
284 r = _gate_result(format_violations=5)
285 assert apply_profile_policy("proto", r) == 0
286
287
288# ---------------------------------------------------------------------
289# apply_profile_policy — exit code 3 (lint + format)
290# ---------------------------------------------------------------------
291
292
295 r = _gate_result(lint_violations=1, format_violations=1)
296 assert apply_profile_policy("compact", r) == 3
297
299 r = _gate_result(lint_violations=10, format_violations=3)
300 assert apply_profile_policy("strict", r) == 3
301
303 # proto blocks neither -> zero even with both non-zero
304 r = _gate_result(lint_violations=4, format_violations=4)
305 assert apply_profile_policy("proto", r) == 0
306
307
308# ---------------------------------------------------------------------
309# apply_profile_policy — exit code 4 (tests)
310# ---------------------------------------------------------------------
311
312
315 r = _gate_result(test_failures=1)
316 assert apply_profile_policy("strict", r) == 4
317
319 # OI-531: compact sets tests="optional" — failures run and are
320 # reported upstream but must NOT drive the exit code. Only
321 # "blocking" (under strict) earns exit 4.
322 r = _gate_result(test_failures=3)
323 assert apply_profile_policy("compact", r) == 0
324
326 # proto: tests=skip -> not blocking
327 r = _gate_result(test_failures=5)
328 assert apply_profile_policy("proto", r) == 0
329
331 # 4 > 3: when tests and lint both fail, tests wins
332 r = _gate_result(
333 lint_violations=1,
334 format_violations=1,
335 test_failures=1,
336 )
337 assert apply_profile_policy("strict", r) == 4
338
339
340# ---------------------------------------------------------------------
341# apply_profile_policy — exit code 5 (secrets always blocks)
342# ---------------------------------------------------------------------
343
344
347 r = _gate_result(secrets_findings=[("f.py", 1, "password")])
348 assert apply_profile_policy("strict", r) == 5
349
351 r = _gate_result(secrets_findings=[("f.py", 1, "password")])
352 assert apply_profile_policy("compact", r) == 5
353
355 # The invariant: proto may warn on lint but MUST block on secrets
356 r = _gate_result(secrets_findings=[("f.py", 1, "password")])
357 assert apply_profile_policy("proto", r) == 5
358
360 r = _gate_result(
361 lint_violations=1,
362 secrets_findings=[("f.py", 1, "password")],
363 )
364 assert apply_profile_policy("compact", r) == 5
365
367 r = _gate_result(
368 format_violations=1,
369 secrets_findings=[("f.py", 1, "password")],
370 )
371 assert apply_profile_policy("compact", r) == 5
372
374 r = _gate_result(
375 lint_violations=3,
376 format_violations=2,
377 secrets_findings=[("f.py", 1, "password")],
378 )
379 assert apply_profile_policy("compact", r) == 5
380
382 r = _gate_result(
383 test_failures=2,
384 secrets_findings=[("f.py", 1, "password")],
385 )
386 assert apply_profile_policy("strict", r) == 5
387
389 r = _gate_result(
390 lint_violations=5,
391 format_violations=5,
392 test_failures=5,
393 secrets_findings=[
394 ("f.py", 1, "password"),
395 ("g.py", 2, "api_key"),
396 ],
397 )
398 assert apply_profile_policy("strict", r) == 5
SimpleNamespace _gate_result(*, int lint_violations=0, int format_violations=0, list|None secrets_findings=None, int test_failures=0)