bureaucat / tests /test_multi_image.py
ravinsingh15's picture
Bureaucat — Build Small Hackathon submission (Qwen3-VL-8B, ZeroGPU, gr.Server)
6b5e47d
Raw
History Blame Contribute Delete
8.85 kB
"""
Unit tests for run_inference_multi, run_inference delegation, and decode gr.File dispatch.
These tests NEVER load model weights — BUREAUCAT_NO_MODEL=1 is set before importing app.
Test coverage:
(a) app.run_inference_multi is callable and accepts a list arg (inspect signature has
'images' as first param)
(b) run_inference delegates — patch app.run_inference_multi with a stub and assert
run_inference(img, ...) forwards [img]
(c) gr.File payload dispatch in decode: skips None paths, truncates a 7-file list to
MAX_PAGES_HARD, and passes exactly MAX_PAGES_HARD images through unchanged.
(Updated from Gallery payload to gr.File list[str] payload in 03-02.)
"""
import inspect
import io
import os
import sys
import tempfile
from unittest.mock import patch, MagicMock
from PIL import Image as PILImage
# Set escape hatch BEFORE importing app so model weights are never downloaded.
os.environ["BUREAUCAT_NO_MODEL"] = "1"
# Ensure project root is on the path when running from tests/
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import app
from app import StructuredResult
def _stub_result(raw="stub"):
"""Build a minimal StructuredResult for use as a stub return value."""
return StructuredResult(
transcription="", quip="", tldr="", why="",
actions="", deadlines="", severity=None, raw=raw,
)
def _make_tmp_png() -> str:
"""
Write a 1×1 white PNG to a NamedTemporaryFile and return its path.
The file is not auto-deleted (delete=False) so decode() can open it.
Callers are responsible for cleanup; for test isolation use teardown or tmp_path.
"""
img = PILImage.new("RGB", (1, 1), color=(255, 255, 255))
buf = io.BytesIO()
img.save(buf, "PNG")
buf.seek(0)
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as fh:
fh.write(buf.read())
return fh.name
# ---------------------------------------------------------------------------
# (a) run_inference_multi signature
# ---------------------------------------------------------------------------
def test_run_inference_multi_exists():
"""app.run_inference_multi should exist as a callable."""
assert hasattr(app, "run_inference_multi"), "app.run_inference_multi not found"
assert callable(app.run_inference_multi), "app.run_inference_multi is not callable"
def test_run_inference_multi_first_param_is_images():
"""First parameter of run_inference_multi should be named 'images'."""
sig = inspect.signature(app.run_inference_multi)
params = list(sig.parameters.keys())
assert params[0] == "images", (
f"Expected first param 'images', got '{params[0]}'"
)
def test_run_inference_multi_accepts_list_type_hint_or_annotation():
"""run_inference_multi signature has at least 6 parameters (images, language,
beginner_mode, mdl, proc, image_patch_size)."""
sig = inspect.signature(app.run_inference_multi)
assert len(sig.parameters) >= 6, (
f"Expected at least 6 params, got {len(sig.parameters)}: {list(sig.parameters)}"
)
# ---------------------------------------------------------------------------
# (b) run_inference delegates to run_inference_multi with a single-element list
# ---------------------------------------------------------------------------
def test_run_inference_delegates_to_multi():
"""run_inference(img, ...) should call run_inference_multi([img], ...) exactly once."""
fake_img = MagicMock(name="PIL.Image")
stub_ret = _stub_result()
captured_calls = []
def stub_multi(images, language, beginner_mode, mdl, proc, image_patch_size):
captured_calls.append({
"images": images,
"language": language,
"beginner_mode": beginner_mode,
})
return stub_ret
with patch.object(app, "run_inference_multi", stub_multi):
result = app.run_inference(
fake_img, "English", False, None, None, 16
)
assert len(captured_calls) == 1, "run_inference_multi should be called exactly once"
assert captured_calls[0]["images"] == [fake_img], (
f"Expected [fake_img], got {captured_calls[0]['images']}"
)
assert result is stub_ret
def test_run_inference_forwards_language_and_beginner_mode():
"""run_inference correctly forwards language and beginner_mode parameters."""
fake_img = MagicMock(name="PIL.Image")
stub_ret = _stub_result()
captured_calls = []
def stub_multi(images, language, beginner_mode, mdl, proc, image_patch_size):
captured_calls.append({"language": language, "beginner_mode": beginner_mode})
return stub_ret
with patch.object(app, "run_inference_multi", stub_multi):
app.run_inference(fake_img, "Swedish", True, None, None, 14)
assert captured_calls[0]["language"] == "Swedish"
assert captured_calls[0]["beginner_mode"] is True
# ---------------------------------------------------------------------------
# (c) decode gr.File payload dispatch and MAX_PAGES_HARD truncation
# Updated in 03-02: decode() now accepts list[str] file paths (gr.File type="filepath")
# instead of the old Gallery list[(PIL.Image, caption)] tuples.
# ---------------------------------------------------------------------------
def test_page_cap_constants_exist():
"""MAX_PAGES_SOFT and MAX_PAGES_HARD constants must exist."""
assert hasattr(app, "MAX_PAGES_SOFT"), "app.MAX_PAGES_SOFT not found"
assert hasattr(app, "MAX_PAGES_HARD"), "app.MAX_PAGES_HARD not found"
assert app.MAX_PAGES_SOFT == 3, f"Expected MAX_PAGES_SOFT=3, got {app.MAX_PAGES_SOFT}"
assert app.MAX_PAGES_HARD == 5, f"Expected MAX_PAGES_HARD=5, got {app.MAX_PAGES_HARD}"
def test_decode_empty_gallery_returns_error_sentinel():
"""decode([]) returns a StructuredResult with error sentinel in .raw."""
result = app.decode([], "English", False)
assert isinstance(result, StructuredResult)
assert "upload" in result.raw.lower() or "please" in result.raw.lower(), (
f"Expected upload error sentinel, got: {result.raw!r}"
)
def test_decode_none_gallery_returns_error_sentinel():
"""decode(None) returns a StructuredResult error sentinel."""
result = app.decode(None, "English", False)
assert isinstance(result, StructuredResult)
assert result.raw, "Expected non-empty raw in error sentinel"
def test_decode_file_list_with_none_paths_filters_them():
"""decode skips None entries in the gr.File list[str] payload."""
# Create 2 real temp PNG files and pass [path1, None, path2] — None is skipped.
path1 = _make_tmp_png()
path2 = _make_tmp_png()
file_payload = [path1, None, path2]
captured_images = []
stub_ret = _stub_result()
def stub_multi(images, language, beginner_mode, mdl, proc, image_patch_size):
captured_images.extend(images)
return stub_ret
try:
with patch.object(app, "run_inference_multi", stub_multi):
result = app.decode(file_payload, "English", False)
assert len(captured_images) == 2, (
f"Expected 2 non-None images, got {len(captured_images)}"
)
assert result is stub_ret
finally:
os.unlink(path1)
os.unlink(path2)
def test_decode_truncates_at_max_pages_hard():
"""decode truncates a 7-file list to MAX_PAGES_HARD (5) images."""
# Create 7 real temp PNG files — all valid images.
paths = [_make_tmp_png() for _ in range(7)]
captured_images = []
stub_ret = _stub_result()
def stub_multi(images, language, beginner_mode, mdl, proc, image_patch_size):
captured_images.extend(images)
return stub_ret
try:
with patch.object(app, "run_inference_multi", stub_multi):
result = app.decode(paths, "English", False)
assert len(captured_images) == app.MAX_PAGES_HARD, (
f"Expected {app.MAX_PAGES_HARD} images after truncation, got {len(captured_images)}"
)
assert result is stub_ret
finally:
for p in paths:
os.unlink(p)
def test_decode_exactly_max_pages_hard_passes_through():
"""decode allows exactly MAX_PAGES_HARD files without truncation."""
paths = [_make_tmp_png() for _ in range(app.MAX_PAGES_HARD)]
captured_images = []
stub_ret = _stub_result()
def stub_multi(images, language, beginner_mode, mdl, proc, image_patch_size):
captured_images.extend(images)
return stub_ret
try:
with patch.object(app, "run_inference_multi", stub_multi):
app.decode(paths, "English", False)
assert len(captured_images) == app.MAX_PAGES_HARD
finally:
for p in paths:
os.unlink(p)
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])