TCPDashboard/tests/data/collector/test_collector_manager.py
2025-06-12 13:27:30 +08:00

354 lines
12 KiB
Python

"""
Unit tests for the CollectorManager class.
"""
import asyncio
import pytest
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock
from utils.logger import get_logger
from data.collector_manager import CollectorManager
from data.collector_types import ManagerStatus, CollectorConfig
from data.base_collector import BaseDataCollector, DataType, CollectorStatus
class MockDataCollector(BaseDataCollector):
"""Mock implementation of BaseDataCollector for testing."""
def __init__(self, exchange_name: str, symbols: list, auto_restart: bool = True):
super().__init__(exchange_name, symbols, [DataType.TICKER], auto_restart=auto_restart)
self.connected = False
self.subscribed = False
self.should_fail_connect = False
self.should_fail_subscribe = False
self.fail_count = 0
async def _actual_connect(self) -> bool:
"""Implementation of actual connection logic for testing."""
if self.should_fail_connect and self.fail_count < 2:
self.fail_count += 1
return False
await asyncio.sleep(0.01)
self.connected = True
return True
async def _actual_disconnect(self) -> None:
"""Implementation of actual disconnection logic for testing."""
await asyncio.sleep(0.01)
self.connected = False
self.subscribed = False
async def connect(self) -> bool:
"""Connect using the connection manager."""
return await self._connection_manager.connect(self._actual_connect)
async def disconnect(self) -> None:
"""Disconnect using the connection manager."""
await self._connection_manager.disconnect(self._actual_disconnect)
async def subscribe_to_data(self, symbols: list, data_types: list) -> bool:
if self.should_fail_subscribe:
return False
if not self.connected:
return False
self.subscribed = True
return True
async def unsubscribe_from_data(self, symbols: list, data_types: list) -> bool:
self.subscribed = False
return True
async def _process_message(self, message) -> None:
# No message processing in mock
pass
async def _handle_messages(self) -> None:
# Simulate light processing
await asyncio.sleep(0.1)
class TestCollectorManager:
"""Test cases for CollectorManager."""
@pytest.fixture
def manager(self):
"""Create a test manager instance."""
test_logger = get_logger()
return CollectorManager("test_manager", global_health_check_interval=1.0, logger=test_logger)
@pytest.fixture
def mock_collector(self):
"""Create a mock collector."""
return MockDataCollector("okx", ["BTC-USDT", "ETH-USDT"])
def test_initialization(self, manager):
"""Test manager initialization."""
assert manager.manager_name == "test_manager"
assert manager.status == ManagerStatus.STOPPED
assert len(manager._collectors) == 0
assert len(manager._enabled_collectors) == 0
def test_add_collector(self, manager, mock_collector):
"""Test adding a collector to the manager."""
# Add collector
manager.add_collector(mock_collector)
assert len(manager._collectors) == 1
assert len(manager._enabled_collectors) == 1
# Verify collector is in the collections
collector_names = manager.list_collectors()
assert len(collector_names) == 1
assert collector_names[0].startswith("okx_")
# Test with custom config using a different collector instance
mock_collector2 = MockDataCollector("binance", ["ETH-USDT"])
config = CollectorConfig(
name="custom_collector",
exchange="binance",
symbols=["ETH-USDT"],
data_types=["ticker"],
enabled=False
)
manager.add_collector(mock_collector2, config)
assert len(manager._collectors) == 2
assert len(manager._enabled_collectors) == 1 # Still 1 since second is disabled
def test_remove_collector(self, manager, mock_collector):
"""Test removing a collector from the manager."""
# Add then remove
manager.add_collector(mock_collector)
collector_names = manager.list_collectors()
collector_name = collector_names[0]
success = manager.remove_collector(collector_name)
assert success
assert len(manager._collectors) == 0
assert len(manager._enabled_collectors) == 0
# Test removing non-existent collector
success = manager.remove_collector("non_existent")
assert not success
def test_enable_disable_collector(self, manager, mock_collector):
"""Test enabling and disabling collectors."""
manager.add_collector(mock_collector)
collector_name = manager.list_collectors()[0]
# Initially enabled
assert collector_name in manager._enabled_collectors
# Disable
success = manager.disable_collector(collector_name)
assert success
assert collector_name not in manager._enabled_collectors
# Enable again
success = manager.enable_collector(collector_name)
assert success
assert collector_name in manager._enabled_collectors
# Test with non-existent collector
success = manager.enable_collector("non_existent")
assert not success
@pytest.mark.asyncio
async def test_start_stop_manager(self, manager, mock_collector):
"""Test starting and stopping the manager."""
# Add a collector
manager.add_collector(mock_collector)
# Start manager
success = await manager.start()
assert success
assert manager.status == ManagerStatus.RUNNING
# Wait a bit for collectors to start
await asyncio.sleep(0.2)
# Check collector is running
running_collectors = manager.get_running_collectors()
assert len(running_collectors) == 1
# Stop manager
await manager.stop()
assert manager.status == ManagerStatus.STOPPED
# Check collector is stopped
running_collectors = manager.get_running_collectors()
assert len(running_collectors) == 0
@pytest.mark.asyncio
async def test_restart_collector(self, manager, mock_collector):
"""Test restarting a specific collector."""
manager.add_collector(mock_collector)
await manager.start()
collector_name = manager.list_collectors()[0]
# Wait for collector to start
await asyncio.sleep(0.2)
# Restart the collector
success = await manager.restart_collector(collector_name)
assert success
# Check statistics
status = manager.get_status()
assert status['statistics']['restarts_performed'] >= 1
await manager.stop()
@pytest.mark.asyncio
async def test_health_monitoring(self, manager):
"""Test health monitoring and auto-restart functionality."""
# Create a collector that will fail initially
failing_collector = MockDataCollector("test", ["BTC-USDT"], auto_restart=True)
failing_collector.should_fail_connect = True
manager.add_collector(failing_collector)
await manager.start()
# Wait for health checks
await asyncio.sleep(2.5) # More than health check interval
# Check that restarts were attempted
status = manager.get_status()
failed_collectors = manager.get_failed_collectors()
# The collector should have been marked as failed and restart attempts made
assert len(failed_collectors) >= 0 # May have recovered
await manager.stop()
def test_get_status(self, manager, mock_collector):
"""Test status reporting."""
manager.add_collector(mock_collector)
status = manager.get_status()
assert status['manager_status'] == 'stopped'
assert status['total_collectors'] == 1
assert len(status['enabled_collectors']) == 1
assert 'statistics' in status
assert 'collectors' in status
def test_get_collector_status(self, manager, mock_collector):
"""Test getting individual collector status."""
manager.add_collector(mock_collector)
collector_name = manager.list_collectors()[0]
collector_status = manager.get_collector_status(collector_name)
assert collector_status is not None
assert collector_status['name'] == collector_name
assert 'config' in collector_status
assert 'status' in collector_status
assert 'health' in collector_status
# Test non-existent collector
non_existent_status = manager.get_collector_status("non_existent")
assert non_existent_status is None
@pytest.mark.asyncio
async def test_restart_all_collectors(self, manager):
"""Test restarting all collectors."""
# Add multiple collectors
collector1 = MockDataCollector("okx", ["BTC-USDT"])
collector2 = MockDataCollector("binance", ["ETH-USDT"])
manager.add_collector(collector1)
manager.add_collector(collector2)
await manager.start()
await asyncio.sleep(0.2) # Let them start
# Restart all
results = await manager.restart_all_collectors()
assert len(results) == 2
assert all(success for success in results.values())
await manager.stop()
def test_get_running_and_failed_collectors(self, manager, mock_collector):
"""Test getting running and failed collector lists."""
manager.add_collector(mock_collector)
# Initially no running collectors
running = manager.get_running_collectors()
failed = manager.get_failed_collectors()
assert len(running) == 0
# Note: failed might be empty since collector hasn't started yet
def test_collector_config(self):
"""Test CollectorConfig dataclass."""
config = CollectorConfig(
name="test_collector",
exchange="okx",
symbols=["BTC-USDT", "ETH-USDT"],
data_types=["ticker", "trade"],
auto_restart=True,
health_check_interval=30.0,
enabled=True
)
assert config.name == "test_collector"
assert config.exchange == "okx"
assert len(config.symbols) == 2
assert len(config.data_types) == 2
assert config.auto_restart is True
assert config.enabled is True
@pytest.mark.asyncio
async def test_manager_with_connection_failures():
"""Test manager handling collectors with connection failures."""
manager = CollectorManager("test_manager", global_health_check_interval=0.5)
# Create a collector that fails connection initially
failing_collector = MockDataCollector("failing_exchange", ["BTC-USDT"])
failing_collector.should_fail_connect = True
manager.add_collector(failing_collector)
# Start manager
success = await manager.start()
assert success # Manager should start even if collectors fail
# Wait for some health checks
await asyncio.sleep(1.5)
# Check that the failing collector is detected
failed_collectors = manager.get_failed_collectors()
status = manager.get_status()
# The collector should be in failed state or have restart attempts
assert status['statistics']['restarts_performed'] >= 0
await manager.stop()
@pytest.mark.asyncio
async def test_manager_graceful_shutdown():
"""Test that manager shuts down gracefully even with problematic collectors."""
manager = CollectorManager("test_manager")
# Add multiple collectors
for i in range(3):
collector = MockDataCollector(f"exchange_{i}", ["BTC-USDT"])
manager.add_collector(collector)
await manager.start()
await asyncio.sleep(0.2)
# Stop should complete even if collectors take time
await manager.stop()
assert manager.status == ManagerStatus.STOPPED
if __name__ == "__main__":
pytest.main([__file__, "-v"])