113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
"""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
|