8The spec-10 §9 discovery-precedence layer for the ``oct test`` v1.0
9seed (Plan 189): workspace / ``--package P`` / bare selection over the
10workspace manifest, plus the ADR-T-12 single-package fallback walk
11(cwd upward for ``pyproject.toml``/``pytest.ini``, four-parent-level
12cap, basename inference with the ``[tool.oct.test].package_id``
17- :func:`discover` - the §9 precedence table, row for row. Flags are
18 plain keyword arguments (this is a LIBRARY: no CLI, no flag
19 parsing, no ``sys.exit``); every exit-4 table branch raises
20 :class:`oct.testing.errors.OctTestError` with its §12.4 code.
21- :func:`walk_for_package` - the ADR-T-12 walk with the ADR's pinned
22 diagnostic message, verbatim.
23- OUT of scope (future slices): pytest collection and the §4
24 collection-check routing (``COLLECTION_ERROR`` /
25 ``NO_TESTS_COLLECTED``), runners, schedulers, reports.
31 L2: lifecycle (discovery start / resolution failures)
32 L3: semantic details (resolved mode, selection, walk result)
37- Discovery is READ-ONLY: nothing is created or mutated.
38- Mutual exclusion (``workspace`` + ``package``) is checked before
39 any filesystem access - a fully valid workspace does not rescue
40 the call (§9 row 7, "Either").
41- The ``--package`` fallback (no manifest found) does NOT walk: the
42 package roots at cwd and ``P`` must equal the cwd basename (§9
43 row 6); the ADR-T-12 walk belongs to the bare row only.
44- A corrupt ``pyproject.toml`` at a discovered root degrades to
45 basename inference - the file's existence marks the package root;
46 only the optional override read is tolerant.
50- oct.core.workspace (read-only: find_workspace_root)
51- oct.core.diagnostics (_dbg)
52- oct.testing.errors (the §12.4 taxonomy as data)
53- oct.testing.manifest (the §9 manifest test view)
56from __future__
import annotations
59from dataclasses
import dataclass, field
60from pathlib
import Path
62from oct.core.diagnostics
import _dbg
63from oct.core.workspace
import WORKSPACE_MANIFEST, find_workspace_root
65 CWD_RESOLUTION_FAILED,
68 WORKSPACE_AND_PACKAGE_BOTH_PASSED,
69 WORKSPACE_FILE_NOT_FOUND,
79MODE_WORKSPACE: str =
"workspace"
82MODE_SINGLE_PACKAGE: str =
"single_package"
90_CONFIG_FILE_NAMES: tuple[str, ...] = (
"pyproject.toml",
"pytest.ini")
94CWD_RESOLUTION_MESSAGE: str = (
95 "oct test: no pyproject.toml or pytest.ini found within 4 parent "
96 "levels of cwd. Run from inside a workspace or package, or pass "
97 "--package NAME to disambiguate."
101@dataclass(frozen=True)
103 """The resolved single-package fallback target.
108 Absolute package root: the config file's directory (ADR-T-12
109 walk, §9 row 2) or cwd itself (``--package`` fallback, row 6).
111 Basename of *root*, unless the discovered ``pyproject.toml``
112 carries a ``[tool.oct.test].package_id`` override (clause 5),
113 or ``--package`` supplied the id (row 6).
115 The discovered config file; ``None`` in the ``--package``
116 fallback, which performs no walk.
121 config_file: Path |
None =
None
124@dataclass(frozen=True)
126 """Outcome of the §9 discovery precedence.
131 :data:`MODE_WORKSPACE` or :data:`MODE_SINGLE_PACKAGE`.
133 Resolved workspace root (workspace mode only).
135 The parsed :class:`~oct.testing.manifest.TestManifest`
136 (workspace mode only).
138 The selected packages, manifest order: every test-enabled
139 package, or exactly the ``--package`` target. Empty in
142 The :class:`SinglePackage` target (single-package mode only).
146 workspace_root: Path |
None =
None
147 manifest: TestManifest |
None =
None
148 selected: tuple[PackageTestConfig, ...] = field(default_factory=tuple)
149 single: SinglePackage |
None =
None
155 workspace: bool =
False,
156 package: str |
None =
None,
158 """Resolve the §9 discovery precedence from *cwd*.
163 Directory the (future) invocation runs from; the upward
164 manifest search and the ADR-T-12 walk both start here.
166 Library mirror of the ``--workspace`` flag.
168 Library mirror of the ``--package P`` flag.
172 :class:`DiscoveryResult`
177 Every exit-4 branch of the §9 table:
178 ``WORKSPACE_AND_PACKAGE_BOTH_PASSED`` (row 7, checked before
179 any IO), ``WORKSPACE_FILE_NOT_FOUND`` (row 4),
180 ``PACKAGE_UNKNOWN`` (rows 5 and 6),
181 ``CWD_RESOLUTION_FAILED`` (row 2), and the manifest ``9101``/
182 ``9102`` codes when a found workspace file is invalid
186 if workspace
and package
is not None:
188 WORKSPACE_AND_PACKAGE_BOTH_PASSED,
189 "--workspace and --package are mutually exclusive",
195 f
"cwd={start} workspace={workspace} package={package!r}", 2,
197 workspace_root = find_workspace_root(start)
200 if workspace_root
is None:
202 WORKSPACE_FILE_NOT_FOUND,
203 f
"--workspace passed but no {WORKSPACE_MANIFEST} "
204 f
"exists in {start} or any parent",
208 if package
is not None:
209 if workspace_root
is not None:
211 resolved = start.resolve()
212 if package != resolved.name:
215 f
"--package {package!r} does not match the cwd "
216 f
"basename {resolved.name!r} and no "
217 f
"{WORKSPACE_MANIFEST} was found",
219 _dbg(
"OCT-TESTING", func, f
"single-package at cwd {resolved}", 3)
221 mode=MODE_SINGLE_PACKAGE,
223 root=resolved, package_id=package, config_file=
None,
227 if workspace_root
is not None:
230 mode=MODE_SINGLE_PACKAGE,
236 workspace_root: Path,
239 """Build the workspace-mode result (§9 rows 1, 3, 5)."""
240 func =
"_workspace_result"
241 manifest = load_test_manifest(workspace_root)
244 selected = manifest.enabled_packages
246 target = manifest.packages_by_name.get(package)
247 if target
is None or not target.enabled:
250 f
"package {package!r} is not listed and test-enabled "
251 f
"in {manifest.workspace.root / WORKSPACE_MANIFEST}",
257 f
"workspace mode: {len(selected)} package(s) selected", 3,
261 workspace_root=manifest.workspace.root,
268 """ADR-T-12: walk upward from *cwd* for a package config file.
270 Checks *cwd* plus up to :data:`WALK_LEVELS` ancestors for
271 ``pyproject.toml`` or ``pytest.ini`` (preference order within a
272 directory); the nearest hit wins. The package id is the config
273 directory's basename unless the discovered ``pyproject.toml``
274 carries a ``[tool.oct.test].package_id`` override (clause 5).
279 ``CWD_RESOLUTION_FAILED`` with the ADR's pinned message when
280 the walk exhausts its levels.
282 func =
"walk_for_package"
283 current = Path(cwd).resolve()
284 candidates = [current, *current.parents[:WALK_LEVELS]]
285 for candidate
in candidates:
286 for name
in _CONFIG_FILE_NAMES:
287 config_file = candidate / name
288 if config_file.is_file():
292 f
"root={candidate} id={package_id!r} "
298 package_id=package_id,
299 config_file=config_file,
301 _dbg(
"OCT-TESTING", func, f
"resolution failed from {cwd}", 2)
302 raise OctTestError(CWD_RESOLUTION_FAILED, CWD_RESOLUTION_MESSAGE)
306 """Infer the package id for *root* (ADR-T-12 clauses 4-5)."""
307 func =
"_package_id_for"
308 if config_file.name !=
"pyproject.toml":
311 with config_file.open(
"rb")
as fh:
312 data = tomllib.load(fh)
313 except (OSError, tomllib.TOMLDecodeError)
as exc:
316 f
"unreadable pyproject at {root}: {exc}; basename stands",
322 for key
in (
"tool",
"oct",
"test"):
323 if not isinstance(node, dict):
326 if not isinstance(node, dict):
328 override = node.get(
"package_id")
329 if isinstance(override, str)
and override.strip():
332 f
"package_id override {override!r} at {root}", 4,
339 "CWD_RESOLUTION_MESSAGE",
341 "MODE_SINGLE_PACKAGE",
DiscoveryResult _workspace_result(Path workspace_root, str|None package)
str _package_id_for(Path root, Path config_file)
DiscoveryResult discover(Path cwd, *, bool workspace=False, str|None package=None)
SinglePackage walk_for_package(Path cwd)