Refactor: PHASE 8: Testing & Integration
This commit is contained in:
parent
af34f4fd08
commit
9e8c6804a7
32 changed files with 17128 additions and 0 deletions
818
tests/unit/streams/test_stream_manager.py
Normal file
818
tests/unit/streams/test_stream_manager.py
Normal file
|
@ -0,0 +1,818 @@
|
|||
"""
|
||||
Unit tests for stream management functionality.
|
||||
"""
|
||||
import pytest
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||
import numpy as np
|
||||
import cv2
|
||||
|
||||
from detector_worker.streams.stream_manager import (
|
||||
StreamManager,
|
||||
StreamInfo,
|
||||
StreamConfig,
|
||||
StreamReader,
|
||||
StreamError,
|
||||
ConnectionError as StreamConnectionError
|
||||
)
|
||||
from detector_worker.streams.frame_reader import FrameReader
|
||||
from detector_worker.core.exceptions import ConfigurationError
|
||||
|
||||
|
||||
class TestStreamConfig:
|
||||
"""Test stream configuration."""
|
||||
|
||||
def test_creation_rtsp(self):
|
||||
"""Test creating RTSP stream config."""
|
||||
config = StreamConfig(
|
||||
stream_url="rtsp://example.com/stream1",
|
||||
stream_type="rtsp",
|
||||
target_fps=15,
|
||||
reconnect_interval=5.0,
|
||||
max_retries=3
|
||||
)
|
||||
|
||||
assert config.stream_url == "rtsp://example.com/stream1"
|
||||
assert config.stream_type == "rtsp"
|
||||
assert config.target_fps == 15
|
||||
assert config.reconnect_interval == 5.0
|
||||
assert config.max_retries == 3
|
||||
|
||||
def test_creation_http_snapshot(self):
|
||||
"""Test creating HTTP snapshot config."""
|
||||
config = StreamConfig(
|
||||
stream_url="http://example.com/snapshot.jpg",
|
||||
stream_type="http_snapshot",
|
||||
snapshot_interval=1.0,
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
assert config.stream_url == "http://example.com/snapshot.jpg"
|
||||
assert config.stream_type == "http_snapshot"
|
||||
assert config.snapshot_interval == 1.0
|
||||
assert config.timeout == 10.0
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test creating config from dictionary."""
|
||||
config_dict = {
|
||||
"stream_url": "rtsp://camera.example.com/live",
|
||||
"stream_type": "rtsp",
|
||||
"target_fps": 20,
|
||||
"reconnect_interval": 3.0,
|
||||
"max_retries": 5,
|
||||
"crop_region": [100, 200, 300, 400],
|
||||
"unknown_field": "ignored"
|
||||
}
|
||||
|
||||
config = StreamConfig.from_dict(config_dict)
|
||||
|
||||
assert config.stream_url == "rtsp://camera.example.com/live"
|
||||
assert config.target_fps == 20
|
||||
assert config.crop_region == [100, 200, 300, 400]
|
||||
|
||||
def test_validation(self):
|
||||
"""Test config validation."""
|
||||
# Valid config
|
||||
valid_config = StreamConfig(
|
||||
stream_url="rtsp://example.com/stream",
|
||||
stream_type="rtsp"
|
||||
)
|
||||
assert valid_config.is_valid() is True
|
||||
|
||||
# Invalid config (empty URL)
|
||||
invalid_config = StreamConfig(
|
||||
stream_url="",
|
||||
stream_type="rtsp"
|
||||
)
|
||||
assert invalid_config.is_valid() is False
|
||||
|
||||
|
||||
class TestStreamInfo:
|
||||
"""Test stream information."""
|
||||
|
||||
def test_creation(self):
|
||||
"""Test stream info creation."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
info = StreamInfo(
|
||||
stream_id="stream_001",
|
||||
config=config,
|
||||
camera_id="camera_001"
|
||||
)
|
||||
|
||||
assert info.stream_id == "stream_001"
|
||||
assert info.config == config
|
||||
assert info.camera_id == "camera_001"
|
||||
assert info.status == "inactive"
|
||||
assert info.reference_count == 0
|
||||
assert info.created_at <= time.time()
|
||||
|
||||
def test_increment_reference(self):
|
||||
"""Test incrementing reference count."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
info = StreamInfo("stream_001", config, "camera_001")
|
||||
|
||||
assert info.reference_count == 0
|
||||
|
||||
info.increment_reference()
|
||||
assert info.reference_count == 1
|
||||
|
||||
info.increment_reference()
|
||||
assert info.reference_count == 2
|
||||
|
||||
def test_decrement_reference(self):
|
||||
"""Test decrementing reference count."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
info = StreamInfo("stream_001", config, "camera_001")
|
||||
|
||||
info.reference_count = 3
|
||||
|
||||
assert info.decrement_reference() == 2
|
||||
assert info.reference_count == 2
|
||||
|
||||
assert info.decrement_reference() == 1
|
||||
assert info.decrement_reference() == 0
|
||||
|
||||
# Should not go below 0
|
||||
assert info.decrement_reference() == 0
|
||||
|
||||
def test_update_status(self):
|
||||
"""Test updating stream status."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
info = StreamInfo("stream_001", config, "camera_001")
|
||||
|
||||
info.update_status("connecting")
|
||||
assert info.status == "connecting"
|
||||
assert info.last_update <= time.time()
|
||||
|
||||
info.update_status("active", frame_count=100)
|
||||
assert info.status == "active"
|
||||
assert info.frame_count == 100
|
||||
|
||||
def test_get_stats(self):
|
||||
"""Test getting stream statistics."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
info = StreamInfo("stream_001", config, "camera_001")
|
||||
|
||||
info.frame_count = 1000
|
||||
info.error_count = 5
|
||||
info.reference_count = 2
|
||||
|
||||
stats = info.get_stats()
|
||||
|
||||
assert stats["stream_id"] == "stream_001"
|
||||
assert stats["status"] == "inactive"
|
||||
assert stats["frame_count"] == 1000
|
||||
assert stats["error_count"] == 5
|
||||
assert stats["reference_count"] == 2
|
||||
assert "uptime" in stats
|
||||
|
||||
|
||||
class TestStreamReader:
|
||||
"""Test stream reader functionality."""
|
||||
|
||||
def test_creation(self):
|
||||
"""Test stream reader creation."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
assert reader.stream_id == "stream_001"
|
||||
assert reader.config == config
|
||||
assert reader.is_running is False
|
||||
assert reader.latest_frame is None
|
||||
assert reader.frame_queue.qsize() == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_rtsp_stream(self):
|
||||
"""Test starting RTSP stream."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp", target_fps=10)
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
# Mock cv2.VideoCapture
|
||||
with patch('cv2.VideoCapture') as mock_cap:
|
||||
mock_cap_instance = Mock()
|
||||
mock_cap.return_value = mock_cap_instance
|
||||
mock_cap_instance.isOpened.return_value = True
|
||||
mock_cap_instance.read.return_value = (True, np.zeros((480, 640, 3), dtype=np.uint8))
|
||||
|
||||
await reader.start()
|
||||
|
||||
assert reader.is_running is True
|
||||
assert reader.capture is not None
|
||||
mock_cap.assert_called_once_with("rtsp://example.com/stream")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_rtsp_connection_failure(self):
|
||||
"""Test RTSP connection failure."""
|
||||
config = StreamConfig("rtsp://invalid.com/stream", "rtsp")
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
with patch('cv2.VideoCapture') as mock_cap:
|
||||
mock_cap_instance = Mock()
|
||||
mock_cap.return_value = mock_cap_instance
|
||||
mock_cap_instance.isOpened.return_value = False
|
||||
|
||||
with pytest.raises(StreamConnectionError):
|
||||
await reader.start()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_http_snapshot(self):
|
||||
"""Test starting HTTP snapshot stream."""
|
||||
config = StreamConfig("http://example.com/snapshot.jpg", "http_snapshot", snapshot_interval=1.0)
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
with patch('requests.get') as mock_get:
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b"fake_image_data"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with patch('cv2.imdecode') as mock_decode:
|
||||
mock_decode.return_value = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
|
||||
await reader.start()
|
||||
|
||||
assert reader.is_running is True
|
||||
mock_get.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_stream(self):
|
||||
"""Test stopping stream."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
# Simulate running state
|
||||
reader.is_running = True
|
||||
reader.capture = Mock()
|
||||
reader.capture.release = Mock()
|
||||
reader._reader_task = Mock()
|
||||
reader._reader_task.cancel = Mock()
|
||||
|
||||
await reader.stop()
|
||||
|
||||
assert reader.is_running is False
|
||||
reader.capture.release.assert_called_once()
|
||||
reader._reader_task.cancel.assert_called_once()
|
||||
|
||||
def test_get_latest_frame(self):
|
||||
"""Test getting latest frame."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
test_frame = np.ones((480, 640, 3), dtype=np.uint8) * 128
|
||||
reader.latest_frame = test_frame
|
||||
|
||||
frame = reader.get_latest_frame()
|
||||
|
||||
assert np.array_equal(frame, test_frame)
|
||||
|
||||
def test_get_frame_from_queue(self):
|
||||
"""Test getting frame from queue."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
test_frame = np.ones((480, 640, 3), dtype=np.uint8) * 128
|
||||
reader.frame_queue.put(test_frame)
|
||||
|
||||
frame = reader.get_frame(timeout=0.1)
|
||||
|
||||
assert np.array_equal(frame, test_frame)
|
||||
|
||||
def test_get_frame_timeout(self):
|
||||
"""Test getting frame with timeout."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
# Queue is empty, should timeout
|
||||
frame = reader.get_frame(timeout=0.1)
|
||||
|
||||
assert frame is None
|
||||
|
||||
def test_get_stats(self):
|
||||
"""Test getting reader statistics."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
reader = StreamReader("stream_001", config)
|
||||
|
||||
reader.frame_count = 500
|
||||
reader.error_count = 2
|
||||
|
||||
stats = reader.get_stats()
|
||||
|
||||
assert stats["stream_id"] == "stream_001"
|
||||
assert stats["frame_count"] == 500
|
||||
assert stats["error_count"] == 2
|
||||
assert stats["is_running"] is False
|
||||
|
||||
|
||||
class TestStreamManager:
|
||||
"""Test stream manager functionality."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test stream manager initialization."""
|
||||
manager = StreamManager()
|
||||
|
||||
assert len(manager.streams) == 0
|
||||
assert len(manager.readers) == 0
|
||||
assert manager.max_streams == 10
|
||||
assert manager.default_timeout == 30.0
|
||||
|
||||
def test_initialization_with_config(self):
|
||||
"""Test initialization with custom configuration."""
|
||||
config = {
|
||||
"max_streams": 20,
|
||||
"default_timeout": 60.0,
|
||||
"frame_buffer_size": 5
|
||||
}
|
||||
|
||||
manager = StreamManager(config)
|
||||
|
||||
assert manager.max_streams == 20
|
||||
assert manager.default_timeout == 60.0
|
||||
assert manager.frame_buffer_size == 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_stream_new(self):
|
||||
"""Test creating new stream."""
|
||||
manager = StreamManager()
|
||||
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock):
|
||||
stream_info = await manager.create_stream("camera_001", config, "sub_001")
|
||||
|
||||
assert "camera_001" in manager.streams
|
||||
assert manager.streams["camera_001"].reference_count == 1
|
||||
assert manager.streams["camera_001"].camera_id == "camera_001"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_stream_shared(self):
|
||||
"""Test creating shared stream (same URL)."""
|
||||
manager = StreamManager()
|
||||
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock):
|
||||
# Create first stream
|
||||
stream_info1 = await manager.create_stream("camera_001", config, "sub_001")
|
||||
|
||||
# Create second stream with same URL
|
||||
stream_info2 = await manager.create_stream("camera_001", config, "sub_002")
|
||||
|
||||
assert stream_info1 == stream_info2 # Should be same stream
|
||||
assert manager.streams["camera_001"].reference_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_stream_max_limit(self):
|
||||
"""Test creating stream when at max limit."""
|
||||
manager = StreamManager({"max_streams": 1})
|
||||
|
||||
config1 = StreamConfig("rtsp://example.com/stream1", "rtsp")
|
||||
config2 = StreamConfig("rtsp://example.com/stream2", "rtsp")
|
||||
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock):
|
||||
# Create first stream (should succeed)
|
||||
await manager.create_stream("camera_001", config1, "sub_001")
|
||||
|
||||
# Try to create second stream (should fail)
|
||||
with pytest.raises(StreamError) as exc_info:
|
||||
await manager.create_stream("camera_002", config2, "sub_002")
|
||||
|
||||
assert "maximum number of streams" in str(exc_info.value).lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_stream_single_reference(self):
|
||||
"""Test removing stream with single reference."""
|
||||
manager = StreamManager()
|
||||
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock):
|
||||
with patch.object(StreamReader, 'stop', new_callable=AsyncMock):
|
||||
# Create stream
|
||||
await manager.create_stream("camera_001", config, "sub_001")
|
||||
|
||||
# Remove stream
|
||||
removed = await manager.remove_stream("camera_001", "sub_001")
|
||||
|
||||
assert removed is True
|
||||
assert "camera_001" not in manager.streams
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_stream_multiple_references(self):
|
||||
"""Test removing stream with multiple references."""
|
||||
manager = StreamManager()
|
||||
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock):
|
||||
# Create shared stream
|
||||
await manager.create_stream("camera_001", config, "sub_001")
|
||||
await manager.create_stream("camera_001", config, "sub_002")
|
||||
|
||||
assert manager.streams["camera_001"].reference_count == 2
|
||||
|
||||
# Remove one reference
|
||||
removed = await manager.remove_stream("camera_001", "sub_001")
|
||||
|
||||
assert removed is True
|
||||
assert "camera_001" in manager.streams # Still exists
|
||||
assert manager.streams["camera_001"].reference_count == 1
|
||||
|
||||
def test_get_stream_info(self):
|
||||
"""Test getting stream information."""
|
||||
manager = StreamManager()
|
||||
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
stream_info = StreamInfo("camera_001", config, "camera_001")
|
||||
manager.streams["camera_001"] = stream_info
|
||||
|
||||
retrieved_info = manager.get_stream_info("camera_001")
|
||||
|
||||
assert retrieved_info == stream_info
|
||||
|
||||
def test_get_nonexistent_stream_info(self):
|
||||
"""Test getting info for non-existent stream."""
|
||||
manager = StreamManager()
|
||||
|
||||
info = manager.get_stream_info("nonexistent_camera")
|
||||
|
||||
assert info is None
|
||||
|
||||
def test_get_latest_frame(self):
|
||||
"""Test getting latest frame from stream."""
|
||||
manager = StreamManager()
|
||||
|
||||
# Create mock reader
|
||||
mock_reader = Mock()
|
||||
test_frame = np.ones((480, 640, 3), dtype=np.uint8) * 128
|
||||
mock_reader.get_latest_frame.return_value = test_frame
|
||||
|
||||
manager.readers["camera_001"] = mock_reader
|
||||
|
||||
frame = manager.get_latest_frame("camera_001")
|
||||
|
||||
assert np.array_equal(frame, test_frame)
|
||||
mock_reader.get_latest_frame.assert_called_once()
|
||||
|
||||
def test_get_frame_from_nonexistent_stream(self):
|
||||
"""Test getting frame from non-existent stream."""
|
||||
manager = StreamManager()
|
||||
|
||||
frame = manager.get_latest_frame("nonexistent_camera")
|
||||
|
||||
assert frame is None
|
||||
|
||||
def test_list_active_streams(self):
|
||||
"""Test listing active streams."""
|
||||
manager = StreamManager()
|
||||
|
||||
# Add streams
|
||||
config1 = StreamConfig("rtsp://example.com/stream1", "rtsp")
|
||||
config2 = StreamConfig("rtsp://example.com/stream2", "rtsp")
|
||||
|
||||
stream1 = StreamInfo("camera_001", config1, "camera_001")
|
||||
stream1.update_status("active")
|
||||
|
||||
stream2 = StreamInfo("camera_002", config2, "camera_002")
|
||||
stream2.update_status("inactive")
|
||||
|
||||
manager.streams["camera_001"] = stream1
|
||||
manager.streams["camera_002"] = stream2
|
||||
|
||||
active_streams = manager.list_active_streams()
|
||||
|
||||
assert len(active_streams) == 1
|
||||
assert active_streams[0]["camera_id"] == "camera_001"
|
||||
assert active_streams[0]["status"] == "active"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_all_streams(self):
|
||||
"""Test stopping all streams."""
|
||||
manager = StreamManager()
|
||||
|
||||
# Add mock streams
|
||||
mock_reader1 = Mock()
|
||||
mock_reader1.stop = AsyncMock()
|
||||
mock_reader2 = Mock()
|
||||
mock_reader2.stop = AsyncMock()
|
||||
|
||||
manager.readers["camera_001"] = mock_reader1
|
||||
manager.readers["camera_002"] = mock_reader2
|
||||
|
||||
stopped_count = await manager.stop_all_streams()
|
||||
|
||||
assert stopped_count == 2
|
||||
mock_reader1.stop.assert_called_once()
|
||||
mock_reader2.stop.assert_called_once()
|
||||
assert len(manager.readers) == 0
|
||||
assert len(manager.streams) == 0
|
||||
|
||||
def test_get_stream_statistics(self):
|
||||
"""Test getting stream statistics."""
|
||||
manager = StreamManager()
|
||||
|
||||
# Add streams
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
|
||||
stream1 = StreamInfo("camera_001", config, "camera_001")
|
||||
stream1.update_status("active")
|
||||
stream1.frame_count = 1000
|
||||
stream1.reference_count = 2
|
||||
|
||||
stream2 = StreamInfo("camera_002", config, "camera_002")
|
||||
stream2.update_status("error")
|
||||
stream2.error_count = 5
|
||||
|
||||
manager.streams["camera_001"] = stream1
|
||||
manager.streams["camera_002"] = stream2
|
||||
|
||||
stats = manager.get_stream_statistics()
|
||||
|
||||
assert stats["total_streams"] == 2
|
||||
assert stats["active_streams"] == 1
|
||||
assert stats["error_streams"] == 1
|
||||
assert stats["total_references"] == 2
|
||||
assert "status_breakdown" in stats
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconnect_stream(self):
|
||||
"""Test reconnecting failed stream."""
|
||||
manager = StreamManager()
|
||||
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
stream_info = StreamInfo("camera_001", config, "camera_001")
|
||||
stream_info.update_status("error")
|
||||
manager.streams["camera_001"] = stream_info
|
||||
|
||||
# Mock reader
|
||||
mock_reader = Mock()
|
||||
mock_reader.start = AsyncMock()
|
||||
mock_reader.stop = AsyncMock()
|
||||
manager.readers["camera_001"] = mock_reader
|
||||
|
||||
result = await manager.reconnect_stream("camera_001")
|
||||
|
||||
assert result is True
|
||||
mock_reader.stop.assert_called_once()
|
||||
mock_reader.start.assert_called_once()
|
||||
assert stream_info.status != "error"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_streams(self):
|
||||
"""Test health check of all streams."""
|
||||
manager = StreamManager()
|
||||
|
||||
# Add streams with different states
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
|
||||
stream1 = StreamInfo("camera_001", config, "camera_001")
|
||||
stream1.update_status("active")
|
||||
|
||||
stream2 = StreamInfo("camera_002", config, "camera_002")
|
||||
stream2.update_status("error")
|
||||
|
||||
manager.streams["camera_001"] = stream1
|
||||
manager.streams["camera_002"] = stream2
|
||||
|
||||
# Mock readers
|
||||
mock_reader1 = Mock()
|
||||
mock_reader1.is_running = True
|
||||
mock_reader2 = Mock()
|
||||
mock_reader2.is_running = False
|
||||
|
||||
manager.readers["camera_001"] = mock_reader1
|
||||
manager.readers["camera_002"] = mock_reader2
|
||||
|
||||
health_report = await manager.health_check()
|
||||
|
||||
assert health_report["total_streams"] == 2
|
||||
assert health_report["healthy_streams"] == 1
|
||||
assert health_report["unhealthy_streams"] == 1
|
||||
assert len(health_report["unhealthy_stream_ids"]) == 1
|
||||
|
||||
|
||||
class TestStreamManagerIntegration:
|
||||
"""Integration tests for stream manager."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_subscribers_same_stream(self):
|
||||
"""Test multiple subscribers to same stream."""
|
||||
manager = StreamManager()
|
||||
|
||||
config = StreamConfig("rtsp://example.com/shared_stream", "rtsp")
|
||||
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock):
|
||||
# Multiple subscribers to same stream
|
||||
stream1 = await manager.create_stream("camera_001", config, "sub_001")
|
||||
stream2 = await manager.create_stream("camera_001", config, "sub_002")
|
||||
stream3 = await manager.create_stream("camera_001", config, "sub_003")
|
||||
|
||||
# All should reference same stream
|
||||
assert stream1 == stream2 == stream3
|
||||
assert manager.streams["camera_001"].reference_count == 3
|
||||
assert len(manager.readers) == 1 # Only one actual reader
|
||||
|
||||
# Remove subscribers one by one
|
||||
with patch.object(StreamReader, 'stop', new_callable=AsyncMock) as mock_stop:
|
||||
await manager.remove_stream("camera_001", "sub_001") # ref_count = 2
|
||||
await manager.remove_stream("camera_001", "sub_002") # ref_count = 1
|
||||
|
||||
# Stream should still exist
|
||||
assert "camera_001" in manager.streams
|
||||
mock_stop.assert_not_called()
|
||||
|
||||
await manager.remove_stream("camera_001", "sub_003") # ref_count = 0
|
||||
|
||||
# Now stream should be stopped and removed
|
||||
assert "camera_001" not in manager.streams
|
||||
mock_stop.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_failure_and_recovery(self):
|
||||
"""Test stream failure and recovery workflow."""
|
||||
manager = StreamManager()
|
||||
|
||||
config = StreamConfig("rtsp://unreliable.com/stream", "rtsp", max_retries=2)
|
||||
|
||||
# Mock reader that fails initially then succeeds
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock) as mock_start:
|
||||
mock_start.side_effect = [
|
||||
StreamConnectionError("Connection failed"), # First attempt fails
|
||||
None # Second attempt succeeds
|
||||
]
|
||||
|
||||
# First attempt should fail
|
||||
with pytest.raises(StreamConnectionError):
|
||||
await manager.create_stream("camera_001", config, "sub_001")
|
||||
|
||||
# Retry should succeed
|
||||
stream_info = await manager.create_stream("camera_001", config, "sub_001")
|
||||
|
||||
assert stream_info is not None
|
||||
assert mock_start.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_stream_operations(self):
|
||||
"""Test concurrent stream operations."""
|
||||
manager = StreamManager()
|
||||
|
||||
configs = [
|
||||
StreamConfig(f"rtsp://example.com/stream{i}", "rtsp")
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock):
|
||||
with patch.object(StreamReader, 'stop', new_callable=AsyncMock):
|
||||
# Create streams concurrently
|
||||
create_tasks = [
|
||||
manager.create_stream(f"camera_{i}", configs[i], f"sub_{i}")
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
results = await asyncio.gather(*create_tasks)
|
||||
|
||||
assert len(results) == 5
|
||||
assert len(manager.streams) == 5
|
||||
|
||||
# Remove streams concurrently
|
||||
remove_tasks = [
|
||||
manager.remove_stream(f"camera_{i}", f"sub_{i}")
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
remove_results = await asyncio.gather(*remove_tasks)
|
||||
|
||||
assert all(remove_results)
|
||||
assert len(manager.streams) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_memory_management_large_scale(self):
|
||||
"""Test memory management with many streams."""
|
||||
manager = StreamManager({"max_streams": 50})
|
||||
|
||||
# Create many streams
|
||||
with patch.object(StreamReader, 'start', new_callable=AsyncMock):
|
||||
for i in range(30):
|
||||
config = StreamConfig(f"rtsp://example.com/stream{i}", "rtsp")
|
||||
await manager.create_stream(f"camera_{i}", config, f"sub_{i}")
|
||||
|
||||
# Verify memory usage is reasonable
|
||||
stats = manager.get_stream_statistics()
|
||||
assert stats["total_streams"] == 30
|
||||
assert stats["active_streams"] <= 30
|
||||
|
||||
# Test bulk cleanup
|
||||
with patch.object(StreamReader, 'stop', new_callable=AsyncMock):
|
||||
stopped_count = await manager.stop_all_streams()
|
||||
|
||||
assert stopped_count == 30
|
||||
assert len(manager.streams) == 0
|
||||
assert len(manager.readers) == 0
|
||||
|
||||
|
||||
class TestFrameReaderIntegration:
|
||||
"""Integration tests for frame reader."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rtsp_frame_processing(self):
|
||||
"""Test RTSP frame processing pipeline."""
|
||||
config = StreamConfig(
|
||||
stream_url="rtsp://example.com/stream",
|
||||
stream_type="rtsp",
|
||||
target_fps=10,
|
||||
crop_region=[100, 100, 400, 300]
|
||||
)
|
||||
|
||||
reader = StreamReader("test_stream", config)
|
||||
|
||||
# Mock cv2.VideoCapture
|
||||
with patch('cv2.VideoCapture') as mock_cap:
|
||||
mock_cap_instance = Mock()
|
||||
mock_cap.return_value = mock_cap_instance
|
||||
mock_cap_instance.isOpened.return_value = True
|
||||
|
||||
# Mock frame sequence
|
||||
test_frame = np.ones((480, 640, 3), dtype=np.uint8) * 128
|
||||
mock_cap_instance.read.side_effect = [
|
||||
(True, test_frame), # First frame
|
||||
(True, test_frame * 0.8), # Second frame
|
||||
(False, None), # Connection lost
|
||||
(True, test_frame * 1.2), # Reconnected
|
||||
]
|
||||
|
||||
await reader.start()
|
||||
|
||||
# Let reader process some frames
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Verify frame processing
|
||||
latest_frame = reader.get_latest_frame()
|
||||
assert latest_frame is not None
|
||||
assert latest_frame.shape == (480, 640, 3)
|
||||
|
||||
await reader.stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_snapshot_processing(self):
|
||||
"""Test HTTP snapshot processing."""
|
||||
config = StreamConfig(
|
||||
stream_url="http://camera.example.com/snapshot.jpg",
|
||||
stream_type="http_snapshot",
|
||||
snapshot_interval=0.5,
|
||||
timeout=5.0
|
||||
)
|
||||
|
||||
reader = StreamReader("snapshot_stream", config)
|
||||
|
||||
with patch('requests.get') as mock_get:
|
||||
# Mock HTTP responses
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.content = b"fake_jpeg_data"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
with patch('cv2.imdecode') as mock_decode:
|
||||
test_frame = np.ones((480, 640, 3), dtype=np.uint8) * 200
|
||||
mock_decode.return_value = test_frame
|
||||
|
||||
await reader.start()
|
||||
|
||||
# Wait for snapshot capture
|
||||
await asyncio.sleep(0.6)
|
||||
|
||||
# Verify snapshot processing
|
||||
latest_frame = reader.get_latest_frame()
|
||||
assert latest_frame is not None
|
||||
assert np.array_equal(latest_frame, test_frame)
|
||||
|
||||
await reader.stop()
|
||||
|
||||
def test_frame_queue_management(self):
|
||||
"""Test frame queue management and buffering."""
|
||||
config = StreamConfig("rtsp://example.com/stream", "rtsp")
|
||||
reader = StreamReader("queue_test", config, frame_buffer_size=3)
|
||||
|
||||
# Add frames to queue
|
||||
frames = [
|
||||
np.ones((100, 100, 3), dtype=np.uint8) * i
|
||||
for i in range(50, 250, 50) # 4 different frames
|
||||
]
|
||||
|
||||
for frame in frames[:3]: # Fill buffer
|
||||
reader._add_frame_to_queue(frame)
|
||||
|
||||
assert reader.frame_queue.qsize() == 3
|
||||
|
||||
# Add one more (should drop oldest)
|
||||
reader._add_frame_to_queue(frames[3])
|
||||
assert reader.frame_queue.qsize() == 3
|
||||
|
||||
# Verify frame order (oldest should be dropped)
|
||||
retrieved_frames = []
|
||||
while not reader.frame_queue.empty():
|
||||
retrieved_frames.append(reader.get_frame(timeout=0.1))
|
||||
|
||||
assert len(retrieved_frames) == 3
|
||||
# First frame should have been dropped, so we should have frames 1,2,3
|
||||
assert not np.array_equal(retrieved_frames[0], frames[0])
|
Loading…
Add table
Add a link
Reference in a new issue