Refactor: PHASE 8: Testing & Integration

This commit is contained in:
ziesorx 2025-09-12 18:55:23 +07:00
parent af34f4fd08
commit 9e8c6804a7
32 changed files with 17128 additions and 0 deletions

View file

@ -0,0 +1,964 @@
"""
Unit tests for Redis client functionality.
"""
import pytest
import asyncio
import json
import base64
import time
from unittest.mock import Mock, MagicMock, patch, AsyncMock
from datetime import datetime, timedelta
import redis
import numpy as np
from detector_worker.storage.redis_client import (
RedisClient,
RedisConfig,
RedisConnectionPool,
RedisPublisher,
RedisSubscriber,
RedisImageStorage,
RedisError,
ConnectionPoolError
)
from detector_worker.detection.detection_result import DetectionResult, BoundingBox
from detector_worker.core.exceptions import ConfigurationError
class TestRedisConfig:
"""Test Redis configuration."""
def test_creation_minimal(self):
"""Test creating Redis config with minimal parameters."""
config = RedisConfig(
host="localhost"
)
assert config.host == "localhost"
assert config.port == 6379 # Default port
assert config.password is None
assert config.db == 0 # Default database
assert config.enabled is True
def test_creation_full(self):
"""Test creating Redis config with all parameters."""
config = RedisConfig(
host="redis.example.com",
port=6380,
password="secure_pass",
db=2,
enabled=True,
connection_timeout=5.0,
socket_timeout=3.0,
socket_connect_timeout=2.0,
max_connections=50,
retry_on_timeout=True,
health_check_interval=30
)
assert config.host == "redis.example.com"
assert config.port == 6380
assert config.password == "secure_pass"
assert config.db == 2
assert config.connection_timeout == 5.0
assert config.max_connections == 50
assert config.retry_on_timeout is True
def test_get_connection_params(self):
"""Test getting Redis connection parameters."""
config = RedisConfig(
host="localhost",
port=6379,
password="test_pass",
db=1,
connection_timeout=10.0
)
params = config.get_connection_params()
assert params["host"] == "localhost"
assert params["port"] == 6379
assert params["password"] == "test_pass"
assert params["db"] == 1
assert params["socket_timeout"] == 10.0
def test_from_dict(self):
"""Test creating config from dictionary."""
config_dict = {
"host": "redis-server",
"port": 6380,
"password": "secret",
"db": 3,
"max_connections": 100,
"unknown_field": "ignored"
}
config = RedisConfig.from_dict(config_dict)
assert config.host == "redis-server"
assert config.port == 6380
assert config.password == "secret"
assert config.db == 3
assert config.max_connections == 100
class TestRedisConnectionPool:
"""Test Redis connection pool management."""
def test_creation(self):
"""Test connection pool creation."""
config = RedisConfig(
host="localhost",
max_connections=20
)
pool = RedisConnectionPool(config)
assert pool.config == config
assert pool.pool is None
assert pool.is_connected is False
@pytest.mark.asyncio
async def test_connect_success(self):
"""Test successful connection to Redis."""
config = RedisConfig(host="localhost")
pool = RedisConnectionPool(config)
with patch('redis.ConnectionPool') as mock_pool_class:
mock_pool = Mock()
mock_pool_class.return_value = mock_pool
with patch('redis.Redis') as mock_redis_class:
mock_redis = Mock()
mock_redis.ping.return_value = True
mock_redis_class.return_value = mock_redis
await pool.connect()
assert pool.is_connected is True
assert pool.pool is not None
mock_pool_class.assert_called_once()
@pytest.mark.asyncio
async def test_connect_failure(self):
"""Test Redis connection failure."""
config = RedisConfig(host="nonexistent-redis")
pool = RedisConnectionPool(config)
with patch('redis.ConnectionPool') as mock_pool_class:
mock_pool_class.side_effect = redis.ConnectionError("Connection failed")
with pytest.raises(RedisError) as exc_info:
await pool.connect()
assert "Connection failed" in str(exc_info.value)
assert pool.is_connected is False
@pytest.mark.asyncio
async def test_disconnect(self):
"""Test Redis disconnection."""
config = RedisConfig(host="localhost")
pool = RedisConnectionPool(config)
# Mock connected state
mock_pool = Mock()
mock_redis = Mock()
pool.pool = mock_pool
pool._redis_client = mock_redis
pool.is_connected = True
await pool.disconnect()
assert pool.is_connected is False
assert pool.pool is None
mock_pool.disconnect.assert_called_once()
def test_get_client_connected(self):
"""Test getting Redis client when connected."""
config = RedisConfig(host="localhost")
pool = RedisConnectionPool(config)
mock_pool = Mock()
mock_redis = Mock()
pool.pool = mock_pool
pool._redis_client = mock_redis
pool.is_connected = True
client = pool.get_client()
assert client == mock_redis
def test_get_client_not_connected(self):
"""Test getting Redis client when not connected."""
config = RedisConfig(host="localhost")
pool = RedisConnectionPool(config)
with pytest.raises(RedisError) as exc_info:
pool.get_client()
assert "not connected" in str(exc_info.value).lower()
def test_health_check(self):
"""Test Redis health check."""
config = RedisConfig(host="localhost")
pool = RedisConnectionPool(config)
mock_redis = Mock()
mock_redis.ping.return_value = True
pool._redis_client = mock_redis
pool.is_connected = True
is_healthy = pool.health_check()
assert is_healthy is True
mock_redis.ping.assert_called_once()
def test_health_check_failure(self):
"""Test Redis health check failure."""
config = RedisConfig(host="localhost")
pool = RedisConnectionPool(config)
mock_redis = Mock()
mock_redis.ping.side_effect = redis.ConnectionError("Connection lost")
pool._redis_client = mock_redis
pool.is_connected = True
is_healthy = pool.health_check()
assert is_healthy is False
class TestRedisImageStorage:
"""Test Redis image storage functionality."""
def test_creation(self, mock_redis_client):
"""Test Redis image storage creation."""
storage = RedisImageStorage(mock_redis_client)
assert storage.redis_client == mock_redis_client
assert storage.default_expiry == 3600 # 1 hour
assert storage.compression_enabled is True
@pytest.mark.asyncio
async def test_store_image_success(self, mock_redis_client, mock_frame):
"""Test successful image storage."""
storage = RedisImageStorage(mock_redis_client)
mock_redis_client.set.return_value = True
mock_redis_client.expire.return_value = True
with patch('cv2.imencode') as mock_imencode:
# Mock successful encoding
encoded_data = np.array([1, 2, 3, 4], dtype=np.uint8)
mock_imencode.return_value = (True, encoded_data)
result = await storage.store_image("test_key", mock_frame, expire_seconds=600)
assert result is True
mock_redis_client.set.assert_called_once()
mock_redis_client.expire.assert_called_once_with("test_key", 600)
mock_imencode.assert_called_once()
@pytest.mark.asyncio
async def test_store_image_cropped(self, mock_redis_client, mock_frame):
"""Test storing cropped image."""
storage = RedisImageStorage(mock_redis_client)
mock_redis_client.set.return_value = True
mock_redis_client.expire.return_value = True
bbox = BoundingBox(x1=100, y1=200, x2=300, y2=400)
with patch('cv2.imencode') as mock_imencode:
encoded_data = np.array([1, 2, 3, 4], dtype=np.uint8)
mock_imencode.return_value = (True, encoded_data)
result = await storage.store_image("cropped_key", mock_frame, crop_bbox=bbox)
assert result is True
mock_redis_client.set.assert_called_once()
@pytest.mark.asyncio
async def test_store_image_encoding_failure(self, mock_redis_client, mock_frame):
"""Test image storage with encoding failure."""
storage = RedisImageStorage(mock_redis_client)
with patch('cv2.imencode') as mock_imencode:
# Mock encoding failure
mock_imencode.return_value = (False, None)
with pytest.raises(RedisError) as exc_info:
await storage.store_image("test_key", mock_frame)
assert "Failed to encode image" in str(exc_info.value)
mock_redis_client.set.assert_not_called()
@pytest.mark.asyncio
async def test_store_image_redis_failure(self, mock_redis_client, mock_frame):
"""Test image storage with Redis failure."""
storage = RedisImageStorage(mock_redis_client)
mock_redis_client.set.side_effect = redis.RedisError("Redis error")
with patch('cv2.imencode') as mock_imencode:
encoded_data = np.array([1, 2, 3, 4], dtype=np.uint8)
mock_imencode.return_value = (True, encoded_data)
with pytest.raises(RedisError) as exc_info:
await storage.store_image("test_key", mock_frame)
assert "Redis error" in str(exc_info.value)
@pytest.mark.asyncio
async def test_retrieve_image_success(self, mock_redis_client):
"""Test successful image retrieval."""
storage = RedisImageStorage(mock_redis_client)
# Mock encoded image data
original_image = np.ones((100, 100, 3), dtype=np.uint8) * 128
with patch('cv2.imencode') as mock_imencode:
encoded_data = np.array([1, 2, 3, 4], dtype=np.uint8)
mock_imencode.return_value = (True, encoded_data)
# Mock Redis returning base64 encoded data
base64_data = base64.b64encode(encoded_data.tobytes()).decode('utf-8')
mock_redis_client.get.return_value = base64_data
with patch('cv2.imdecode') as mock_imdecode:
mock_imdecode.return_value = original_image
retrieved_image = await storage.retrieve_image("test_key")
assert retrieved_image is not None
assert retrieved_image.shape == (100, 100, 3)
mock_redis_client.get.assert_called_once_with("test_key")
@pytest.mark.asyncio
async def test_retrieve_image_not_found(self, mock_redis_client):
"""Test image retrieval when key not found."""
storage = RedisImageStorage(mock_redis_client)
mock_redis_client.get.return_value = None
retrieved_image = await storage.retrieve_image("nonexistent_key")
assert retrieved_image is None
mock_redis_client.get.assert_called_once_with("nonexistent_key")
@pytest.mark.asyncio
async def test_delete_image(self, mock_redis_client):
"""Test image deletion."""
storage = RedisImageStorage(mock_redis_client)
mock_redis_client.delete.return_value = 1
result = await storage.delete_image("test_key")
assert result is True
mock_redis_client.delete.assert_called_once_with("test_key")
@pytest.mark.asyncio
async def test_delete_image_not_found(self, mock_redis_client):
"""Test deleting non-existent image."""
storage = RedisImageStorage(mock_redis_client)
mock_redis_client.delete.return_value = 0
result = await storage.delete_image("nonexistent_key")
assert result is False
mock_redis_client.delete.assert_called_once_with("nonexistent_key")
@pytest.mark.asyncio
async def test_bulk_delete_images(self, mock_redis_client):
"""Test bulk image deletion."""
storage = RedisImageStorage(mock_redis_client)
keys = ["key1", "key2", "key3"]
mock_redis_client.delete.return_value = 3
deleted_count = await storage.bulk_delete_images(keys)
assert deleted_count == 3
mock_redis_client.delete.assert_called_once_with(*keys)
@pytest.mark.asyncio
async def test_cleanup_expired_images(self, mock_redis_client):
"""Test cleanup of expired images."""
storage = RedisImageStorage(mock_redis_client)
# Mock scan to return image keys
mock_redis_client.scan_iter.return_value = [
b"inference:camera1:image1",
b"inference:camera2:image2",
b"inference:camera1:image3"
]
# Mock ttl to return different expiry times
mock_redis_client.ttl.side_effect = [-1, 100, -2] # No expiry, valid, expired
mock_redis_client.delete.return_value = 1
deleted_count = await storage.cleanup_expired_images("inference:*")
assert deleted_count == 1 # Only expired images deleted
mock_redis_client.delete.assert_called_once()
def test_get_image_info(self, mock_redis_client):
"""Test getting image metadata."""
storage = RedisImageStorage(mock_redis_client)
mock_redis_client.exists.return_value = 1
mock_redis_client.ttl.return_value = 1800 # 30 minutes
mock_redis_client.memory_usage.return_value = 4096 # 4KB
info = storage.get_image_info("test_key")
assert info["exists"] is True
assert info["ttl"] == 1800
assert info["size_bytes"] == 4096
mock_redis_client.exists.assert_called_once_with("test_key")
mock_redis_client.ttl.assert_called_once_with("test_key")
class TestRedisPublisher:
"""Test Redis publisher functionality."""
def test_creation(self, mock_redis_client):
"""Test Redis publisher creation."""
publisher = RedisPublisher(mock_redis_client)
assert publisher.redis_client == mock_redis_client
@pytest.mark.asyncio
async def test_publish_message_string(self, mock_redis_client):
"""Test publishing string message."""
publisher = RedisPublisher(mock_redis_client)
mock_redis_client.publish.return_value = 2 # 2 subscribers
result = await publisher.publish("test_channel", "Hello, Redis!")
assert result == 2
mock_redis_client.publish.assert_called_once_with("test_channel", "Hello, Redis!")
@pytest.mark.asyncio
async def test_publish_message_json(self, mock_redis_client):
"""Test publishing JSON message."""
publisher = RedisPublisher(mock_redis_client)
mock_redis_client.publish.return_value = 1
message_data = {
"camera_id": "camera_001",
"detection_class": "car",
"confidence": 0.95,
"timestamp": 1640995200000
}
result = await publisher.publish("detections", message_data)
assert result == 1
# Should have been JSON serialized
expected_json = json.dumps(message_data)
mock_redis_client.publish.assert_called_once_with("detections", expected_json)
@pytest.mark.asyncio
async def test_publish_detection_event(self, mock_redis_client):
"""Test publishing detection event."""
publisher = RedisPublisher(mock_redis_client)
mock_redis_client.publish.return_value = 3
detection = DetectionResult("car", 0.92, BoundingBox(100, 200, 300, 400), 1001, 1640995200000)
result = await publisher.publish_detection_event(
"camera_detections",
detection,
camera_id="camera_001",
session_id="session_123"
)
assert result == 3
# Verify the published message structure
call_args = mock_redis_client.publish.call_args
channel = call_args[0][0]
message_str = call_args[0][1]
message_data = json.loads(message_str)
assert channel == "camera_detections"
assert message_data["event_type"] == "detection"
assert message_data["camera_id"] == "camera_001"
assert message_data["session_id"] == "session_123"
assert message_data["detection"]["class"] == "car"
assert message_data["detection"]["confidence"] == 0.92
@pytest.mark.asyncio
async def test_publish_batch_messages(self, mock_redis_client):
"""Test publishing multiple messages in batch."""
publisher = RedisPublisher(mock_redis_client)
mock_pipeline = Mock()
mock_redis_client.pipeline.return_value = mock_pipeline
mock_pipeline.execute.return_value = [1, 2, 1] # Subscriber counts
messages = [
("channel1", "message1"),
("channel2", {"data": "message2"}),
("channel1", "message3")
]
results = await publisher.publish_batch(messages)
assert results == [1, 2, 1]
mock_redis_client.pipeline.assert_called_once()
assert mock_pipeline.publish.call_count == 3
mock_pipeline.execute.assert_called_once()
@pytest.mark.asyncio
async def test_publish_error_handling(self, mock_redis_client):
"""Test error handling in publishing."""
publisher = RedisPublisher(mock_redis_client)
mock_redis_client.publish.side_effect = redis.RedisError("Publish failed")
with pytest.raises(RedisError) as exc_info:
await publisher.publish("test_channel", "test_message")
assert "Publish failed" in str(exc_info.value)
class TestRedisSubscriber:
"""Test Redis subscriber functionality."""
def test_creation(self, mock_redis_client):
"""Test Redis subscriber creation."""
subscriber = RedisSubscriber(mock_redis_client)
assert subscriber.redis_client == mock_redis_client
assert subscriber.pubsub is None
assert subscriber.subscriptions == set()
@pytest.mark.asyncio
async def test_subscribe_to_channel(self, mock_redis_client):
"""Test subscribing to a channel."""
subscriber = RedisSubscriber(mock_redis_client)
mock_pubsub = Mock()
mock_redis_client.pubsub.return_value = mock_pubsub
await subscriber.subscribe("test_channel")
assert "test_channel" in subscriber.subscriptions
mock_pubsub.subscribe.assert_called_once_with("test_channel")
@pytest.mark.asyncio
async def test_subscribe_to_pattern(self, mock_redis_client):
"""Test subscribing to a pattern."""
subscriber = RedisSubscriber(mock_redis_client)
mock_pubsub = Mock()
mock_redis_client.pubsub.return_value = mock_pubsub
await subscriber.subscribe_pattern("detection:*")
assert "detection:*" in subscriber.subscriptions
mock_pubsub.psubscribe.assert_called_once_with("detection:*")
@pytest.mark.asyncio
async def test_unsubscribe_from_channel(self, mock_redis_client):
"""Test unsubscribing from a channel."""
subscriber = RedisSubscriber(mock_redis_client)
mock_pubsub = Mock()
mock_redis_client.pubsub.return_value = mock_pubsub
subscriber.pubsub = mock_pubsub
subscriber.subscriptions.add("test_channel")
await subscriber.unsubscribe("test_channel")
assert "test_channel" not in subscriber.subscriptions
mock_pubsub.unsubscribe.assert_called_once_with("test_channel")
@pytest.mark.asyncio
async def test_listen_for_messages(self, mock_redis_client):
"""Test listening for messages."""
subscriber = RedisSubscriber(mock_redis_client)
mock_pubsub = Mock()
mock_redis_client.pubsub.return_value = mock_pubsub
# Mock message stream
messages = [
{"type": "subscribe", "channel": "test", "data": 1},
{"type": "message", "channel": "test", "data": "Hello"},
{"type": "message", "channel": "test", "data": '{"key": "value"}'}
]
mock_pubsub.listen.return_value = iter(messages)
received_messages = []
message_count = 0
async for message in subscriber.listen():
received_messages.append(message)
message_count += 1
if message_count >= 2: # Only process actual messages
break
# Should receive 2 actual messages (excluding subscribe confirmation)
assert len(received_messages) == 2
assert received_messages[0]["data"] == "Hello"
assert received_messages[1]["data"] == {"key": "value"} # Should be parsed as JSON
@pytest.mark.asyncio
async def test_close_subscription(self, mock_redis_client):
"""Test closing subscription."""
subscriber = RedisSubscriber(mock_redis_client)
mock_pubsub = Mock()
subscriber.pubsub = mock_pubsub
subscriber.subscriptions = {"channel1", "pattern:*"}
await subscriber.close()
assert len(subscriber.subscriptions) == 0
mock_pubsub.close.assert_called_once()
assert subscriber.pubsub is None
class TestRedisClient:
"""Test main Redis client functionality."""
def test_initialization(self):
"""Test Redis client initialization."""
config = RedisConfig(host="localhost", port=6379)
client = RedisClient(config)
assert client.config == config
assert isinstance(client.connection_pool, RedisConnectionPool)
assert client.image_storage is None
assert client.publisher is None
assert client.subscriber is None
@pytest.mark.asyncio
async def test_connect_and_initialize_components(self):
"""Test connecting and initializing all components."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
with patch.object(client.connection_pool, 'connect', new_callable=AsyncMock) as mock_connect:
mock_redis_client = Mock()
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
await client.connect()
assert client.image_storage is not None
assert client.publisher is not None
assert client.subscriber is not None
assert isinstance(client.image_storage, RedisImageStorage)
assert isinstance(client.publisher, RedisPublisher)
assert isinstance(client.subscriber, RedisSubscriber)
mock_connect.assert_called_once()
@pytest.mark.asyncio
async def test_disconnect(self):
"""Test disconnection."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.is_connected = True
client.subscriber = Mock()
client.subscriber.close = AsyncMock()
with patch.object(client.connection_pool, 'disconnect', new_callable=AsyncMock) as mock_disconnect:
await client.disconnect()
client.subscriber.close.assert_called_once()
mock_disconnect.assert_called_once()
assert client.image_storage is None
assert client.publisher is None
assert client.subscriber is None
@pytest.mark.asyncio
async def test_store_and_retrieve_data(self, mock_redis_client):
"""Test storing and retrieving data."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
# Test storing data
mock_redis_client.set.return_value = True
result = await client.set("test_key", "test_value", expire_seconds=300)
assert result is True
mock_redis_client.set.assert_called_once_with("test_key", "test_value")
mock_redis_client.expire.assert_called_once_with("test_key", 300)
# Test retrieving data
mock_redis_client.get.return_value = "test_value"
value = await client.get("test_key")
assert value == "test_value"
mock_redis_client.get.assert_called_once_with("test_key")
@pytest.mark.asyncio
async def test_delete_keys(self, mock_redis_client):
"""Test deleting keys."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
mock_redis_client.delete.return_value = 2
result = await client.delete("key1", "key2")
assert result == 2
mock_redis_client.delete.assert_called_once_with("key1", "key2")
@pytest.mark.asyncio
async def test_exists_check(self, mock_redis_client):
"""Test checking key existence."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
mock_redis_client.exists.return_value = 1
exists = await client.exists("test_key")
assert exists is True
mock_redis_client.exists.assert_called_once_with("test_key")
@pytest.mark.asyncio
async def test_expire_key(self, mock_redis_client):
"""Test setting key expiration."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
mock_redis_client.expire.return_value = True
result = await client.expire("test_key", 600)
assert result is True
mock_redis_client.expire.assert_called_once_with("test_key", 600)
@pytest.mark.asyncio
async def test_get_ttl(self, mock_redis_client):
"""Test getting key TTL."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
mock_redis_client.ttl.return_value = 300
ttl = await client.ttl("test_key")
assert ttl == 300
mock_redis_client.ttl.assert_called_once_with("test_key")
@pytest.mark.asyncio
async def test_scan_keys(self, mock_redis_client):
"""Test scanning for keys."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
mock_redis_client.scan_iter.return_value = [b"key1", b"key2", b"key3"]
keys = await client.scan_keys("test:*")
assert keys == ["key1", "key2", "key3"]
mock_redis_client.scan_iter.assert_called_once_with(match="test:*")
@pytest.mark.asyncio
async def test_flush_database(self, mock_redis_client):
"""Test flushing database."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
mock_redis_client.flushdb.return_value = True
result = await client.flush_db()
assert result is True
mock_redis_client.flushdb.assert_called_once()
def test_get_connection_info(self):
"""Test getting connection information."""
config = RedisConfig(
host="redis.example.com",
port=6380,
db=2
)
client = RedisClient(config)
client.connection_pool.is_connected = True
info = client.get_connection_info()
assert info["connected"] is True
assert info["host"] == "redis.example.com"
assert info["port"] == 6380
assert info["database"] == 2
@pytest.mark.asyncio
async def test_pipeline_operations(self, mock_redis_client):
"""Test Redis pipeline operations."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
mock_pipeline = Mock()
mock_redis_client.pipeline.return_value = mock_pipeline
mock_pipeline.execute.return_value = [True, True, 1]
async with client.pipeline() as pipe:
pipe.set("key1", "value1")
pipe.set("key2", "value2")
pipe.delete("key3")
results = await pipe.execute()
assert results == [True, True, 1]
mock_redis_client.pipeline.assert_called_once()
mock_pipeline.execute.assert_called_once()
class TestRedisClientIntegration:
"""Integration tests for Redis client."""
@pytest.mark.asyncio
async def test_complete_image_workflow(self, mock_redis_client, mock_frame):
"""Test complete image storage workflow."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state and components
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
client.image_storage = RedisImageStorage(mock_redis_client)
client.publisher = RedisPublisher(mock_redis_client)
# Mock Redis operations
mock_redis_client.set.return_value = True
mock_redis_client.expire.return_value = True
mock_redis_client.publish.return_value = 2
with patch('cv2.imencode') as mock_imencode:
encoded_data = np.array([1, 2, 3, 4], dtype=np.uint8)
mock_imencode.return_value = (True, encoded_data)
# Store image
store_result = await client.image_storage.store_image(
"detection:camera001:1640995200:session123",
mock_frame,
expire_seconds=600
)
# Publish detection event
detection_event = {
"camera_id": "camera001",
"session_id": "session123",
"detection_class": "car",
"confidence": 0.95,
"timestamp": 1640995200000
}
publish_result = await client.publisher.publish("detections:camera001", detection_event)
assert store_result is True
assert publish_result == 2
# Verify Redis operations
mock_redis_client.set.assert_called_once()
mock_redis_client.expire.assert_called_once()
mock_redis_client.publish.assert_called_once()
@pytest.mark.asyncio
async def test_error_recovery_and_reconnection(self):
"""Test error recovery and reconnection."""
config = RedisConfig(host="localhost", retry_on_timeout=True)
client = RedisClient(config)
with patch.object(client.connection_pool, 'connect', new_callable=AsyncMock) as mock_connect:
with patch.object(client.connection_pool, 'health_check') as mock_health_check:
# First health check fails, second succeeds
mock_health_check.side_effect = [False, True]
# First connection attempt fails, second succeeds
mock_connect.side_effect = [RedisError("Connection failed"), None]
# Simulate connection recovery
try:
await client.connect()
except RedisError:
# Retry connection
await client.connect()
assert mock_connect.call_count == 2
@pytest.mark.asyncio
async def test_bulk_operations_performance(self, mock_redis_client):
"""Test bulk operations for performance."""
config = RedisConfig(host="localhost")
client = RedisClient(config)
# Mock connected state
client.connection_pool.get_client = Mock(return_value=mock_redis_client)
client.connection_pool.is_connected = True
client.publisher = RedisPublisher(mock_redis_client)
# Mock pipeline operations
mock_pipeline = Mock()
mock_redis_client.pipeline.return_value = mock_pipeline
mock_pipeline.execute.return_value = [1] * 100 # 100 successful operations
# Prepare bulk messages
messages = [
(f"channel_{i}", f"message_{i}")
for i in range(100)
]
start_time = time.time()
results = await client.publisher.publish_batch(messages)
execution_time = time.time() - start_time
assert len(results) == 100
assert all(result == 1 for result in results)
# Should be faster than individual operations
assert execution_time < 1.0 # Should complete in less than 1 second
# Pipeline should be used for efficiency
mock_redis_client.pipeline.assert_called_once()
assert mock_pipeline.publish.call_count == 100
mock_pipeline.execute.assert_called_once()