oc_chat
Loading...
Searching...
No Matches
cli.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/testing/cli.py
4
5"""
6The §20.1 argv contract for the stand-in (Plan 184).
7
8Purpose
9-------
10Parse exactly the stand-in grammar: the eight §20.1 flags plus
11``--per-test-timeout`` (ADR-T-07) plus accepted-but-ignored
12``--output-tests-limit`` (§20.3 no-sink rule). Anything else — unknown
13flags, bare positionals, a literal ``--`` (ADR-T-16), or the
14v1.0-grammar flags the stand-in does not support (``--workspace``,
15``--ndjson``) — produces an exit-4-class configuration error with the
16right catalog code.
17
18Responsibilities
19----------------
20- ``parse_argv`` — pure argv → (config, errors); first error wins.
21- ``StandInConfig`` — the parsed invocation.
22
23Diagnostics
24-----------
25Domain: OC_TESTING
26Levels:
27 L2 — lifecycle events
28 L3 — semantic details
29 L4 — deep tracing
30
31Contracts
32---------
33- No prefix abbreviation: tokens match the grammar exactly.
34- Both ``--flag value`` and ``--flag=value`` forms are accepted.
35- ``parse_argv`` performs no filesystem or environment access.
36
37Dependencies
38------------
39- oc_diagnostics (structured debug logging via _dbg)
40- oc_chat.testing.report (error constructors)
41"""
42from __future__ import annotations
43
44from dataclasses import dataclass, field
45
46from oc_diagnostics import _dbg
47
48from oc_chat.testing.report import make_error
49
50_PROFILES = ("strict", "smoke", "full")
51
52#: Flags that take a value.
53_VALUE_FLAGS = {
54 "--package", "--profile", "--seed", "--workers", "--marker",
55 "--nodeid", "--per-test-timeout", "--output-tests-limit", "--ndjson",
56}
57#: Flags that take no value.
58_BARE_FLAGS = {"--dry-run", "--json", "--workspace"}
59
60
61@dataclass
63 """The parsed §20.1 stand-in invocation."""
64
65 package: str | None = None
66 profile: str = "strict"
67 profile_explicit: bool = False
68 seed: int | None = None
69 workers: int | str = "auto"
70 marker: str | None = None
71 nodeids: list[str] = field(default_factory=list)
72 per_test_timeout: int | None = None
73 output_tests_limit: int | None = None
74 dry_run: bool = False
75 json_out: bool = False
76
77
78def _fatal(code: str, message: str) -> list[dict]:
79 return [make_error("configuration", code, "fatal", message)]
80
81
82def parse_argv(argv: list[str]) -> tuple[StandInConfig | None, list[dict]]:
83 """Parse *argv* against the stand-in grammar.
84
85 Returns ``(config, [])`` on success or ``(None, [error])`` with a
86 single fatal configuration error (first error wins; nothing runs).
87 """
88 func = "parse_argv"
89 _dbg("OC_TESTING", func, f"argv={argv}", 3)
90
91 if "--" in argv:
92 return None, _fatal(
93 "UNSUPPORTED_ARGUMENT",
94 "a literal '--' (pytest passthrough) is outside the grammar; "
95 "the surface accepts no pytest passthrough (ADR-T-16)",
96 )
97
98 cfg = StandInConfig()
99 i = 0
100 while i < len(argv):
101 token = argv[i]
102 name, eq, inline_value = token.partition("=")
103 if name in _BARE_FLAGS:
104 if eq:
105 return None, _fatal(
106 "UNSUPPORTED_ARGUMENT",
107 f"flag {name} takes no value (got {token!r})",
108 )
109 value: str | None = None
110 elif name in _VALUE_FLAGS:
111 if eq:
112 value = inline_value
113 else:
114 if i + 1 >= len(argv):
115 return None, _fatal(
116 "UNSUPPORTED_ARGUMENT",
117 f"flag {name} requires a value",
118 )
119 i += 1
120 value = argv[i]
121 else:
122 return None, _fatal(
123 "UNSUPPORTED_ARGUMENT",
124 f"unrecognized argument {token!r}: the stand-in accepts "
125 "only the section-20.1 grammar",
126 )
127
128 if name == "--workspace":
129 return None, _fatal(
130 "9003",
131 "--workspace is rejected with exit code 4 until workspace "
132 "mode exists (section 20.1); use --package oc_chat",
133 )
134 if name == "--ndjson":
135 return None, _fatal(
136 "9003",
137 "the ndjson flag belongs to the full grammar; the stand-in "
138 "implements no ndjson sink (section 20.3) and rejects it",
139 )
140 if name == "--dry-run":
141 cfg.dry_run = True
142 elif name == "--json":
143 cfg.json_out = True
144 elif name == "--package":
145 cfg.package = value
146 elif name == "--profile":
147 if value not in _PROFILES:
148 return None, _fatal(
149 "PROFILE_INVALID",
150 f"--profile {value!r} is not one of strict, smoke, full",
151 )
152 cfg.profile = str(value)
153 cfg.profile_explicit = True
154 elif name == "--seed":
155 try:
156 cfg.seed = int(str(value), 10)
157 except ValueError:
158 return None, _fatal(
159 "9004", f"--seed {value!r} is not an integer",
160 )
161 elif name == "--workers":
162 if value == "auto":
163 cfg.workers = "auto"
164 else:
165 try:
166 count = int(str(value), 10)
167 except ValueError:
168 count = 0
169 if count < 1:
170 return None, _fatal(
171 "9001",
172 f"--workers {value!r} is neither a positive integer "
173 "nor 'auto'",
174 )
175 cfg.workers = count
176 elif name == "--marker":
177 cfg.marker = str(value)
178 elif name == "--nodeid":
179 cfg.nodeids.append(str(value))
180 elif name == "--per-test-timeout":
181 try:
182 seconds = int(str(value), 10)
183 except ValueError:
184 seconds = 0
185 if seconds < 1:
186 return None, _fatal(
187 "9005",
188 f"--per-test-timeout {value!r} is not a positive "
189 "integer number of seconds",
190 )
191 cfg.per_test_timeout = seconds
192 elif name == "--output-tests-limit":
193 try:
194 limit = int(str(value), 10)
195 except ValueError:
196 limit = 0
197 if limit < 1:
198 return None, _fatal(
199 "9006",
200 f"--output-tests-limit {value!r} is not a positive "
201 "integer",
202 )
203 cfg.output_tests_limit = limit
204 i += 1
205
206 _dbg("OC_TESTING", func, f"parsed profile={cfg.profile} "
207 f"workers={cfg.workers} nodeids={len(cfg.nodeids)}", 3)
208 return cfg, []
209
210
211_dbg("OC_TESTING", "module", "loaded", 4)
212
213__all__ = ["StandInConfig", "parse_argv"]
tuple[StandInConfig|None, list[dict]] parse_argv(list[str] argv)
Definition cli.py:82
list[dict] _fatal(str code, str message)
Definition cli.py:78