Refactor: PHASE 8: Testing & Integration
This commit is contained in:
parent
af34f4fd08
commit
9e8c6804a7
32 changed files with 17128 additions and 0 deletions
964
tests/unit/storage/test_redis_client.py
Normal file
964
tests/unit/storage/test_redis_client.py
Normal 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()
|
Loading…
Add table
Add a link
Reference in a new issue