""" 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()