358 pytest_args: Sequence[str],
362 sig_state: _SignalState |
None =
None,
363 install_signal_handlers: bool =
True,
364 violation_file: str |
None =
None,
365 violation_scope: str =
"",
367 """Run ONE guarded in-process pytest session (§20.2).
369 Everything the §20.2 process-level contract needs happens here and
370 only here: the recursion guard, the CWD move and its restoration,
371 the tracked-environment snapshot taken around the run, the static
372 gate, the per-test watchdog and the leak diff. The serial stand-in
373 calls it once; the scheduler calls it once per child, in that
374 child's own process, which is what makes the §20.2 findings
375 attributable to a group at all.
380 The ``file`` field of any runtime finding; defaults to *root*.
382 Prefixed to a finding's message when non-empty, so a child's
383 leak names the worker and group that produced it.
387 dry_run=dry_run, timeout=timeout,
388 thread_ident=threading.get_ident(),
390 prior_handlers: list[tuple[int, object]] = []
391 on_main_thread = threading.current_thread()
is threading.main_thread()
393 install_signal_handlers
and on_main_thread
and sig_state
is not None
395 outer_cwd = os.getcwd()
396 guard_prior = os.environ.get(STANDIN_ACTIVE_ENV)
398 k: v
for k, v
in os.environ.items()
399 if k.startswith(TRACKED_ENV_PREFIXES)
401 buffer = io.StringIO()
404 if install
and sig_state
is not None:
405 for signame
in (
"SIGINT",
"SIGTERM",
"SIGBREAK"):
406 signum = getattr(signal, signame,
None)
407 if signum
is not None:
408 prior_handlers.append(
409 (signum, signal.signal(signum, sig_state.on_signal))
411 os.environ[STANDIN_ACTIVE_ENV] =
"1"
413 ref_cwd = os.getcwd()
414 _dbg(
"OC_TESTING", func, f
"pytest args: {list(pytest_args)}", 3)
415 with redirect_stdout(buffer):
417 rc = int(pytest.main(list(pytest_args), plugins=[collector]))
418 except KeyboardInterrupt:
420 post_cwd = os.getcwd()
422 for signum, handler
in prior_handlers:
423 signal.signal(signum, handler)
425 if guard_prior
is None:
426 os.environ.pop(STANDIN_ACTIVE_ENV,
None)
428 os.environ[STANDIN_ACTIVE_ENV] = guard_prior
431 k: v
for k, v
in os.environ.items()
432 if k.startswith(TRACKED_ENV_PREFIXES)
440 where = violation_file
if violation_file
is not None else str(root)
441 tag = f
"[{violation_scope}] " if violation_scope
else ""
442 violations: list[dict] = []
443 cwd_leak: tuple[str, str] |
None =
None
444 start_cwd = collector.session_start_cwd
or ref_cwd
446 collector.last_seen_cwd
is not None
447 and collector.last_seen_cwd != start_cwd
449 cwd_leak = (start_cwd, collector.last_seen_cwd)
450 elif post_cwd != ref_cwd:
451 cwd_leak = (ref_cwd, post_cwd)
452 if cwd_leak
is not None:
453 violations.append(make_violation(
454 "cwd_chdir", where,
None,
"error",
455 f
"{tag}process CWD not restored around the run: "
456 f
"{cwd_leak[0]!r} -> {cwd_leak[1]!r} (section 20.2 "
457 "process-level check)",
460 set(env_after) - set(env_before)
461 | {k
for k
in env_before
if k
in env_after
462 and env_before[k] != env_after[k]}
464 removed = sorted(set(env_before) - set(env_after))
465 if leaked
or removed:
468 details.append(
"set/changed: " +
", ".join(leaked))
470 details.append(
"removed: " +
", ".join(removed))
471 violations.append(make_violation(
472 "env_mutation", where,
None,
"error",
473 f
"{tag}tracked environment not restored around the run "
474 f
"({'; '.join(details)}) — prefixes {TRACKED_ENV_PREFIXES} "
475 "(section 20.2 process-level check)",
478 os.environ.pop(key,
None)
480 os.environ[key] = env_before[key]
484 records=collector.records,
485 timed_out=tuple(sorted(collector.timed_out)),
486 execution_order=list(collector.execution_order),
487 collected_pre=list(collector.collected_pre),
488 selected=list(collector.selected),
489 whole_file=collector.whole_file,
490 collect_errors=list(collector.collect_errors),
491 static_violations=list(collector.static_violations),
492 blocked=collector.blocked,
493 violations=violations,
494 output=buffer.getvalue(),
681def _run(argv: list[str], base_cwd: Path, run_id: str, t0: float) -> RunResult:
683 cfg, parse_errors = parse_argv(argv)
686 code = parse_errors[0][
"code"]
687 walk: WalkResult |
None =
None
688 if code ==
"PROFILE_INVALID":
689 walk, _werr = walk_for_package(base_cwd)
690 failed_step =
"profile"
692 failed_step =
"workspace"
696 run_id=run_id, t0=t0, errors=parse_errors, cfg=cfg,
697 walk=walk, failed_step=failed_step,
700 assert cfg
is not None
701 walk, walk_error = walk_for_package(base_cwd)
702 if walk_error
is not None:
704 run_id=run_id, t0=t0, errors=[walk_error], cfg=cfg,
705 walk=
None, failed_step=
"workspace",
707 assert walk
is not None
709 if cfg.package
is not None and cfg.package != walk.package_id:
711 "configuration",
"PACKAGE_UNKNOWN",
"fatal",
712 f
"--package {cfg.package!r} does not match the discovered "
713 f
"package {walk.package_id!r} (single-package fallback; no "
714 "workspace file in scope)",
717 run_id=run_id, t0=t0, errors=[error], cfg=cfg,
718 walk=walk, failed_step=
"package",
721 profile = cfg.profile
if cfg.profile_explicit
else (
722 walk.profile_pin
if walk.profile_pin
in _PROFILE_EXPRESSIONS
725 seed = cfg.seed
if cfg.seed
is not None else 0
727 errors: list[dict] = []
728 violations: list[dict] = []
730 def _resolved_report(status: str, exit_code: int, *, collected: int = 0,
731 tests: list[dict] |
None =
None) -> RunResult:
733 run_id=run_id, status=status, exit_code=exit_code,
734 workspace_root=str(walk.root), workspace_mode=
"single_package",
735 packages=[walk.package_id], profile=profile, marker=cfg.marker,
736 seed=seed, nodeids=list(cfg.nodeids),
737 workers_requested=cfg.workers, workers_actual=1,
738 dist_mode=
"serial", collected=collected,
739 tests=tests
or [], violations=violations, errors=errors,
740 duration_seconds=time.monotonic() - t0,
742 return RunResult(report=report, exit_code=exit_code)
745 undeclared = scan_undeclared_roots(
746 walk.root, walk.roots, walk.python_files
749 severity =
"fatal" if profile ==
"strict" else "warning"
750 for directory
in undeclared:
752 shown = directory.relative_to(walk.root)
755 errors.append(make_error(
756 "configuration",
"UNDECLARED_TEST_ROOT", severity,
757 f
"undeclared test root: {shown} contains test files but "
758 "is absent from the effective test roots (testpaths; the "
761 if profile ==
"strict":
762 return _resolved_report(
"configuration_error", 4)
765 profile_expr = _PROFILE_EXPRESSIONS[profile]
767 f
"({profile_expr}) and ({cfg.marker})" if cfg.marker
else profile_expr
770 from _pytest.mark.expression
import Expression
772 Expression.compile(effective_expr)
773 except SyntaxError
as exc:
776 errors.append(make_error(
777 "configuration",
"MARKER_PARSE_ERROR",
"fatal",
778 f
"marker expression {cfg.marker!r} failed to parse when "
779 f
"composed with the {profile} profile: {exc}",
781 return _resolved_report(
"configuration_error", 4)
783 _dbg(
"OC_TESTING", func,
"Expression API unavailable; deferring "
784 "marker validation to pytest", 2)
787 root_pairs = list(zip(walk.testpaths
or [
"."], walk.roots))
788 existing_pairs = [(tp, rp)
for tp, rp
in root_pairs
if rp.is_dir()]
790 for tp, rp
in root_pairs:
792 errors.append(make_error(
793 "configuration",
"EMPTY_TEST_ROOT",
"warning",
794 f
"declared test root {tp!r} does not exist (stale "
797 if not existing_pairs:
798 errors.append(make_error(
799 "configuration",
"NO_TESTS_COLLECTED",
"fatal",
800 "the union of the effective test roots contains no "
801 "existing directories; the configuration points at no "
804 return _resolved_report(
"configuration_error", 4)
810 base_args = [
"-c", str(walk.config_file),
"--rootdir", str(walk.root)]
811 if cfg.seed
is not None:
812 base_args += [
"-p",
"randomly", f
"--randomly-seed={cfg.seed}"]
814 base_args += [
"-p",
"no:randomly"]
815 base_args += [
"-m", effective_expr]
817 selection = list(cfg.nodeids)
819 selection = [tp
for tp, _rp
in existing_pairs]
820 pytest_args = base_args + selection
824 workspace_root = walk.root.parent
825 worker_plan: WorkerPlan |
None =
None
826 manifest_path: Path |
None =
None
827 serial_reason: str |
None =
None
830 serial_reason =
"dry_run"
831 elif cfg.workers == 1:
832 serial_reason =
"workers_1"
835 manifest_path = manifest_path_for(
836 walk.root, walk.testpaths, workspace_root,
838 except SchedulerUnavailable
as exc:
839 _dbg(
"OC_TESTING", func, f
"classifier unavailable: {exc}", 2)
841 serial_reason =
"classifier_unavailable"
842 if manifest_path
is not None and not manifest_path.is_file():
843 _dbg(
"OC_TESTING", func,
844 f
"no manifest at {manifest_path}; serial", 2)
846 serial_reason =
"no_manifest"
847 if manifest_path
is not None:
849 worker_plan = resolve_worker_plan(
850 cfg.workers, workspace_root, walk.package_id,
852 except SchedulerUnavailable
as exc:
853 _dbg(
"OC_TESTING", func, f
"worker cap unusable: {exc}", 1)
855 serial_reason =
"worker_cap_unusable"
856 if worker_plan
is not None:
857 if worker_plan.clamped:
858 errors.append(clamp_error(worker_plan))
859 if worker_plan.resolved <= 1:
860 serial_reason =
"single_worker"
865 dist_mode = DIST_MODE_SERIAL
866 session: SessionOutcome |
None =
None
867 unit_outcomes: list[UnitOutcome] = []
868 pool_interrupted =
False
872 if serial_reason
is None and worker_plan
is not None:
873 assert manifest_path
is not None
875 pytest_args=pytest_args, root=walk.root,
876 timeout=cfg.per_test_timeout, dry_run=
True,
877 sig_state=sig_state, violation_file=str(walk.root),
879 nodeids_by_file: dict[str, list[str]] = {}
880 for nodeid, _path
in collect.selected:
881 nodeids_by_file.setdefault(nodeid.split(
"::")[0], []).append(
884 missing: tuple[str, ...] = ()
886 groups, missing = load_groups(
887 manifest_path, nodeids_by_file, workspace_root,
889 except SchedulerUnavailable
as exc:
891 serial_reason =
"classifier_unavailable"
892 _dbg(
"OC_TESTING", func, f
"classifier vanished: {exc}", 1)
893 except Exception
as exc:
895 serial_reason =
"manifest_unreadable"
896 errors.append(make_error(
897 "configuration",
"MANIFEST_UNREADABLE",
"warning",
898 f
"the parallel manifest {manifest_path.name} could not be "
899 f
"read ({exc}); the run fell back to serial execution",
901 if serial_reason
is None:
902 units = build_schedule(
903 groups, nodeids_by_file, whole_file=collect.whole_file,
906 serial_reason =
"static_gate"
907 elif sig_state.interrupted
is not None:
908 serial_reason =
"interrupted_during_collection"
909 elif len(units) <= 1:
910 serial_reason =
"single_unit"
911 if serial_reason
is None:
913 violations.append(unclassified_violation(walk.root, missing))
914 workers_actual = min(worker_plan.resolved, len(units))
916 DIST_MODE_PARALLEL
if workers_actual > 1
else DIST_MODE_SERIAL
918 guard_prior = os.environ.get(STANDIN_ACTIVE_ENV)
919 prior_handlers: list[tuple[int, object]] = []
920 on_main = threading.current_thread()
is threading.main_thread()
923 for signame
in (
"SIGINT",
"SIGTERM",
"SIGBREAK"):
924 signum = getattr(signal, signame,
None)
925 if signum
is not None:
926 prior_handlers.append((
928 signal.signal(signum, sig_state.on_signal),
933 os.environ[STANDIN_ACTIVE_ENV] =
"1"
935 unit_outcomes, pool_interrupted = run_units(
936 units, workers=workers_actual, root=walk.root,
937 pytest_args=base_args, timeout=cfg.per_test_timeout,
941 for signum, handler
in prior_handlers:
942 signal.signal(signum, handler)
943 if guard_prior
is None:
944 os.environ.pop(STANDIN_ACTIVE_ENV,
None)
946 os.environ[STANDIN_ACTIVE_ENV] = guard_prior
949 manifest_path=manifest_path, root=walk.root,
950 plan=worker_plan, outcomes=unit_outcomes, missing=missing,
956 if session
is not None:
960 _dbg(
"OC_TESTING", func,
961 f
"falling back to serial ({serial_reason})", 2)
963 pytest_args=pytest_args, root=walk.root,
964 timeout=cfg.per_test_timeout, dry_run=cfg.dry_run,
965 sig_state=sig_state, violation_file=str(walk.root),
969 dist_mode = DIST_MODE_SERIAL
972 assert session
is not None
973 violations = session.static_violations + session.violations + violations
974 for outcome
in unit_outcomes:
975 violations.extend(outcome.violations)
977 violations.append(crash_violation(outcome))
978 errors.append(crash_error(outcome))
981 captured = session.output
982 execution_order = list(session.execution_order)
986 + [f
"\n--- {o.unit.worker_id} ({o.unit.group}) ---\n{o.output}"
987 for o
in unit_outcomes]
990 nodeid
for outcome
in unit_outcomes
991 for nodeid
in outcome.unit.nodeids
992 if nodeid
in outcome.records
995 def _finish(status: str, exit_code: int, tests: list[dict],
996 collected: int) -> RunResult:
998 run_id=run_id, status=status, exit_code=exit_code,
999 workspace_root=str(walk.root), workspace_mode=
"single_package",
1000 packages=[walk.package_id], profile=profile, marker=cfg.marker,
1001 seed=seed, nodeids=list(cfg.nodeids),
1002 workers_requested=cfg.workers, workers_actual=workers_actual,
1003 dist_mode=dist_mode, collected=collected, tests=tests,
1004 violations=violations, errors=errors,
1005 duration_seconds=time.monotonic() - t0,
1006 scheduling=scheduling,
1009 report=report, exit_code=exit_code,
1010 execution_order=execution_order,
1011 captured_output=captured,
1017 message
for outcome
in unit_outcomes
1018 for message
in outcome.collect_errors
1020 rcs = [outcome.rc
for outcome
in unit_outcomes
1021 if not outcome.crashed]
1026 "outcome":
_outcome_for(nodeid, rec, set(session.timed_out)),
1028 "duration_seconds": round(rec[
"duration"], 2),
1029 "failure_summary": (
1032 in (
"failed",
"timeout")
1036 for nodeid, rec
in session.records.items()
1038 collect_errors = list(session.collect_errors)
1041 collected_pre = len(session.collected_pre)
1042 selected_post = len(session.selected)
1043 interrupted = sig_state.interrupted
is not None or pool_interrupted
1046 errors.append(make_error(
1047 "signal",
"ORCHESTRATOR_INTERRUPTED",
"fatal",
1048 f
"received {sig_state.interrupted}",
"orchestrator",
1050 return _finish(
"orchestrator_interrupted", 3, tests, collected_pre)
1053 errors.append(make_error(
1054 "configuration",
"COLLECTION_ERROR",
"fatal",
1055 "pytest collection raised for the selected package: "
1056 + collect_errors[0],
1058 return _finish(
"configuration_error", 4, tests, collected_pre)
1060 unexpected = [rc
for rc
in rcs
if rc
in (2, 3)]
1062 errors.append(make_error(
1063 "orchestrator",
"9008",
"fatal",
1064 f
"in-process pytest returned unexpected exit code "
1067 return _finish(
"orchestrator_error", 3, tests, collected_pre)
1068 if any(rc == 4
for rc
in rcs):
1069 errors.append(make_error(
1070 "configuration",
"9008",
"fatal",
1071 "in-process pytest reported a usage error (exit 4); see the "
1074 return _finish(
"configuration_error", 4, tests, collected_pre)
1076 if collected_pre == 0:
1077 errors.append(make_error(
1078 "configuration",
"NO_TESTS_COLLECTED",
"fatal",
1079 "collection completed but the union of the effective roots "
1080 "yielded zero collectable items",
1082 return _finish(
"configuration_error", 4, tests, 0)
1086 item_paths = [p.resolve()
for _n, p
in session.collected_pre]
1087 for tp, rp
in existing_pairs:
1088 resolved_root = rp.resolve()
1090 1
for p
in item_paths
1091 if p == resolved_root
or resolved_root
in p.parents
1094 errors.append(make_error(
1095 "configuration",
"EMPTY_TEST_ROOT",
"warning",
1096 f
"declared test root {tp!r} collected zero items "
1097 "while the run's union is non-empty",
1100 if selected_post == 0
and not session.blocked:
1101 if profile ==
"smoke":
1102 _dbg(
"OC_TESTING", func,
1103 "smoke fallback: empty effective selection; package "
1104 "import/collection checks passed", 2)
1106 errors.append(make_error(
1107 "configuration",
"MARKER_PROFILE_CONFLICT",
"warning",
1108 "the effective selection (profile + --marker + --nodeid) "
1109 "evaluated to an empty set; nothing ran",
1112 error_violations = any(v[
"severity"] ==
"error" for v
in violations)
1113 failed_or_timeout = any(
1114 t[
"outcome"]
in (
"failed",
"timeout")
for t
in tests
1116 exit_code, status = compute_exit_and_status(
1117 fatal_config=
False, interrupted=
False, orchestrator_error=
False,
1118 error_violations=error_violations,
1119 failed_or_timeout=failed_or_timeout,
1121 return _finish(status, exit_code, tests, collected_pre)