| """ |
| tests/test_gallery.py — Zero-GPU gallery loader tests (Plan 02-04, UI-01). |
| |
| Verifies: |
| (a) load_example returns a 7-element tuple matching render_result's output arity |
| and does NOT raise. |
| (b) load_example is NOT wrapped by @spaces.GPU and never calls the model — |
| run_inference_multi is patched to raise; load_example must still succeed. |
| (c) data/gallery/*-result.json fixtures round-trip into StructuredResult correctly |
| (severity is int or None, all fields present). |
| |
| BUREAUCAT_NO_MODEL=1 is set BEFORE import to prevent model weight loading. |
| """ |
|
|
| import json |
| import os |
| import sys |
| import types |
| import unittest |
| from pathlib import Path |
| from unittest.mock import patch |
|
|
| |
| os.environ["BUREAUCAT_NO_MODEL"] = "1" |
|
|
| |
| _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| if _REPO_ROOT not in sys.path: |
| sys.path.insert(0, _REPO_ROOT) |
|
|
| import app |
| from app import EXAMPLE_LETTERS, StructuredResult, load_example |
|
|
|
|
| |
| |
| |
|
|
| _GALLERY_DIR = Path(_REPO_ROOT) / "data" / "gallery" |
|
|
| _STRUCTURED_RESULT_FIELDS = frozenset( |
| StructuredResult.__dataclass_fields__.keys() |
| ) |
|
|
| _RENDER_RESULT_ARITY = 7 |
|
|
|
|
| def _make_select_event(index: int): |
| """Return a SimpleNamespace that looks like gr.SelectData(index=N).""" |
| return types.SimpleNamespace(index=index) |
|
|
|
|
| |
| |
| |
|
|
| class TestExampleLettersList(unittest.TestCase): |
| """EXAMPLE_LETTERS has >= 4 entries with required keys.""" |
|
|
| def test_length(self): |
| self.assertGreaterEqual(len(EXAMPLE_LETTERS), 4, "need >= 4 gallery entries") |
|
|
| def test_keys(self): |
| for entry in EXAMPLE_LETTERS: |
| for key in ("slug", "label", "image", "cached"): |
| self.assertIn(key, entry, f"missing key '{key}' in entry {entry}") |
|
|
| def test_image_paths_under_public(self): |
| for entry in EXAMPLE_LETTERS: |
| self.assertIn( |
| "data/letters/public/", |
| entry["image"], |
| f"image path should be under data/letters/public/: {entry['image']}", |
| ) |
|
|
| def test_cached_paths_under_gallery(self): |
| for entry in EXAMPLE_LETTERS: |
| self.assertIn( |
| "data/gallery/", |
| entry["cached"], |
| f"cached path should be under data/gallery/: {entry['cached']}", |
| ) |
|
|
|
|
| class TestLoadExampleArity(unittest.TestCase): |
| """load_example returns correct arity without raising.""" |
|
|
| def setUp(self): |
| """Skip if gallery JSONs don't exist yet (generated by Task 1).""" |
| json_path = Path(_REPO_ROOT) / EXAMPLE_LETTERS[0]["cached"] |
| if not json_path.exists(): |
| self.skipTest(f"Gallery JSON not yet generated: {json_path}") |
|
|
| def test_returns_tuple_of_correct_arity(self): |
| """load_example(index=0) returns a 7-element tuple.""" |
| evt = _make_select_event(0) |
| result_tuple = load_example(evt) |
| self.assertIsInstance(result_tuple, tuple) |
| self.assertEqual( |
| len(result_tuple), |
| _RENDER_RESULT_ARITY, |
| f"expected {_RENDER_RESULT_ARITY}-element tuple, got {len(result_tuple)}", |
| ) |
|
|
| def test_all_indices(self): |
| """load_example works for every valid index in EXAMPLE_LETTERS.""" |
| for i, entry in enumerate(EXAMPLE_LETTERS): |
| json_path = Path(_REPO_ROOT) / entry["cached"] |
| if not json_path.exists(): |
| continue |
| with self.subTest(index=i, slug=entry["slug"]): |
| evt = _make_select_event(i) |
| tpl = load_example(evt) |
| self.assertEqual(len(tpl), _RENDER_RESULT_ARITY) |
|
|
|
|
| class TestLoadExampleNoModelCall(unittest.TestCase): |
| """load_example never calls run_inference_multi — zero GPU guarantee.""" |
|
|
| def setUp(self): |
| json_path = Path(_REPO_ROOT) / EXAMPLE_LETTERS[0]["cached"] |
| if not json_path.exists(): |
| self.skipTest(f"Gallery JSON not yet generated: {json_path}") |
|
|
| def test_no_model_call_when_inference_patched_to_raise(self): |
| """ |
| Even if run_inference_multi would raise, load_example still succeeds. |
| This proves load_example reads only disk (never calls the model). |
| """ |
| def _boom(*args, **kwargs): |
| raise RuntimeError("load_example must NOT call run_inference_multi") |
|
|
| with patch.object(app, "run_inference_multi", side_effect=_boom): |
| evt = _make_select_event(0) |
| |
| result_tuple = load_example(evt) |
| self.assertEqual(len(result_tuple), _RENDER_RESULT_ARITY) |
|
|
| def test_load_example_not_spaces_gpu_wrapped(self): |
| """ |
| load_example must not be decorated with @spaces.GPU. |
| Presence of __wrapped__ or _spaces_fn_wrapped attributes indicates GPU decoration. |
| """ |
| self.assertFalse( |
| hasattr(load_example, "__wrapped__"), |
| "load_example should NOT be wrapped with @spaces.GPU (__wrapped__ found)", |
| ) |
| self.assertFalse( |
| hasattr(load_example, "_spaces_fn_wrapped"), |
| "load_example should NOT be wrapped with @spaces.GPU (_spaces_fn_wrapped found)", |
| ) |
|
|
|
|
| class TestGalleryJsonFixtures(unittest.TestCase): |
| """data/gallery/*-result.json files round-trip into StructuredResult.""" |
|
|
| def _iter_gallery_jsons(self): |
| """Yield (stem, dict) for each existing gallery JSON.""" |
| for entry in EXAMPLE_LETTERS: |
| json_path = Path(_REPO_ROOT) / entry["cached"] |
| if json_path.exists(): |
| yield entry["slug"], json.loads(json_path.read_text(encoding="utf-8")) |
|
|
| def test_all_fields_present(self): |
| found_any = False |
| for slug, data in self._iter_gallery_jsons(): |
| found_any = True |
| with self.subTest(slug=slug): |
| missing = _STRUCTURED_RESULT_FIELDS - set(data.keys()) |
| self.assertEqual( |
| missing, |
| set(), |
| f"{slug}: missing StructuredResult fields: {missing}", |
| ) |
|
|
| if not found_any: |
| self.skipTest("No gallery JSONs exist yet — run Task 1 first") |
|
|
| def test_severity_type(self): |
| found_any = False |
| for slug, data in self._iter_gallery_jsons(): |
| found_any = True |
| with self.subTest(slug=slug): |
| sev = data.get("severity") |
| self.assertTrue( |
| sev is None or isinstance(sev, int), |
| f"{slug}: severity must be int or None, got {type(sev).__name__}", |
| ) |
|
|
| if not found_any: |
| self.skipTest("No gallery JSONs exist yet — run Task 1 first") |
|
|
| def test_no_quip_prefix(self): |
| """quip field must NOT contain 'Bureaucat says:' prefix (prevents double-prefix).""" |
| found_any = False |
| for slug, data in self._iter_gallery_jsons(): |
| found_any = True |
| with self.subTest(slug=slug): |
| quip = data.get("quip", "") |
| self.assertNotIn( |
| "Bureaucat says:", |
| quip, |
| f"{slug}: quip contains 'Bureaucat says:' prefix — would double-prefix in UI", |
| ) |
|
|
| if not found_any: |
| self.skipTest("No gallery JSONs exist yet — run Task 1 first") |
|
|
| def test_structured_result_roundtrip(self): |
| """StructuredResult(**data) does not raise for any gallery JSON.""" |
| found_any = False |
| for slug, data in self._iter_gallery_jsons(): |
| found_any = True |
| with self.subTest(slug=slug): |
| |
| result = StructuredResult(**data) |
| self.assertIsInstance(result, StructuredResult) |
| self.assertIsNotNone(result.raw, f"{slug}: raw field is None") |
|
|
| if not found_any: |
| self.skipTest("No gallery JSONs exist yet — run Task 1 first") |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main() |
|
|