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
184 lines
7.1 KiB
Python
184 lines
7.1 KiB
Python
"""Spec-loading tests.
|
|
|
|
A malformed spec must raise ``SpecError`` (harness error, exit 2) rather than
|
|
producing a permissive spec that silently skips gates -- a spec typo that
|
|
turned gates off would be indistinguishable from art that passed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from python_pkg.artgate.codes import Code
|
|
from python_pkg.artgate.spec import SpecError, TargetSpec, load_spec
|
|
|
|
VALID = """
|
|
name = "pixel_icon"
|
|
canvas = [32, 32]
|
|
palette = ["#1b1d21", "#ffffff"]
|
|
max_colors = 16
|
|
alpha_binary = true
|
|
scale = 1
|
|
min_margin = 1
|
|
gates = ["ALPHA_NOT_BINARY", "OFF_PALETTE", "TOO_MANY_COLORS"]
|
|
"""
|
|
|
|
|
|
def _write(tmp_path: Path, text: str) -> Path:
|
|
"""Write a spec file and return its path.
|
|
|
|
Args:
|
|
tmp_path: Pytest temporary directory.
|
|
text: TOML contents.
|
|
|
|
Returns:
|
|
The written path.
|
|
"""
|
|
path = tmp_path / "spec.toml"
|
|
path.write_text(text, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
class TestValidSpec:
|
|
"""A well-formed spec parses into the expected frozen dataclass."""
|
|
|
|
def test_fields_round_trip(self, tmp_path: Path) -> None:
|
|
"""Every declared field is parsed."""
|
|
spec = load_spec(_write(tmp_path, VALID))
|
|
assert spec.name == "pixel_icon"
|
|
assert spec.canvas == (32, 32)
|
|
assert spec.max_colors == 16
|
|
assert spec.alpha_binary is True
|
|
assert spec.min_margin == 1
|
|
assert (27, 29, 33) in spec.palette
|
|
|
|
def test_gate_applicability(self, tmp_path: Path) -> None:
|
|
"""``runs`` reports only the declared gates."""
|
|
spec = load_spec(_write(tmp_path, VALID))
|
|
assert spec.runs(Code.OFF_PALETTE)
|
|
assert not spec.runs(Code.SEAM_ENERGY)
|
|
|
|
def test_defaults_when_optional_fields_absent(self, tmp_path: Path) -> None:
|
|
"""Optional fields default, for a gate needing no extra config.
|
|
|
|
``SILHOUETTE_LOW_CONTRAST`` is used deliberately: it is the only
|
|
declarable gate with a self-contained threshold, so it exercises the
|
|
defaults without tripping the require-config check.
|
|
"""
|
|
body = 'name = "m"\ngates = ["SILHOUETTE_LOW_CONTRAST"]\n'
|
|
spec = load_spec(_write(tmp_path, body))
|
|
assert spec.canvas is None
|
|
assert spec.palette == frozenset()
|
|
assert spec.max_colors is None
|
|
assert spec.scale is None
|
|
assert spec.min_margin == 0
|
|
assert spec.alpha_binary is False
|
|
|
|
def test_default_construction_has_no_gates(self) -> None:
|
|
"""A bare TargetSpec runs nothing."""
|
|
assert not TargetSpec(name="bare").runs(Code.OFF_PALETTE)
|
|
|
|
|
|
class TestMalformedSpec:
|
|
"""Every malformed input raises SpecError, never a permissive spec."""
|
|
|
|
def test_missing_file(self, tmp_path: Path) -> None:
|
|
"""A missing spec file is a harness error."""
|
|
with pytest.raises(SpecError, match="cannot read spec"):
|
|
load_spec(tmp_path / "absent.toml")
|
|
|
|
def test_invalid_toml(self, tmp_path: Path) -> None:
|
|
"""Unparsable TOML is a harness error."""
|
|
with pytest.raises(SpecError, match="malformed spec"):
|
|
load_spec(_write(tmp_path, "name = [unclosed"))
|
|
|
|
@pytest.mark.parametrize("body", ["gates = []", 'name = ""\ngates = []'])
|
|
def test_missing_name(self, tmp_path: Path, body: str) -> None:
|
|
"""A blank or absent name is rejected."""
|
|
with pytest.raises(SpecError, match="non-empty 'name'"):
|
|
load_spec(_write(tmp_path, body))
|
|
|
|
@pytest.mark.parametrize("gates", ["", "gates = []", 'gates = "nope"'])
|
|
def test_missing_gates(self, tmp_path: Path, gates: str) -> None:
|
|
"""A spec with no gates would pass everything, so it is rejected."""
|
|
with pytest.raises(SpecError, match="at least one gate"):
|
|
load_spec(_write(tmp_path, f'name = "m"\n{gates}\n'))
|
|
|
|
def test_unknown_gate(self, tmp_path: Path) -> None:
|
|
"""A typo in a gate name must not silently disable it."""
|
|
with pytest.raises(SpecError, match="unknown gate"):
|
|
load_spec(_write(tmp_path, 'name = "m"\ngates = ["NOT_A_GATE"]\n'))
|
|
|
|
@pytest.mark.parametrize(
|
|
"palette",
|
|
['palette = "#fff"', 'palette = ["fff"]', 'palette = ["#gggggg"]'],
|
|
)
|
|
def test_bad_palette(self, tmp_path: Path, palette: str) -> None:
|
|
"""Palette entries must be '#rrggbb'."""
|
|
body = f'name = "m"\ngates = ["OFF_PALETTE"]\n{palette}\n'
|
|
with pytest.raises(SpecError, match="palette"):
|
|
load_spec(_write(tmp_path, body))
|
|
|
|
@pytest.mark.parametrize(
|
|
"canvas",
|
|
["canvas = 32", "canvas = [32]", "canvas = [0, 32]", 'canvas = ["a","b"]'],
|
|
)
|
|
def test_bad_canvas(self, tmp_path: Path, canvas: str) -> None:
|
|
"""Canvas must be a pair of positive integers."""
|
|
body = f'name = "m"\ngates = ["CANVAS_SIZE"]\n{canvas}\n'
|
|
with pytest.raises(SpecError, match="canvas must be"):
|
|
load_spec(_write(tmp_path, body))
|
|
|
|
|
|
class TestFailClosed:
|
|
"""A declared gate that cannot actually run is a spec fault, not a pass.
|
|
|
|
Regression: ``hand_painted.toml`` declared ``SILHOUETTE_LOW_CONTRAST``
|
|
while no implementation existed. The spec parsed, the gate never ran, and
|
|
a featureless image scored exit 0 -- indistinguishable from real art
|
|
passing. All three mechanisms below are now rejected at load time.
|
|
"""
|
|
|
|
def test_unimplemented_gate_is_rejected(self, tmp_path: Path) -> None:
|
|
"""A gate with no implementation cannot be declared."""
|
|
body = 'name = "m"\ngates = ["SEAM_ENERGY"]\n'
|
|
with pytest.raises(SpecError, match="not implemented"):
|
|
load_spec(_write(tmp_path, body))
|
|
|
|
def test_result_only_code_is_rejected(self, tmp_path: Path) -> None:
|
|
"""A result code is emitted by a gate, never requested by a spec."""
|
|
body = 'name = "m"\ncanvas = [8, 8]\ngates = ["NO_OPAQUE_PIXELS"]\n'
|
|
with pytest.raises(SpecError, match="emitted by gates"):
|
|
load_spec(_write(tmp_path, body))
|
|
|
|
@pytest.mark.parametrize(
|
|
("body", "match"),
|
|
[
|
|
('gates = ["CANVAS_SIZE"]', "CANVAS_SIZE needs"),
|
|
('gates = ["TOO_MANY_COLORS"]', "TOO_MANY_COLORS needs"),
|
|
('gates = ["OFF_PALETTE"]', "OFF_PALETTE needs"),
|
|
('gates = ["ALPHA_NOT_BINARY"]', "ALPHA_NOT_BINARY needs"),
|
|
('gates = ["MARGIN_TOO_SMALL"]', "MARGIN_TOO_SMALL needs"),
|
|
],
|
|
)
|
|
def test_gate_without_config_is_rejected(
|
|
self, tmp_path: Path, body: str, match: str
|
|
) -> None:
|
|
"""Declaring a gate without its config would silently skip it."""
|
|
with pytest.raises(SpecError, match=match):
|
|
load_spec(_write(tmp_path, f'name = "m"\n{body}\n'))
|
|
|
|
def test_shipped_styles_load(self) -> None:
|
|
"""Every style shipped in the package must itself be valid."""
|
|
styles = Path(__file__).resolve().parent.parent / "styles"
|
|
loaded = [load_spec(p) for p in sorted(styles.glob("*.toml"))]
|
|
assert loaded
|
|
assert all(spec.gates for spec in loaded)
|
|
|
|
|
|
def test_code_str_is_bare_value() -> None:
|
|
"""Codes format without an enum prefix, so JSONL stays greppable."""
|
|
assert f"{Code.OFF_PALETTE}" == "OFF_PALETTE"
|