Files
codex-mmo/scripts/run_tests.py
T
2026-08-24 08:11:59 -07:00

120 lines
3.6 KiB
Python
Executable File

#!/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())