# SPDX-License-Identifier: Apache-2.0 """Shared test fixtures and utilities for cache controller tests.""" # Standard from unittest.mock import AsyncMock, MagicMock, Mock, patch import time # Third Party import pytest import torch # First Party from lmcache.utils import CacheEngineKey from lmcache.v1.cache_controller.controllers.full_sync_tracker import FullSyncTracker from lmcache.v1.cache_controller.controllers.kv_controller import KVController from lmcache.v1.cache_controller.message import ( BatchedKVOperationMsg, FullSyncBatchMsg, FullSyncEndMsg, FullSyncStartMsg, FullSyncStartRetMsg, FullSyncStatusMsg, FullSyncStatusRetMsg, KVOpEvent, RegisterMsg, ) from lmcache.v1.cache_controller.utils import RegistryTree, WorkerInfo from lmcache.v1.config import LMCacheEngineConfig # Constants LOCATION = "LocalCPUBackend" # ============= Config & Key Creation ============= def create_test_config( batch_size: int = 100, batch_interval_ms: int = 0, startup_delay_s: float = 0.0, status_poll_interval_s: float = 0.01, max_retry_count: int = 3, retry_delay_s: float = 0.01, ): """Create a test configuration.""" config = LMCacheEngineConfig.from_defaults( chunk_size=256, local_cpu=True, lmcache_instance_id="test_instance", ) config.extra_config = { "full_sync_batch_size": batch_size, "full_sync_batch_interval_ms": batch_interval_ms, "full_sync_startup_delay_s": startup_delay_s, "full_sync_status_poll_interval_s": status_poll_interval_s, "full_sync_max_retry_count": max_retry_count, "full_sync_retry_delay_s": retry_delay_s, } return config def create_test_key(key_id: int) -> CacheEngineKey: """Create a test CacheEngineKey.""" return CacheEngineKey( model_name="test_model", world_size=3, worker_id=0, chunk_hash=key_id, dtype=torch.bfloat16, ) # ============= Mock Classes ============= class MockWorker: """Mock LMCacheWorker for testing.""" def __init__(self): self.worker_id = 0 self.messages = [] self.req_responses = [] self._response_index = 0 def put_msg(self, msg): """Record pushed messages.""" self.messages.append(msg) async def async_put_and_wait_msg(self, msg): """Return predefined responses for REQ-REP messages.""" if self._response_index < len(self.req_responses): response = self.req_responses[self._response_index] self._response_index += 1 return response # Default responses if isinstance(msg, FullSyncStartMsg): return FullSyncStartRetMsg(sync_id=msg.sync_id, accepted=True) elif isinstance(msg, FullSyncStatusMsg): return FullSyncStatusRetMsg( sync_id=msg.sync_id, is_complete=True, global_progress=1.0, can_exit_freeze=True, ) return None def set_responses(self, responses): """Set predefined responses.""" self.req_responses = responses self._response_index = 0 class MockLMCacheEngine: """Mock LMCacheEngine for testing.""" def __init__(self): self._freeze = False self.freeze_calls = [] def freeze(self, enabled: bool): """Record freeze mode changes.""" self.freeze_calls.append(enabled) self._freeze = enabled class MockLocalCPUBackend: """Mock LocalCPUBackend for testing.""" def __init__(self, keys=None): self._keys = keys or [] def get_keys(self): """Return predefined keys.""" return self._keys def __str__(self): return LOCATION # ============= Helper Class ============= class H: """Helper class for test utilities.""" @staticmethod def create_tracker(threshold=0.8, timeout=300.0): """Create FullSyncTracker with registry.""" reg = RegistryTree() tracker = FullSyncTracker(reg, threshold, timeout) return tracker, reg @staticmethod def create_controller(threshold=0.8, timeout=300.0): """Create KVController with test settings.""" reg = RegistryTree() ctrl = KVController(reg, threshold, timeout) ctrl.cluster_executor = MagicMock() return ctrl @staticmethod def reg_worker(reg, inst, wid, ip="127.0.0.1", port=8000): """Register a worker.""" reg.register_worker(inst, wid, ip, port, None, MagicMock(), time.time()) @staticmethod async def reg_worker_async(ctrl, inst, wid, ip="127.0.0.1", port=8000): """Register worker via registration controller.""" with patch( "lmcache.v1.cache_controller.controllers.registration_controller.get_zmq_socket" ) as mock: mock.return_value = MagicMock() mock.return_value.sent_messages = [] return await ctrl.register(RegisterMsg(inst, wid, ip, port, None)) @staticmethod def start_msg(inst, wid, sid, keys=100, batches=5): """Create FullSyncStartMsg.""" return FullSyncStartMsg(inst, wid, LOCATION, sid, keys, batches) @staticmethod def batch_msg(inst, wid, sid, bid, keys): """Create FullSyncBatchMsg.""" return FullSyncBatchMsg(inst, wid, LOCATION, sid, bid, keys) @staticmethod def end_msg(inst, wid, sid, total): """Create FullSyncEndMsg.""" return FullSyncEndMsg(inst, wid, LOCATION, sid, total) @staticmethod def batched_op(inst, wid, op, key, seq=0): """Create BatchedKVOperationMsg.""" return BatchedKVOperationMsg(inst, wid, LOCATION, [KVOpEvent(op, key, seq)]) @staticmethod def start_ret(sync_id, accepted=True, error_msg=None): """Create FullSyncStartRetMsg.""" return FullSyncStartRetMsg( sync_id=sync_id, accepted=accepted, error_msg=error_msg ) @staticmethod def status_ret(sync_id, is_complete=True, progress=1.0, can_exit=True): """Create FullSyncStatusRetMsg.""" return FullSyncStatusRetMsg( sync_id=sync_id, is_complete=is_complete, global_progress=progress, can_exit_freeze=can_exit, ) # ============= Pytest Fixtures ============= @pytest.fixture def mock_reg_controller(): """Create a mock RegistrationController.""" controller = Mock() controller.get_workers = Mock(return_value=[0, 1]) controller.get_socket = Mock() controller.get_peer_init_url = Mock(return_value="tcp://localhost:5000") # Create a mock registry with all required methods mock_registry = Mock() mock_registry.seq_tracker = {} mock_registry.kv_pool = {} def mock_get_seq_num(instance_id, worker_id, location): key = (instance_id, worker_id, location) return mock_registry.seq_tracker.get(key) def mock_update_seq_num(instance_id, worker_id, location, seq_num): key = (instance_id, worker_id, location) mock_registry.seq_tracker[key] = seq_num return True def mock_admit_kv(instance_id, worker_id, location, key): report_id = (instance_id, worker_id) if report_id not in mock_registry.kv_pool: mock_registry.kv_pool[report_id] = {} if location not in mock_registry.kv_pool[report_id]: mock_registry.kv_pool[report_id][location] = set() mock_registry.kv_pool[report_id][location].add(key) return True def mock_evict_kv(instance_id, worker_id, location, key): report_id = (instance_id, worker_id) if report_id in mock_registry.kv_pool: if location in mock_registry.kv_pool[report_id]: if key in mock_registry.kv_pool[report_id][location]: mock_registry.kv_pool[report_id][location].remove(key) if not mock_registry.kv_pool[report_id][location]: del mock_registry.kv_pool[report_id][location] if not mock_registry.kv_pool[report_id]: del mock_registry.kv_pool[report_id] return True return False def mock_find_kv(key, exclude_instance_id=None): for report_id, locations in mock_registry.kv_pool.items(): instance_id, worker_id = report_id if exclude_instance_id and instance_id == exclude_instance_id: continue for location, keys in locations.items(): if key in keys: # Return a mock KVChunkInfo # First Party from lmcache.v1.cache_controller.utils import KVChunkInfo return KVChunkInfo(instance_id, worker_id, location) return None def mock_get_worker_kv_keys(instance_id, worker_id, location): report_id = (instance_id, worker_id) if report_id in mock_registry.kv_pool: return mock_registry.kv_pool[report_id].get(location, set()) return set() def mock_get_total_kv_count(): count = 0 for locations in mock_registry.kv_pool.values(): for keys in locations.values(): count += len(keys) return count def mock_get_seq_discontinuity_count(): # Simple implementation for testing return getattr(mock_registry, "_seq_discontinuity_count", 0) def mock_deregister_worker(instance_id, worker_id): report_id = (instance_id, worker_id) if report_id in mock_registry.kv_pool: del mock_registry.kv_pool[report_id] # Also clean up seq_tracker keys_to_remove = [ k for k in mock_registry.seq_tracker.keys() if k[0] == instance_id and k[1] == worker_id ] for key in keys_to_remove: del mock_registry.seq_tracker[key] return True def mock_find_kv_with_worker_info(key, exclude_instance_id=None): """Find KV and return (kv_info, peer_init_url, current_keys)""" for report_id, locations in mock_registry.kv_pool.items(): instance_id, worker_id = report_id if exclude_instance_id and instance_id == exclude_instance_id: continue for location, keys in locations.items(): if key in keys: # First Party from lmcache.v1.cache_controller.utils import KVChunkInfo kv_info = KVChunkInfo(instance_id, worker_id, location) # Get peer_init_url from controller peer_init_url = controller.get_peer_init_url(instance_id, worker_id) return (kv_info, peer_init_url, keys) return None def mock_handle_batched_kv_operations(msg): """Handle batched KV operations""" # First Party from lmcache.v1.cache_controller.message import OpType report_id = (msg.instance_id, msg.worker_id) for op in msg.operations: # Check for sequence discontinuity key = (msg.instance_id, msg.worker_id, msg.location) last_seq_num = mock_registry.seq_tracker.get(key) if last_seq_num is not None: expected_seq = last_seq_num + 1 if op.seq_num != expected_seq: # Increment discontinuity counter mock_registry._seq_discontinuity_count += 1 # Update sequence number mock_registry.seq_tracker[key] = op.seq_num # Handle operation if op.op_type == OpType.ADMIT: if report_id not in mock_registry.kv_pool: mock_registry.kv_pool[report_id] = {} if msg.location not in mock_registry.kv_pool[report_id]: mock_registry.kv_pool[report_id][msg.location] = set() mock_registry.kv_pool[report_id][msg.location].add(op.key) elif op.op_type == OpType.EVICT: if report_id in mock_registry.kv_pool: if msg.location in mock_registry.kv_pool[report_id]: mock_registry.kv_pool[report_id][msg.location].discard(op.key) if not mock_registry.kv_pool[report_id][msg.location]: del mock_registry.kv_pool[report_id][msg.location] if not mock_registry.kv_pool[report_id]: del mock_registry.kv_pool[report_id] return True mock_registry.get_seq_num = Mock(side_effect=mock_get_seq_num) mock_registry.update_seq_num = Mock(side_effect=mock_update_seq_num) mock_registry.admit_kv = Mock(side_effect=mock_admit_kv) mock_registry.evict_kv = Mock(side_effect=mock_evict_kv) mock_registry.find_kv = Mock(side_effect=mock_find_kv) mock_registry.get_worker_kv_keys = Mock(side_effect=mock_get_worker_kv_keys) mock_registry.get_total_kv_count = Mock(side_effect=mock_get_total_kv_count) mock_registry.get_seq_discontinuity_count = Mock( side_effect=mock_get_seq_discontinuity_count ) mock_registry.deregister_worker = Mock(side_effect=mock_deregister_worker) mock_registry.find_kv_with_worker_info = Mock( side_effect=mock_find_kv_with_worker_info ) mock_registry.handle_batched_kv_operations = Mock( side_effect=mock_handle_batched_kv_operations ) mock_registry._seq_discontinuity_count = 0 controller.registry = mock_registry return controller @pytest.fixture def mock_kv_controller(): """Create a mock KVController.""" controller = Mock() controller.admit = AsyncMock() controller.evict = AsyncMock() controller.lookup = AsyncMock() controller.clear = AsyncMock() controller.pin = AsyncMock() controller.compress = AsyncMock() controller.decompress = AsyncMock() controller.move = AsyncMock() controller.deregister = AsyncMock() controller.kv_pool = {} controller.seq_tracker = {} # Mock full_sync_tracker mock_tracker = Mock() mock_tracker.should_request_full_sync = Mock(return_value=(False, None)) controller.full_sync_tracker = mock_tracker return controller @pytest.fixture def mock_cluster_executor(): """Create a mock LMCacheClusterExecutor.""" executor = Mock() executor.execute = AsyncMock() executor.clear = AsyncMock() executor.pin = AsyncMock() executor.compress = AsyncMock() executor.decompress = AsyncMock() executor.move = AsyncMock() executor.health = AsyncMock() executor.check_finish = AsyncMock() return executor @pytest.fixture def mock_zmq_socket(): """Create a mock ZMQ socket.""" socket = AsyncMock() socket.send = AsyncMock() socket.recv = AsyncMock() socket.send_multipart = AsyncMock() socket.recv_multipart = AsyncMock() socket.close = Mock() socket.get = Mock(return_value=0) return socket @pytest.fixture def mock_zmq_context(): """Create a mock ZMQ context.""" context = Mock() context.socket = Mock() context.term = Mock() return context @pytest.fixture def sample_worker_info(): """Create sample worker info for testing.""" return WorkerInfo( instance_id="test_instance", worker_id=0, ip="127.0.0.1", port=5000, peer_init_url="tcp://127.0.0.1:6000", registration_time=time.time(), last_heartbeat_time=time.time(), ) @pytest.fixture def mock_lmcache_engine(): """Create a mock LMCacheEngine.""" engine = Mock() engine.move = Mock(return_value=100) engine.compress = Mock(return_value=100) engine.decompress = Mock(return_value=100) engine.lookup = Mock(return_value=100) engine.clear = Mock(return_value=100) engine.health = Mock(return_value=0) return engine