Files
2026-07-13 12:24:33 +08:00

335 lines
13 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
from unittest.mock import MagicMock
import json
# Third Party
from fastapi.testclient import TestClient
import pytest
# First Party
from lmcache.v1.internal_api_server.api_server import app
def _make_metadata(role: str) -> MagicMock:
"""Create a mock metadata with the given role."""
metadata = MagicMock()
metadata.role = role
return metadata
class TestLookupAPI:
"""Test suite for the /lookup/* API endpoints."""
@pytest.fixture
def mock_scheduler_manager(self):
"""Create a mock LMCacheManager for scheduler role."""
manager = MagicMock()
manager.lmcache_engine_metadata = _make_metadata("scheduler")
manager.get_lookup_info.return_value = {
"client": "HitLimitLookupClient(LMCacheBypassLookupClient)",
"server": "None",
}
manager.close_lookup_client.return_value = {
"old": "HitLimitLookupClient(LMCacheBypassLookupClient)"
}
manager.create_lookup_client.return_value = {"new": "LMCacheLookupClient"}
manager.recreate_lookup_client.return_value = {
"old": "HitLimitLookupClient(LMCacheBypassLookupClient)",
"new": "LMCacheLookupClient",
}
return manager
@pytest.fixture
def mock_worker_manager(self):
"""Create a mock LMCacheManager for worker role."""
manager = MagicMock()
manager.lmcache_engine_metadata = _make_metadata("worker")
manager.get_lookup_info.return_value = {
"client": "None",
"server": "LMCacheLookupServer",
}
manager.close_lookup_server.return_value = {"old": "LMCacheLookupServer"}
manager.create_lookup_server.return_value = {"new": "LMCacheAsyncLookupServer"}
manager.recreate_lookup_server.return_value = {
"old": "LMCacheLookupServer",
"new": "LMCacheAsyncLookupServer",
}
return manager
@pytest.fixture
def scheduler_client(self, mock_scheduler_manager):
"""Create a test client with mocked scheduler manager."""
app.state.lmcache_adapter = mock_scheduler_manager
client = TestClient(app)
yield client
client.close()
@pytest.fixture
def worker_client(self, mock_worker_manager):
"""Create a test client with mocked worker manager."""
app.state.lmcache_adapter = mock_worker_manager
client = TestClient(app)
yield client
client.close()
# ==================== GET /lookup/info Tests ====================
def test_get_lookup_info_scheduler(self, scheduler_client, mock_scheduler_manager):
"""Test get lookup info for scheduler role."""
response = scheduler_client.get("/lookup/info")
assert response.status_code == 200
data = json.loads(response.text)
assert "LMCacheBypassLookupClient" in data["client"]
assert data["server"] == "None"
mock_scheduler_manager.get_lookup_info.assert_called_once()
def test_get_lookup_info_worker(self, worker_client, mock_worker_manager):
"""Test get lookup info for worker role."""
response = worker_client.get("/lookup/info")
assert response.status_code == 200
data = json.loads(response.text)
assert data["client"] == "None"
assert data["server"] == "LMCacheLookupServer"
mock_worker_manager.get_lookup_info.assert_called_once()
def test_get_lookup_info_not_supported(self):
"""Test get lookup info when manager doesn't support it."""
class SimpleAdapter:
pass
app.state.lmcache_adapter = SimpleAdapter()
with TestClient(app) as client:
response = client.get("/lookup/info")
assert response.status_code == 503
data = json.loads(response.text)
assert "error" in data
# ==================== POST /lookup/close Tests ====================
def test_close_lookup_scheduler(self, scheduler_client, mock_scheduler_manager):
"""Test close lookup client for scheduler."""
response = scheduler_client.post("/lookup/close")
assert response.status_code == 200
data = json.loads(response.text)
assert "LMCacheBypassLookupClient" in data["old"]
assert data["role"] == "scheduler"
mock_scheduler_manager.close_lookup_client.assert_called_once()
def test_close_lookup_worker(self, worker_client, mock_worker_manager):
"""Test close lookup server for worker."""
response = worker_client.post("/lookup/close")
assert response.status_code == 200
data = json.loads(response.text)
assert data["old"] == "LMCacheLookupServer"
assert data["role"] == "worker"
mock_worker_manager.close_lookup_server.assert_called_once()
# ==================== POST /lookup/create Tests ====================
def test_create_lookup_scheduler(self, scheduler_client, mock_scheduler_manager):
"""Test create lookup client for scheduler."""
response = scheduler_client.post("/lookup/create")
assert response.status_code == 200
data = json.loads(response.text)
assert data["new"] == "LMCacheLookupClient"
assert data["role"] == "scheduler"
mock_scheduler_manager.create_lookup_client.assert_called_once_with(
dryrun=False
)
def test_create_lookup_worker(self, worker_client, mock_worker_manager):
"""Test create lookup server for worker."""
response = worker_client.post("/lookup/create")
assert response.status_code == 200
data = json.loads(response.text)
assert data["new"] == "LMCacheAsyncLookupServer"
assert data["role"] == "worker"
mock_worker_manager.create_lookup_server.assert_called_once_with(dryrun=False)
def test_create_lookup_dryrun_scheduler(
self, scheduler_client, mock_scheduler_manager
):
"""Test dryrun create lookup client for scheduler."""
mock_scheduler_manager.create_lookup_client.return_value = {
"new": "LMCacheLookupClient",
"dryrun": True,
}
response = scheduler_client.post("/lookup/create?dryrun=true")
assert response.status_code == 200
data = json.loads(response.text)
assert data["new"] == "LMCacheLookupClient"
assert data["dryrun"] is True
mock_scheduler_manager.create_lookup_client.assert_called_once_with(dryrun=True)
def test_create_lookup_dryrun_worker(self, worker_client, mock_worker_manager):
"""Test dryrun create lookup server for worker."""
mock_worker_manager.create_lookup_server.return_value = {
"new": "LMCacheAsyncLookupServer",
"dryrun": True,
}
response = worker_client.post("/lookup/create?dryrun=true")
assert response.status_code == 200
data = json.loads(response.text)
assert data["new"] == "LMCacheAsyncLookupServer"
assert data["dryrun"] is True
mock_worker_manager.create_lookup_server.assert_called_once_with(dryrun=True)
def test_create_lookup_error(self, scheduler_client, mock_scheduler_manager):
"""Test create lookup with error."""
mock_scheduler_manager.create_lookup_client.return_value = {
"error": "metadata not available"
}
response = scheduler_client.post("/lookup/create")
assert response.status_code == 400
data = json.loads(response.text)
assert data["error"] == "metadata not available"
# ==================== POST /lookup/recreate Tests ====================
def test_recreate_lookup_scheduler(self, scheduler_client, mock_scheduler_manager):
"""Test recreate lookup client for scheduler."""
response = scheduler_client.post("/lookup/recreate")
assert response.status_code == 200
data = json.loads(response.text)
assert "LMCacheBypassLookupClient" in data["old"]
assert data["new"] == "LMCacheLookupClient"
assert data["role"] == "scheduler"
mock_scheduler_manager.recreate_lookup_client.assert_called_once()
def test_recreate_lookup_worker(self, worker_client, mock_worker_manager):
"""Test recreate lookup server for worker."""
response = worker_client.post("/lookup/recreate")
assert response.status_code == 200
data = json.loads(response.text)
assert data["old"] == "LMCacheLookupServer"
assert data["new"] == "LMCacheAsyncLookupServer"
assert data["role"] == "worker"
mock_worker_manager.recreate_lookup_server.assert_called_once()
def test_recreate_lookup_error(self, scheduler_client, mock_scheduler_manager):
"""Test recreate lookup with error."""
mock_scheduler_manager.recreate_lookup_client.return_value = {
"error": "only supported for scheduler role"
}
response = scheduler_client.post("/lookup/recreate")
assert response.status_code == 400
data = json.loads(response.text)
assert "error" in data
def test_recreate_lookup_unknown_role(self):
"""Test recreation with unknown role."""
manager = MagicMock()
manager.lmcache_engine_metadata = _make_metadata("unknown")
app.state.lmcache_adapter = manager
with TestClient(app) as client:
response = client.post("/lookup/recreate")
assert response.status_code == 400
data = json.loads(response.text)
assert data["error"] == "Unknown role"
def test_recreate_lookup_no_metadata(self):
"""Test recreation when metadata is not available."""
manager = MagicMock()
manager.lmcache_engine_metadata = None
app.state.lmcache_adapter = manager
with TestClient(app) as client:
response = client.post("/lookup/recreate")
assert response.status_code == 400
data = json.loads(response.text)
assert data["error"] == "Unknown role"
def test_recreate_lookup_not_supported(self):
"""Test recreation when manager doesn't support it."""
class SimpleAdapter:
lmcache_engine_metadata = _make_metadata("scheduler")
app.state.lmcache_adapter = SimpleAdapter()
with TestClient(app) as client:
response = client.post("/lookup/recreate")
assert response.status_code == 503
data = json.loads(response.text)
assert "error" in data
# ==================== Integration Flow Tests ====================
def test_full_flow_worker_then_scheduler(
self, mock_worker_manager, mock_scheduler_manager
):
"""
Test the complete flow:
1. POST /lookup/recreate on worker (recreate server)
2. POST /lookup/recreate on scheduler (recreate client)
Note: Config should be updated via /conf API before each step.
"""
# Step 1: Worker side - recreate server
app.state.lmcache_adapter = mock_worker_manager
with TestClient(app) as worker_client:
response = worker_client.post("/lookup/recreate")
assert response.status_code == 200
data = json.loads(response.text)
assert data["old"] == "LMCacheLookupServer"
assert data["new"] == "LMCacheAsyncLookupServer"
mock_worker_manager.recreate_lookup_server.assert_called_once()
# Step 2: Scheduler side - recreate client
app.state.lmcache_adapter = mock_scheduler_manager
with TestClient(app) as scheduler_client:
response = scheduler_client.post("/lookup/recreate")
assert response.status_code == 200
data = json.loads(response.text)
assert "LMCacheBypassLookupClient" in data["old"]
assert data["new"] == "LMCacheLookupClient"
mock_scheduler_manager.recreate_lookup_client.assert_called_once()
def test_step_by_step_flow(self, mock_scheduler_manager):
"""
Test step-by-step flow: dryrun -> close -> create.
"""
app.state.lmcache_adapter = mock_scheduler_manager
with TestClient(app) as client:
# Step 1: Dryrun - check what would be created
mock_scheduler_manager.create_lookup_client.return_value = {
"new": "LMCacheLookupClient",
"dryrun": True,
}
response = client.post("/lookup/create?dryrun=true")
assert response.status_code == 200
data = json.loads(response.text)
assert data["dryrun"] is True
# Step 2: Close current client
response = client.post("/lookup/close")
assert response.status_code == 200
mock_scheduler_manager.close_lookup_client.assert_called_once()
# Step 3: Create new client
mock_scheduler_manager.create_lookup_client.return_value = {
"new": "LMCacheLookupClient"
}
response = client.post("/lookup/create")
assert response.status_code == 200