Spaces:
Runtime error
Runtime error
| """L2 mocked tests for modes.py — no GPU, no torch, no diffusers required.""" | |
| from __future__ import annotations | |
| import sys | |
| from typing import Any | |
| from unittest.mock import MagicMock | |
| import pytest | |
| from PIL import Image | |
| import models | |
| import modes | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| _DUMMY_IMAGE = Image.new("RGB", (8, 8)) | |
| def _pipe_result() -> MagicMock: | |
| """A fake pipeline output whose .images[0] is a small PIL image.""" | |
| result = MagicMock() | |
| result.images = [Image.new("RGB", (8, 8))] | |
| return result | |
| def _make_params( | |
| *, | |
| speed: str = "Fast", | |
| steps: int = 4, | |
| true_cfg: float = 1.0, | |
| seed: int = 42, | |
| images: list[Any] | None = None, | |
| ) -> dict[str, Any]: | |
| return { | |
| "prompt": "make it raining", | |
| "images": images if images is not None else [_DUMMY_IMAGE], | |
| "speed": speed, | |
| "steps": steps, | |
| "true_cfg": true_cfg, | |
| "negative_prompt": " ", | |
| "seed": seed, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Fixtures | |
| # --------------------------------------------------------------------------- | |
| def fake_pipe() -> MagicMock: | |
| """A MagicMock pipeline with all attributes needed by modes._run.""" | |
| pipe = MagicMock() | |
| pipe.device = "cpu" | |
| pipe.return_value = _pipe_result() | |
| pipe._qie_lightning_scheduler = MagicMock() | |
| pipe._qie_default_scheduler = MagicMock() | |
| return pipe | |
| def _patch_fit_dimensions(monkeypatch: pytest.MonkeyPatch) -> None: | |
| """Patch models.fit_dimensions to return (64, 64) so no real image maths runs.""" | |
| monkeypatch.setattr(models, "fit_dimensions", lambda img: (64, 64)) | |
| def _patch_torch(monkeypatch: pytest.MonkeyPatch) -> None: | |
| """Inject a stub torch module so the lazy 'import torch' in _run succeeds in CI.""" | |
| mock_torch = MagicMock() | |
| gen = MagicMock() | |
| mock_torch.Generator.return_value = gen | |
| gen.manual_seed.return_value = gen | |
| monkeypatch.setitem(sys.modules, "torch", mock_torch) | |
| # --------------------------------------------------------------------------- | |
| # call_edit | |
| # --------------------------------------------------------------------------- | |
| def test_edit_passes_target_image_in_image_kwarg(fake_pipe: MagicMock) -> None: | |
| """call_edit must forward exactly [target] in the image= positional kwarg.""" | |
| target = Image.new("RGB", (16, 16)) | |
| params = _make_params(images=[target]) | |
| modes.call_edit(fake_pipe, params) | |
| _, call_kwargs = fake_pipe.call_args | |
| assert call_kwargs["image"] == [target] | |
| def test_edit_sets_mode_in_meta(fake_pipe: MagicMock) -> None: | |
| """call_edit must record mode='edit' in the returned metadata.""" | |
| _, meta = modes.call_edit(fake_pipe, _make_params()) | |
| assert meta["mode"] == "edit" | |
| # --------------------------------------------------------------------------- | |
| # call_compose | |
| # --------------------------------------------------------------------------- | |
| def test_compose_drops_none_images(fake_pipe: MagicMock) -> None: | |
| """Compose with [target, None, ref2] must call pipe with image=[target, ref2].""" | |
| target = Image.new("RGB", (8, 8)) | |
| ref2 = Image.new("RGB", (8, 8)) | |
| params = _make_params(images=[target, None, ref2]) | |
| modes.call_compose(fake_pipe, params) | |
| _, call_kwargs = fake_pipe.call_args | |
| assert call_kwargs["image"] == [target, ref2] | |
| assert len(call_kwargs["image"]) == 2 | |
| def test_compose_sets_mode_in_meta(fake_pipe: MagicMock) -> None: | |
| """call_compose must record mode='compose' in the returned metadata.""" | |
| _, meta = modes.call_compose(fake_pipe, _make_params()) | |
| assert meta["mode"] == "compose" | |
| # --------------------------------------------------------------------------- | |
| # Speed modes | |
| # --------------------------------------------------------------------------- | |
| def test_fast_speed_calls_set_adapters(fake_pipe: MagicMock) -> None: | |
| """Fast speed must call pipe.set_adapters with the Lightning adapter at weight 1.0.""" | |
| modes.call_edit(fake_pipe, _make_params(speed="Fast", steps=4, true_cfg=1.0)) | |
| fake_pipe.set_adapters.assert_called_once_with([models.LORA_ADAPTER_NAME], [1.0]) | |
| def test_fast_speed_pipe_call_kwargs(fake_pipe: MagicMock) -> None: | |
| """Fast speed must pass num_inference_steps=4 and true_cfg_scale=1.0 to pipe.""" | |
| modes.call_edit(fake_pipe, _make_params(speed="Fast", steps=4, true_cfg=1.0)) | |
| _, call_kwargs = fake_pipe.call_args | |
| assert call_kwargs["num_inference_steps"] == 4 | |
| assert call_kwargs["true_cfg_scale"] == 1.0 | |
| def test_fast_speed_uses_lightning_scheduler(fake_pipe: MagicMock) -> None: | |
| """Fast speed must swap in the Lightning scheduler before the pipe call.""" | |
| modes.call_edit(fake_pipe, _make_params(speed="Fast")) | |
| assert fake_pipe.scheduler is fake_pipe._qie_lightning_scheduler | |
| def _call_order(pipe: MagicMock, *names: str) -> list[str]: | |
| """Ordered list of the named methods as actually invoked on the pipe (for sequencing).""" | |
| return [c[0] for c in pipe.mock_calls if c[0] in names] | |
| def test_fast_speed_enables_lora_before_set_adapters(fake_pipe: MagicMock) -> None: | |
| """Regression: set_adapters() only flips the active adapter NAME — it does NOT re-enable | |
| layers a prior Quality disable_lora() turned off. Fast must call enable_lora() FIRST or | |
| Lightning is silently bypassed on the long-lived MPS process (each request reuses one pipe).""" | |
| modes.call_edit(fake_pipe, _make_params(speed="Fast", steps=4, true_cfg=1.0)) | |
| order = _call_order(fake_pipe, "enable_lora", "set_adapters") | |
| assert order[: 2] == ["enable_lora", "set_adapters"], f"enable_lora must precede set_adapters; got {order}" | |
| def test_quality_speed_calls_disable_lora(fake_pipe: MagicMock) -> None: | |
| """Quality speed must call pipe.disable_lora() to deactivate the Lightning adapter.""" | |
| modes.call_edit(fake_pipe, _make_params(speed="Quality", steps=40, true_cfg=4.0)) | |
| fake_pipe.disable_lora.assert_called_once() | |
| def test_quality_speed_pipe_call_kwargs(fake_pipe: MagicMock) -> None: | |
| """Quality speed must pass num_inference_steps=40 and true_cfg_scale=4.0 to pipe.""" | |
| modes.call_edit(fake_pipe, _make_params(speed="Quality", steps=40, true_cfg=4.0)) | |
| _, call_kwargs = fake_pipe.call_args | |
| assert call_kwargs["num_inference_steps"] == 40 | |
| assert call_kwargs["true_cfg_scale"] == 4.0 | |
| def test_quality_speed_uses_default_scheduler(fake_pipe: MagicMock) -> None: | |
| """Quality speed must restore the default scheduler before the pipe call.""" | |
| modes.call_edit(fake_pipe, _make_params(speed="Quality")) | |
| assert fake_pipe.scheduler is fake_pipe._qie_default_scheduler | |
| # --------------------------------------------------------------------------- | |
| # GPU-path user LoRA (device="mps" → the budgeted GPU branch, where LoRA applies) | |
| # --------------------------------------------------------------------------- | |
| def _mock_gpu_memory(monkeypatch: pytest.MonkeyPatch, *, speed: str = "Quality") -> None: | |
| """Stub memory.* so modes._run's GPU branch runs without real budgeting, and make the | |
| torch stub's mps allocator return a real number.""" | |
| import memory | |
| plan = { | |
| "refused": False, "width": 1024, "height": 1024, "steps": 8, "true_cfg": 4.0, | |
| "speed": speed, "n_ref": 0, "degrades": [], "note": "ok", "budget_gb": 90.0, "need_gb": 60.0, | |
| } | |
| monkeypatch.setattr(memory, "plan_request", lambda *a, **k: dict(plan)) | |
| monkeypatch.setattr(memory, "record_peak", lambda *a, **k: None) | |
| monkeypatch.setattr(memory, "penalize", lambda *a, **k: None) | |
| monkeypatch.setattr(memory, "activation_budget_gb", lambda *a, **k: 90.0) | |
| sys.modules["torch"].mps.driver_allocated_memory.return_value = 60 * 1024**3 | |
| def test_gpu_quality_user_lora_loads_sets_and_cleans_up(fake_pipe: MagicMock, monkeypatch) -> None: | |
| """Quality + user LoRA on the GPU path: load 'user', activate it, surface it in meta, and | |
| ALWAYS delete it afterwards (mandatory MPS cleanup — no per-call re-fork).""" | |
| fake_pipe.device = "mps" | |
| _mock_gpu_memory(monkeypatch, speed="Quality") | |
| p = _make_params(speed="Quality", steps=8, true_cfg=4.0) | |
| p["lora_path"] = "/tmp/foo.safetensors" | |
| p["lora_weight"] = 0.8 | |
| _, meta = modes.call_edit(fake_pipe, p) | |
| fake_pipe.load_lora_weights.assert_called_once_with("/tmp/foo.safetensors", adapter_name="user") | |
| assert any(c.args == (["user"], [0.8]) for c in fake_pipe.set_adapters.call_args_list) | |
| fake_pipe.delete_adapters.assert_any_call("user") # cleanup ran | |
| assert meta.get("lora", {}).get("weight") == 0.8 | |
| # The activate sequence must be enable_lora() THEN set_adapters(["user"], ...): Quality's | |
| # disable_lora() left the layers off, and set_adapters alone wouldn't re-enable them — | |
| # without enable_lora() the LoRA loads but has ZERO effect (output identical to no-LoRA). | |
| seq = [(c[0], c[1]) for c in fake_pipe.mock_calls if c[0] in ("enable_lora", "set_adapters")] | |
| user_idx = next(i for i, (n, a) in enumerate(seq) if n == "set_adapters" and a == (["user"], [0.8])) | |
| assert any(n == "enable_lora" for n, _ in seq[:user_idx]), f"enable_lora must precede user set_adapters; got {seq}" | |
| def test_gpu_quality_without_lora_touches_no_user_adapter(fake_pipe: MagicMock, monkeypatch) -> None: | |
| """No LoRA requested → never load or delete a 'user' adapter; meta has no 'lora'.""" | |
| fake_pipe.device = "mps" | |
| _mock_gpu_memory(monkeypatch, speed="Quality") | |
| _, meta = modes.call_edit(fake_pipe, _make_params(speed="Quality", steps=8, true_cfg=4.0)) | |
| fake_pipe.load_lora_weights.assert_not_called() | |
| fake_pipe.delete_adapters.assert_not_called() | |
| assert "lora" not in meta | |
| def test_gpu_fast_ignores_user_lora(fake_pipe: MagicMock, monkeypatch) -> None: | |
| """A user LoRA is Quality-only — in Fast it must NOT be loaded or applied.""" | |
| fake_pipe.device = "mps" | |
| _mock_gpu_memory(monkeypatch, speed="Fast") | |
| p = _make_params(speed="Fast", steps=4, true_cfg=1.0) | |
| p["lora_path"] = "/tmp/foo.safetensors" | |
| p["lora_weight"] = 0.9 | |
| _, meta = modes.call_edit(fake_pipe, p) | |
| fake_pipe.load_lora_weights.assert_not_called() | |
| assert "lora" not in meta | |
| def test_gpu_user_lora_cleaned_up_on_error(fake_pipe: MagicMock, monkeypatch) -> None: | |
| """If inference raises (non-OOM), the 'user' adapter must STILL be deleted (finally).""" | |
| fake_pipe.device = "mps" | |
| _mock_gpu_memory(monkeypatch, speed="Quality") | |
| fake_pipe.side_effect = ValueError("boom") # pipe(...) raises | |
| p = _make_params(speed="Quality", steps=8, true_cfg=4.0) | |
| p["lora_path"] = "/tmp/foo.safetensors" | |
| p["lora_weight"] = 0.8 | |
| with pytest.raises(ValueError, match="boom"): | |
| modes.call_edit(fake_pipe, p) | |
| fake_pipe.delete_adapters.assert_any_call("user") # leak-proof cleanup | |
| # --------------------------------------------------------------------------- | |
| # Seed handling | |
| # --------------------------------------------------------------------------- | |
| def test_negative_seed_produces_int_in_meta(fake_pipe: MagicMock) -> None: | |
| """seed=-1 must resolve to a non-negative int stored in meta['seed'].""" | |
| _, meta = modes.call_edit(fake_pipe, _make_params(seed=-1)) | |
| assert isinstance(meta["seed"], int) | |
| assert meta["seed"] >= 0 | |
| def test_explicit_seed_preserved_in_meta(fake_pipe: MagicMock) -> None: | |
| """An explicit non-negative seed must appear unchanged in meta['seed'].""" | |
| _, meta = modes.call_edit(fake_pipe, _make_params(seed=123)) | |
| assert meta["seed"] == 123 | |
| def test_negative_seed_varies(fake_pipe: MagicMock) -> None: | |
| """Two calls with seed=-1 should (with overwhelming probability) produce different seeds.""" | |
| _, meta1 = modes.call_edit(fake_pipe, _make_params(seed=-1)) | |
| _, meta2 = modes.call_edit(fake_pipe, _make_params(seed=-1)) | |
| # Not deterministically different, but the chance of collision is < 1/2^32. | |
| # We just assert both are valid ints — determinism is tested via explicit seed. | |
| assert isinstance(meta1["seed"], int) | |
| assert isinstance(meta2["seed"], int) | |
| # --------------------------------------------------------------------------- | |
| # Error cases | |
| # --------------------------------------------------------------------------- | |
| def test_empty_images_raises_value_error(fake_pipe: MagicMock) -> None: | |
| """_run must raise ValueError when the images list is empty.""" | |
| params = _make_params(images=[]) | |
| with pytest.raises(ValueError): | |
| modes.call_edit(fake_pipe, params) | |
| def test_compose_all_none_raises_value_error(fake_pipe: MagicMock) -> None: | |
| """call_compose with all-None images must raise ValueError after filtering.""" | |
| params = _make_params(images=[None, None]) | |
| with pytest.raises(ValueError): | |
| modes.call_compose(fake_pipe, params) | |
| # --------------------------------------------------------------------------- | |
| # Meta dict completeness | |
| # --------------------------------------------------------------------------- | |
| _META_KEYS = {"mode", "speed", "steps", "true_cfg", "seed", "width", "height", "num_inputs"} | |
| def test_meta_has_all_documented_keys_edit(fake_pipe: MagicMock) -> None: | |
| """call_edit must return a metadata dict containing every documented key.""" | |
| _, meta = modes.call_edit(fake_pipe, _make_params()) | |
| assert _META_KEYS <= set(meta.keys()) | |
| def test_meta_has_all_documented_keys_compose(fake_pipe: MagicMock) -> None: | |
| """call_compose must return a metadata dict containing every documented key.""" | |
| target = Image.new("RGB", (8, 8)) | |
| ref = Image.new("RGB", (8, 8)) | |
| _, meta = modes.call_compose(fake_pipe, _make_params(images=[target, ref])) | |
| assert _META_KEYS <= set(meta.keys()) | |
| def test_meta_num_inputs_reflects_image_count(fake_pipe: MagicMock) -> None: | |
| """meta['num_inputs'] must equal the number of images actually passed to the pipeline.""" | |
| target = Image.new("RGB", (8, 8)) | |
| ref = Image.new("RGB", (8, 8)) | |
| # compose with [target, None, ref] — None is dropped, so num_inputs should be 2 | |
| _, meta = modes.call_compose(fake_pipe, _make_params(images=[target, None, ref])) | |
| assert meta["num_inputs"] == 2 | |
| def test_meta_dimensions_come_from_fit_dimensions(fake_pipe: MagicMock) -> None: | |
| """meta width/height must match the values returned by models.fit_dimensions (mocked to 64,64).""" | |
| _, meta = modes.call_edit(fake_pipe, _make_params()) | |
| assert meta["width"] == 64 | |
| assert meta["height"] == 64 | |
| # --------------------------------------------------------------------------- | |
| # DISPATCH table | |
| # --------------------------------------------------------------------------- | |
| def test_dispatch_contains_edit_and_compose() -> None: | |
| """DISPATCH must map 'edit' and 'compose' to the correct callables.""" | |
| assert modes.DISPATCH["edit"] is modes.call_edit | |
| assert modes.DISPATCH["compose"] is modes.call_compose | |
| def test_dispatch_via_table(fake_pipe: MagicMock) -> None: | |
| """Calling DISPATCH['edit'] must behave identically to calling call_edit directly.""" | |
| params = _make_params() | |
| img, meta = modes.DISPATCH["edit"](fake_pipe, params) | |
| assert isinstance(img, Image.Image) | |
| assert meta["mode"] == "edit" | |