Files
go-omni/scripts/verifyModules.py

158 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Verify feature modules and real Redis in sequential resource-limited containers."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import shutil
import signal
import subprocess
import tempfile
import time
import uuid
def capture(command):
return subprocess.check_output(command, text=True, timeout=15).strip()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--image", default="golang:1.25")
parser.add_argument("--redis-image", default="redis:7.4-alpine")
parser.add_argument("--goproxy", default="")
parser.add_argument("--wall-timeout", type=int, default=900)
parser.add_argument("--output", type=Path, default=Path("performance/modulesLinux.json"))
args = parser.parse_args()
if args.wall_timeout < 1:
parser.error("wall timeout must be positive")
docker = shutil.which("docker")
if not docker:
raise SystemExit("docker is required")
root = Path(__file__).resolve().parent.parent
running = capture([docker, "ps", "--format", "{{.ID}} {{.Names}}"])
if any("go-omni-" in line for line in running.splitlines()):
raise SystemExit("another go-omni validation container is active")
before = {line.split()[0] for line in running.splitlines()}
output_path = args.output.resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
log_path = output_path.with_suffix(".txt")
prefix = "go-omni-modules-" + uuid.uuid4().hex[:12]
names = [prefix + "-" + stage for stage in ("build", "redis")]
paths = [root / "go.mod", root / "go.sum", root / "httpx/context.go", Path(__file__).resolve()]
for directory in ("cache", "httpc", "task", "middleware", "cmd/go-omni", "examples/modules"):
paths.extend(p for p in (root / directory).rglob("*") if p.is_file() and p.suffix in (".go", ".tmpl"))
report = {
"status": "running", "startedAt": datetime.now(timezone.utc).isoformat(),
"testedCommit": capture(["git", "-C", str(root), "rev-parse", "HEAD"]),
"workingTreeStatus": capture(["git", "-C", str(root), "status", "--porcelain"]),
"sourceSha256": {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(set(paths))},
"containerLimits": {"cpus": 2, "memoryBytes": 2147483648, "memoryAndSwapBytes": 2147483648, "pidsLimit": 256},
"wallTimeoutSeconds": args.wall_timeout, "imageIds": {}, "commands": [], "cleanedContainers": [],
"notes": [
"Full repository race/vet/build plus independent generated-module CRUD in the compiler container.",
"Real Redis uses a statically linked non-race test binary, loopback only, network=none and no published ports.",
"At most one validation container; no pre-existing service or database is accessed.",
],
}
output_path.write_text(json.dumps(report, indent=2) + "\n")
deadline = time.monotonic() + args.wall_timeout
failure = None
def interrupt(signum, frame):
raise KeyboardInterrupt
previous = {sig: signal.signal(sig, interrupt) for sig in (signal.SIGTERM, signal.SIGHUP)}
with log_path.open("w") as log:
def execute(command):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("module verification wall timeout")
report["commands"].append(command)
subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=remaining)
log.flush()
def container(name, arguments):
try:
execute([docker, "run", "--rm", "--name", name,
"--cpus", "2", "--memory", "2048m", "--memory-swap", "2048m", "--pids-limit", "256",
"--log-opt", "max-size=10m", "--log-opt", "max-file=1"] + arguments)
finally:
result = subprocess.run([docker, "rm", "--force", name], capture_output=True, text=True, timeout=15)
if result.returncode and "No such container" not in result.stderr:
raise RuntimeError("could not clean up " + name)
report["cleanedContainers"].append(name)
try:
for image in (args.image, args.redis_image):
result = subprocess.run([docker, "image", "inspect", image], capture_output=True, timeout=15)
if result.returncode:
print("Pulling " + image, flush=True)
execute([docker, "pull", image])
report["imageIds"][image] = capture([docker, "image", "inspect", image, "--format", "{{.Id}}"])
with tempfile.TemporaryDirectory(prefix="goOmniModules") as temporary:
# The Redis stage runs as an unprivileged uid and must traverse
# this directory to execute the read-only test binary.
Path(temporary).chmod(0o755)
print("Full Linux race/vet/build and generated-project CRUD; 2 CPUs, 2048 MiB, 256 pids", flush=True)
arguments = ["-v", f"{root}:/workspace:ro", "-v", f"{temporary}:/output", "-w", "/workspace",
"-e", "GOWORK=off", "-e", "GOMAXPROCS=2", "-e", "GO_OMNI_SCAFFOLD_INTEGRATION=1"]
if args.goproxy:
arguments += ["-e", "GOPROXY=" + args.goproxy]
arguments += [args.image, "sh", "-c",
"go version && go test -race -count=1 -timeout=240s ./... && go vet ./... && go build ./... && "
"CGO_ENABLED=0 go test -c -o /output/redis.test ./cache/redis"]
container(names[0], arguments)
print("Real Redis TTL/read/write/delete/failure checks; external network disabled", flush=True)
redis_check = '''set -eu
redis-server --version
redis-server --bind 127.0.0.1 --port 6379 --save '' --appendonly no --maxmemory 32mb --daemonize yes --pidfile /tmp/redis.pid --logfile /tmp/redis.log --dir /tmp
trap 'redis-cli -h 127.0.0.1 shutdown nosave >/dev/null 2>&1 || true' EXIT
attempt=0
until redis-cli -h 127.0.0.1 ping >/dev/null 2>&1; do
attempt=$((attempt + 1))
if [ "$attempt" -gt 50 ]; then cat /tmp/redis.log; exit 1; fi
sleep 0.1
done
/test/redis.test -test.v -test.run '^TestRedisIntegration$' -test.count=1 -test.timeout=30s
'''
container(names[1], ["--network", "none", "--read-only", "--cap-drop", "ALL", "--security-opt", "no-new-privileges",
"--user", "999:999", "--tmpfs", "/tmp:rw,nosuid,noexec,size=64m,mode=1777",
"-v", f"{temporary}:/test:ro", "--entrypoint", "sh",
"-e", "GOMAXPROCS=2", "-e", "GO_OMNI_REDIS_TEST_ADDR=127.0.0.1:6379",
args.redis_image, "-c", redis_check])
log.flush()
content = log_path.read_text()
if "--- PASS: TestRedisIntegration" not in content:
raise RuntimeError("missing real Redis pass evidence")
report["status"] = "passed"
except (Exception, KeyboardInterrupt) as error:
failure = str(error) or type(error).__name__
report["status"], report["error"] = "failed", failure
finally:
for sig, handler in previous.items():
signal.signal(sig, handler)
log.flush()
report["logSha256"] = hashlib.sha256(log_path.read_bytes()).hexdigest()
report["finishedAt"] = datetime.now(timezone.utc).isoformat()
try:
after = set(capture([docker, "ps", "--format", "{{.ID}}"]).split())
if not before.issubset(after):
raise RuntimeError("a pre-existing container is no longer running")
report["preExistingContainersStillRunning"] = len(before)
if failure is None and set(names) != set(report["cleanedContainers"]):
raise RuntimeError("container cleanup incomplete")
except Exception as error:
failure = str(error)
report["status"], report["error"] = "failed", failure
output_path.write_text(json.dumps(report, indent=2) + "\n")
if failure:
raise SystemExit(f"FAIL: {failure}; see {log_path}")
print(f"PASS feature module acceptance; report: {output_path}", flush=True)
if __name__ == "__main__":
main()