158 lines
8.4 KiB
Python
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()
|