"""Reproducible isolated Docker tests. --vm supplies a kernel when WSL lacks NFT_SOCKET.""" import argparse import json from pathlib import Path import subprocess import time import uuid import xml.etree.ElementTree as ET ROOT = Path(__file__).resolve().parents[1] def docker(*args, check=True, **kw): return subprocess.run(["docker", *map(str, args)], cwd=ROOT, check=check, **kw) def reports_ok(results): valid = True for path in sorted(results.glob("mcp-*.xml")): cases = list(ET.parse(path).iter("testcase")) failed = [c for c in cases if c.find("failure") is not None or c.find("error") is not None] skipped = [c for c in cases if c.find("skipped") is not None] mcp_skipped = [c for c in skipped if "test_mcp" in c.get("classname", "")] print(f"{path.name}: {len(cases)} cases, {len(failed)} failures, {len(skipped)} skipped", flush=True) for case in mcp_skipped: print(f"INCOMPLETE: {case.get('name')}: {case.find('skipped').get('message')}", flush=True) valid &= not failed and not mcp_skipped return valid and len(list(results.glob("mcp-*.xml"))) == 2 def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--vm", action="store_true") parser.add_argument("--no-build", action="store_true") parser.add_argument("--full", action="store_true", help="Run all unit and integration regressions") parser.add_argument("--tests", nargs="+", help="Run selected pytest paths instead of the default selection") parser.add_argument("--timeout", type=int, default=3600, help="VM runtime limit in seconds") args = parser.parse_args() tag = uuid.uuid4().hex[:10] results = ROOT / ".test-results" / f"mcp-{tag}" results.mkdir(parents=True) selection = (args.tests or ["unit", "integration" if args.full else "integration/test_mcp.py"]) + ["-q", "-x"] (results / "selection.json").write_text(json.dumps(selection), encoding="utf-8") if not args.no_build: docker("build", "-t", "firegex-mcp:dev", ".") docker("build", "-f", "tests/Dockerfile.mcp", "-t", "firegex-mcp:test", ".") if args.vm: docker("build", "-f", "tests/Dockerfile.mcp-vm", "-t", "firegex-mcp:vm", ".") name = f"firegex-mcp-{tag}" created = False try: command = ["create", "--name", name, "--mount", f"type=bind,source={results},target=/results"] if args.vm: command += ["firegex-mcp:vm"] else: command += ["--cap-add", "NET_ADMIN", "--cap-add", "NET_RAW", "--cap-add", "SYS_NICE", "--sysctl", "net.ipv4.conf.all.route_localnet=1", "-e", "NTHREADS=2", "firegex-mcp:test"] docker(*command) created = True docker("start", name) if args.vm: # Poll in bounded intervals, so Ctrl-C always cleans up our container. started = time.monotonic() print(f"VM running; logs and reports: {results}", flush=True) while docker("inspect", "-f", "{{.State.Running}}", name, capture_output=True, text=True).stdout.strip() == "true": if time.monotonic() - started > args.timeout: raise TimeoutError("VM test deadline exceeded; inspect the saved logs") time.sleep(5) exits = [results / f"exit-{i}" for i in (1, 2)] + [results / "exit-restart"] passed = all(p.exists() and p.read_text().strip() == "0" for p in exits) else: passed = True for i in (1, 2): result = docker("exec", "-u", "root", name, "bash", "/test/tests/run_tests.sh", *(["--set-pass"] if i == 1 else []), *selection, f"--junitxml=/results/mcp-{i}.xml", check=False) passed &= result.returncode == 0 if not passed: break py = ["exec", "-u", "root", name, "/test/tests/.venv/bin/python", "/test/tests/mcp_restart_probe.py"] docker(*py, "prepare") docker("stop", "-t", "1", name) time.sleep(6) docker("start", name) # Health polling does not create/revoke any credentials. for _ in range(60): if docker("exec", name, "curl", "-fsS", "http://127.0.0.1:4444/api/status", check=False, capture_output=True).returncode == 0: break time.sleep(1) docker(*py, "verify") passed &= reports_ok(results) print(f"{'PASS' if passed else 'FAILED/INCOMPLETE'}; reports: {results}", flush=True) return 0 if passed else 1 finally: if created: with (results / "container.log").open("w", encoding="utf-8") as log: docker("logs", name, check=False, stdout=log, stderr=subprocess.STDOUT) docker("rm", "-f", name, check=False) if __name__ == "__main__": raise SystemExit(main())