mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 15:30:59 +00:00
Expose 28 tools with shared REST operations, role-scoped bearer authentication, revisions, durable retries, snapshots, jobs and timed rollback. Add Docker verification harnesses and setup/workflow documentation in docs/mcp.md and inst.md. Validated with 330 unit tests, two consecutive 25-case MCP/import runs, and restart recovery; the full upstream regression run remains incomplete.
105 lines
4.9 KiB
Python
105 lines
4.9 KiB
Python
"""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())
|