This commit is contained in:
rUv
2025-06-07 11:44:19 +00:00
parent 43e92c5494
commit c378b705ca
95 changed files with 43677 additions and 0 deletions

View File

@@ -0,0 +1,729 @@
"""
Integration tests for real-time streaming pipeline.
Tests the complete real-time data flow from CSI collection to client delivery.
"""
import pytest
import asyncio
import numpy as np
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional, AsyncGenerator
from unittest.mock import AsyncMock, MagicMock, patch
import json
import queue
import threading
from dataclasses import dataclass
@dataclass
class StreamFrame:
"""Streaming frame data structure."""
frame_id: str
timestamp: datetime
router_id: str
pose_data: Dict[str, Any]
processing_time_ms: float
quality_score: float
class MockStreamBuffer:
"""Mock streaming buffer for testing."""
def __init__(self, max_size: int = 100):
self.max_size = max_size
self.buffer = asyncio.Queue(maxsize=max_size)
self.dropped_frames = 0
self.total_frames = 0
async def put_frame(self, frame: StreamFrame) -> bool:
"""Add frame to buffer."""
self.total_frames += 1
try:
self.buffer.put_nowait(frame)
return True
except asyncio.QueueFull:
self.dropped_frames += 1
return False
async def get_frame(self, timeout: float = 1.0) -> Optional[StreamFrame]:
"""Get frame from buffer."""
try:
return await asyncio.wait_for(self.buffer.get(), timeout=timeout)
except asyncio.TimeoutError:
return None
def get_stats(self) -> Dict[str, Any]:
"""Get buffer statistics."""
return {
"buffer_size": self.buffer.qsize(),
"max_size": self.max_size,
"total_frames": self.total_frames,
"dropped_frames": self.dropped_frames,
"drop_rate": self.dropped_frames / max(self.total_frames, 1)
}
class MockStreamProcessor:
"""Mock stream processor for testing."""
def __init__(self):
self.is_running = False
self.processing_rate = 30 # FPS
self.frame_counter = 0
self.error_rate = 0.0
async def start_processing(self, input_buffer: MockStreamBuffer, output_buffer: MockStreamBuffer):
"""Start stream processing."""
self.is_running = True
while self.is_running:
try:
# Get frame from input
frame = await input_buffer.get_frame(timeout=0.1)
if frame is None:
continue
# Simulate processing error
if np.random.random() < self.error_rate:
continue # Skip frame due to error
# Process frame
processed_frame = await self._process_frame(frame)
# Put to output buffer
await output_buffer.put_frame(processed_frame)
# Control processing rate
await asyncio.sleep(1.0 / self.processing_rate)
except Exception as e:
# Handle processing errors
continue
async def _process_frame(self, frame: StreamFrame) -> StreamFrame:
"""Process a single frame."""
# Simulate processing time
await asyncio.sleep(0.01)
# Add processing metadata
processed_pose_data = frame.pose_data.copy()
processed_pose_data["processed_at"] = datetime.utcnow().isoformat()
processed_pose_data["processor_id"] = "stream_processor_001"
return StreamFrame(
frame_id=f"processed_{frame.frame_id}",
timestamp=frame.timestamp,
router_id=frame.router_id,
pose_data=processed_pose_data,
processing_time_ms=frame.processing_time_ms + 10, # Add processing overhead
quality_score=frame.quality_score * 0.95 # Slight quality degradation
)
def stop_processing(self):
"""Stop stream processing."""
self.is_running = False
def set_error_rate(self, error_rate: float):
"""Set processing error rate."""
self.error_rate = error_rate
class MockWebSocketManager:
"""Mock WebSocket manager for testing."""
def __init__(self):
self.connected_clients = {}
self.message_queue = asyncio.Queue()
self.total_messages_sent = 0
self.failed_sends = 0
async def add_client(self, client_id: str, websocket_mock) -> bool:
"""Add WebSocket client."""
if client_id in self.connected_clients:
return False
self.connected_clients[client_id] = {
"websocket": websocket_mock,
"connected_at": datetime.utcnow(),
"messages_sent": 0,
"last_ping": datetime.utcnow()
}
return True
async def remove_client(self, client_id: str) -> bool:
"""Remove WebSocket client."""
if client_id in self.connected_clients:
del self.connected_clients[client_id]
return True
return False
async def broadcast_frame(self, frame: StreamFrame) -> Dict[str, bool]:
"""Broadcast frame to all connected clients."""
results = {}
message = {
"type": "pose_update",
"frame_id": frame.frame_id,
"timestamp": frame.timestamp.isoformat(),
"router_id": frame.router_id,
"pose_data": frame.pose_data,
"processing_time_ms": frame.processing_time_ms,
"quality_score": frame.quality_score
}
for client_id, client_info in self.connected_clients.items():
try:
# Simulate WebSocket send
success = await self._send_to_client(client_id, message)
results[client_id] = success
if success:
client_info["messages_sent"] += 1
self.total_messages_sent += 1
else:
self.failed_sends += 1
except Exception:
results[client_id] = False
self.failed_sends += 1
return results
async def _send_to_client(self, client_id: str, message: Dict[str, Any]) -> bool:
"""Send message to specific client."""
# Simulate network issues
if np.random.random() < 0.05: # 5% failure rate
return False
# Simulate send delay
await asyncio.sleep(0.001)
return True
def get_client_stats(self) -> Dict[str, Any]:
"""Get client statistics."""
return {
"connected_clients": len(self.connected_clients),
"total_messages_sent": self.total_messages_sent,
"failed_sends": self.failed_sends,
"clients": {
client_id: {
"messages_sent": info["messages_sent"],
"connected_duration": (datetime.utcnow() - info["connected_at"]).total_seconds()
}
for client_id, info in self.connected_clients.items()
}
}
class TestStreamingPipelineBasic:
"""Test basic streaming pipeline functionality."""
@pytest.fixture
def stream_buffer(self):
"""Create stream buffer."""
return MockStreamBuffer(max_size=50)
@pytest.fixture
def stream_processor(self):
"""Create stream processor."""
return MockStreamProcessor()
@pytest.fixture
def websocket_manager(self):
"""Create WebSocket manager."""
return MockWebSocketManager()
@pytest.fixture
def sample_frame(self):
"""Create sample stream frame."""
return StreamFrame(
frame_id="frame_001",
timestamp=datetime.utcnow(),
router_id="router_001",
pose_data={
"persons": [
{
"person_id": "person_1",
"confidence": 0.85,
"bounding_box": {"x": 100, "y": 150, "width": 80, "height": 180},
"activity": "standing"
}
],
"zone_summary": {"zone1": 1, "zone2": 0}
},
processing_time_ms=45.2,
quality_score=0.92
)
@pytest.mark.asyncio
async def test_buffer_frame_operations_should_fail_initially(self, stream_buffer, sample_frame):
"""Test buffer frame operations - should fail initially."""
# Put frame in buffer
result = await stream_buffer.put_frame(sample_frame)
# This will fail initially
assert result is True
# Get frame from buffer
retrieved_frame = await stream_buffer.get_frame()
assert retrieved_frame is not None
assert retrieved_frame.frame_id == sample_frame.frame_id
assert retrieved_frame.router_id == sample_frame.router_id
# Buffer should be empty now
empty_frame = await stream_buffer.get_frame(timeout=0.1)
assert empty_frame is None
@pytest.mark.asyncio
async def test_buffer_overflow_handling_should_fail_initially(self, sample_frame):
"""Test buffer overflow handling - should fail initially."""
small_buffer = MockStreamBuffer(max_size=2)
# Fill buffer to capacity
result1 = await small_buffer.put_frame(sample_frame)
result2 = await small_buffer.put_frame(sample_frame)
# This will fail initially
assert result1 is True
assert result2 is True
# Next frame should be dropped
result3 = await small_buffer.put_frame(sample_frame)
assert result3 is False
# Check statistics
stats = small_buffer.get_stats()
assert stats["total_frames"] == 3
assert stats["dropped_frames"] == 1
assert stats["drop_rate"] > 0
@pytest.mark.asyncio
async def test_stream_processing_should_fail_initially(self, stream_processor, sample_frame):
"""Test stream processing - should fail initially."""
input_buffer = MockStreamBuffer()
output_buffer = MockStreamBuffer()
# Add frame to input buffer
await input_buffer.put_frame(sample_frame)
# Start processing task
processing_task = asyncio.create_task(
stream_processor.start_processing(input_buffer, output_buffer)
)
# Wait for processing
await asyncio.sleep(0.2)
# Stop processing
stream_processor.stop_processing()
await processing_task
# Check output
processed_frame = await output_buffer.get_frame(timeout=0.1)
# This will fail initially
assert processed_frame is not None
assert processed_frame.frame_id.startswith("processed_")
assert "processed_at" in processed_frame.pose_data
assert processed_frame.processing_time_ms > sample_frame.processing_time_ms
@pytest.mark.asyncio
async def test_websocket_client_management_should_fail_initially(self, websocket_manager):
"""Test WebSocket client management - should fail initially."""
mock_websocket = MagicMock()
# Add client
result = await websocket_manager.add_client("client_001", mock_websocket)
# This will fail initially
assert result is True
assert "client_001" in websocket_manager.connected_clients
# Try to add duplicate client
result = await websocket_manager.add_client("client_001", mock_websocket)
assert result is False
# Remove client
result = await websocket_manager.remove_client("client_001")
assert result is True
assert "client_001" not in websocket_manager.connected_clients
@pytest.mark.asyncio
async def test_frame_broadcasting_should_fail_initially(self, websocket_manager, sample_frame):
"""Test frame broadcasting - should fail initially."""
# Add multiple clients
for i in range(3):
await websocket_manager.add_client(f"client_{i:03d}", MagicMock())
# Broadcast frame
results = await websocket_manager.broadcast_frame(sample_frame)
# This will fail initially
assert len(results) == 3
assert all(isinstance(success, bool) for success in results.values())
# Check statistics
stats = websocket_manager.get_client_stats()
assert stats["connected_clients"] == 3
assert stats["total_messages_sent"] >= 0
class TestStreamingPipelineIntegration:
"""Test complete streaming pipeline integration."""
@pytest.fixture
async def streaming_pipeline(self):
"""Create complete streaming pipeline."""
class StreamingPipeline:
def __init__(self):
self.input_buffer = MockStreamBuffer(max_size=100)
self.output_buffer = MockStreamBuffer(max_size=100)
self.processor = MockStreamProcessor()
self.websocket_manager = MockWebSocketManager()
self.is_running = False
self.processing_task = None
self.broadcasting_task = None
async def start(self):
"""Start the streaming pipeline."""
if self.is_running:
return False
self.is_running = True
# Start processing task
self.processing_task = asyncio.create_task(
self.processor.start_processing(self.input_buffer, self.output_buffer)
)
# Start broadcasting task
self.broadcasting_task = asyncio.create_task(
self._broadcast_loop()
)
return True
async def stop(self):
"""Stop the streaming pipeline."""
if not self.is_running:
return False
self.is_running = False
self.processor.stop_processing()
# Cancel tasks
if self.processing_task:
self.processing_task.cancel()
if self.broadcasting_task:
self.broadcasting_task.cancel()
return True
async def add_frame(self, frame: StreamFrame) -> bool:
"""Add frame to pipeline."""
return await self.input_buffer.put_frame(frame)
async def add_client(self, client_id: str, websocket_mock) -> bool:
"""Add WebSocket client."""
return await self.websocket_manager.add_client(client_id, websocket_mock)
async def _broadcast_loop(self):
"""Broadcasting loop."""
while self.is_running:
try:
frame = await self.output_buffer.get_frame(timeout=0.1)
if frame:
await self.websocket_manager.broadcast_frame(frame)
except asyncio.TimeoutError:
continue
except Exception:
continue
def get_pipeline_stats(self) -> Dict[str, Any]:
"""Get pipeline statistics."""
return {
"is_running": self.is_running,
"input_buffer": self.input_buffer.get_stats(),
"output_buffer": self.output_buffer.get_stats(),
"websocket_clients": self.websocket_manager.get_client_stats()
}
return StreamingPipeline()
@pytest.mark.asyncio
async def test_end_to_end_streaming_should_fail_initially(self, streaming_pipeline):
"""Test end-to-end streaming - should fail initially."""
# Start pipeline
result = await streaming_pipeline.start()
# This will fail initially
assert result is True
assert streaming_pipeline.is_running is True
# Add clients
for i in range(2):
await streaming_pipeline.add_client(f"client_{i}", MagicMock())
# Add frames
for i in range(5):
frame = StreamFrame(
frame_id=f"frame_{i:03d}",
timestamp=datetime.utcnow(),
router_id="router_001",
pose_data={"persons": [], "zone_summary": {}},
processing_time_ms=30.0,
quality_score=0.9
)
await streaming_pipeline.add_frame(frame)
# Wait for processing
await asyncio.sleep(0.5)
# Stop pipeline
await streaming_pipeline.stop()
# Check statistics
stats = streaming_pipeline.get_pipeline_stats()
assert stats["input_buffer"]["total_frames"] == 5
assert stats["websocket_clients"]["connected_clients"] == 2
@pytest.mark.asyncio
async def test_pipeline_performance_should_fail_initially(self, streaming_pipeline):
"""Test pipeline performance - should fail initially."""
await streaming_pipeline.start()
# Add multiple clients
for i in range(10):
await streaming_pipeline.add_client(f"client_{i:03d}", MagicMock())
# Measure throughput
start_time = datetime.utcnow()
frame_count = 50
for i in range(frame_count):
frame = StreamFrame(
frame_id=f"perf_frame_{i:03d}",
timestamp=datetime.utcnow(),
router_id="router_001",
pose_data={"persons": [], "zone_summary": {}},
processing_time_ms=25.0,
quality_score=0.88
)
await streaming_pipeline.add_frame(frame)
# Wait for processing
await asyncio.sleep(2.0)
end_time = datetime.utcnow()
duration = (end_time - start_time).total_seconds()
await streaming_pipeline.stop()
# This will fail initially
# Check performance metrics
stats = streaming_pipeline.get_pipeline_stats()
throughput = frame_count / duration
assert throughput > 10 # Should process at least 10 FPS
assert stats["input_buffer"]["drop_rate"] < 0.1 # Less than 10% drop rate
@pytest.mark.asyncio
async def test_pipeline_error_recovery_should_fail_initially(self, streaming_pipeline):
"""Test pipeline error recovery - should fail initially."""
await streaming_pipeline.start()
# Set high error rate
streaming_pipeline.processor.set_error_rate(0.5) # 50% error rate
# Add frames
for i in range(20):
frame = StreamFrame(
frame_id=f"error_frame_{i:03d}",
timestamp=datetime.utcnow(),
router_id="router_001",
pose_data={"persons": [], "zone_summary": {}},
processing_time_ms=30.0,
quality_score=0.9
)
await streaming_pipeline.add_frame(frame)
# Wait for processing
await asyncio.sleep(1.0)
await streaming_pipeline.stop()
# This will fail initially
# Pipeline should continue running despite errors
stats = streaming_pipeline.get_pipeline_stats()
assert stats["input_buffer"]["total_frames"] == 20
# Some frames should be processed despite errors
assert stats["output_buffer"]["total_frames"] > 0
class TestStreamingLatency:
"""Test streaming latency characteristics."""
@pytest.mark.asyncio
async def test_end_to_end_latency_should_fail_initially(self):
"""Test end-to-end latency - should fail initially."""
class LatencyTracker:
def __init__(self):
self.latencies = []
async def measure_latency(self, frame: StreamFrame) -> float:
"""Measure processing latency."""
start_time = datetime.utcnow()
# Simulate processing pipeline
await asyncio.sleep(0.05) # 50ms processing time
end_time = datetime.utcnow()
latency = (end_time - start_time).total_seconds() * 1000 # Convert to ms
self.latencies.append(latency)
return latency
tracker = LatencyTracker()
# Measure latency for multiple frames
for i in range(10):
frame = StreamFrame(
frame_id=f"latency_frame_{i}",
timestamp=datetime.utcnow(),
router_id="router_001",
pose_data={},
processing_time_ms=0,
quality_score=1.0
)
latency = await tracker.measure_latency(frame)
# This will fail initially
assert latency > 0
assert latency < 200 # Should be less than 200ms
# Check average latency
avg_latency = sum(tracker.latencies) / len(tracker.latencies)
assert avg_latency < 100 # Average should be less than 100ms
@pytest.mark.asyncio
async def test_concurrent_stream_handling_should_fail_initially(self):
"""Test concurrent stream handling - should fail initially."""
async def process_stream(stream_id: str, frame_count: int) -> Dict[str, Any]:
"""Process a single stream."""
buffer = MockStreamBuffer()
processed_frames = 0
for i in range(frame_count):
frame = StreamFrame(
frame_id=f"{stream_id}_frame_{i}",
timestamp=datetime.utcnow(),
router_id=stream_id,
pose_data={},
processing_time_ms=20.0,
quality_score=0.9
)
success = await buffer.put_frame(frame)
if success:
processed_frames += 1
await asyncio.sleep(0.01) # Simulate frame rate
return {
"stream_id": stream_id,
"processed_frames": processed_frames,
"total_frames": frame_count
}
# Process multiple streams concurrently
streams = ["router_001", "router_002", "router_003"]
tasks = [process_stream(stream_id, 20) for stream_id in streams]
results = await asyncio.gather(*tasks)
# This will fail initially
assert len(results) == 3
for result in results:
assert result["processed_frames"] == result["total_frames"]
assert result["stream_id"] in streams
class TestStreamingResilience:
"""Test streaming pipeline resilience."""
@pytest.mark.asyncio
async def test_client_disconnection_handling_should_fail_initially(self):
"""Test client disconnection handling - should fail initially."""
websocket_manager = MockWebSocketManager()
# Add clients
client_ids = [f"client_{i:03d}" for i in range(5)]
for client_id in client_ids:
await websocket_manager.add_client(client_id, MagicMock())
# Simulate frame broadcasting
frame = StreamFrame(
frame_id="disconnect_test_frame",
timestamp=datetime.utcnow(),
router_id="router_001",
pose_data={},
processing_time_ms=30.0,
quality_score=0.9
)
# Broadcast to all clients
results = await websocket_manager.broadcast_frame(frame)
# This will fail initially
assert len(results) == 5
# Simulate client disconnections
await websocket_manager.remove_client("client_001")
await websocket_manager.remove_client("client_003")
# Broadcast again
results = await websocket_manager.broadcast_frame(frame)
assert len(results) == 3 # Only remaining clients
# Check statistics
stats = websocket_manager.get_client_stats()
assert stats["connected_clients"] == 3
@pytest.mark.asyncio
async def test_memory_pressure_handling_should_fail_initially(self):
"""Test memory pressure handling - should fail initially."""
# Create small buffers to simulate memory pressure
small_buffer = MockStreamBuffer(max_size=5)
# Generate many frames quickly
frames_generated = 0
frames_accepted = 0
for i in range(20):
frame = StreamFrame(
frame_id=f"memory_pressure_frame_{i}",
timestamp=datetime.utcnow(),
router_id="router_001",
pose_data={},
processing_time_ms=25.0,
quality_score=0.85
)
frames_generated += 1
success = await small_buffer.put_frame(frame)
if success:
frames_accepted += 1
# This will fail initially
# Buffer should handle memory pressure gracefully
stats = small_buffer.get_stats()
assert stats["total_frames"] == frames_generated
assert stats["dropped_frames"] > 0 # Some frames should be dropped
assert frames_accepted <= small_buffer.max_size
# Drop rate should be reasonable
assert stats["drop_rate"] > 0.5 # More than 50% dropped due to small buffer