"""Shared fixtures for donor characterization tests (US01-01). Builds a small deterministic synthetic photo library in a temp dir — never the real library, never _IGNORE/ contents. Every image has a stable logical fixture ID (FX_*) so captured outputs stay keyed to identity, not to paths. """ import shutil import sqlite3 import sys from pathlib import Path import numpy as np import pytest from PIL import Image REPO = Path(__file__).resolve().parents[2] # The donors are frozen in the read-only archive (US07-01). Only this suite — and # the parity test that compares against them — puts that directory on sys.path; # production never does, which tests/unit/test_legacy_archive.py enforces. ARCHIVED_SOURCES = REPO / "legacy_cli_archive" / "src" sys.path.insert(0, str(ARCHIVED_SOURCES)) sys.path.insert(0, str(REPO)) EXIFTOOL = shutil.which("exiftool") # Stable logical fixture IDs → deterministic generation recipe (seed, size). # Structured low-frequency content (blocks + gradient) so phash survives resize. FIXTURES = { "fx-blocks-01": {"seed": 1, "size": (800, 600)}, "fx-blocks-02": {"seed": 2, "size": (800, 600)}, "fx-blocks-03": {"seed": 3, "size": (640, 480)}, "fx-blocks-04": {"seed": 4, "size": (800, 600)}, } def make_image_array(fixture_id: str) -> np.ndarray: spec = FIXTURES[fixture_id] w, h = spec["size"] rng = np.random.default_rng(spec["seed"]) base = np.zeros((h, w, 3), dtype=np.uint8) for _ in range(6): x0 = int(rng.integers(0, w - 150)) y0 = int(rng.integers(0, h - 150)) col = rng.integers(0, 256, 3) base[y0:y0 + 150, x0:x0 + 150] = col grad = np.linspace(0, 120, w, dtype=np.uint8) base[:, :, 0] = np.clip(base[:, :, 0].astype(int) + grad[None, :], 0, 255) return base def write_fixture(fixture_id: str, path: Path, quality: int = 95): path.parent.mkdir(parents=True, exist_ok=True) Image.fromarray(make_image_array(fixture_id)).save(path, quality=quality) return path @pytest.fixture def img(tmp_path): """One fx-blocks-01 JPEG in a temp dir.""" return write_fixture("fx-blocks-01", tmp_path / "fx-blocks-01.jpg") @pytest.fixture def library(tmp_path): """Temp library: root/fx-blocks-01.jpg root/album_a/fx-blocks-02.jpg root/album_a/fx-blocks-02.PNG (uppercase ext, distinct file) root/album_b/fx-blocks-03.jpg root/_IGNORE/secret.jpg (must never be discovered) root/album_b/.@__thumb/thumb.jpg (must never be discovered) root/notes.txt (unsupported) """ root = tmp_path / "lib" write_fixture("fx-blocks-01", root / "fx-blocks-01.jpg") write_fixture("fx-blocks-02", root / "album_a" / "fx-blocks-02.jpg") img = Image.fromarray(make_image_array("fx-blocks-02")) (root / "album_a").mkdir(parents=True, exist_ok=True) img.save(root / "album_a" / "fx-blocks-02.PNG") write_fixture("fx-blocks-03", root / "album_b" / "fx-blocks-03.jpg") write_fixture("fx-blocks-04", root / "_IGNORE" / "secret.jpg") write_fixture("fx-blocks-04", root / "album_b" / ".@__thumb" / "thumb.jpg") (root / "notes.txt").write_text("not an image") return root @pytest.fixture def db(tmp_path): import photo_analyzer as pa conn = pa.get_db(str(tmp_path / "test.db")) yield conn conn.close() def seed_analyzed(conn: sqlite3.Connection, path: str, **overrides): """Insert a row and mark it analyzed with a deterministic result.""" import photo_analyzer as pa result = { "description": "A red block pattern.", "tags": ["blocks", "test"], "people_count": 0, "setting": "indoor", "time_of_day": "unknown", "season": "unknown", "mood": "calm", "location_hint": None, "approx_year": None, } result.update(overrides) pa.upsert_pending(conn, path) pa.mark_analyzed(conn, path, result, raw="{}") return result