mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 23:07:03 +01:00
Closes the remaining bake-off cells. The phase's real result was not a route
score but a MISSING GATE.
All twelve SDXL icons measured 100% opaque coverage -- square tiles with no
sprite silhouette -- and passed every gate in the harness. Gate-pass was
measuring pipeline hygiene, not usability. MARGIN_TOO_SMALL inverts the table
from a meaningless 12/12/12 tie to:
diffusion 0/12 (every output full-bleed)
vector 10/12
procedural 11/12
The two non-diffusion failures were true positives in MY shapes, not gate
error: the procedural sword pommel ran off the bottom edge and the vector meat
bone clipped the top. Both fixed; both routes back to 12/12.
Diffusion recovers to 12/12 with flood-fill background removal, but visual
inspection shows the fill eats into the art (bone reduced to a fragment, sword
nearly erased). Reported as inadequate rather than shipped -- proper matting is
needed before route (b) is a usable pipeline.
Blender exits 0 even when its embedded script raises: BLENDER_EEVEE_NEXT does
not exist in 5.2, and the failure produced a clean exit and no file. render()
now asserts the output exists, or the route would report phantom successes.
Model gating verified at execution time, never recalled: SDXL base and
nerijs/pixel-art-xl are ungated, FLUX.1-dev is gated=auto, and
runwayml/stable-diffusion-v1-5 now 307-redirects.
The finishing stage now generalises across three continuous-tone sources
(SVG raster, Blender render, SDXL): each is 0/N raw and N/N finished.
Verified: 114 tests, 100% branch coverage on 13 modules, zero suppressions,
all hooks pass. Note: a local pytest run masked a missing branch that the
pre-commit hook caught -- the hook's run is authoritative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
182 lines
6.0 KiB
Python
182 lines
6.0 KiB
Python
"""CLI tests, covering all three exit codes.
|
|
|
|
The three-valued exit code is the contract the whole bake-off rests on, so
|
|
each value is asserted explicitly: a harness fault must never be recorded as
|
|
an art rejection.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
import pytest
|
|
|
|
from python_pkg.artgate.cli import evaluate, main
|
|
from python_pkg.artgate.codes import (
|
|
EXIT_GATE_FAILURE,
|
|
EXIT_HARNESS_ERROR,
|
|
EXIT_PASS,
|
|
Code,
|
|
)
|
|
from python_pkg.artgate.spec import TargetSpec
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
SPEC = """
|
|
name = "t"
|
|
canvas = [4, 4]
|
|
max_colors = 4
|
|
alpha_binary = true
|
|
palette = ["#0a141e", "#28323c"]
|
|
gates = [
|
|
"ALPHA_NOT_BINARY", "TOO_MANY_COLORS", "OFF_PALETTE",
|
|
"CANVAS_SIZE", "SCALE_NOT_INVARIANT",
|
|
]
|
|
"""
|
|
|
|
|
|
def _write_png(path: Path, rgb: tuple[int, int, int], size: int = 4) -> Path:
|
|
"""Write a uniform opaque PNG.
|
|
|
|
Args:
|
|
path: Destination file.
|
|
rgb: The fill colour.
|
|
size: Square edge length.
|
|
|
|
Returns:
|
|
The written path.
|
|
"""
|
|
arr = np.zeros((size, size, 4), dtype=np.uint8)
|
|
arr[:, :, :3] = rgb
|
|
arr[:, :, 3] = 255
|
|
Image.fromarray(arr, "RGBA").save(path)
|
|
return path
|
|
|
|
|
|
@pytest.fixture(name="spec_file")
|
|
def fixture_spec_file(tmp_path: Path) -> Path:
|
|
"""Write the shared test spec.
|
|
|
|
Args:
|
|
tmp_path: Pytest temporary directory.
|
|
|
|
Returns:
|
|
Path to the spec.
|
|
"""
|
|
path = tmp_path / "t.toml"
|
|
path.write_text(SPEC, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
class TestExitCodes:
|
|
"""Each exit code is reachable and means exactly one thing."""
|
|
|
|
def test_pass(
|
|
self, tmp_path: Path, spec_file: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""Conforming art exits 0."""
|
|
img = _write_png(tmp_path / "ok.png", (10, 20, 30))
|
|
assert main([str(img), "--spec", str(spec_file)]) == EXIT_PASS
|
|
assert "PASS" in capsys.readouterr().out
|
|
|
|
def test_gate_failure(
|
|
self, tmp_path: Path, spec_file: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""Off-palette art exits 1 and names the code."""
|
|
img = _write_png(tmp_path / "bad.png", (200, 10, 10))
|
|
assert main([str(img), "--spec", str(spec_file)]) == EXIT_GATE_FAILURE
|
|
assert "OFF_PALETTE" in capsys.readouterr().out
|
|
|
|
def test_missing_spec_is_harness_error(
|
|
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""A missing spec exits 2, not 1."""
|
|
img = _write_png(tmp_path / "x.png", (10, 20, 30))
|
|
code = main([str(img), "--spec", str(tmp_path / "absent.toml")])
|
|
assert code == EXIT_HARNESS_ERROR
|
|
assert "artgate:" in capsys.readouterr().err
|
|
|
|
def test_unreadable_image_is_harness_error(
|
|
self, tmp_path: Path, spec_file: Path
|
|
) -> None:
|
|
"""A non-image file exits 2, not 1."""
|
|
junk = tmp_path / "junk.png"
|
|
junk.write_text("not an image", encoding="utf-8")
|
|
code = main([str(junk), "--spec", str(spec_file)])
|
|
assert code == EXIT_HARNESS_ERROR
|
|
|
|
def test_json_output(
|
|
self, tmp_path: Path, spec_file: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""JSON mode emits the machine-readable verdict."""
|
|
img = _write_png(tmp_path / "ok.png", (10, 20, 30))
|
|
main([str(img), "--spec", str(spec_file), "--json"])
|
|
out = capsys.readouterr().out
|
|
assert '"exit": 0' in out
|
|
assert '"style": "t"' in out
|
|
|
|
|
|
class TestEvaluate:
|
|
"""Gate selection is driven by the spec, not by hardcoded branching."""
|
|
|
|
def test_canvas_mismatch_reported(self, tmp_path: Path) -> None:
|
|
"""A wrong-sized canvas is caught."""
|
|
arr = np.zeros((8, 8, 4), dtype=np.uint8)
|
|
arr[:, :, 3] = 255
|
|
spec = TargetSpec(name="s", canvas=(4, 4), gates=frozenset({Code.CANVAS_SIZE}))
|
|
assert evaluate(arr, spec) == [Code.CANVAS_SIZE]
|
|
|
|
def test_gates_not_declared_are_not_run(self) -> None:
|
|
"""An off-palette image passes when the palette gate is not declared."""
|
|
arr = np.zeros((2, 2, 4), dtype=np.uint8)
|
|
arr[:, :, :3] = (99, 99, 99)
|
|
arr[:, :, 3] = 255
|
|
spec = TargetSpec(
|
|
name="s",
|
|
palette=frozenset({(1, 2, 3)}),
|
|
gates=frozenset({Code.CANVAS_SIZE}),
|
|
)
|
|
assert evaluate(arr, spec) == []
|
|
|
|
def test_codes_are_deduplicated(self) -> None:
|
|
"""A code raised by two gates is reported once."""
|
|
arr = np.zeros((2, 2, 4), dtype=np.uint8)
|
|
spec = TargetSpec(
|
|
name="s",
|
|
max_colors=4,
|
|
palette=frozenset({(1, 2, 3)}),
|
|
gates=frozenset({Code.TOO_MANY_COLORS, Code.OFF_PALETTE}),
|
|
)
|
|
assert evaluate(arr, spec) == [Code.NO_OPAQUE_PIXELS]
|
|
|
|
def test_margin_gate_runs_when_declared(self) -> None:
|
|
"""A full-bleed asset fails once the margin gate is declared.
|
|
|
|
Regression: twelve diffusion icons were 100% opaque -- square tiles,
|
|
not sprites -- and passed every other gate, because no gate asked
|
|
whether the asset had a silhouette at all.
|
|
"""
|
|
arr = np.zeros((8, 8, 4), dtype=np.uint8)
|
|
arr[:, :, 3] = 255
|
|
spec = TargetSpec(
|
|
name="s", min_margin=1, gates=frozenset({Code.MARGIN_TOO_SMALL})
|
|
)
|
|
assert evaluate(arr, spec) == [Code.MARGIN_TOO_SMALL]
|
|
|
|
def test_silhouette_gate_runs_when_declared(self) -> None:
|
|
"""A near-white asset fails the silhouette gate.
|
|
|
|
Regression: ``SILHOUETTE_LOW_CONTRAST`` was declarable in a spec but
|
|
had no implementation, so it parsed cleanly, ran nothing and reported
|
|
a pass. ``load_spec`` now rejects undeclarable gates, and this asserts
|
|
the gate genuinely executes once declared.
|
|
"""
|
|
arr = np.zeros((8, 8, 4), dtype=np.uint8)
|
|
arr[:, :, :3] = 250
|
|
arr[:, :, 3] = 255
|
|
spec = TargetSpec(name="s", gates=frozenset({Code.SILHOUETTE_LOW_CONTRAST}))
|
|
assert evaluate(arr, spec) == [Code.SILHOUETTE_LOW_CONTRAST]
|