import argparse
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
REPO_ROOT = HERE.parent.parent
DOCKER_DIR = HERE / "docker"
CONTAINER_DIR = HERE / "container"
sys.path.insert(0, str(HERE))
from matrix import DISTROS, MATRIX, SHELLS, Entry
def image_tag(distro: str, version: str | None) -> str:
return f"zenops-test:{distro}-{version}" if version else f"zenops-test:{distro}"
def build_image(distro: str, version: str | None, *, pull: bool) -> str:
tag = image_tag(distro, version)
dockerfile = DOCKER_DIR / f"{distro}.Dockerfile"
if not dockerfile.exists():
sys.exit(f"no Dockerfile for distro={distro}: {dockerfile}")
cmd = ["docker", "build", "-f", str(dockerfile), "-t", tag]
if version is not None:
cmd += ["--build-arg", f"VERSION={version}"]
if pull:
cmd.append("--pull")
cmd.append(str(DOCKER_DIR))
print(f"\n>>> building {tag}")
print(f" $ {' '.join(shlex.quote(c) for c in cmd)}")
subprocess.run(cmd, check=True)
return tag
def run_combo(
tag: str,
entry: Entry,
*,
install: str,
scenario: str,
keep: bool,
) -> bool:
label = entry.distro + (f"-{entry.version}" if entry.version else "")
prefix = f"[{label}/{entry.shell}]"
print(f"\n>>> {prefix} scenario={scenario} install={install}")
docker_args = ["docker", "run"]
if keep:
docker_args += ["--name", f"zenops-test-{label}-{entry.shell}"]
else:
docker_args.append("--rm")
docker_args += [
"--network", "bridge",
"-v", f"{REPO_ROOT}:/src:ro",
"-v", f"{CONTAINER_DIR}:/test:ro",
tag,
"python3", "/test/runner.py",
"--scenario", scenario,
"--shell", entry.shell,
"--install", install,
"--source-path", "/src",
]
print(f" $ {' '.join(shlex.quote(c) for c in docker_args)}")
proc = subprocess.Popen(
docker_args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
assert proc.stdout is not None
for line in proc.stdout:
print(f"{prefix} {line.rstrip()}")
proc.wait()
ok = proc.returncode == 0
print(f"{prefix} {'PASS' if ok else 'FAIL'} (exit {proc.returncode})")
return ok
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--only", choices=DISTROS, help="limit to one distro family")
p.add_argument("--shell", choices=SHELLS, help="limit to one shell")
p.add_argument("--tier", choices=["1", "2", "3", "gate", "all"], default="gate",
help="which matrix tiers to run (default: gate = tiers 1 and 3)")
p.add_argument("--install", choices=["local", "registry"], default="local")
p.add_argument("--scenario", default="basic")
p.add_argument("--keep", action="store_true",
help="don't `docker rm` (use `docker exec` to inspect)")
p.add_argument("--pull", action="store_true",
help="docker build --pull (refresh base images)")
p.add_argument("--no-build", action="store_true",
help="skip docker build (assume images already exist)")
args = p.parse_args()
if not shutil.which("docker"):
sys.exit("docker not on PATH")
tier_filter = {"1": {1}, "2": {2}, "3": {3}, "gate": {1, 3}, "all": {1, 2, 3}}[args.tier]
selected = [
e for e in MATRIX
if e.tier in tier_filter
and (args.only is None or e.distro == args.only)
and (args.shell is None or e.shell == args.shell)
]
if not selected:
sys.exit("no matrix entries selected; check --only/--shell/--tier")
print(f"matrix: {len(selected)} combination(s)")
for e in selected:
version_label = e.version or "rolling"
print(f" - {e.distro} {version_label} / {e.shell} / {e.package_manager} (tier {e.tier})")
builds_needed = sorted({(e.distro, e.version) for e in selected})
images: dict[tuple[str, str | None], str] = {}
for distro, version in builds_needed:
if args.no_build:
images[(distro, version)] = image_tag(distro, version)
else:
images[(distro, version)] = build_image(distro, version, pull=args.pull)
results: list[tuple[Entry, bool]] = []
for entry in selected:
ok = run_combo(
images[(entry.distro, entry.version)],
entry,
install=args.install,
scenario=args.scenario,
keep=args.keep,
)
results.append((entry, ok))
print("\n=== summary ===")
for entry, ok in results:
label = entry.distro + (f"-{entry.version}" if entry.version else "")
print(f" {label}/{entry.shell}: {'PASS' if ok else 'FAIL'}")
failures = [r for r in results if not r[1]]
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())