| """ |
| 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 |
|
|
| |
| os.environ["BUREAUCAT_NO_MODEL"] = "1" |
|
|
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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)}" |
| ) |
|
|
|
|
| |
| |
| |
|
|
| 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 |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| 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.""" |
| |
| 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.""" |
| |
| 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"]) |
|
|