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

273 lines
8.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Unit tests for FullSyncSender."""
# Third Party
import pytest
# First Party
from lmcache.v1.cache_controller.full_sync_sender import FullSyncSender
from lmcache.v1.cache_controller.message import (
FullSyncBatchMsg,
FullSyncEndMsg,
)
# Local
# Test utilities from conftest
from .conftest import (
H,
MockLMCacheEngine,
MockLocalCPUBackend,
MockWorker,
create_test_config,
create_test_key,
)
@pytest.fixture
def full_sync_sender():
"""Create a FullSyncSender instance for testing."""
config = create_test_config()
worker = MockWorker()
engine = MockLMCacheEngine()
backend = MockLocalCPUBackend()
sender = FullSyncSender(
config=config,
worker=worker,
lmcache_engine=engine,
local_cpu_backend=backend,
)
return sender, worker, engine, backend
class TestFullSyncSenderInit:
"""Test cases for FullSyncSender initialization."""
@pytest.mark.parametrize(
"batch_size,batch_interval_ms,startup_delay,max_retry",
[(100, 0, 0.0, 3), (500, 10, 2.0, 5)],
)
def test_init_config(self, batch_size, batch_interval_ms, startup_delay, max_retry):
"""Test initialization with various configs."""
config = create_test_config(
batch_size=batch_size,
batch_interval_ms=batch_interval_ms,
startup_delay_s=startup_delay,
max_retry_count=max_retry,
)
sender = FullSyncSender(
config=config,
worker=MockWorker(),
lmcache_engine=MockLMCacheEngine(),
local_cpu_backend=MockLocalCPUBackend(),
)
assert sender.batch_size == batch_size
assert sender.batch_interval_ms == batch_interval_ms
assert sender.startup_delay_range_s == startup_delay
assert sender.max_retry_count == max_retry
assert sender._is_syncing is False
class TestFullSyncSenderProperties:
"""Test cases for FullSyncSender properties."""
@pytest.mark.parametrize(
"prop,expected",
[
("instance_id", "test_instance"),
("worker_id", 0),
("location", "LocalCPUBackend"),
("is_syncing", False),
],
)
def test_properties(self, full_sync_sender, prop, expected):
"""Test all properties."""
sender, _, _, _ = full_sync_sender
assert getattr(sender, prop) == expected
class TestFullSyncSenderSyncId:
"""Test cases for sync ID generation."""
def test_generate_sync_id_format_and_uniqueness(self, full_sync_sender):
"""Test sync ID format and uniqueness."""
sender, _, _, _ = full_sync_sender
sync_ids = [sender._generate_sync_id() for _ in range(100)]
# Check format
assert all(s.startswith("test_instance_0_") for s in sync_ids)
# Check uniqueness
assert len(set(sync_ids)) == 100
class TestFullSyncSenderGetKeys:
"""Test cases for getting hot cache keys."""
@pytest.mark.parametrize("key_count", [0, 10])
def test_get_all_hot_cache_keys(self, key_count):
"""Test getting keys from cache."""
test_keys = [create_test_key(i) for i in range(key_count)]
sender = FullSyncSender(
config=create_test_config(),
worker=MockWorker(),
lmcache_engine=MockLMCacheEngine(),
local_cpu_backend=MockLocalCPUBackend(keys=test_keys),
)
keys = sender._get_all_hot_cache_keys()
assert len(keys) == key_count
assert all(isinstance(k, int) for k in keys)
class TestFullSyncSenderSendBatch:
"""Test cases for sending sync batch messages."""
def test_send_sync_batch(self, full_sync_sender):
"""Test sending batch messages."""
sender, worker, _, _ = full_sync_sender
for i in range(3):
sender._send_sync_batch("sync_123", i, [i * 3, i * 3 + 1, i * 3 + 2])
assert len(worker.messages) == 3
for i, msg in enumerate(worker.messages):
assert isinstance(msg, FullSyncBatchMsg)
assert msg.sync_id == "sync_123"
assert msg.batch_id == i
class TestFullSyncSenderSendEnd:
"""Test cases for sending sync end message."""
def test_send_sync_end(self, full_sync_sender):
"""Test sending end message."""
sender, worker, _, _ = full_sync_sender
sender._send_sync_end("sync_123", 1000)
assert len(worker.messages) == 1
msg = worker.messages[0]
assert isinstance(msg, FullSyncEndMsg)
assert msg.sync_id == "sync_123"
assert msg.actual_total_keys == 1000
class TestFullSyncSenderStartSync:
"""Test cases for the full sync process."""
@pytest.mark.asyncio
async def test_start_full_sync_empty_cache(self, full_sync_sender):
"""Test full sync with empty cache."""
sender, worker, engine, _ = full_sync_sender
worker.set_responses([H.start_ret("test"), H.status_ret("test")])
success = await sender.start_full_sync("test_reason")
assert success is True
assert sender.is_syncing is False
assert engine._freeze is False
@pytest.mark.asyncio
async def test_start_full_sync_with_keys(self):
"""Test full sync with keys in cache."""
config = create_test_config(batch_size=5)
worker = MockWorker()
engine = MockLMCacheEngine()
test_keys = [create_test_key(i) for i in range(12)] # 3 batches
sender = FullSyncSender(
config=config,
worker=worker,
lmcache_engine=engine,
local_cpu_backend=MockLocalCPUBackend(keys=test_keys),
)
worker.set_responses([H.start_ret("test"), H.status_ret("test")])
success = await sender.start_full_sync("test_reason")
assert success is True
batch_msgs = [m for m in worker.messages if isinstance(m, FullSyncBatchMsg)]
end_msgs = [m for m in worker.messages if isinstance(m, FullSyncEndMsg)]
assert len(batch_msgs) == 3
assert len(end_msgs) == 1
@pytest.mark.asyncio
async def test_start_full_sync_already_syncing(self, full_sync_sender):
"""Test that concurrent sync is prevented."""
sender, _, _, _ = full_sync_sender
sender._is_syncing = True
assert await sender.start_full_sync("test_reason") is False
@pytest.mark.asyncio
@pytest.mark.parametrize(
"responses,expected",
[
# All retries rejected
([H.start_ret("t", False, "Busy")] * 3, False),
# First fails, second succeeds
(
[H.start_ret("t", False, "Busy"), H.start_ret("t"), H.status_ret("t")],
True,
),
],
)
async def test_start_full_sync_retry(self, full_sync_sender, responses, expected):
"""Test sync retry scenarios."""
sender, worker, engine, _ = full_sync_sender
worker.set_responses(responses)
success = await sender.start_full_sync("test_reason")
assert success is expected
assert engine._freeze is False
@pytest.mark.asyncio
async def test_start_full_sync_freeze_mode(self, full_sync_sender):
"""Test that freeze mode is properly managed."""
sender, worker, engine, _ = full_sync_sender
worker.set_responses([H.start_ret("test"), H.status_ret("test")])
await sender.start_full_sync("test_reason")
assert True in engine.freeze_calls
assert False in engine.freeze_calls
assert engine.freeze_calls[-1] is False
class TestFullSyncSenderMessages:
"""Test cases for sync message methods."""
@pytest.mark.asyncio
@pytest.mark.parametrize(
"accepted,error_msg",
[(True, None), (False, "Already syncing")],
)
async def test_send_sync_start(self, full_sync_sender, accepted, error_msg):
"""Test sync start responses."""
sender, worker, _, _ = full_sync_sender
worker.set_responses([H.start_ret("sync_123", accepted, error_msg)])
ret = await sender._send_sync_start("sync_123", 100, 5)
assert ret.accepted is accepted
if error_msg:
assert ret.error_msg == error_msg
@pytest.mark.asyncio
@pytest.mark.parametrize(
"is_complete,progress,can_exit",
[(True, 0.85, True), (False, 0.3, False)],
)
async def test_query_sync_status(
self, full_sync_sender, is_complete, progress, can_exit
):
"""Test sync status queries."""
sender, worker, _, _ = full_sync_sender
worker.set_responses(
[H.status_ret("sync_123", is_complete, progress, can_exit)]
)
ret = await sender._query_sync_status("sync_123")
assert ret.is_complete is is_complete
assert ret.global_progress == progress
assert ret.can_exit_freeze is can_exit