255 lines
8.6 KiB
Python
255 lines
8.6 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Test cases for the /backends/* API endpoints.
|
|
|
|
Tests cover:
|
|
- GET /backends — list active backends
|
|
- DELETE /backends/{name} — close and remove a backend
|
|
- POST /backends — create new backends from current config
|
|
- POST /backends/{name}/recreate — atomic close + create
|
|
- End-to-end flow: close → conf update → create → verify
|
|
"""
|
|
|
|
# Standard
|
|
from collections import OrderedDict
|
|
from unittest.mock import MagicMock
|
|
|
|
# Third Party
|
|
from fastapi.testclient import TestClient
|
|
import pytest
|
|
|
|
# First Party
|
|
from lmcache.v1.internal_api_server.api_server import app
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Fixtures
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
class FakeBackend:
|
|
"""Minimal fake backend for testing."""
|
|
|
|
def __init__(self, name: str = "FakeBackend"):
|
|
self._name = name
|
|
self.closed = False
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
def __str__(self):
|
|
return self._name
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_storage_manager():
|
|
"""Create a mock StorageManager with two fake backends."""
|
|
sm = MagicMock()
|
|
fake_cpu = FakeBackend("LocalCPUBackend")
|
|
fake_remote = FakeBackend("RemoteBackend")
|
|
backends = OrderedDict(
|
|
[
|
|
("LocalCPUBackend", fake_cpu),
|
|
("RemoteBackend", fake_remote),
|
|
]
|
|
)
|
|
sm.storage_backends = backends
|
|
|
|
# Wire real-ish list_backends
|
|
sm.list_backends.side_effect = lambda: {
|
|
n: type(b).__name__ for n, b in sm.storage_backends.items()
|
|
}
|
|
return sm
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_engine(mock_storage_manager):
|
|
engine = MagicMock()
|
|
engine.storage_manager = mock_storage_manager
|
|
return engine
|
|
|
|
|
|
@pytest.fixture
|
|
def client_with_engine(mock_engine):
|
|
adapter = MagicMock()
|
|
adapter.lmcache_engine = mock_engine
|
|
app.state.lmcache_adapter = adapter
|
|
client = TestClient(app)
|
|
yield client
|
|
client.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def client_without_engine():
|
|
adapter = MagicMock()
|
|
adapter.lmcache_engine = None
|
|
app.state.lmcache_adapter = adapter
|
|
client = TestClient(app)
|
|
yield client
|
|
client.close()
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# GET /backends
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
class TestListBackends:
|
|
def test_list_success(self, client_with_engine, mock_storage_manager):
|
|
resp = client_with_engine.get("/backends")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "LocalCPUBackend" in data
|
|
assert "RemoteBackend" in data
|
|
|
|
def test_list_no_engine(self, client_without_engine):
|
|
resp = client_without_engine.get("/backends")
|
|
assert resp.status_code == 503
|
|
|
|
def test_list_exception(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.list_backends.side_effect = RuntimeError("boom")
|
|
resp = client_with_engine.get("/backends")
|
|
assert resp.status_code == 500
|
|
data = resp.json()
|
|
assert "boom" in data["message"]
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# DELETE /backends/{name}
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
class TestCloseBackend:
|
|
def test_close_success(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.close_backend.return_value = True
|
|
resp = client_with_engine.delete("/backends/RemoteBackend")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] == "success"
|
|
mock_storage_manager.close_backend.assert_called_once_with("RemoteBackend")
|
|
|
|
def test_close_not_found(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.close_backend.return_value = False
|
|
resp = client_with_engine.delete("/backends/NonExistent")
|
|
assert resp.status_code == 404
|
|
data = resp.json()
|
|
assert data["status"] == "not_found"
|
|
|
|
def test_close_no_engine(self, client_without_engine):
|
|
resp = client_without_engine.delete("/backends/RemoteBackend")
|
|
assert resp.status_code == 503
|
|
|
|
def test_close_exception(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.close_backend.side_effect = RuntimeError("close error")
|
|
resp = client_with_engine.delete("/backends/RemoteBackend")
|
|
assert resp.status_code == 500
|
|
data = resp.json()
|
|
assert "close error" in data["message"]
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# POST /backends
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
class TestCreateBackends:
|
|
def test_create_success(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.create_backends.return_value = {"GdsBackend": "GdsBackend"}
|
|
resp = client_with_engine.post("/backends")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] == "success"
|
|
assert "GdsBackend" in data["created"]
|
|
|
|
def test_create_no_engine(self, client_without_engine):
|
|
resp = client_without_engine.post("/backends")
|
|
assert resp.status_code == 503
|
|
|
|
def test_create_exception(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.create_backends.side_effect = RuntimeError("create error")
|
|
resp = client_with_engine.post("/backends")
|
|
assert resp.status_code == 500
|
|
data = resp.json()
|
|
assert "create error" in data["message"]
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# POST /backends/{name}/recreate
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
class TestRecreateBackend:
|
|
def test_recreate_success(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.recreate_backend.return_value = {
|
|
"RemoteBackend": "RemoteBackend"
|
|
}
|
|
resp = client_with_engine.post("/backends/RemoteBackend/recreate")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] == "success"
|
|
assert "RemoteBackend" in data["recreated"]
|
|
mock_storage_manager.recreate_backend.assert_called_once_with("RemoteBackend")
|
|
|
|
def test_recreate_not_found(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.recreate_backend.side_effect = KeyError(
|
|
"Backend NonExistent not found"
|
|
)
|
|
resp = client_with_engine.post("/backends/NonExistent/recreate")
|
|
assert resp.status_code == 404
|
|
data = resp.json()
|
|
assert data["status"] == "not_found"
|
|
|
|
def test_recreate_no_engine(self, client_without_engine):
|
|
resp = client_without_engine.post("/backends/RemoteBackend/recreate")
|
|
assert resp.status_code == 503
|
|
|
|
def test_recreate_exception(self, client_with_engine, mock_storage_manager):
|
|
mock_storage_manager.recreate_backend.side_effect = RuntimeError(
|
|
"recreate error"
|
|
)
|
|
resp = client_with_engine.post("/backends/RemoteBackend/recreate")
|
|
assert resp.status_code == 500
|
|
data = resp.json()
|
|
assert "recreate error" in data["message"]
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# End-to-end flow: close → conf → create
|
|
# ------------------------------------------------------------------ #
|
|
|
|
|
|
class TestBackendSwitchFlow:
|
|
"""Simulate the full backend-switch workflow via API."""
|
|
|
|
def test_close_update_conf_create_flow(
|
|
self, client_with_engine, mock_storage_manager
|
|
):
|
|
"""
|
|
1. DELETE /backends/RemoteBackend
|
|
2. POST /conf to update remote_url
|
|
3. POST /backends to create new RemoteBackend
|
|
4. GET /backends to verify
|
|
"""
|
|
# Step 1: close RemoteBackend
|
|
mock_storage_manager.close_backend.return_value = True
|
|
resp = client_with_engine.delete("/backends/RemoteBackend")
|
|
assert resp.status_code == 200
|
|
|
|
# Step 2: update config via /conf (uses conf_api)
|
|
# We patch the config object on the adapter
|
|
mock_config = MagicMock()
|
|
client_with_engine.app.state.lmcache_adapter.config = mock_config
|
|
|
|
# Step 3: create new backends
|
|
mock_storage_manager.create_backends.return_value = {
|
|
"RemoteBackend": "RemoteBackend"
|
|
}
|
|
resp = client_with_engine.post("/backends")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["status"] == "success"
|
|
assert "RemoteBackend" in data["created"]
|
|
|
|
# Step 4: list backends
|
|
resp = client_with_engine.get("/backends")
|
|
assert resp.status_code == 200
|