Spaces:
Running on Zero
Running on Zero
| """ | |
| 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"]) | |