#!/usr/bin/env python3 """Run each offline integration-test module in an isolated process group.""" from __future__ import annotations import argparse import os import signal import subprocess import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "libexec")) from mmo_util import filtered_environment, terminate_process_group # noqa: E402 DEFAULT_MODULES = ( "tests.test_catalog_profiles", "tests.test_runtime", "tests.test_runtime_advanced", "tests.test_tui_metadata", "tests.test_install_eval", "tests.test_cli_ux", "tests.test_release", ) _CURRENT_PROCESS: subprocess.Popen[str] | None = None def _terminate_group(process: subprocess.Popen[str]) -> None: terminate_process_group(process.pid, grace_seconds=3.0) try: process.wait(timeout=2) except subprocess.TimeoutExpired: pass def _handle_termination(_signum: int, _frame: object) -> None: if _CURRENT_PROCESS is not None: _terminate_group(_CURRENT_PROCESS) raise KeyboardInterrupt def run_module(module: str, timeout: float) -> tuple[bool, float]: global _CURRENT_PROCESS environment = filtered_environment( extra={ "PYTHONDONTWRITEBYTECODE": "1", "PYTHONWARNINGS": "error::ResourceWarning", "PYTHONPATH": os.pathsep.join((str(ROOT / "libexec"), str(ROOT / "tests"))), } ) command = [sys.executable, "-m", "unittest", "-v", module] print(f"\n=== {module} ===", flush=True) started = time.monotonic() process = subprocess.Popen( command, cwd=ROOT, env=environment, text=True, start_new_session=True, ) _CURRENT_PROCESS = process try: try: return_code = process.wait(timeout=timeout) except subprocess.TimeoutExpired: print( f"ERROR: {module} exceeded {timeout:g} seconds; terminating its process group", file=sys.stderr, flush=True, ) return False, time.monotonic() - started finally: # A test process can exit while leaving a child in its isolated group. # Retire that group on success, timeout, interruption, and wait errors. _terminate_group(process) _CURRENT_PROCESS = None return return_code == 0, time.monotonic() - started def main() -> int: parser = argparse.ArgumentParser( description="Run Codex MMO offline tests with per-module process isolation" ) parser.add_argument("modules", nargs="*", default=list(DEFAULT_MODULES)) parser.add_argument("--timeout", type=float, default=300.0) args = parser.parse_args() if args.timeout <= 0: parser.error("--timeout must be positive") signal.signal(signal.SIGTERM, _handle_termination) signal.signal(signal.SIGINT, _handle_termination) failures: list[str] = [] durations: dict[str, float] = {} for module in args.modules: passed, duration = run_module(module, args.timeout) durations[module] = duration if not passed: failures.append(module) print("\n=== isolated suite summary ===") for module, duration in durations.items(): status = "PASS" if module not in failures else "FAIL" print(f"{status:4} {duration:8.2f}s {module}") if failures: print( "failed modules: " + ", ".join(failures), file=sys.stderr, ) return 1 print(f"all {len(durations)} modules passed") return 0 if __name__ == "__main__": raise SystemExit(main())