😏
This commit is contained in:
Executable
+119
@@ -0,0 +1,119 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user