"""Transport for the `visual` domain — args in, delegate, format out.""" from __future__ import annotations from pathlib import Path import typer from tooling.core import cli, console from tooling.core.command import command from tooling.core.errors import ReachError from tooling.domains.visual import blank_check as blank_module from tooling.domains.visual import diff as diff_module from tooling.domains.visual import thumbnail as thumbnail_module app = cli.domain("visual", "Compare captures against goldens, and catch blank ones.") @app.callback() def _domain() -> None: """Keeps `visual` a group (Typer collapses a single-command app).""" @app.command("diff") @command def diff( expected: str = typer.Argument(..., help="Path to the golden PNG."), actual: str = typer.Argument(..., help="Path to the captured PNG."), tolerance: int = typer.Option(None, "--tolerance", help="Per-channel pixel tolerance."), max_diff_pct: float = typer.Option( None, "--max-diff-pct", help="Maximum allowed differing percentage." ), diff_output: str = typer.Option( None, "--diff-output", help="Write a PNG highlighting the changed pixels." ), config: str = typer.Option(None, "--config", help="Path to tests/visual.json."), ) -> None: """Pixel-level comparison of a capture against its golden.""" code = diff_module.run(expected, actual, tolerance, max_diff_pct, diff_output, config) if code == 0: return raise ReachError( f"visual-diff: {expected} and {actual} differ beyond the threshold", fix="inspect the diff PNG with --diff-output, or update the golden with " "make visual-update if the change is intended", exit_code=code, ) @app.command("blank-check") @command def blank_check( path: Path = typer.Argument(..., help="The capture to inspect."), max_modal: float = typer.Option( blank_module.DEFAULT_MAX_MODAL, "--max-modal", help="Fail above this single-colour fraction.", ), quiet: bool = typer.Option(False, "--quiet", help="Say nothing when the frame has content."), ) -> None: """Fail if a capture is overwhelmingly one colour.""" code = blank_module.run(path, max_modal, quiet) if code == 0: return raise ReachError( f"visual-blank-check: {path} has no content", fix="the renderer drew nothing but chrome — check the scenario actually " "loaded before recording a golden from this", exit_code=code, ) @app.command("thumbnail") @command def thumbnail( target: Path = typer.Argument(..., help="Capture directory, or an image in crop mode."), crop: str = typer.Option(None, "--crop", help="Extract this named region instead."), config: Path = typer.Option(None, "--config", help="Config JSON path."), ) -> None: """Build a contact sheet from a flow capture, or crop a named region.""" code = thumbnail_module.run(target, crop, config) if code == 0: console.verdict(f"visual-thumbnail: OK — {target}") return raise ReachError( f"visual-thumbnail: failed for {target}", fix="check the target exists and the config names the region you asked for", exit_code=code, )