from __future__ import annotations import hashlib import json import os from pathlib import Path import pytest from PIL import Image from render_visual_tables import CODECS, load_prompt_ids, load_rows, render, render_contact_sheet def test_load_prompt_ids_accepts_manifest_object(tmp_path: Path) -> None: path = tmp_path / "prompts.json" path.write_text(json.dumps({"prompt_ids": ["p2", "p1"]}), encoding="utf-8") assert load_prompt_ids(path) == ["p2", "p1"] def test_load_rows_fails_closed_when_a_success_has_no_sample(tmp_path: Path) -> None: run = tmp_path / "run" (run / "results").mkdir(parents=True) row = {"row_type": "generation", "condition": "png_baseline", "prompt_id": "p1", "success": True} (run / "results/raw_rows.jsonl").write_text(json.dumps(row) + "\n", encoding="utf-8") with pytest.raises(ValueError, match="sample"): load_rows(run, ["p1"]) def test_load_rows_rejects_raw_rows_symlink(tmp_path: Path) -> None: run = tmp_path / "run" (run / "results").mkdir(parents=True) target = tmp_path / "raw_rows.jsonl" target.write_text("", encoding="utf-8") (run / "results/raw_rows.jsonl").symlink_to(target) with pytest.raises(ValueError, match="symlink"): load_rows(run, ["p1"]) def test_load_rows_rejects_raw_rows_path_escape(tmp_path: Path) -> None: run = tmp_path / "run" (run / "results").mkdir(parents=True) (run / "results/raw_rows.jsonl").symlink_to(tmp_path / "outside.jsonl") (tmp_path / "outside.jsonl").write_text("", encoding="utf-8") with pytest.raises(ValueError, match="escapes run|symlink"): load_rows(run, ["p1"]) def test_render_rejects_sample_replaced_with_symlink_after_validation(tmp_path: Path) -> None: run = tmp_path / "run" sample = run / "samples/sample.png" sample.parent.mkdir(parents=True) Image.new("RGB", (4, 4), "blue").save(sample) sample_hash = hashlib.sha256(sample.read_bytes()).hexdigest() conditions = {"png_baseline", "webp_lossless"} | { condition for codec in CODECS.values() for condition in codec[1:] } rows = [ { "row_type": "generation", "condition": condition, "prompt_id": "p1", "prompt": "test prompt", "category": "test", "success": True, "sample_path": "samples/sample.png", "sample_hash": sample_hash, "clip_score": 1.0, } for condition in sorted(conditions) ] results = run / "results" results.mkdir() (results / "raw_rows.jsonl").write_text( "".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8", ) loaded = load_rows(run, ["p1"]) outside = tmp_path / "outside.png" Image.new("RGB", (4, 4), "red").save(outside) sample.unlink() sample.symlink_to(outside) with pytest.raises(ValueError, match="symlink"): render( "jpeg_q100", CODECS["jpeg_q100"], run.resolve(), [loaded[("png_baseline", "p1")]], loaded, tmp_path / "output", ) def test_render_rejects_regular_sample_changed_after_validation(tmp_path: Path) -> None: run = tmp_path / "run" sample = run / "samples/sample.png" sample.parent.mkdir(parents=True) Image.new("RGB", (4, 4), "blue").save(sample) sample_hash = hashlib.sha256(sample.read_bytes()).hexdigest() conditions = {"png_baseline", "webp_lossless"} | { condition for codec in CODECS.values() for condition in codec[1:] } rows = [ { "row_type": "generation", "condition": condition, "prompt_id": "p1", "prompt": "test prompt", "category": "test", "success": True, "sample_path": "samples/sample.png", "sample_hash": sample_hash, "clip_score": 1.0, } for condition in sorted(conditions) ] results = run / "results" results.mkdir() (results / "raw_rows.jsonl").write_text( "".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8", ) loaded = load_rows(run, ["p1"]) Image.new("RGB", (4, 4), "red").save(sample) with pytest.raises(ValueError, match="hash mismatch"): render( "jpeg_q100", CODECS["jpeg_q100"], run.resolve(), [loaded[("png_baseline", "p1")]], loaded, tmp_path / "output", ) @pytest.mark.skipif(not hasattr(os, "mkfifo"), reason="FIFO is unavailable on this platform") def test_render_rejects_fifo_replacement_without_blocking(tmp_path: Path) -> None: run = tmp_path / "run" sample = run / "samples/sample.png" sample.parent.mkdir(parents=True) Image.new("RGB", (4, 4), "blue").save(sample) sample_hash = hashlib.sha256(sample.read_bytes()).hexdigest() conditions = {"png_baseline", "webp_lossless"} | { condition for codec in CODECS.values() for condition in codec[1:] } rows = [ { "row_type": "generation", "condition": condition, "prompt_id": "p1", "prompt": "test prompt", "category": "test", "success": True, "sample_path": "samples/sample.png", "sample_hash": sample_hash, "clip_score": 1.0, } for condition in sorted(conditions) ] results = run / "results" results.mkdir() (results / "raw_rows.jsonl").write_text( "".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8", ) loaded = load_rows(run, ["p1"]) sample.unlink() os.mkfifo(sample) with pytest.raises(ValueError, match="not a regular file"): render( "jpeg_q100", CODECS["jpeg_q100"], run.resolve(), [loaded[("png_baseline", "p1")]], loaded, tmp_path / "output", ) def test_render_contact_sheet_stacks_tables_at_full_width(tmp_path: Path) -> None: first = tmp_path / "first.png" second = tmp_path / "second.png" Image.new("RGB", (100, 50), "red").save(first) Image.new("RGB", (80, 60), "blue").save(second) output = render_contact_sheet([first, second], tmp_path / "output", gap=20) with Image.open(output) as image: assert image.size == (100, 130) assert image.getpixel((0, 0)) == (255, 0, 0) assert image.getpixel((10, 60)) == (13, 20, 27) assert image.getpixel((10, 70)) == (0, 0, 255) assert image.getpixel((10, 129)) == (0, 0, 255)