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

560 lines
19 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Tests for LMCacheManager.
"""
# Standard
from unittest.mock import MagicMock, patch
# Third Party
import pytest
# First Party
from lmcache.integration.base_service_factory import BaseServiceFactory
from lmcache.v1.config import LMCacheEngineConfig
from lmcache.v1.manager import LMCacheManager
def _make_mock_factory(**overrides):
"""Create a mock BaseServiceFactory with sensible defaults."""
factory = MagicMock(spec=BaseServiceFactory)
factory.get_or_create_metadata.return_value = overrides.get("metadata", None)
factory.get_or_create_lmcache_engine.return_value = overrides.get("engine", None)
factory.maybe_create_lookup_client.return_value = overrides.get(
"lookup_client", None
)
factory.maybe_create_lookup_server.return_value = overrides.get(
"lookup_server", None
)
factory.maybe_create_offload_server.return_value = overrides.get(
"offload_server", None
)
factory.maybe_create_runtime_plugin_launcher.return_value = overrides.get(
"plugin_launcher", None
)
factory.maybe_create_internal_api_server.return_value = overrides.get(
"api_server", None
)
factory.maybe_create_health_monitor.return_value = overrides.get(
"health_monitor", None
)
return factory
class TestLMCacheManagerInit:
"""Tests for LMCacheManager initialization."""
def test_init_stores_config(self):
"""Test that __init__ stores config correctly."""
config = LMCacheEngineConfig.from_defaults()
connector = MagicMock()
factory = _make_mock_factory()
manager = LMCacheManager(
config=config,
service_factory=factory,
connector=connector,
)
assert manager._config is config
assert manager._connector is connector
def test_init_calls_factory_methods(self):
"""Test that __init__ calls all factory creation methods."""
config = LMCacheEngineConfig.from_defaults()
factory = _make_mock_factory()
LMCacheManager(
config=config,
service_factory=factory,
)
factory.get_or_create_metadata.assert_called_once()
factory.get_or_create_lmcache_engine.assert_called_once()
factory.maybe_create_lookup_client.assert_called_once()
factory.maybe_create_lookup_server.assert_called_once()
factory.maybe_create_offload_server.assert_called_once()
factory.maybe_create_runtime_plugin_launcher.assert_called_once()
factory.maybe_create_internal_api_server.assert_called_once()
def test_init_does_not_call_health_monitor(self):
"""Test that __init__ does NOT create health monitor (deferred to post_init)."""
config = LMCacheEngineConfig.from_defaults()
factory = _make_mock_factory()
LMCacheManager(
config=config,
service_factory=factory,
)
factory.maybe_create_health_monitor.assert_not_called()
class TestLMCacheManagerProperties:
"""Tests for LMCacheManager property accessors."""
@pytest.fixture
def manager_with_mocked_init(self):
"""Create a manager with mocked factory."""
config = LMCacheEngineConfig.from_defaults()
factory = _make_mock_factory()
manager = LMCacheManager(
config=config,
service_factory=factory,
connector=MagicMock(),
)
return manager
def test_lmcache_engine_property(self, manager_with_mocked_init):
"""Test lmcache_engine property returns the engine."""
manager = manager_with_mocked_init
mock_engine = MagicMock()
manager._lmcache_engine = mock_engine
assert manager.lmcache_engine is mock_engine
def test_lmcache_engine_metadata_property(self, manager_with_mocked_init):
"""Test lmcache_engine_metadata property returns the metadata."""
manager = manager_with_mocked_init
mock_metadata = MagicMock()
manager._lmcache_engine_metadata = mock_metadata
assert manager.lmcache_engine_metadata is mock_metadata
def test_lookup_client_property(self, manager_with_mocked_init):
"""Test lookup_client property returns the client."""
manager = manager_with_mocked_init
mock_client = MagicMock()
manager._lookup_client = mock_client
assert manager.lookup_client is mock_client
def test_lookup_server_property(self, manager_with_mocked_init):
"""Test lookup_server property returns the server."""
manager = manager_with_mocked_init
mock_server = MagicMock()
manager._lookup_server = mock_server
assert manager.lookup_server is mock_server
def test_offload_server_property(self, manager_with_mocked_init):
"""Test offload_server property returns the server."""
manager = manager_with_mocked_init
mock_server = MagicMock()
manager._offload_server = mock_server
assert manager.offload_server is mock_server
def test_config_property(self, manager_with_mocked_init):
"""Test config property returns the config."""
manager = manager_with_mocked_init
assert manager.config is manager._config
class TestLMCacheManagerStart:
"""Tests for LMCacheManager start method."""
def test_start_calls_api_server_start(self):
"""Test start_services() calls api_server.start() when api_server exists."""
config = LMCacheEngineConfig.from_defaults()
mock_api_server = MagicMock()
mock_plugin_launcher = MagicMock()
factory = _make_mock_factory(
api_server=mock_api_server,
plugin_launcher=mock_plugin_launcher,
)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
manager.start_services()
mock_api_server.start.assert_called_once()
mock_plugin_launcher.launch_plugins.assert_called_once()
def test_start_handles_none_api_server(self):
"""Test start_services() handles None api_server gracefully."""
config = LMCacheEngineConfig.from_defaults()
factory = _make_mock_factory()
manager = LMCacheManager(
config=config,
service_factory=factory,
)
# Should not raise any exception
manager.start_services()
class TestLMCacheManagerPostInit:
"""Tests for LMCacheManager post_init method."""
def test_post_init_without_engine(self):
"""Test post_init initializes health monitor when engine is None."""
config = LMCacheEngineConfig.from_defaults()
mock_health_monitor = MagicMock()
factory = _make_mock_factory(health_monitor=mock_health_monitor)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
manager.post_init()
# Health monitor should still be created even without engine
factory.maybe_create_health_monitor.assert_called_once_with(
lmcache_manager=manager
)
def test_post_init_with_engine_and_async_loading(self):
"""Test post_init calls engine.post_init with async_lookup_server."""
config = LMCacheEngineConfig.from_defaults()
config.enable_async_loading = True
mock_engine = MagicMock()
factory = _make_mock_factory(engine=mock_engine)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
manager.post_init()
# When lookup_server is None, engine.post_init should be called
# with async_lookup_server=None
mock_engine.post_init.assert_called_once_with(async_lookup_server=None)
def test_post_init_with_engine_and_async_server(self):
"""Test post_init calls engine.post_init when async lookup server exists."""
# First Party
from lmcache.v1.lookup_client.lmcache_async_lookup_client import (
LMCacheAsyncLookupServer,
)
config = LMCacheEngineConfig.from_defaults()
config.enable_async_loading = True
mock_engine = MagicMock()
mock_lookup_server = MagicMock(spec=LMCacheAsyncLookupServer)
factory = _make_mock_factory(
engine=mock_engine,
lookup_server=mock_lookup_server,
)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
manager.post_init()
# When lookup_server is LMCacheAsyncLookupServer, it should be passed
mock_engine.post_init.assert_called_once_with(
async_lookup_server=mock_lookup_server
)
class TestLMCacheManagerShutdown:
"""Tests for LMCacheManager shutdown method."""
def test_shutdown_closes_all_components(self):
"""Test stop_services() closes all components."""
config = LMCacheEngineConfig.from_defaults()
mock_offload = MagicMock()
mock_plugin = MagicMock()
mock_api = MagicMock()
mock_lookup_server = MagicMock()
mock_lookup_client = MagicMock()
factory = _make_mock_factory(
offload_server=mock_offload,
plugin_launcher=mock_plugin,
api_server=mock_api,
lookup_server=mock_lookup_server,
lookup_client=mock_lookup_client,
)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
with patch("lmcache.v1.manager.LMCacheEngineBuilder") as mock_builder:
manager.stop_services()
# Verify all components were closed
mock_offload.close.assert_called_once()
mock_plugin.stop_plugins.assert_called_once()
mock_api.stop.assert_called_once()
mock_lookup_server.close.assert_called_once()
mock_lookup_client.close.assert_called_once()
mock_builder.destroy.assert_called_once()
def test_shutdown_handles_none_components(self):
"""Test stop_services() handles None components gracefully."""
config = LMCacheEngineConfig.from_defaults()
factory = _make_mock_factory()
manager = LMCacheManager(
config=config,
service_factory=factory,
)
with patch("lmcache.v1.manager.LMCacheEngineBuilder"):
# Should not raise any exception
manager.stop_services()
def test_shutdown_handles_component_errors(self):
"""Test stop_services() handles errors from components gracefully."""
config = LMCacheEngineConfig.from_defaults()
mock_offload = MagicMock()
mock_offload.close.side_effect = RuntimeError("Test error")
mock_lookup_client = MagicMock()
factory = _make_mock_factory(
offload_server=mock_offload,
lookup_client=mock_lookup_client,
)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
with patch("lmcache.v1.manager.LMCacheEngineBuilder"):
# Should not raise exception, but should continue shutdown
manager.stop_services()
# lookup_client should still be closed even if offload_server failed
mock_lookup_client.close.assert_called_once()
class TestLMCacheManagerHelpers:
"""Tests for LMCacheManager helper methods."""
def test_need_gpu_interm_buffer_returns_not_enable_pd(self):
"""Test _need_gpu_interm_buffer returns opposite of enable_pd."""
config = LMCacheEngineConfig.from_defaults()
config.enable_pd = False
# First Party
from lmcache.v1.gpu_connector.utils import need_gpu_interm_buffer
assert need_gpu_interm_buffer(config) is True
config.enable_pd = True
assert need_gpu_interm_buffer(config) is False
class TestLMCacheManagerValidation:
"""Tests for validate_mla_config (now in utils)."""
def test_validate_mla_config_raises_on_wrong_serde(self):
"""Test validate_mla_config raises error for non-naive serde with MLA."""
# First Party
from lmcache.integration.vllm.utils import validate_mla_config
config = LMCacheEngineConfig.from_defaults()
config.remote_serde = "cachegen"
with pytest.raises(ValueError, match="MLA only works with naive serde mode"):
validate_mla_config(config, use_mla=True)
def test_validate_mla_config_raises_on_layerwise_with_blending(self):
"""Test validate_mla_config raises with MLA + layerwise + blending."""
# First Party
from lmcache.integration.vllm.utils import validate_mla_config
config = LMCacheEngineConfig.from_defaults()
config.remote_serde = "naive"
config.use_layerwise = True
config.enable_blending = True
with pytest.raises(ValueError, match="MLA with Cacheblend"):
validate_mla_config(config, use_mla=True)
class TestLMCacheManagerCalculateDraftLayers:
"""Tests for calculate_draft_layers (now in utils)."""
def test_calculate_draft_layers_no_speculative_config(self):
"""Test returns 0 when no speculative_config."""
# First Party
from lmcache.integration.vllm.utils import calculate_draft_layers
vllm_config = MagicMock()
vllm_config.speculative_config = None
assert calculate_draft_layers(vllm_config) == 0
def test_calculate_draft_layers_deepseek_mtp(self):
"""Test returns correct layers for deepseek_mtp method."""
# First Party
from lmcache.integration.vllm.utils import calculate_draft_layers
vllm_config = MagicMock()
vllm_config.speculative_config = MagicMock()
vllm_config.speculative_config.method = "deepseek_mtp"
vllm_config.model_config = MagicMock()
vllm_config.model_config.hf_config = MagicMock()
vllm_config.model_config.hf_config.num_nextn_predict_layers = 3
assert calculate_draft_layers(vllm_config) == 3
class TestLMCacheManagerInitFailure:
"""Tests for LMCacheManager initialization failure handling."""
def test_init_components_exception_makes_unhealthy(self):
"""Test that exception during factory calls makes manager unhealthy."""
config = LMCacheEngineConfig.from_defaults()
factory = _make_mock_factory()
factory.get_or_create_metadata.side_effect = RuntimeError("Test init failure")
manager = LMCacheManager(
config=config,
service_factory=factory,
)
# Verify through is_healthy API
assert manager.is_healthy() is False
def test_post_init_skipped_when_init_failed(self):
"""Test that post_init marks engine unhealthy when init failed."""
config = LMCacheEngineConfig.from_defaults()
factory = _make_mock_factory()
factory.get_or_create_metadata.side_effect = RuntimeError("Test init failure")
manager = LMCacheManager(
config=config,
service_factory=factory,
)
mock_engine = MagicMock()
manager._lmcache_engine = mock_engine
manager.post_init()
# Engine should be marked as init failed
mock_engine.mark_init_failed.assert_called_once()
# Manager should report unhealthy
assert manager.is_healthy() is False
def test_post_init_exception_makes_unhealthy(self):
"""Test that exception during post_init makes system unhealthy."""
config = LMCacheEngineConfig.from_defaults()
mock_engine = MagicMock()
mock_engine.post_init.side_effect = RuntimeError("Test post_init failure")
factory = _make_mock_factory(engine=mock_engine)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
manager.post_init()
# Verify engine was marked as failed
mock_engine.mark_init_failed.assert_called_once()
# Manager should report unhealthy
assert manager.is_healthy() is False
def test_unhealthy_engine_makes_manager_unhealthy(self):
"""Test that manager is unhealthy when engine is unhealthy."""
config = LMCacheEngineConfig.from_defaults()
mock_engine = MagicMock()
mock_engine.is_healthy.return_value = False
factory = _make_mock_factory(engine=mock_engine)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
assert manager.is_healthy() is False
def test_healthy_when_all_components_healthy(self):
"""Test that manager is healthy when all components are healthy."""
config = LMCacheEngineConfig.from_defaults()
mock_engine = MagicMock()
mock_engine.is_healthy.return_value = True
factory = _make_mock_factory(engine=mock_engine)
manager = LMCacheManager(
config=config,
service_factory=factory,
)
mock_health_monitor = MagicMock()
mock_health_monitor.is_healthy.return_value = True
manager._health_monitor = mock_health_monitor
assert manager.is_healthy() is True
class TestLMCacheEngineInitFailure:
"""Tests for LMCacheEngine init failure behavior on lookup/retrieve."""
def test_lookup_returns_zero_when_init_failed(self):
"""Test that lookup returns 0 when engine is marked as init failed."""
# First Party
from lmcache.v1.cache_engine import LMCacheEngine
engine = MagicMock(spec=LMCacheEngine)
engine._init_failed = False
engine._health_monitor = None
# Attach real methods to the mock
engine.mark_init_failed = LMCacheEngine.mark_init_failed.__get__(
engine, LMCacheEngine
)
engine.is_healthy = LMCacheEngine.is_healthy.__get__(engine, LMCacheEngine)
# Call mark_init_failed and verify is_healthy returns False
engine.mark_init_failed("Test reason")
assert engine.is_healthy() is False
def test_retrieve_returns_empty_mask_when_init_failed(self):
"""Test retrieve returns all-False mask when engine init failed."""
# First Party
from lmcache.v1.cache_engine import LMCacheEngine
engine = MagicMock(spec=LMCacheEngine)
engine._init_failed = False
engine._health_monitor = None
# Attach real methods to the mock
engine.mark_init_failed = LMCacheEngine.mark_init_failed.__get__(
engine, LMCacheEngine
)
engine.is_healthy = LMCacheEngine.is_healthy.__get__(engine, LMCacheEngine)
# Mark as init failed and verify is_healthy returns False
# (which would cause retrieve to return all-False mask)
engine.mark_init_failed("Test reason")
assert engine.is_healthy() is False
def test_mark_init_failed_makes_engine_unhealthy(self):
"""Test that mark_init_failed makes engine report unhealthy."""
# First Party
from lmcache.v1.cache_engine import LMCacheEngine
engine = MagicMock(spec=LMCacheEngine)
engine._init_failed = False
engine._health_monitor = None
# Attach real methods to the mock
engine.mark_init_failed = LMCacheEngine.mark_init_failed.__get__(
engine, LMCacheEngine
)
engine.is_healthy = LMCacheEngine.is_healthy.__get__(engine, LMCacheEngine)
# Verify initially healthy
assert engine.is_healthy() is True
# Call the method under test
engine.mark_init_failed("Test reason")
# Assert on the outcome
assert engine.is_healthy() is False