zenops 0.20.0

Declarative system configuration management for shell config and dotfiles.
#!/usr/bin/env python3
"""Host driver for the cross-platform integration test harness.

Builds a Docker image per (distro, version) pair (cached across runs)
and runs the in-container scenario for each filtered matrix entry.
Stdlib only — no pip install needed on the host.

By default the gate rows run — tiers 1 and 3 (see `matrix.py` for the
split). Pass ``--tier all`` or ``--tier 2`` to include the opt-in
older / newer versions.

Examples:
    python tests/cross-platform/run.py
    python tests/cross-platform/run.py --only ubuntu
    python tests/cross-platform/run.py --only ubuntu --shell bash
    python tests/cross-platform/run.py --install registry
    python tests/cross-platform/run.py --scenario basic
    python tests/cross-platform/run.py --tier all
"""
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  # noqa: E402


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 += [
        # cargo install needs to fetch crates.io regardless of mode; bridge
        # is required for both local (deps) and registry (zenops itself).
        "--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())