oc_chat
Loading...
Searching...
No Matches
scheduler.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/testing/scheduler.py
4
5"""
6The §20 stand-in's parallel scheduler (Plan 240; ADR-T-15 slice).
7
8Purpose
9-------
10Turn ``--workers N | auto`` into real child processes without inventing
11a second classifier. The plan-239 mechanism in ``oct.testing.parallel``
12owns the resource vocabulary, the committed
13``parallel_manifest.json``, the grouping and the worker cap; this
14module imports all four and does exactly three things the pytest side
15does not need: it resolves the requested worker count against the cap,
16it turns the manifest's groups into scheduling units, and it runs those
17units in a bounded pool of children, reconciling their results into the
18single §11 report.
19
20Responsibilities
21----------------
22- ``resolve_worker_plan`` — P2: ``auto`` = min(cap, cores); an explicit
23 count above the cap CLAMPS and carries the note that says so.
24- ``build_schedule`` — P3 as Plan 286 amended it: a group is one unit;
25 a ``free`` file is cut, in collection order, into units of at most
26 ``MAX_FREE_UNIT_TESTS`` tests, unless it carries the ``whole_file``
27 opt-out; units run largest first by collected count; unclassified
28 files share one serial unit. Deterministic for any input order.
29- ``load_groups`` — the manifest read, delegated to plan 239.
30- ``run_units`` — the bounded pool: one spawned child per unit, each
31 with its own §20.2 snapshot, reconciled fail-closed.
32- ``child_entry`` — the child-side entry point (module level so the
33 ``spawn`` start method can import it).
34
35Diagnostics
36-----------
37Domain: OC_TESTING
38Levels:
39 L2 — lifecycle events (pool start/stop, unit completion)
40 L3 — semantic details (resolved counts, unit shapes)
41 L4 — deep tracing
42
43Contracts
44---------
45- This module NEVER classifies a test file. Every group it uses comes
46 from the committed manifest through ``oct.testing.parallel``; a file
47 with no entry is :data:`GROUP_UNCLASSIFIED` and is reported, never
48 guessed at (P1).
49- A child that dies without writing its result is a CRASH: its nodeids
50 are reported failed, never passed and never dropped (P3, spec-10
51 §8.4 worker-crash semantics).
52- The start method is always ``spawn``, so a child inherits the
53 parent's ``sys.path`` (the F17 shadow guard) and nothing else of the
54 parent's in-process state.
55- Ordering of everything this module returns is derived from the
56 schedule, not from completion order.
57
58Dependencies
59------------
60- oc_diagnostics (structured debug logging via _dbg)
61- oct.testing.parallel (lazy; Plan 239: manifest, groups, worker cap)
62- oc_chat.testing.report (violation/error constructors)
63- oc_chat.testing.runner (lazy; the in-process session, imported in the child to keep the module graph acyclic)
64"""
65from __future__ import annotations
66
67from dataclasses import dataclass, field
68import json
69import multiprocessing
70import os
71from pathlib import Path
72import tempfile
73import time
74from typing import Any, Iterable, Mapping, Sequence
75
76from oc_diagnostics import _dbg
77
78from oc_chat.testing.report import make_error, make_violation
79
80#: Shipped worker cap when the workspace manifest declares none. Kept
81#: in step with ``oct.testing.parallel.DEFAULT_MAX_WORKERS`` (Plan 239
82#: P1, operator ruling 2026-09-06); the literal is the fallback for an
83#: installed ``oct`` that predates the key.
84DEFAULT_WORKER_CAP: int = 24
85
86#: Group name of a file the manifest does not classify (Plan 239 P4).
87GROUP_UNCLASSIFIED: str = "unclassified-serial"
88
89#: Group name of a file that touches no shared resource.
90GROUP_FREE: str = "free"
91
92#: Marker that keeps ONE free file's tests in a single unit (Plan 286
93#: P2) — the opt-out for a file whose tests depend on each other's
94#: order or on module state. Mirrors
95#: ``oct.testing.parallel.WHOLE_FILE_MARKER``, which the xdist path
96#: reads off the collected item; here the marked PATHS arrive from the
97#: parent's collect pass, because a child is handed nodeids and never
98#: sees a mark.
99WHOLE_FILE_MARKER: str = "whole_file"
100
101#: Most tests one ``free`` scheduling unit may hold (Plan 286 P2).
102#:
103#: A unit is one spawned child, and a child pays a full pytest
104#: start-up before it runs anything. Measured on the live oc_chat tree
105#: from this worktree at BASE ``3297fae7`` (``--workers auto``, 20
106#: children, 283 free units): the cheapest unit took 2.2 s wall, which
107#: is that start-up, and the free set's 3877 tests took 819.6 s of
108#: work beyond it — 0.21 s per test. The plan's rule is that start-up
109#: stays under a TENTH of a unit's run time, so a unit must carry at
110#: least 22 s of work, which is 104 tests at that rate. Hence 100: a
111#: free file above it splits into chunks in collection order, and the
112#: 275 free files below it stay exactly as they were, because spending
113#: 2.2 s to save a fraction of a second is not parallelism.
114MAX_FREE_UNIT_TESTS: int = 100
115
116#: ``workers.dist_mode`` when the scheduler distributed the run
117#: (spec-10 §11 value set ``{oct_det_loadscope, serial}``).
118DIST_MODE_PARALLEL: str = "oct_det_loadscope"
119
120#: ``workers.dist_mode`` when no scope distribution happened.
121DIST_MODE_SERIAL: str = "serial"
122
123#: ``9007`` — the clamp note (see the report module's catalog).
124CODE_WORKERS_CLAMPED: str = "9007"
125
126#: How often the pool polls its children for completion.
127_POLL_SECONDS: float = 0.02
128
129
130class SchedulerUnavailable(Exception):
131 """Raised when the plan-239 mechanism cannot be imported."""
132
133
134# ---------------------------------------------------------------------
135# P1 — the plan-239 mechanism, imported and never re-implemented
136# ---------------------------------------------------------------------
137
138def load_parallel_module(workspace_root: Path | None = None) -> Any:
139 """Import ``oct.testing.parallel``, falling back to the checkout.
140
141 In a worktree the editable ``oct`` install may resolve to ANOTHER
142 checkout that does not carry the module yet; the sibling
143 subproject in this repository always does. Only that ONE module is
144 loaded by path — the rest of ``oct`` keeps resolving normally.
145 This mirrors the root conftest's fallback (Plan 239), because the
146 stand-in runs in exactly the same trees.
147 """
148 func = "load_parallel_module"
149 try:
150 from oct.testing import parallel
151 except ImportError:
152 parallel = None
153 if parallel is not None:
154 return parallel
155 if workspace_root is None:
156 raise SchedulerUnavailable("oct.testing.parallel is not importable")
157 candidate = (
158 Path(workspace_root) / "oct" / "oct" / "testing" / "parallel.py"
159 )
160 if not candidate.is_file():
161 raise SchedulerUnavailable(
162 f"oct.testing.parallel is not importable and {candidate} "
163 "does not exist",
164 )
165 import importlib.util
166 import sys
167
168 spec = importlib.util.spec_from_file_location(
169 "oct_testing_parallel_checkout", candidate,
170 )
171 if spec is None or spec.loader is None: # pragma: no cover - defensive
172 raise SchedulerUnavailable(f"{candidate} is not loadable as a module")
173 module = importlib.util.module_from_spec(spec)
174 sys.modules[spec.name] = module
175 spec.loader.exec_module(module)
176 _dbg("OC_TESTING", func, f"loaded the classifier from {candidate}", 2)
177 return module
178
179
181 root: Path,
182 testpaths: Sequence[str],
183 workspace_root: Path | None = None,
184) -> Path:
185 """The committed manifest implied by *testpaths* (Plan 239 P4)."""
186 parallel = load_parallel_module(workspace_root)
187 roots = list(testpaths) or ["."]
188 return parallel.default_manifest_path(Path(root), roots)
189
190
192 manifest_path: Path,
193 relpaths: Iterable[str],
194 workspace_root: Path | None = None,
195) -> tuple[dict[str, str], tuple[str, ...]]:
196 """Map root-relative files to their groups through plan 239.
197
198 Returns
199 -------
200 tuple
201 ``(path -> group, missing)``. A collected file with no manifest
202 entry maps to :data:`GROUP_UNCLASSIFIED` and is listed in
203 *missing* — the fail-closed state a stale manifest produces.
204 """
205 func = "load_groups"
206 parallel = load_parallel_module(workspace_root)
207 manifest = parallel.load_manifest(Path(manifest_path))
208 groups, missing = parallel.resolve_groups(manifest, list(relpaths))
209 _dbg("OC_TESTING", func,
210 f"{len(groups)} file(s), {len(missing)} unclassified", 3)
211 return dict(groups), tuple(missing)
212
213
214# ---------------------------------------------------------------------
215# P2 — the worker count
216# ---------------------------------------------------------------------
217
218@dataclass(frozen=True)
220 """The resolved ``--workers`` decision.
221
222 Attributes
223 ----------
224 requested
225 The CLI value verbatim: a positive integer or ``"auto"``.
226 cap
227 ``subprojects[].test.max_workers`` for this package, or the
228 shipped default when the workspace manifest declares none.
229 resolved
230 The pool size this run may use.
231 clamped
232 True when an explicit request exceeded *cap*.
233 clamp_message
234 The human note naming both numbers; ``None`` when not clamped.
235 """
236
237 requested: int | str
238 cap: int
239 resolved: int
240 clamped: bool = False
241 clamp_message: str | None = None
242
243
245 requested: int | str,
246 workspace_root: Path,
247 package: str,
248 *,
249 cpu_count: int | None = None,
250) -> WorkerPlan:
251 """Resolve *requested* against the plan-239 cap (P2).
252
253 ``auto`` resolves to ``min(cap, cores)``; an explicit count above
254 the cap is clamped to it and the clamp is reported, never silent.
255 """
256 func = "resolve_worker_plan"
257 parallel = load_parallel_module(Path(workspace_root))
258 try:
259 cap = int(parallel.resolve_max_workers(Path(workspace_root), package))
260 except Exception as exc: # EXTERNAL_ERROR(L2) - a malformed manifest
261 # Plan 239 P1 refuses rather than degrades on an invalid
262 # ``max_workers``. The stand-in keeps that invariant the only way
263 # it safely can: it declines to parallelise at all, which is more
264 # serialisation and never less.
265 _dbg("OC_TESTING", func, f"cap unusable: {exc}", 1)
267 f"the workspace manifest's worker cap for {package!r} is "
268 f"unusable: {exc}",
269 ) from exc
270 cap = max(1, cap)
271 cores = cpu_count if cpu_count is not None else (os.cpu_count() or 1)
272 cores = max(1, cores)
273
274 if requested == "auto":
275 resolved = max(1, min(cap, cores))
276 _dbg("OC_TESTING", func, f"auto -> {resolved} (cap {cap}, "
277 f"{cores} core(s))", 3)
278 return WorkerPlan(requested="auto", cap=cap, resolved=resolved)
279
280 count = max(1, int(requested))
281 if count > cap:
282 message = (
283 f"--workers {count} exceeds the manifest cap for {package!r} "
284 # OCT-LINT: disable=no_hardcoded_secrets reason="the dotted workspace-manifest key path is prose telling an operator which key to edit; it is not a credential"
285 f"and was clamped to {cap} "
286 "(subprojects[].test.max_workers, Plan 239 P1)"
287 )
288 _dbg("OC_TESTING", func, message, 2)
289 return WorkerPlan(requested=count, cap=cap, resolved=cap,
290 clamped=True, clamp_message=message)
291 return WorkerPlan(requested=count, cap=cap, resolved=count)
292
293
294def clamp_error(plan: WorkerPlan) -> dict:
295 """The §11 ``errors[]`` entry that reports a clamp."""
296 return make_error(
297 "configuration", CODE_WORKERS_CLAMPED, "warning",
298 plan.clamp_message or "",
299 )
300
301
302# ---------------------------------------------------------------------
303# P3 — groups become scheduling units
304# ---------------------------------------------------------------------
305
306@dataclass(frozen=True)
308 """One unit of scheduling: everything one child will run.
309
310 Attributes
311 ----------
312 group
313 The plan-239 group name this unit carries.
314 worker_id
315 ``gw<n>``, assigned by the unit's position in the deterministic
316 schedule, so every nodeid of a group shares one worker id
317 regardless of which OS process picked it up.
318 files
319 Root-relative POSIX paths, sorted.
320 nodeids
321 The parent's selection for those files, in collection order.
322 """
323
324 group: str
325 worker_id: str
326 files: tuple[str, ...]
327 nodeids: tuple[str, ...]
328
329
331 groups: Mapping[str, str],
332 nodeids_by_file: Mapping[str, Sequence[str]],
333 *,
334 whole_file: Iterable[str] = (),
335 max_free_unit_tests: int | None = None,
336) -> tuple[ScheduleUnit, ...]:
337 """Turn the plan-239 grouping into scheduling units (P3, Plan 286).
338
339 A group is the unit of scheduling: every file of a group runs in
340 ONE child, because that is what the group means, and the
341 unclassified set shares one serial unit because fail-closed means
342 serial.
343
344 A ``free`` file shares no resource with anything, so its tests may
345 run anywhere — including in different children (Plan 286 P2). Its
346 nodeids are therefore cut, in collection order, into units of at
347 most *max_free_unit_tests* tests, so that one file with hundreds of
348 tests no longer holds a child for its whole length. A file listed
349 in *whole_file* is the opt-out: it stays ONE unit however many
350 tests it has.
351
352 Units are scheduled largest first by collected count (longest
353 processing time first — the classic bound for a bounded pool) and
354 the result is deterministic for any input order.
355
356 Parameters
357 ----------
358 groups
359 Root-relative path -> plan-239 group name.
360 nodeids_by_file
361 Root-relative path -> the parent's selection, in collection
362 order.
363 whole_file
364 Root-relative paths whose tests must stay in one unit: the
365 files the parent's collect pass saw carrying
366 :data:`WHOLE_FILE_MARKER`.
367 max_free_unit_tests
368 Override for :data:`MAX_FREE_UNIT_TESTS`; values below 1 are
369 read as 1, so a caller can never ask for an empty unit.
370 """
371 func = "build_schedule"
372 limit = (
373 MAX_FREE_UNIT_TESTS if max_free_unit_tests is None
374 else max(1, int(max_free_unit_tests))
375 )
376 intact = {str(path) for path in whole_file}
377 # key -> the files it holds; one entry per unit, in a stable order.
378 buckets: dict[str, list[str]] = {}
379 # key -> the unit's own nodeids, for a free file cut into chunks.
380 chunks: dict[str, tuple[str, ...]] = {}
381 for path in sorted(nodeids_by_file):
382 group = groups.get(path, GROUP_UNCLASSIFIED)
383 if group != GROUP_FREE:
384 buckets.setdefault(group, []).append(path)
385 continue
386 nodeids = tuple(nodeids_by_file.get(path, ()))
387 span = len(nodeids) if path in intact else limit
388 for start in range(0, max(1, len(nodeids)), max(1, span)):
389 key = f"{path}#{start // max(1, span):04d}"
390 buckets.setdefault(key, []).append(path)
391 chunks[key] = nodeids[start:start + span]
392
393 def _tests(key: str, paths: Sequence[str]) -> int:
394 if key in chunks:
395 return len(chunks[key])
396 return sum(len(nodeids_by_file.get(path, ())) for path in paths)
397
398 ordered = sorted(
399 buckets.items(),
400 key=lambda item: (-_tests(item[0], item[1]), item[0]),
401 )
402 units: list[ScheduleUnit] = []
403 for index, (key, paths) in enumerate(ordered):
404 files = tuple(sorted(paths))
405 nodeids = chunks.get(key) or tuple(
406 nodeid for path in files
407 for nodeid in nodeids_by_file.get(path, ())
408 )
409 if not nodeids:
410 continue
411 units.append(ScheduleUnit(
412 group=groups.get(files[0], GROUP_UNCLASSIFIED),
413 worker_id=f"gw{index}",
414 files=files,
415 nodeids=nodeids,
416 ))
417 _dbg("OC_TESTING", func, f"{len(units)} unit(s) from "
418 f"{len(nodeids_by_file)} file(s)", 3)
419 return tuple(units)
420
421
422# ---------------------------------------------------------------------
423# The child protocol
424# ---------------------------------------------------------------------
425
426@dataclass(frozen=True)
428 """Everything a child needs; picklable by construction."""
429
430 root: str
431 pytest_args: tuple[str, ...]
432 nodeids: tuple[str, ...]
433 group: str
434 worker_id: str
435 timeout: int | None
436
437
438@dataclass
440 """One unit's reconciled result.
441
442 ``crashed`` is the fail-closed state: the child died without
443 writing a result, so nothing it may have reported can be trusted.
444 """
445
446 unit: ScheduleUnit
447 records: dict[str, dict] = field(default_factory=dict)
448 timed_out: tuple[str, ...] = ()
449 collect_errors: tuple[str, ...] = ()
450 violations: list[dict] = field(default_factory=list)
451 rc: int = 0
452 pid: int | None = None
453 started_seconds: float = 0.0
454 finished_seconds: float = 0.0
455 crashed: bool = False
456 crash_detail: str = ""
457 output: str = ""
458
459 @property
460 def duration_seconds(self) -> float:
461 """Wall clock the unit occupied, in seconds."""
462 return max(0.0, self.finished_seconds - self.started_seconds)
463
464
465def child_entry(request: ChildRequest, result_path: str) -> None:
466 """Run one unit and write its result document (the spawn target).
467
468 Imports the runner lazily: the runner imports THIS module at module
469 scope, and a child that re-imported it eagerly would close the
470 cycle. Any exception is recorded in the result document rather than
471 raised, so the parent distinguishes "the unit failed" from "the
472 child died".
473 """
474 func = "child_entry"
475 payload: dict[str, Any]
476 try:
477 from oc_chat.testing.runner import run_session
478
479 outcome = run_session(
480 pytest_args=[*request.pytest_args, *request.nodeids],
481 root=Path(request.root),
482 timeout=request.timeout,
483 dry_run=False,
484 install_signal_handlers=False,
485 violation_file=request.root,
486 violation_scope=f"{request.worker_id}/{request.group}",
487 )
488 payload = {
489 "rc": outcome.rc,
490 "records": outcome.records,
491 "timed_out": sorted(outcome.timed_out),
492 "collect_errors": list(outcome.collect_errors),
493 "violations": outcome.violations,
494 "pid": os.getpid(),
495 "output": outcome.output,
496 "internal_error": None,
497 }
498 except BaseException as exc: # noqa: BLE001 - reported, never raised
499 _dbg("OC_TESTING", func, f"child internal error: {exc!r}", 1)
500 payload = {
501 "rc": 3,
502 "records": {},
503 "timed_out": [],
504 "collect_errors": [],
505 "violations": [],
506 "pid": os.getpid(),
507 "output": "",
508 "internal_error": f"{type(exc).__name__}: {exc}"[:2000],
509 }
510 Path(result_path).write_text(
511 json.dumps(payload), encoding="utf-8", newline="\n",
512 )
513
514
515# ---------------------------------------------------------------------
516# The bounded pool
517# ---------------------------------------------------------------------
518
520 unit: ScheduleUnit,
521 *,
522 root: Path,
523 pytest_args: Sequence[str],
524 timeout: int | None,
525 result_path: Path,
526) -> Any:
527 """Start one child for *unit* and return the live process."""
528 context = multiprocessing.get_context("spawn")
529 request = ChildRequest(
530 root=str(root),
531 pytest_args=tuple(pytest_args),
532 nodeids=unit.nodeids,
533 group=unit.group,
534 worker_id=unit.worker_id,
535 timeout=timeout,
536 )
537 process = context.Process(
538 target=child_entry,
539 args=(request, str(result_path)),
540 name=f"oc-standin-{unit.worker_id}",
541 daemon=False,
542 )
543 process.start()
544 return process
545
546
548 unit: ScheduleUnit,
549 process: Any,
550 result_path: Path,
551 started: float,
552 finished: float,
553) -> UnitOutcome:
554 """Turn a finished child into a :class:`UnitOutcome`, fail-closed."""
555 func = "_read_result"
556 outcome = UnitOutcome(
557 unit=unit, pid=process.pid,
558 started_seconds=started, finished_seconds=finished,
559 )
560 try:
561 payload = json.loads(result_path.read_text(encoding="utf-8"))
562 except (OSError, json.JSONDecodeError) as exc:
563 outcome.crashed = True
564 outcome.crash_detail = (
565 f"exit status {process.exitcode}; no readable result document "
566 f"({type(exc).__name__})"
567 )
568 _dbg("OC_TESTING", func,
569 f"{unit.worker_id} crashed: {outcome.crash_detail}", 1)
570 return outcome
571 if payload.get("internal_error"):
572 outcome.crashed = True
573 outcome.crash_detail = str(payload["internal_error"])
574 outcome.pid = payload.get("pid", outcome.pid)
575 return outcome
576 outcome.records = dict(payload.get("records") or {})
577 outcome.timed_out = tuple(payload.get("timed_out") or ())
578 outcome.collect_errors = tuple(payload.get("collect_errors") or ())
579 outcome.violations = list(payload.get("violations") or [])
580 outcome.rc = int(payload.get("rc", 0))
581 outcome.pid = payload.get("pid", outcome.pid)
582 outcome.output = str(payload.get("output") or "")
583 return outcome
584
585
587 units: Sequence[ScheduleUnit],
588 *,
589 workers: int,
590 root: Path,
591 pytest_args: Sequence[str],
592 timeout: int | None,
593 origin: float,
594) -> tuple[list[UnitOutcome], bool]:
595 """Run *units* in a bounded pool of children.
596
597 Parameters
598 ----------
599 workers
600 Maximum number of children alive at once.
601 origin
602 The run's ``time.monotonic()`` zero, so every window this
603 returns is relative to the report's own clock.
604
605 Returns
606 -------
607 tuple
608 ``(outcomes in schedule order, interrupted)``. On interruption
609 the still-running children are terminated and the units that
610 never started are absent — the caller reports a partial run.
611 """
612 func = "run_units"
613 _dbg("OC_TESTING", func,
614 f"{len(units)} unit(s) over {workers} worker(s)", 2)
615 pending = list(units)
616 running: list[tuple[ScheduleUnit, Any, Path, float]] = []
617 done: dict[str, UnitOutcome] = {}
618 interrupted = False
619
620 with tempfile.TemporaryDirectory(prefix="oc_standin_") as scratch:
621 scratch_dir = Path(scratch)
622 try:
623 while pending or running:
624 while pending and len(running) < workers:
625 unit = pending.pop(0)
626 result_path = scratch_dir / f"{unit.worker_id}.json"
627 started = time.monotonic() - origin
628 process = _spawn(
629 unit, root=root, pytest_args=pytest_args,
630 timeout=timeout, result_path=result_path,
631 )
632 running.append((unit, process, result_path, started))
633 time.sleep(_POLL_SECONDS)
634 for entry in list(running):
635 unit, process, result_path, started = entry
636 if process.is_alive():
637 continue
638 process.join()
639 finished = time.monotonic() - origin
640 done[unit.worker_id] = _read_result(
641 unit, process, result_path, started, finished,
642 )
643 running.remove(entry)
644 except KeyboardInterrupt:
645 interrupted = True
646 _dbg("OC_TESTING", func, "interrupted; terminating children", 1)
647 for _unit, process, _path, _started in running:
648 if process.is_alive():
649 process.terminate()
650 for _unit, process, _path, _started in running:
651 process.join(timeout=10)
652
653 ordered = [done[unit.worker_id] for unit in units
654 if unit.worker_id in done]
655 _dbg("OC_TESTING", func,
656 f"{len(ordered)} unit(s) reconciled, interrupted={interrupted}", 2)
657 return ordered, interrupted
658
659
660def crash_violation(outcome: UnitOutcome) -> dict:
661 """The §11 ``contract_violations[]`` row a dead child produces."""
662 return make_violation(
663 "child_crash", outcome.unit.files[0], None, "error",
664 f"scheduler child {outcome.unit.worker_id} running group "
665 f"{outcome.unit.group!r} died before reporting "
666 f"({outcome.crash_detail}); its "
667 f"{len(outcome.unit.nodeids)} nodeid(s) are reported failed, "
668 "never passed (Plan 240 P3, spec-10 section 8.4)",
669 )
670
671
672def crash_error(outcome: UnitOutcome) -> dict:
673 """The §12.4 ``WORKER_CRASHED`` entry a dead child produces."""
674 return make_error(
675 "orchestrator", "WORKER_CRASHED", "warning",
676 f"worker {outcome.unit.worker_id} (group {outcome.unit.group!r}) "
677 f"crashed: {outcome.crash_detail}",
678 )
679
680
681def unclassified_violation(root: Path, missing: Sequence[str]) -> dict:
682 """The §11 row an absent manifest entry produces (P1)."""
683 listed = ", ".join(sorted(missing))
684 return make_violation(
685 "unclassified_test_file", str(root), None, "error",
686 f"{len(missing)} collected file(s) carry no parallel_manifest.json "
687 f"entry and were run serially rather than in parallel: {listed}. "
688 "Regenerate the manifest (python -m oct.testing.parallel classify) "
689 "— a stale manifest may never silently widen parallelism",
690 )
691
692
693_dbg("OC_TESTING", "module", "loaded", 4)
694
695__all__ = [
696 "CODE_WORKERS_CLAMPED",
697 "DEFAULT_WORKER_CAP",
698 "DIST_MODE_PARALLEL",
699 "DIST_MODE_SERIAL",
700 "GROUP_FREE",
701 "GROUP_UNCLASSIFIED",
702 "MAX_FREE_UNIT_TESTS",
703 "WHOLE_FILE_MARKER",
704 "ChildRequest",
705 "ScheduleUnit",
706 "SchedulerUnavailable",
707 "UnitOutcome",
708 "WorkerPlan",
709 "build_schedule",
710 "child_entry",
711 "clamp_error",
712 "crash_error",
713 "crash_violation",
714 "load_groups",
715 "load_parallel_module",
716 "manifest_path_for",
717 "resolve_worker_plan",
718 "run_units",
719 "unclassified_violation",
720]
tuple[dict[str, str], tuple[str,...]] load_groups(Path manifest_path, Iterable[str] relpaths, Path|None workspace_root=None)
Definition scheduler.py:195
UnitOutcome _read_result(ScheduleUnit unit, Any process, Path result_path, float started, float finished)
Definition scheduler.py:553
dict unclassified_violation(Path root, Sequence[str] missing)
Definition scheduler.py:681
tuple[list[UnitOutcome], bool] run_units(Sequence[ScheduleUnit] units, *, int workers, Path root, Sequence[str] pytest_args, int|None timeout, float origin)
Definition scheduler.py:594
Any load_parallel_module(Path|None workspace_root=None)
Definition scheduler.py:138
dict crash_violation(UnitOutcome outcome)
Definition scheduler.py:660
None child_entry(ChildRequest request, str result_path)
Definition scheduler.py:465
dict clamp_error(WorkerPlan plan)
Definition scheduler.py:294
Any _spawn(ScheduleUnit unit, *, Path root, Sequence[str] pytest_args, int|None timeout, Path result_path)
Definition scheduler.py:526
dict crash_error(UnitOutcome outcome)
Definition scheduler.py:672
tuple[ScheduleUnit,...] build_schedule(Mapping[str, str] groups, Mapping[str, Sequence[str]] nodeids_by_file, *, Iterable[str] whole_file=(), int|None max_free_unit_tests=None)
Definition scheduler.py:336
WorkerPlan resolve_worker_plan(int|str requested, Path workspace_root, str package, *, int|None cpu_count=None)
Definition scheduler.py:250
Path manifest_path_for(Path root, Sequence[str] testpaths, Path|None workspace_root=None)
Definition scheduler.py:184