alien-riddle / tests /test_local_backend.py
Huba179's picture
deploy from GitHub
6491ed1 verified
Raw
History Blame Contribute Delete
9.87 kB
"""Unit tests for the LocalGPU4BitBackend and AutoBackend classes."""
import json
import pytest
from pytest_mock import MockerFixture
class TestLocalGPU4BitBackendIsAvailable:
"""Tests for the static ``is_available`` method."""
def test_returns_false_when_torch_not_importable(self, mocker: MockerFixture) -> None:
"""When torch cannot be imported, is_available returns False."""
import builtins
original_import = builtins.__import__
def mock_import(name, *args, **kwargs):
if name == "torch":
raise ImportError("No module named 'torch'")
return original_import(name, *args, **kwargs)
mocker.patch("builtins.__import__", side_effect=mock_import)
from importlib import reload
import alien_obfuscator.riddle_generator as mod
reload(mod)
result = mod.LocalGPU4BitBackend.is_available()
assert result is False
def test_returns_false_when_no_cuda(self, mocker: MockerFixture) -> None:
"""When torch is available but CUDA is not, is_available returns False."""
mock_torch = mocker.MagicMock()
mock_torch.cuda.is_available.return_value = False
mocker.patch.dict("sys.modules", {"torch": mock_torch})
from alien_obfuscator.riddle_generator import LocalGPU4BitBackend
result = LocalGPU4BitBackend.is_available()
assert result is False
def test_returns_true_when_torch_and_cuda_available(self, mocker: MockerFixture) -> None:
"""When torch and CUDA are both available, is_available returns True."""
mock_torch = mocker.MagicMock()
mock_torch.cuda.is_available.return_value = True
mocker.patch.dict("sys.modules", {"torch": mock_torch})
from alien_obfuscator.riddle_generator import LocalGPU4BitBackend
result = LocalGPU4BitBackend.is_available()
assert result is True
class TestLocalGPU4BitBackend:
"""Tests for the local GPU backend."""
def test_init_raises_without_cuda(self, mocker: MockerFixture) -> None:
"""When CUDA is not available, __init__ raises RuntimeError."""
mock_torch = mocker.MagicMock()
mock_torch.cuda.is_available.return_value = False
mocker.patch.dict("sys.modules", {"torch": mock_torch})
from alien_obfuscator.riddle_generator import LocalGPU4BitBackend
with pytest.raises(RuntimeError, match="CUDA-capable GPU"):
LocalGPU4BitBackend("dummy-model")
def test_init_loads_model_on_startup(self, mocker: MockerFixture) -> None:
"""When CUDA is available, the model and tokenizer are loaded on init."""
mock_torch = mocker.MagicMock()
mock_torch.cuda.is_available.return_value = True
mock_torch.float16 = "float16"
mock_torch.no_grad.return_value.__enter__ = mocker.MagicMock()
mocker.patch.dict("sys.modules", {"torch": mock_torch})
mock_tokenizer = mocker.MagicMock()
mock_tokenizer.pad_token = None
mock_tokenizer.eos_token = "[EOS]"
mock_model = mocker.MagicMock()
mock_auto_model = mocker.MagicMock()
mock_auto_model.from_pretrained.return_value = mock_model
mock_auto_tokenizer = mocker.MagicMock()
mock_auto_tokenizer.from_pretrained.return_value = mock_tokenizer
mock_bnb = mocker.MagicMock()
mock_transformers = mocker.MagicMock()
mock_transformers.AutoModelForCausalLM = mock_auto_model
mock_transformers.AutoTokenizer = mock_auto_tokenizer
mock_transformers.BitsAndBytesConfig = mock_bnb
mocker.patch.dict("sys.modules", {"transformers": mock_transformers})
from alien_obfuscator.riddle_generator import LocalGPU4BitBackend
backend = LocalGPU4BitBackend("dummy-model", quantize="fp16")
assert backend._model is mock_model
assert backend._tokenizer is mock_tokenizer
mock_auto_tokenizer.from_pretrained.assert_called_once_with("dummy-model")
mock_auto_model.from_pretrained.assert_called_once()
class TestAutoBackend:
"""Tests for the configurable auto-backend with fallback chain."""
def test_default_order_modal_local_mock(self, mocker: MockerFixture) -> None:
"""Default fallback chain is modal → local → mock when all available."""
mocker.patch.dict("os.environ", {"MODAL_API_URL": "https://foo.modal.run"}, clear=True)
mock_torch = mocker.MagicMock()
mock_torch.cuda.is_available.return_value = True
mock_torch.float16 = "float16"
mocker.patch.dict("sys.modules", {"torch": mock_torch})
mock_transformers = mocker.MagicMock()
mock_tokenizer_cls = mocker.MagicMock()
mock_tokenizer = mocker.MagicMock()
mock_tokenizer.pad_token = None
mock_tokenizer.eos_token = "[EOS]"
mock_tokenizer_cls.from_pretrained.return_value = mock_tokenizer
mock_model_cls = mocker.MagicMock()
mock_model_cls.from_pretrained.return_value = mocker.MagicMock()
mock_transformers.AutoTokenizer = mock_tokenizer_cls
mock_transformers.AutoModelForCausalLM = mock_model_cls
mock_transformers.BitsAndBytesConfig = mocker.MagicMock()
mocker.patch.dict("sys.modules", {"transformers": mock_transformers})
mocker.patch("alien_obfuscator.riddle_generator.ModalBackend.__init__", return_value=None)
from alien_obfuscator.riddle_generator import AutoBackend
backend = AutoBackend("test-model")
names = [type(b).__name__ for b in backend._backends]
assert names == ["ModalBackend", "LocalGPU4BitBackend", "MockBackend"]
def test_custom_fallback_order(self, mocker: MockerFixture) -> None:
"""Custom fallback_order is respected."""
mocker.patch.dict("os.environ", {}, clear=True)
mocker.patch(
"alien_obfuscator.riddle_generator.LocalGPU4BitBackend.is_available",
return_value=False,
)
from alien_obfuscator.riddle_generator import AutoBackend
backend = AutoBackend("test-model", fallback_order=["local", "mock"])
names = [type(b).__name__ for b in backend._backends]
# local unavailable, so only mock
assert names == ["MockBackend"]
def test_skips_unavailable_modal(self, mocker: MockerFixture) -> None:
"""When MODAL_API_URL is not set, modal is skipped."""
mocker.patch.dict("os.environ", {}, clear=True)
mocker.patch(
"alien_obfuscator.riddle_generator.LocalGPU4BitBackend.is_available",
return_value=False,
)
from alien_obfuscator.riddle_generator import AutoBackend
backend = AutoBackend("test-model")
names = [type(b).__name__ for b in backend._backends]
assert names == ["MockBackend"]
def test_skips_unavailable_local(self, mocker: MockerFixture) -> None:
"""When local GPU is not available, it is skipped."""
mocker.patch.dict("os.environ", {"MODAL_API_URL": "https://foo.modal.run"}, clear=True)
mocker.patch(
"alien_obfuscator.riddle_generator.LocalGPU4BitBackend.is_available",
return_value=False,
)
mocker.patch(
"alien_obfuscator.riddle_generator.ModalBackend.__init__",
return_value=None,
)
from alien_obfuscator.riddle_generator import AutoBackend
backend = AutoBackend("test-model", fallback_order=["local", "modal", "mock"])
names = [type(b).__name__ for b in backend._backends]
assert names == ["ModalBackend", "MockBackend"]
def test_tries_first_then_falls_back(self, mocker: MockerFixture) -> None:
"""When first backend fails, the second is tried."""
from alien_obfuscator.riddle_generator import AutoBackend
mock_primary = mocker.MagicMock()
mock_primary.generate.side_effect = RuntimeError("boom")
mock_fallback = mocker.MagicMock()
mock_fallback.generate.return_value = (
'{"riddle":"r","options":["a","b","c","d","e"],"correct_index":0,"theme":"t"}'
)
backend = AutoBackend.__new__(AutoBackend)
backend._backends = [mock_primary, mock_fallback]
result = backend.generate("test prompt")
mock_primary.generate.assert_called_once()
mock_fallback.generate.assert_called_once()
data = json.loads(result)
assert data["riddle"] == "r"
def test_falls_through_all_to_mock(self, mocker: MockerFixture) -> None:
"""When all backends fail, mock always succeeds."""
from alien_obfuscator.riddle_generator import AutoBackend
mock_one = mocker.MagicMock()
mock_one.generate.side_effect = RuntimeError("fail1")
mock_two = mocker.MagicMock()
mock_two.generate.side_effect = RuntimeError("fail2")
mock_mock = mocker.MagicMock()
mock_mock.generate.return_value = '{"riddle":"r","options":["a","b","c","d","e"],"correct_index":0,"theme":"t"}'
backend = AutoBackend.__new__(AutoBackend)
backend._backends = [mock_one, mock_two, mock_mock]
result = backend.generate("test prompt")
mock_one.generate.assert_called_once()
mock_two.generate.assert_called_once()
mock_mock.generate.assert_called_once()
data = json.loads(result)
assert data["riddle"] == "r"
def test_unknown_backend_name_skipped(self, mocker: MockerFixture) -> None:
"""Unknown backend names in fallback_order are silently skipped."""
mocker.patch.dict("os.environ", {}, clear=True)
from alien_obfuscator.riddle_generator import AutoBackend
backend = AutoBackend("test-model", fallback_order=["bogus", "mock"])
names = [type(b).__name__ for b in backend._backends]
assert names == ["MockBackend"]