Files
hermes-agent/tests/scripts/test_icon_generation_exit.py
ethernet 31a26c5896 feat(release): shared assembly, scoped publication, harness fold (rounds 2 cont.)
Checkpoint remainder: the unstaged half of the reduction work.

- channel_artifacts.py becomes the single native manifest/feed writer;
  release_artifacts.py and channel release paths delegate to it
- r2_scope fork isolation binds namespace before credential access;
  disposable runs refuse unscoped requests
- canary bootstrap verifies canary's own promoted outputs
- retained-link inventory + empty-directory preservation in strict
  migration snapshots; connection-collision resolution and URL-credential
  rejection in retirement connection adoption
- harness controller test relocated to tests/scripts/ (canonical pytest
  name); channel-retirement install-e2e jobs wired
- S/T receiver candidates build with stable update ownership
- test fixture updates across release/source-channel suites
2026-09-14 10:26:48 -04:00

153 lines
7.8 KiB
Python

"""Icon generation reports per-target failures without hiding later targets."""
import importlib.util
import io
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from types import ModuleType, SimpleNamespace
import pytest
from PIL import Image
@pytest.mark.parametrize("bom", [b"", b"\xef\xbb\xbf"])
@pytest.mark.parametrize("editor_export", [False, True])
def test_svg_readers_accept_bom_without_rewriting_assets(tmp_path, monkeypatch, bom, editor_export):
monkeypatch.setitem(sys.modules, "resvg_py", ModuleType("resvg_py"))
script = Path(__file__).resolve().parents[2] / "scripts/generate_icons.py"
spec = importlib.util.spec_from_file_location("icon_readers_under_test", script)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
path = tmp_path / "art.svg"
element = '<path d="M0 0 L1 1" aria-label="café 東京"/>'
declaration = '<?xml version="1.0" encoding="UTF-8"?>\n' if editor_export else ""
namespaces = ' xmlns="http://www.w3.org/2000/svg" xmlns:editor="urn:editor"'
metadata = '<editor:namedview editor:zoom="1"/>' if editor_export else ""
document = f'<svg{namespaces} viewBox="0 0 20 30">{metadata}{element}</svg>'
raw = bom + (declaration + document).encode("utf-8")
path.write_bytes(raw)
art = SimpleNamespace(girls={"black": path}, paths={}, backgrounds=tmp_path, colors=None)
assert module.girl_path(art, "black") == element
inner, width, height = module.background_inner(art, path.name)
composed = ET.fromstring(f'<svg xmlns="http://www.w3.org/2000/svg">{inner}</svg>')
assert (width, height) == (20, 30)
assert [ET.tostring(child) for child in composed] == [
ET.tostring(child) for child in ET.fromstring(document)
]
assert path.read_bytes() == raw
path.write_bytes(bom + b"<svg/>")
art.paths.clear()
with pytest.raises(AssertionError, match="no <path>"):
module.girl_path(art, "black")
with pytest.raises(AssertionError, match="viewBox"):
module.background_inner(art, path.name)
@pytest.mark.parametrize("failure", [None, "render", "directory", "verify"])
def test_write_status_includes_every_target(tmp_path, monkeypatch, capsys, failure):
# The renderer is build-only. This test injects failures at its byte boundary.
monkeypatch.setitem(sys.modules, "resvg_py", ModuleType("resvg_py"))
script = Path(__file__).resolve().parents[2] / "scripts" / "generate_icons.py"
spec = importlib.util.spec_from_file_location("icon_generator_under_test", script)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
source = tmp_path / "immutable source"
source.mkdir()
monkeypatch.setattr(module, "IconArt", lambda root: root)
monkeypatch.setattr(sys, "argv", [str(script), "--source", str(source), "--out", str(tmp_path)])
image = io.BytesIO()
Image.new("RGBA", (2, 2), (0, 0, 0, 0)).save(image, "PNG")
good_bytes = image.getvalue()
first = "blocked/icon.png" if failure == "directory" else "first.png"
if failure == "directory":
(tmp_path / "blocked").write_text("not a directory", encoding="utf-8")
monkeypatch.setattr(module, "TARGETS", [(first, "png", "first"), ("last.png", "png", "last")])
def target_bytes(art, kind, target):
assert art == source
assert not list(source.iterdir())
if target == "first":
if failure == "render":
raise RuntimeError("injected render failure")
if failure == "verify":
return b"not an image"
return good_bytes
monkeypatch.setattr(module, "target_bytes", target_bytes)
code = 0
try:
module.main()
except SystemExit as stopped:
code = stopped.code
assert bool(code) is (failure is not None)
assert (tmp_path / "last.png").read_bytes() == good_bytes
output = capsys.readouterr().out
assert "last.png: PNG (2, 2)" in output
assert ("FAILED" in output) is (failure is not None)
@pytest.mark.parametrize("platform", ["", "mac-"])
@pytest.mark.parametrize("appearance,girl", [("light", "black"), ("dark", "white")])
@pytest.mark.parametrize("colors", [None, ("#f5cc32", "#443808"), ("#e34850", "#4a1117")])
def test_icon_portrait_overlays_border_inside_outer_silhouette(monkeypatch, platform, appearance, girl, colors):
monkeypatch.setitem(sys.modules, "resvg_py", ModuleType("resvg_py"))
source = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location("icon_geometry_under_test", source / "scripts/generate_icons.py")
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
art = SimpleNamespace(
backgrounds=source / "assets/backgrounds", colors=colors, commit="0123456",
bboxes={girl: (0, 0, 100, 90)}, paths={girl: '<path d="M 0 0 L 100 0 L 100 90 L 0 90 z"/>'},
)
name = f"squircle-{platform}{appearance}.svg"
ns = {"svg": "http://www.w3.org/2000/svg"}
original = ET.parse(art.backgrounds / name).find("svg:rect", ns)
result = ET.fromstring(module.compose_svg(art, girl, name))
tile = result.find("svg:rect", ns)
assert original is not None and tile is not None
x, y, width, height, radius = (float(original.attrib[key]) for key in ("x", "y", "width", "height", "rx"))
thickness = width * module.BORDER_FRACTION
assert float(tile.attrib["stroke-width"]) == pytest.approx(thickness)
assert tile.get("stroke") == ("#000000" if appearance == "light" else "#ffffff")
assert float(tile.attrib["x"]) - thickness / 2 == pytest.approx(x)
assert float(tile.attrib["y"]) - thickness / 2 == pytest.approx(y)
assert float(tile.attrib["width"]) + thickness == pytest.approx(width)
assert float(tile.attrib["height"]) + thickness == pytest.approx(height)
assert float(tile.attrib["rx"]) + thickness / 2 == pytest.approx(radius)
expected_fill = (colors or ("#ffffff", module.DARK_HEX))[appearance == "dark"]
assert tile.get("fill") == expected_fill
clip = result.find("svg:defs/svg:clipPath/svg:rect", ns)
assert clip is not None
assert tuple(float(clip.attrib[key]) for key in ("x", "y", "width", "height", "rx")) == (
x, y, width, height, radius,
)
group = result[-1]
portrait = group[-1]
assert len(group) == 1, "extend the existing contour, not a duplicate strip"
assert portrait is not None and group is not None
assert portrait.get("preserveAspectRatio") == "xMidYMax meet"
assert tuple(float(portrait.attrib[key]) for key in ("x", "y", "width", "height")) == module.GIRL_BOXES[name]
clip_path = result.find("svg:defs/svg:clipPath", ns)
assert clip_path is not None
assert group.get("clip-path") == f"url(#{clip_path.attrib['id']})"
assert portrait.get("overflow") == "visible"
assert len(result.findall(".//svg:path", ns)) == 2 # one badge and one portrait
def test_bottom_node_drag_preserves_upper_geometry_and_path_transform(monkeypatch):
monkeypatch.setitem(sys.modules, "resvg_py", ModuleType("resvg_py"))
script = Path(__file__).resolve().parents[2] / "scripts/generate_icons.py"
spec = importlib.util.spec_from_file_location("icon_nodes_under_test", script)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
path = ET.fromstring('<path transform="matrix(2,0,0,2,10,-100)" d="M 0 0 C 0 90 10 98 20 100 L 0 100 z"/>')
module.drag_bottom_nodes(path, cutoff=80, band=20, distance=10)
assert path.get("transform") == "matrix(2,0,0,2,10,-100)"
assert path.get("d") == "M 0 0 C 0 90 10 102.48 20 105 L 0 105 z"
with pytest.raises(ValueError, match="absolute M/L/C"):
module.drag_bottom_nodes(ET.fromstring('<path d="m 0 0 l 1 1"/>'), cutoff=0, band=1, distance=1)