oct
Loading...
Searching...
No Matches
errors.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oct/testing/errors.py
4
5# OCT-LINT: disable=no_hardcoded_secrets reason="Operator-approved 2026-08-12: spec-anchor-dense prose trips entropy thresholds (module docstring 5.10 bits/char at L5, 9101 catalog description 4.56 at L118); static documentation strings, no credentials"
6"""
7Purpose
8-------
9The spec-10 §12.4 error taxonomy for the ``oct test`` v1.0 seed
10(Plan 189) - AS DATA. The library never calls ``sys.exit``; it raises
11:class:`OctTestError` carrying the stable §12.4 code, the error kind,
12and the severity, plus the kind → exit-code mapping the future v1.0
13CLI slice applies at its boundary.
14
15Responsibilities
16----------------
17- Define the §12.4 stable identifiers the discovery layer emits
18 (``WORKSPACE_FILE_NOT_FOUND``, ``WORKSPACE_AND_PACKAGE_BOTH_PASSED``,
19 ``PACKAGE_UNKNOWN``, ``CWD_RESOLUTION_FAILED``).
20- Define and CATALOG the implementation-specific ``9xxx`` extension
21 codes this seed uses (§12.4 reserves the range and requires the
22 implementation to document its codes; ``IMPLEMENTATION_CODE_CATALOG``
23 is that catalog).
24- Provide :class:`OctTestError` and its §11 ``errors[]``-entry
25 projection, and :func:`exit_code_for_kind` (configuration → 4,
26 orchestrator → 3, signal → 3 per §12.3/§12.6/§12.7).
27
28Diagnostics
29-----------
30Domain: OCT-TESTING
31Levels:
32 L2: lifecycle events (none emitted here - pure data module)
33 L3: semantic details (none emitted here)
34 L4: deep tracing (none emitted here)
35
36Contracts
37---------
38- NO ``sys.exit`` and no I/O in this module - exits are data the CLI
39 maps later (Plan 189 pinned decision).
40- Two conditions never share a code (§12.8 house rule): the base
41 v0.25 loader rejecting an existing manifest is ``9101``; a
42 test-block/test-config content violation is ``9102``.
43- Every §12.4 stable identifier is its own value, so consumers can
44 compare against these constants or against the spec's literals.
45
46Dependencies
47------------
48- oct.core.diagnostics (_dbg structured logger)
49"""
50
51from __future__ import annotations
52from oct.core.diagnostics import _dbg
53
54
55# ---------------------------------------------------------------------
56# Error kinds (§11 ``errors[].kind``) and severities
57# ---------------------------------------------------------------------
58
59#: Configuration error class (§12.7): catchable before pytest starts.
60KIND_CONFIGURATION: str = "configuration"
61
62#: Orchestrator error class (§12.6): independent of the test suite.
63KIND_ORCHESTRATOR: str = "orchestrator"
64
65#: Signal class (§12.3): interrupt/termination handling.
66KIND_SIGNAL: str = "signal"
67
68#: Fatal severity - the run cannot proceed / the exit code changes.
69SEVERITY_FATAL: str = "fatal"
70
71#: Warning severity - recorded, the run continues (not used by the
72#: discovery layer; part of the shared taxonomy surface).
73SEVERITY_WARNING: str = "warning"
74
75
76# ---------------------------------------------------------------------
77# §12.4 stable identifiers (the discovery-layer subset)
78# ---------------------------------------------------------------------
79
80#: ``--workspace`` was passed but no ``.option_c_workspace.json``
81#: exists in any parent.
82WORKSPACE_FILE_NOT_FOUND: str = "WORKSPACE_FILE_NOT_FOUND"
83
84#: The mutually exclusive ``--workspace`` and ``--package`` flags were
85#: both supplied.
86WORKSPACE_AND_PACKAGE_BOTH_PASSED: str = "WORKSPACE_AND_PACKAGE_BOTH_PASSED"
87
88#: ``--package P`` was passed but ``P`` is not listed and test-enabled
89#: in the workspace file (or does not match the cwd basename in the
90#: single-package fallback).
91PACKAGE_UNKNOWN: str = "PACKAGE_UNKNOWN"
92
93#: Bare invocation walked four parent levels without finding
94#: ``pyproject.toml`` or ``pytest.ini`` (ADR-T-12).
95CWD_RESOLUTION_FAILED: str = "CWD_RESOLUTION_FAILED"
96
97
98# ---------------------------------------------------------------------
99# Implementation-specific 9xxx extension codes (§12.4 reserved range)
100# ---------------------------------------------------------------------
101
102#: The workspace manifest EXISTS but the shipping v0.25 base loader
103#: (``oct.core.workspace.load_workspace``) rejects it - wrong
104#: ``version``, missing ``name``, malformed subprojects, unreadable
105#: JSON. §12.7 pins this condition to the implementation range.
106WORKSPACE_MANIFEST_INVALID: str = "9101"
107
108#: The base schema is valid but the §9 test configuration is not: a
109#: malformed ``test`` block (``enabled`` missing/non-bool, a profile
110#: outside strict/smoke/full, missing/empty/absolute/escaping
111#: ``test_roots`` on an enabled package) or an invalid top-level
112#: ``default_test_profile``.
113MANIFEST_TEST_BLOCK_INVALID: str = "9102"
114
115#: §12.4: "The implementation MUST document its codes in a sibling
116#: artifact (release notes or its own catalog)." This dict is that
117#: catalog for the ``oct.testing`` seed.
118IMPLEMENTATION_CODE_CATALOG: dict[str, str] = {
119 # OCT-LINT: disable=no_hardcoded_secrets reason="Exit-code catalogue prose, not a secret"
120 WORKSPACE_MANIFEST_INVALID: (
121 "workspace manifest exists but the v0.25 base loader rejects "
122 "it (schema/version/name/subprojects/JSON failure)"
123 ),
124 MANIFEST_TEST_BLOCK_INVALID: (
125 "workspace manifest base schema is valid but a §9 test block "
126 "or the top-level default_test_profile is invalid"
127 ),
128}
129
130
131# ---------------------------------------------------------------------
132# Exit-code mapping (data for the future CLI slice - never applied here)
133# ---------------------------------------------------------------------
134
135#: Orchestrator errors take exit code 3 (§12.6); signals force 3
136#: (§12.3).
137EXIT_ORCHESTRATOR_ERROR: int = 3
138
139#: Fatal configuration errors take exit code 4 (§12.7).
140EXIT_CONFIGURATION_ERROR: int = 4
141
142_KIND_TO_EXIT: dict[str, int] = {
143 KIND_CONFIGURATION: EXIT_CONFIGURATION_ERROR,
144 KIND_ORCHESTRATOR: EXIT_ORCHESTRATOR_ERROR,
145 KIND_SIGNAL: EXIT_ORCHESTRATOR_ERROR,
146}
147
148
149def exit_code_for_kind(kind: str) -> int:
150 """Return the §12 exit code for an error *kind*.
151
152 Parameters
153 ----------
154 kind
155 One of :data:`KIND_CONFIGURATION`, :data:`KIND_ORCHESTRATOR`,
156 :data:`KIND_SIGNAL`.
157
158 Returns
159 -------
160 int
161 ``4`` for configuration errors, ``3`` for orchestrator and
162 signal errors.
163
164 Raises
165 ------
166 ValueError
167 If *kind* is not one of the three taxonomy kinds (a caller
168 bug, not a runtime condition - VALIDATION_ERROR class).
169 """
170 func = "exit_code_for_kind"
171 _dbg("OCT-TESTING", func, f"mapping failure kind {kind!r} to an exit code", 4)
172 try:
173 return _KIND_TO_EXIT[kind]
174 except KeyError:
175 raise ValueError(
176 f"unknown error kind {kind!r}; expected one of "
177 f"{sorted(_KIND_TO_EXIT)}",
178 ) from None
179
180
181class OctTestError(Exception):
182 """A spec-10 taxonomy error carried as data (no exit applied).
183
184 Attributes
185 ----------
186 code
187 A §12.4 stable identifier or a documented ``9xxx`` code.
188 message
189 Human-readable condition detail. For ``CWD_RESOLUTION_FAILED``
190 this is the ADR-T-12 pinned text, verbatim.
191 kind
192 §11 error kind; the discovery layer only raises
193 :data:`KIND_CONFIGURATION`.
194 severity
195 §11 severity; discovery errors are always
196 :data:`SEVERITY_FATAL`.
197 source
198 Dotted producer name, or ``"orchestrator"`` when config
199 validation itself is the immediate cause (§12.4 population
200 rule).
201 """
202
204 self,
205 code: str,
206 message: str,
207 *,
208 kind: str = KIND_CONFIGURATION,
209 severity: str = SEVERITY_FATAL,
210 source: str = "orchestrator",
211 ) -> None:
212 super().__init__(f"{code}: {message}")
213 self.code = code
214 self.message = message
215 self.kind = kind
216 self.severity = severity
217 self.source = source
218
219 @property
220 def exit_code(self) -> int:
221 """The exit code the CLI slice maps this error to (§12)."""
222 return exit_code_for_kind(self.kind)
223
224 def to_error_entry(self) -> dict[str, str]:
225 """Project this error as a §11 report ``errors[]`` entry."""
226 return {
227 "kind": self.kind,
228 "code": self.code,
229 "severity": self.severity,
230 "message": self.message,
231 "source": self.source,
232 }
233
234
235__all__ = [
236 "CWD_RESOLUTION_FAILED",
237 "EXIT_CONFIGURATION_ERROR",
238 "EXIT_ORCHESTRATOR_ERROR",
239 "IMPLEMENTATION_CODE_CATALOG",
240 "KIND_CONFIGURATION",
241 "KIND_ORCHESTRATOR",
242 "KIND_SIGNAL",
243 "MANIFEST_TEST_BLOCK_INVALID",
244 "OctTestError",
245 "PACKAGE_UNKNOWN",
246 "SEVERITY_FATAL",
247 "SEVERITY_WARNING",
248 "WORKSPACE_AND_PACKAGE_BOTH_PASSED",
249 "WORKSPACE_FILE_NOT_FOUND",
250 "WORKSPACE_MANIFEST_INVALID",
251 "exit_code_for_kind",
252]
None __init__(self, str code, str message, *, str kind=KIND_CONFIGURATION, str severity=SEVERITY_FATAL, str source="orchestrator")
Definition errors.py:211
dict[str, str] to_error_entry(self)
Definition errors.py:224
int exit_code_for_kind(str kind)
Definition errors.py:149