ForcePilot/backend/test/unit/channel/worker/test_pool.py
Kris 6c95dc006a test: 新增渠道模块全链路单元测试用例与目录结构
完成渠道模块的单元测试目录搭建,新增多个领域模型、端口、中间件、事件、服务以及基础设施层的单元测试文件,同时补充了conftest.py的环境变量配置,完善测试基础环境。
2026-05-30 21:55:35 +08:00

228 lines
6.5 KiB
Python

from __future__ import annotations
import asyncio
from unittest.mock import AsyncMock, MagicMock
import pytest
from yuxi.channel.worker.pool import WorkerPool, WorkerPoolConfig
@pytest.fixture
def mock_queue_port():
port = MagicMock()
port.ensure_group = AsyncMock()
port.dequeue = AsyncMock(return_value=[])
port.ack = AsyncMock()
return port
@pytest.fixture
def worker_pool(mock_queue_port):
config = WorkerPoolConfig(num_workers=2, max_concurrent=5, poll_timeout_ms=1000)
return WorkerPool(
queue_port=mock_queue_port,
dispatch_fn=AsyncMock(),
config=config,
session_router=True,
)
@pytest.fixture
def legacy_worker_pool(mock_queue_port):
config = WorkerPoolConfig(num_workers=2, max_concurrent=5, poll_timeout_ms=1000)
return WorkerPool(
queue_port=mock_queue_port,
dispatch_fn=AsyncMock(),
config=config,
session_router=False,
)
@pytest.mark.unit
async def test_worker_pool_start_stop(worker_pool, mock_queue_port):
await worker_pool.start()
assert worker_pool.is_running is True
assert len(worker_pool._tasks) == 3
mock_queue_port.ensure_group.assert_awaited_once()
await worker_pool.stop()
assert worker_pool.is_running is False
assert len(worker_pool._tasks) == 0
@pytest.mark.unit
async def test_worker_pool_start_without_session_router(legacy_worker_pool, mock_queue_port):
await legacy_worker_pool.start()
assert legacy_worker_pool.is_running is True
assert len(legacy_worker_pool._tasks) == 2
await legacy_worker_pool.stop()
assert legacy_worker_pool.is_running is False
@pytest.mark.unit
async def test_worker_pool_start_without_ensure_group(mock_queue_port):
del mock_queue_port.ensure_group
config = WorkerPoolConfig(num_workers=1, max_concurrent=5, poll_timeout_ms=1000)
pool = WorkerPool(
queue_port=mock_queue_port,
dispatch_fn=AsyncMock(),
config=config,
session_router=True,
)
await pool.start()
assert pool.is_running is True
await pool.stop()
@pytest.mark.unit
async def test_dispatch_loop_routes_messages(worker_pool, mock_queue_port):
messages = [
{"session_id": "sess-1", "content": "msg1", "_stream_id": "stream-1"},
{"session_id": "sess-2", "content": "msg2", "_stream_id": "stream-2"},
]
call_count = 0
async def dequeue_side_effect(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
return messages
return []
mock_queue_port.dequeue = AsyncMock(side_effect=dequeue_side_effect)
await worker_pool.start()
await asyncio.sleep(0.2)
await worker_pool.stop()
assert mock_queue_port.dequeue.await_count >= 1
@pytest.mark.unit
async def test_worker_loop_dispatches_and_acks(worker_pool, mock_queue_port):
msg = {"session_id": "sess-1", "content": "hello", "_stream_id": "stream-1"}
call_count = 0
async def dequeue_side_effect(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
return [msg]
return []
mock_queue_port.dequeue = AsyncMock(side_effect=dequeue_side_effect)
await worker_pool.start()
await asyncio.sleep(0.3)
await worker_pool.stop()
worker_pool._dispatch.assert_awaited()
mock_queue_port.ack.assert_awaited_with("stream-1")
@pytest.mark.unit
async def test_worker_loop_handles_dispatch_error(worker_pool, mock_queue_port):
worker_pool._dispatch = AsyncMock(side_effect=Exception("dispatch failed"))
msg = {"session_id": "sess-1", "content": "hello", "_stream_id": "stream-1"}
call_count = 0
async def dequeue_side_effect(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
return [msg]
return []
mock_queue_port.dequeue = AsyncMock(side_effect=dequeue_side_effect)
await worker_pool.start()
await asyncio.sleep(0.3)
await worker_pool.stop()
worker_pool._dispatch.assert_awaited()
@pytest.mark.unit
async def test_route_with_session_id(worker_pool):
for i in range(worker_pool._config.num_workers):
worker_pool._session_queues.append(asyncio.Queue(maxsize=worker_pool._config.max_concurrent))
msg = {"session_id": "sess-abc", "content": "hello"}
idx = hash("sess-abc") % worker_pool._config.num_workers
queue = worker_pool._session_queues[idx]
await worker_pool._route(msg)
assert queue.qsize() == 1
assert queue.get_nowait() == msg
@pytest.mark.unit
async def test_route_without_session_id(worker_pool):
for i in range(worker_pool._config.num_workers):
worker_pool._session_queues.append(asyncio.Queue(maxsize=worker_pool._config.max_concurrent))
msg = {"content": "hello"}
initial_rr = worker_pool._rr_index
await worker_pool._route(msg)
assert worker_pool._rr_index == initial_rr + 1
assert worker_pool._session_queues[initial_rr % worker_pool._config.num_workers].qsize() == 1
@pytest.mark.unit
async def test_legacy_worker_loop(worker_pool, mock_queue_port):
worker_pool._session_router = False
msg = {"content": "legacy", "_stream_id": "stream-legacy"}
call_count = 0
async def dequeue_side_effect(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
return [msg]
return []
mock_queue_port.dequeue = AsyncMock(side_effect=dequeue_side_effect)
await worker_pool.start()
await asyncio.sleep(0.3)
await worker_pool.stop()
worker_pool._dispatch.assert_awaited()
mock_queue_port.ack.assert_awaited_with("stream-legacy")
@pytest.mark.unit
async def test_worker_loop_without_stream_id(worker_pool, mock_queue_port):
msg = {"session_id": "sess-1", "content": "no stream"}
call_count = 0
async def dequeue_side_effect(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
return [msg]
return []
mock_queue_port.dequeue = AsyncMock(side_effect=dequeue_side_effect)
await worker_pool.start()
await asyncio.sleep(0.3)
await worker_pool.stop()
worker_pool._dispatch.assert_awaited()
mock_queue_port.ack.assert_not_awaited()
@pytest.mark.unit
async def test_dispatch_loop_error_handling(worker_pool, mock_queue_port):
mock_queue_port.dequeue = AsyncMock(side_effect=Exception("dequeue error"))
await worker_pool.start()
await asyncio.sleep(0.3)
await worker_pool.stop()
assert mock_queue_port.dequeue.await_count >= 1