964 lines
No EOL
35 KiB
Python
964 lines
No EOL
35 KiB
Python
"""
|
|
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() |