qwen-image-editor / tests /test_backend.py
techfreakworm's picture
fix(ui): replace track_tqdm with a manual step callback for clear progress labels
b848e06 unverified
Raw
History Blame Contribute Delete
5.95 kB
"""Tests for backend.py — duration estimator, generate routing, and retry logic."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
import backend
import modes
# ---------------------------------------------------------------------------
# Fixture: a QwenImageEditBackend whose _build_pipeline is replaced with a
# MagicMock so no torch/diffusers import occurs.
# ---------------------------------------------------------------------------
@pytest.fixture
def fake_backend(monkeypatch):
monkeypatch.setattr(backend, "_build_pipeline", lambda *a, **k: MagicMock())
return backend.QwenImageEditBackend()
# ---------------------------------------------------------------------------
# duration_for — clamping and Fast shortcut
# ---------------------------------------------------------------------------
# The Space runs on the xlarge ZeroGPU tier (58 GB model), which DOUBLES the
# requested duration for ZeroGPU's per-call ceiling check. Requests at/above the
# ceiling are rejected ("ZeroGPU illegal duration"). These tests lock in the two
# invariants that keep the GPU path working, rather than a magic number.
_XLARGE_MULTIPLIER = 2
_ZEROGPU_CEILING = 300 # requested-seconds; reaching this is rejected on this Space
_DURATION_CASES = (
{"speed": "Fast"},
{"speed": "Fast", "steps": 100},
{"speed": "Quality"},
{"speed": "Quality", "steps": 40},
{"speed": "Quality", "steps": 1000},
)
def test_duration_for_legal_on_xlarge():
"""The doubled request must stay under ZeroGPU's per-call ceiling.
This is the regression guard against the illegal-duration bug: a 180 s
request became 360 s on xlarge and every call was rejected.
"""
for params in _DURATION_CASES:
d = backend.duration_for("edit", params)
assert d * _XLARGE_MULTIPLIER < _ZEROGPU_CEILING, (
f"duration {d}s -> {d * _XLARGE_MULTIPLIER}s requested >= {_ZEROGPU_CEILING}s ceiling (illegal on xlarge)"
)
def test_duration_for_covers_materialization():
"""Budget must cover the ~15-40 s per-call model materialization plus
inference, else the GPU task aborts mid-load."""
for params in _DURATION_CASES:
assert backend.duration_for("edit", params) >= 50
def test_duration_for_returns_legal_int():
"""Default budget is a positive int whose doubled xlarge request stays under
the 360 s value ZeroGPU rejected."""
d = backend.duration_for("edit", {"speed": "Fast", "steps": 4})
assert isinstance(d, int)
assert 60 <= d * 2 < 360
def test_duration_for_env_override(monkeypatch):
"""QIE_GPU_DURATION retunes the budget without a code change; junk is ignored."""
monkeypatch.setenv("QIE_GPU_DURATION", "100")
assert backend.duration_for("edit", {"speed": "Fast", "steps": 4}) == 100
monkeypatch.setenv("QIE_GPU_DURATION", "not-an-int")
assert isinstance(backend.duration_for("edit", {"speed": "Fast"}), int)
def test_duration_for_returns_int():
assert isinstance(backend.duration_for("edit", {"speed": "Quality", "steps": 40}), int)
assert isinstance(backend.duration_for("edit", {"speed": "Fast"}), int)
# ---------------------------------------------------------------------------
# generate — dispatch routing and unknown-mode guard
# ---------------------------------------------------------------------------
def test_generate_routes_to_dispatch(monkeypatch, fake_backend):
"""generate must call modes.DISPATCH[mode] with (pipeline, params, progress)."""
sentinel = ("output_image", {"meta": True})
fake_handler = MagicMock(return_value=sentinel)
monkeypatch.setattr(modes, "DISPATCH", {"edit": fake_handler})
params = {"speed": "Fast", "prompt": "test"}
result = fake_backend.generate("edit", params)
# progress defaults to None when not supplied (e.g. non-UI callers / CI).
fake_handler.assert_called_once_with(fake_backend.pipeline, params, None)
assert result is sentinel
def test_generate_unknown_mode_raises_value_error(monkeypatch, fake_backend):
"""generate must raise ValueError — not KeyError — for unrecognised modes."""
monkeypatch.setattr(modes, "DISPATCH", {"edit": MagicMock()})
with pytest.raises(ValueError, match="unknown mode"):
fake_backend.generate("no_such_mode", {})
# ---------------------------------------------------------------------------
# generate_with_retry — retry on GPU abort, re-raise on other errors
# ---------------------------------------------------------------------------
def test_generate_with_retry_retries_exactly_once_on_gpu_abort(monkeypatch, fake_backend):
"""generate_with_retry retries once when generate raises 'GPU task aborted'."""
good_result = ("img", {"seed": 0})
mock_generate = MagicMock(side_effect=[Exception("GPU task aborted"), good_result])
monkeypatch.setattr(fake_backend, "generate", mock_generate)
result = backend.generate_with_retry(fake_backend, "edit", {"speed": "Fast"})
assert mock_generate.call_count == 2
assert result is good_result
def test_generate_with_retry_case_insensitive_match(monkeypatch, fake_backend):
"""GPU-abort detection is case-insensitive."""
good_result = ("img", {})
mock_generate = MagicMock(side_effect=[Exception("GPU Task Aborted"), good_result])
monkeypatch.setattr(fake_backend, "generate", mock_generate)
backend.generate_with_retry(fake_backend, "edit", {})
assert mock_generate.call_count == 2
def test_generate_with_retry_reraises_non_gpu_error(monkeypatch, fake_backend):
"""generate_with_retry must NOT retry on unrelated exceptions."""
mock_generate = MagicMock(side_effect=Exception("boom"))
monkeypatch.setattr(fake_backend, "generate", mock_generate)
with pytest.raises(Exception, match="boom"):
backend.generate_with_retry(fake_backend, "edit", {})
assert mock_generate.call_count == 1 # no retry