PMFlow/tests/test_events.py

341 lines
9.6 KiB
Python

"""
Neural Sync - Event Bus Unit Tests
"""
import pytest
import asyncio
from datetime import datetime
from backend.app.events import (
EventType,
Event,
EventBus,
get_event_bus,
event_bus_context,
publish_event,
subscribe_event,
)
@pytest.fixture
async def event_bus():
"""创建事件总线 fixture"""
bus = EventBus()
await bus.start()
yield bus
await bus.stop()
@pytest.fixture
async def event_bus_context_fixture():
"""创建事件总线上下文 fixture"""
async with event_bus_context() as bus:
yield bus
class TestEvent:
"""测试 Event 数据类"""
def test_create_event(self):
"""测试创建事件"""
event = Event(
type=EventType.SIGNAL_INGESTED,
data={"signal_id": "123", "content": "Test signal"},
source="test",
)
assert event.type == EventType.SIGNAL_INGESTED
assert event.data["signal_id"] == "123"
assert event.source == "test"
assert isinstance(event.timestamp, float)
def test_event_to_dict(self):
"""测试事件转字典"""
event = Event(
type=EventType.SIGNAL_ANALYZED,
data={"score": 0.95},
correlation_id="test-123",
)
event_dict = event.to_dict()
assert event_dict["type"] == "signal.analyzed"
assert event_dict["data"]["score"] == 0.95
assert event_dict["correlation_id"] == "test-123"
class TestEventBus:
"""测试 EventBus"""
@pytest.mark.asyncio
async def test_start_stop(self, event_bus):
"""测试启动和停止"""
assert event_bus._running is True
await event_bus.stop()
assert event_bus._running is False
@pytest.mark.asyncio
async def test_subscribe_and_publish(self, event_bus):
"""测试订阅和发布"""
received_events = []
async def handler(event: Event):
received_events.append(event)
# 订阅事件
unsubscribe = event_bus.subscribe(EventType.SIGNAL_INGESTED, handler)
# 发布事件
event = Event(
type=EventType.SIGNAL_INGESTED, data={"signal_id": "123"}
)
await event_bus.publish(event)
# 等待异步处理
await asyncio.sleep(0.1)
assert len(received_events) == 1
assert received_events[0].data["signal_id"] == "123"
# 取消订阅
unsubscribe()
# 发布另一个事件
await event_bus.publish(event)
await asyncio.sleep(0.1)
# 应该没有新事件
assert len(received_events) == 1
@pytest.mark.asyncio
async def test_wildcard_subscription(self, event_bus):
"""测试通配符订阅"""
received_events = []
async def handler(event: Event):
received_events.append(event.type)
event_bus.subscribe_wildcard(handler)
# 发布不同类型的事件
await event_bus.publish(
Event(type=EventType.SIGNAL_INGESTED, data={})
)
await event_bus.publish(
Event(type=EventType.TASK_ASSIGNED, data={})
)
await asyncio.sleep(0.1)
assert len(received_events) == 2
assert EventType.SIGNAL_INGESTED in received_events
assert EventType.TASK_ASSIGNED in received_events
@pytest.mark.asyncio
async def test_event_filter(self, event_bus):
"""测试事件过滤"""
received_events = []
async def handler(event: Event):
received_events.append(event)
# 只接收 signal_id > 100 的事件
def filter_func(event: Event):
return event.data.get("signal_id", 0) > 100
event_bus.subscribe(
EventType.SIGNAL_INGESTED, handler, filter_func=filter_func
)
# 发布事件
await event_bus.publish(
Event(type=EventType.SIGNAL_INGESTED, data={"signal_id": 50})
)
await event_bus.publish(
Event(type=EventType.SIGNAL_INGESTED, data={"signal_id": 150})
)
await asyncio.sleep(0.1)
# 只应该收到一个事件
assert len(received_events) == 1
assert received_events[0].data["signal_id"] == 150
@pytest.mark.asyncio
async def test_publish_and_wait(self, event_bus):
"""测试发布并等待处理完成"""
results = []
async def handler1(event: Event):
results.append("handler1")
await asyncio.sleep(0.1)
return "result1"
async def handler2(event: Event):
results.append("handler2")
return "result2"
event_bus.subscribe(EventType.SIGNAL_INGESTED, handler1)
event_bus.subscribe(EventType.SIGNAL_INGESTED, handler2)
event = Event(type=EventType.SIGNAL_INGESTED, data={})
handler_results = await event_bus.publish_and_wait(event, timeout=1.0)
assert len(results) == 2
assert len(handler_results) == 2
@pytest.mark.asyncio
async def test_metrics(self, event_bus):
"""测试事件指标"""
async def handler(event: Event):
pass
event_bus.subscribe(EventType.SIGNAL_INGESTED, handler)
await event_bus.publish(
Event(type=EventType.SIGNAL_INGESTED, data={})
)
await asyncio.sleep(0.1)
metrics = event_bus.metrics
assert metrics["published"] >= 1
assert metrics["delivered"] >= 1
@pytest.mark.asyncio
async def test_event_history(self, event_bus):
"""测试事件历史"""
event_bus._max_history = 5
# 发布超过历史限制的事件
for i in range(10):
await event_bus.publish(
Event(type=EventType.SIGNAL_INGESTED, data={"index": i})
)
history = event_bus.history
assert len(history) == 5
# 最新的事件应该在历史中
assert history[-1]["data"]["index"] == 9
@pytest.mark.asyncio
async def test_subscriber_count(self, event_bus):
"""测试订阅者计数"""
async def handler1(event: Event):
pass
async def handler2(event: Event):
pass
# 特定事件订阅
event_bus.subscribe(EventType.SIGNAL_INGESTED, handler1)
event_bus.subscribe(EventType.SIGNAL_INGESTED, handler2)
# 通配符订阅
event_bus.subscribe_wildcard(handler1)
assert event_bus.get_subscriber_count(EventType.SIGNAL_INGESTED) == 2
assert event_bus.get_subscriber_count() == 3 # 包括通配符订阅
@pytest.mark.asyncio
async def test_handler_error_handling(self, event_bus):
"""测试处理器错误处理"""
received_normal = []
async def failing_handler(event: Event):
raise ValueError("Test error")
async def normal_handler(event: Event):
received_normal.append(event)
event_bus.subscribe(EventType.SIGNAL_INGESTED, failing_handler)
event_bus.subscribe(EventType.SIGNAL_INGESTED, normal_handler)
await event_bus.publish(
Event(type=EventType.SIGNAL_INGESTED, data={})
)
await asyncio.sleep(0.1)
# 正常处理器应该仍然被调用
assert len(received_normal) == 1
# 错误应该被记录
assert event_bus.metrics["failed"] >= 1
class TestGlobalEventBus:
"""测试全局事件总线"""
@pytest.mark.asyncio
async def test_get_event_bus_singleton(self):
"""测试获取单例"""
bus1 = get_event_bus()
bus2 = get_event_bus()
assert bus1 is bus2
@pytest.mark.asyncio
async def test_convenience_functions(self):
"""测试便捷函数"""
received = []
async def handler(event: Event):
received.append(event)
# 使用便捷函数订阅
unsubscribe = subscribe_event(EventType.SIGNAL_INGESTED, handler)
# 使用便捷函数发布
await publish_event(EventType.SIGNAL_INGESTED, {"test": True})
await asyncio.sleep(0.1)
assert len(received) == 1
# 取消订阅
unsubscribe()
@pytest.mark.asyncio
async def test_event_bus_context(self):
"""测试事件总线上下文管理器"""
async with event_bus_context() as bus:
assert bus._running is True
received = []
async def handler(event: Event):
received.append(event)
bus.subscribe(EventType.SIGNAL_INGESTED, handler)
await bus.publish(Event(type=EventType.SIGNAL_INGESTED, data={}))
await asyncio.sleep(0.1)
assert len(received) == 1
# 退出上下文后应该停止
assert bus._running is False
@pytest.mark.asyncio
async def test_multiple_event_types():
"""测试多种事件类型"""
bus = EventBus()
await bus.start()
try:
counters = {event_type: 0 for event_type in EventType}
async def count_handler(event: Event):
counters[event.type] += 1
# 订阅所有事件类型
for event_type in EventType:
bus.subscribe(event_type, count_handler)
# 发布各种事件
await bus.publish(Event(type=EventType.SIGNAL_INGESTED, data={}))
await bus.publish(Event(type=EventType.TASK_ASSIGNED, data={}))
await bus.publish(Event(type=EventType.CODE_COMMIT, data={}))
await asyncio.sleep(0.1)
assert counters[EventType.SIGNAL_INGESTED] == 1
assert counters[EventType.TASK_ASSIGNED] == 1
assert counters[EventType.CODE_COMMIT] == 1
finally:
await bus.stop()
if __name__ == "__main__":
pytest.main([__file__, "-v"])