"""Drive the chromakey garment-clipping QA harness (T-1089) — capture, then analyze. Step 1 launches Godot on the client project with the capture scene, handing it the config through GARMENT_QA_CONFIG. Step 2 runs the pixel analyzer over the PNGs the scene wrote. See GARMENT_QA.md beside this file for what the two passes measure. Formerly tooling/garment-qa/run-garment-qa, a bash script (T-1290). Rewritten per D-263's guarded-exec rule: the DECISIONS — which config, which Godot, whether a virtual display is needed — are `capture_plan()`, pure and testable without launching anything; only the launch goes through core/process. The bash exit codes are kept: 2 for a missing config, 3 for no Godot. """ from __future__ import annotations import os import shutil from dataclasses import dataclass from pathlib import Path from tooling.core import config, console, process from tooling.core.errors import ReachError CONFIG_DIR = Path(__file__).resolve().parent / "qa_configs" DEFAULT_CONFIG = "peasant" SCENE = "res://tools/garment_qa/chromakey_scene.tscn" @dataclass(frozen=True) class CapturePlan: config: Path argv: list[str] env_config: str # the value for GARMENT_QA_CONFIG def resolve_config(name_or_path: str | None) -> Path: """A config name (`peasant`) resolves inside qa_configs/; anything else is a path.""" given = name_or_path or DEFAULT_CONFIG if "/" not in given and not given.endswith(".json"): path = CONFIG_DIR / f"{given}.json" else: path = Path(given) if not path.is_file(): known = ", ".join(sorted(p.stem for p in CONFIG_DIR.glob("*.json"))) raise ReachError( f"garment-qa config not found: {path}", fix=f"pass a config path, or one of: {known}", exit_code=2, ) return path def resolve_godot(env: dict[str, str], home: Path, which=shutil.which) -> str: """$GODOT, else ~/bin/godot4, else godot4/godot on PATH — the bash script's order.""" candidate = env.get("GODOT") or str(home / "bin" / "godot4") if os.path.isfile(candidate) and os.access(candidate, os.X_OK): return candidate found = which("godot4") or which("godot") if found: return found raise ReachError( "godot binary not found", fix="set GODOT=/path/to/godot4, or install it: make setup-godot", exit_code=3, ) def capture_plan(config_arg: str | None, env: dict[str, str], home: Path, which=shutil.which) -> CapturePlan: """What the capture step would run — decided, not performed.""" cfg = resolve_config(config_arg) godot = resolve_godot(env, home, which) argv = [godot, "--path", str(config.repo_root() / "client"), "--rendering-driver", "opengl3", SCENE] # The capture needs a display. Headless (no $DISPLAY) runs under a virtual one. if not env.get("DISPLAY"): argv = ["xvfb-run", "-a", *argv] return CapturePlan(config=cfg, argv=argv, env_config=str(cfg.resolve())) def capture(plan: CapturePlan) -> None: """Perform the capture step. Godot's own output streams straight through.""" console.event(f"config = {plan.config}") console.event(f"godot = {plan.argv[2] if plan.argv[0] == 'xvfb-run' else plan.argv[0]}") env = {**os.environ, "GARMENT_QA_CONFIG": plan.env_config} process.run( plan.argv, env=env, capture=False, missing_fix="install xvfb-run (it provides the virtual display a headless capture needs), " "or run from a session with $DISPLAY set", )