Files
FabledCurator/tests/test_backfill_thumbnails.py
T
bvandeusen 43b778aa04
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 21s
CI / frontend-build (push) Successful in 22s
CI / intimp (push) Successful in 3m41s
CI / intapi (push) Successful in 7m45s
CI / intcore (push) Successful in 8m11s
fix(test): include 'scanned' in all backfill result assertions
Same 'change shared shape, miss pinned tests' trap as cfa4fb4 — the
prior commit updated only test_backfill_mixed_aggregate; three sibling
tests in the same file also pinned the old {enqueued,ok,regenerated}
shape without 'scanned' and CI bounced.
2026-06-01 22:09:02 -04:00

257 lines
8.0 KiB
Python

"""Thumbnail backfill: _thumb_is_valid helper + backfill_thumbnails planner."""
from pathlib import Path
import pytest
from backend.app.models import ImageRecord
from backend.app.tasks.thumbnail import _thumb_is_valid
pytestmark = pytest.mark.integration
def test_thumb_is_valid_jpeg(tmp_path):
p = tmp_path / "good.jpg"
# Real thumbnails are at least ~2KB; size check (MIN_THUMB_BYTES=256)
# requires the file body be plausible. 300 bytes here clears the
# floor with margin.
p.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 300)
assert _thumb_is_valid(p) is True
def test_thumb_is_valid_png(tmp_path):
p = tmp_path / "good.png"
p.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 300)
assert _thumb_is_valid(p) is True
def test_thumb_is_valid_too_short(tmp_path):
p = tmp_path / "tiny"
p.write_bytes(b"\xff\xd8")
assert _thumb_is_valid(p) is False
def test_thumb_is_valid_wrong_magic(tmp_path):
p = tmp_path / "garbage"
p.write_bytes(b"\x00" * 12)
assert _thumb_is_valid(p) is False
def test_thumb_is_valid_missing_file(tmp_path):
assert _thumb_is_valid(tmp_path / "nope") is False
def test_thumb_is_valid_header_only_below_min_size(tmp_path):
"""Operator-flagged 2026-06-01: header-only corrupt files were
silently passing the magic-byte check and backfill counted them as
`ok`, so the UI's broken-image tiles never got regenerated. Files
smaller than MIN_THUMB_BYTES are now invalid even with valid magic."""
p = tmp_path / "header_only.jpg"
p.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 50) # 54 bytes total
assert _thumb_is_valid(p) is False
# --- backfill_thumbnails planner tests ------------------------------------
class _Ctx:
def __init__(self, s):
self.s = s
def __enter__(self):
return self.s
def __exit__(self, *a):
return False
def _sf(db_sync):
"""sessionmaker-like returning the test's bound session, matching the
pattern in tests/test_backfill_phash.py."""
class _SM:
def __call__(self):
return _Ctx(db_sync)
return _SM()
def _sha(prefix: str) -> str:
return f"{prefix}".ljust(64, "0")[:64]
def _rec(db_sync, path, *, sha, thumb_path=None, mime="image/jpeg"):
rec = ImageRecord(
path=str(path), sha256=sha, size_bytes=1, mime=mime,
width=64, height=64, origin="imported_filesystem",
integrity_status="unknown",
thumbnail_path=str(thumb_path) if thumb_path is not None else None,
)
db_sync.add(rec)
db_sync.flush()
return rec
def _write_jpeg(p: Path) -> Path:
p.parent.mkdir(parents=True, exist_ok=True)
# ≥ MIN_THUMB_BYTES (256) so the size floor doesn't reject it.
p.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 300)
return p
def _write_png(p: Path) -> Path:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 300)
return p
def _write_garbage(p: Path) -> Path:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(b"\x00" * 12)
return p
def test_backfill_null_path_enqueued(db_sync, tmp_path, monkeypatch):
from backend.app.tasks import thumbnail as m
src = tmp_path / "a.bin"
src.write_bytes(b"x")
rec = _rec(db_sync, src, sha=_sha("a"), thumb_path=None)
db_sync.commit()
monkeypatch.setattr(m, "_sync_session_factory", lambda: _sf(db_sync))
delayed: list[int] = []
monkeypatch.setattr(
m.generate_thumbnail, "delay", lambda image_id: delayed.append(image_id)
)
result = m.backfill_thumbnails()
assert result == {"scanned": 1, "enqueued": 1, "ok": 0, "regenerated": 0}
assert delayed == [rec.id]
def test_backfill_missing_file_clears_and_enqueues(db_sync, tmp_path, monkeypatch):
from backend.app.tasks import thumbnail as m
src = tmp_path / "b.bin"
src.write_bytes(b"x")
rec = _rec(
db_sync, src, sha=_sha("b"),
thumb_path=tmp_path / "thumbs" / "missing.jpg",
)
db_sync.commit()
monkeypatch.setattr(m, "_sync_session_factory", lambda: _sf(db_sync))
delayed: list[int] = []
monkeypatch.setattr(
m.generate_thumbnail, "delay", lambda image_id: delayed.append(image_id)
)
result = m.backfill_thumbnails()
db_sync.expire_all()
assert result == {"scanned": 1, "enqueued": 1, "ok": 0, "regenerated": 1}
assert delayed == [rec.id]
assert db_sync.get(ImageRecord, rec.id).thumbnail_path is None
def test_backfill_valid_jpeg_skipped(db_sync, tmp_path, monkeypatch):
from backend.app.tasks import thumbnail as m
src = tmp_path / "c.bin"
src.write_bytes(b"x")
thumb = _write_jpeg(tmp_path / "thumbs" / "c.jpg")
rec = _rec(db_sync, src, sha=_sha("c"), thumb_path=thumb)
db_sync.commit()
monkeypatch.setattr(m, "_sync_session_factory", lambda: _sf(db_sync))
delayed: list[int] = []
monkeypatch.setattr(
m.generate_thumbnail, "delay", lambda image_id: delayed.append(image_id)
)
result = m.backfill_thumbnails()
db_sync.expire_all()
assert result == {"scanned": 1, "enqueued": 0, "ok": 1, "regenerated": 0}
assert delayed == []
assert db_sync.get(ImageRecord, rec.id).thumbnail_path == str(thumb)
def test_backfill_valid_png_skipped(db_sync, tmp_path, monkeypatch):
from backend.app.tasks import thumbnail as m
src = tmp_path / "d.bin"
src.write_bytes(b"x")
thumb = _write_png(tmp_path / "thumbs" / "d.png")
_rec(db_sync, src, sha=_sha("d"), thumb_path=thumb)
db_sync.commit()
monkeypatch.setattr(m, "_sync_session_factory", lambda: _sf(db_sync))
delayed: list[int] = []
monkeypatch.setattr(
m.generate_thumbnail, "delay", lambda image_id: delayed.append(image_id)
)
result = m.backfill_thumbnails()
assert result == {"scanned": 1, "enqueued": 0, "ok": 1, "regenerated": 0}
assert delayed == []
def test_backfill_corrupt_magic_clears_and_enqueues(db_sync, tmp_path, monkeypatch):
from backend.app.tasks import thumbnail as m
src = tmp_path / "e.bin"
src.write_bytes(b"x")
thumb = _write_garbage(tmp_path / "thumbs" / "e.jpg")
rec = _rec(db_sync, src, sha=_sha("e"), thumb_path=thumb)
db_sync.commit()
monkeypatch.setattr(m, "_sync_session_factory", lambda: _sf(db_sync))
delayed: list[int] = []
monkeypatch.setattr(
m.generate_thumbnail, "delay", lambda image_id: delayed.append(image_id)
)
result = m.backfill_thumbnails()
db_sync.expire_all()
assert result == {"scanned": 1, "enqueued": 1, "ok": 0, "regenerated": 1}
assert delayed == [rec.id]
assert db_sync.get(ImageRecord, rec.id).thumbnail_path is None
def test_backfill_mixed_aggregate(db_sync, tmp_path, monkeypatch):
from backend.app.tasks import thumbnail as m
src_null = tmp_path / "src_null.bin"
src_null.write_bytes(b"x")
src_jpeg = tmp_path / "src_jpeg.bin"
src_jpeg.write_bytes(b"x")
src_png = tmp_path / "src_png.bin"
src_png.write_bytes(b"x")
src_missing = tmp_path / "src_missing.bin"
src_missing.write_bytes(b"x")
src_bad = tmp_path / "src_bad.bin"
src_bad.write_bytes(b"x")
jpeg = _write_jpeg(tmp_path / "thumbs" / "ok.jpg")
png = _write_png(tmp_path / "thumbs" / "ok.png")
bad = _write_garbage(tmp_path / "thumbs" / "bad.jpg")
r_null = _rec(db_sync, src_null, sha=_sha("aa"), thumb_path=None)
_rec(db_sync, src_jpeg, sha=_sha("bb"), thumb_path=jpeg)
_rec(db_sync, src_png, sha=_sha("cc"), thumb_path=png)
r_missing = _rec(
db_sync, src_missing, sha=_sha("dd"),
thumb_path=tmp_path / "thumbs" / "missing.jpg",
)
r_bad = _rec(db_sync, src_bad, sha=_sha("ee"), thumb_path=bad)
db_sync.commit()
monkeypatch.setattr(m, "_sync_session_factory", lambda: _sf(db_sync))
delayed: list[int] = []
monkeypatch.setattr(
m.generate_thumbnail, "delay", lambda image_id: delayed.append(image_id)
)
result = m.backfill_thumbnails()
assert result == {"scanned": 5, "enqueued": 3, "ok": 2, "regenerated": 2}
assert sorted(delayed) == sorted([r_null.id, r_missing.id, r_bad.id])