""" Unit tests for session cache management. """ import pytest import time import uuid from unittest.mock import Mock, patch from datetime import datetime, timedelta from collections import defaultdict from detector_worker.storage.session_cache import ( SessionCache, SessionCacheManager, SessionData, CacheConfig, CacheEntry, CacheStats, SessionError, CacheError ) from detector_worker.detection.detection_result import DetectionResult, BoundingBox class TestCacheConfig: """Test cache configuration.""" def test_creation_default(self): """Test creating cache config with default values.""" config = CacheConfig() assert config.max_size == 1000 assert config.ttl_seconds == 3600 # 1 hour assert config.cleanup_interval == 300 # 5 minutes assert config.eviction_policy == "lru" assert config.enable_persistence is False def test_creation_custom(self): """Test creating cache config with custom values.""" config = CacheConfig( max_size=5000, ttl_seconds=7200, cleanup_interval=600, eviction_policy="lfu", enable_persistence=True, persistence_path="/tmp/cache" ) assert config.max_size == 5000 assert config.ttl_seconds == 7200 assert config.cleanup_interval == 600 assert config.eviction_policy == "lfu" assert config.enable_persistence is True assert config.persistence_path == "/tmp/cache" def test_from_dict(self): """Test creating config from dictionary.""" config_dict = { "max_size": 2000, "ttl_seconds": 1800, "eviction_policy": "fifo", "enable_persistence": True, "unknown_field": "ignored" } config = CacheConfig.from_dict(config_dict) assert config.max_size == 2000 assert config.ttl_seconds == 1800 assert config.eviction_policy == "fifo" assert config.enable_persistence is True class TestCacheEntry: """Test cache entry data structure.""" def test_creation(self): """Test cache entry creation.""" data = {"key": "value", "number": 42} entry = CacheEntry(data, ttl_seconds=600) assert entry.data == data assert entry.ttl_seconds == 600 assert entry.created_at <= time.time() assert entry.last_accessed <= time.time() assert entry.access_count == 1 assert entry.size > 0 def test_is_expired(self): """Test expiration checking.""" # Non-expired entry entry = CacheEntry({"data": "test"}, ttl_seconds=600) assert entry.is_expired() is False # Expired entry (simulate by setting old creation time) entry.created_at = time.time() - 700 # Created 700 seconds ago assert entry.is_expired() is True # Entry without expiration entry_no_ttl = CacheEntry({"data": "test"}) assert entry_no_ttl.is_expired() is False def test_touch(self): """Test updating access time and count.""" entry = CacheEntry({"data": "test"}) original_access_time = entry.last_accessed original_access_count = entry.access_count time.sleep(0.01) # Small delay entry.touch() assert entry.last_accessed > original_access_time assert entry.access_count == original_access_count + 1 def test_age(self): """Test age calculation.""" entry = CacheEntry({"data": "test"}) time.sleep(0.01) # Small delay age = entry.age() assert age > 0 assert age < 1 # Should be less than 1 second def test_size_estimation(self): """Test size estimation.""" small_entry = CacheEntry({"key": "value"}) large_entry = CacheEntry({"key": "x" * 1000, "data": list(range(100))}) assert large_entry.size > small_entry.size class TestSessionData: """Test session data structure.""" def test_creation(self): """Test session data creation.""" session_data = SessionData( session_id="session_123", camera_id="camera_001", display_id="display_001" ) assert session_data.session_id == "session_123" assert session_data.camera_id == "camera_001" assert session_data.display_id == "display_001" assert session_data.created_at <= time.time() assert session_data.last_activity <= time.time() assert session_data.detection_data == {} assert session_data.metadata == {} def test_update_activity(self): """Test updating last activity.""" session_data = SessionData("session_123", "camera_001", "display_001") original_activity = session_data.last_activity time.sleep(0.01) session_data.update_activity() assert session_data.last_activity > original_activity def test_add_detection_data(self): """Test adding detection data.""" session_data = SessionData("session_123", "camera_001", "display_001") detection_data = { "class": "car", "confidence": 0.95, "bbox": [100, 200, 300, 400] } session_data.add_detection_data("main_detection", detection_data) assert "main_detection" in session_data.detection_data assert session_data.detection_data["main_detection"] == detection_data def test_add_metadata(self): """Test adding metadata.""" session_data = SessionData("session_123", "camera_001", "display_001") session_data.add_metadata("model_version", "v2.1") session_data.add_metadata("inference_time", 0.15) assert session_data.metadata["model_version"] == "v2.1" assert session_data.metadata["inference_time"] == 0.15 def test_is_expired(self): """Test session expiration.""" session_data = SessionData("session_123", "camera_001", "display_001") # Not expired with default timeout assert session_data.is_expired() is False # Expired with short timeout assert session_data.is_expired(timeout_seconds=0.001) is True def test_to_dict(self): """Test converting session to dictionary.""" session_data = SessionData("session_123", "camera_001", "display_001") session_data.add_detection_data("detection", {"class": "car", "confidence": 0.9}) session_data.add_metadata("model_id", "yolo_v8") data_dict = session_data.to_dict() assert data_dict["session_id"] == "session_123" assert data_dict["camera_id"] == "camera_001" assert data_dict["detection_data"]["detection"]["class"] == "car" assert data_dict["metadata"]["model_id"] == "yolo_v8" assert "created_at" in data_dict assert "last_activity" in data_dict class TestCacheStats: """Test cache statistics.""" def test_creation(self): """Test cache stats creation.""" stats = CacheStats() assert stats.hits == 0 assert stats.misses == 0 assert stats.evictions == 0 assert stats.size == 0 assert stats.memory_usage == 0 def test_hit_rate_calculation(self): """Test hit rate calculation.""" stats = CacheStats() # No requests yet assert stats.hit_rate() == 0.0 # Some hits and misses stats.hits = 8 stats.misses = 2 assert stats.hit_rate() == 0.8 # 8 / (8 + 2) def test_total_requests(self): """Test total requests calculation.""" stats = CacheStats() stats.hits = 15 stats.misses = 5 assert stats.total_requests() == 20 class TestSessionCache: """Test session cache functionality.""" def test_creation(self): """Test session cache creation.""" config = CacheConfig(max_size=100, ttl_seconds=300) cache = SessionCache(config) assert cache.config == config assert cache.max_size == 100 assert cache.ttl_seconds == 300 assert len(cache._cache) == 0 assert len(cache._access_order) == 0 def test_put_and_get_session(self): """Test putting and getting session data.""" cache = SessionCache(CacheConfig(max_size=10)) session_data = SessionData("session_123", "camera_001", "display_001") session_data.add_detection_data("main", {"class": "car", "confidence": 0.9}) # Put session cache.put("session_123", session_data) # Get session retrieved_data = cache.get("session_123") assert retrieved_data is not None assert retrieved_data.session_id == "session_123" assert retrieved_data.camera_id == "camera_001" assert "main" in retrieved_data.detection_data def test_get_nonexistent_session(self): """Test getting non-existent session.""" cache = SessionCache(CacheConfig(max_size=10)) result = cache.get("nonexistent_session") assert result is None def test_contains_check(self): """Test checking if session exists.""" cache = SessionCache(CacheConfig(max_size=10)) session_data = SessionData("session_123", "camera_001", "display_001") cache.put("session_123", session_data) assert cache.contains("session_123") is True assert cache.contains("nonexistent_session") is False def test_remove_session(self): """Test removing session.""" cache = SessionCache(CacheConfig(max_size=10)) session_data = SessionData("session_123", "camera_001", "display_001") cache.put("session_123", session_data) assert cache.contains("session_123") is True removed_data = cache.remove("session_123") assert removed_data is not None assert removed_data.session_id == "session_123" assert cache.contains("session_123") is False def test_size_tracking(self): """Test cache size tracking.""" cache = SessionCache(CacheConfig(max_size=10)) assert cache.size() == 0 assert cache.is_empty() is True # Add sessions for i in range(3): session_data = SessionData(f"session_{i}", "camera_001", "display_001") cache.put(f"session_{i}", session_data) assert cache.size() == 3 assert cache.is_empty() is False def test_lru_eviction(self): """Test LRU eviction policy.""" cache = SessionCache(CacheConfig(max_size=3, eviction_policy="lru")) # Fill cache to capacity for i in range(3): session_data = SessionData(f"session_{i}", "camera_001", "display_001") cache.put(f"session_{i}", session_data) # Access session_1 to make it recently used cache.get("session_1") # Add another session (should evict session_0, the least recently used) new_session = SessionData("session_3", "camera_001", "display_001") cache.put("session_3", new_session) assert cache.size() == 3 assert cache.contains("session_0") is False # Evicted assert cache.contains("session_1") is True # Recently accessed assert cache.contains("session_2") is True assert cache.contains("session_3") is True # Newly added def test_ttl_expiration(self): """Test TTL-based expiration.""" cache = SessionCache(CacheConfig(max_size=10, ttl_seconds=0.1)) # 100ms TTL session_data = SessionData("session_123", "camera_001", "display_001") cache.put("session_123", session_data) # Should exist immediately assert cache.contains("session_123") is True # Wait for expiration time.sleep(0.2) # Should be expired (but might still be in cache until cleanup) entry = cache._cache.get("session_123") if entry: assert entry.is_expired() is True # Getting expired entry should return None and clean it up retrieved = cache.get("session_123") assert retrieved is None assert cache.contains("session_123") is False def test_cleanup_expired_entries(self): """Test cleanup of expired entries.""" cache = SessionCache(CacheConfig(max_size=10, ttl_seconds=0.1)) # Add multiple sessions for i in range(3): session_data = SessionData(f"session_{i}", "camera_001", "display_001") cache.put(f"session_{i}", session_data) assert cache.size() == 3 # Wait for expiration time.sleep(0.2) # Cleanup expired entries cleaned_count = cache.cleanup_expired() assert cleaned_count == 3 assert cache.size() == 0 def test_clear_cache(self): """Test clearing entire cache.""" cache = SessionCache(CacheConfig(max_size=10)) # Add sessions for i in range(5): session_data = SessionData(f"session_{i}", "camera_001", "display_001") cache.put(f"session_{i}", session_data) assert cache.size() == 5 cache.clear() assert cache.size() == 0 assert cache.is_empty() is True def test_get_all_sessions(self): """Test getting all sessions.""" cache = SessionCache(CacheConfig(max_size=10)) sessions = [] for i in range(3): session_data = SessionData(f"session_{i}", f"camera_{i}", "display_001") cache.put(f"session_{i}", session_data) sessions.append(session_data) all_sessions = cache.get_all() assert len(all_sessions) == 3 for session_id, session_data in all_sessions.items(): assert session_id.startswith("session_") assert session_data.session_id == session_id def test_get_sessions_by_camera(self): """Test getting sessions by camera ID.""" cache = SessionCache(CacheConfig(max_size=10)) # Add sessions for different cameras for i in range(2): session_data1 = SessionData(f"session_cam1_{i}", "camera_001", "display_001") session_data2 = SessionData(f"session_cam2_{i}", "camera_002", "display_001") cache.put(f"session_cam1_{i}", session_data1) cache.put(f"session_cam2_{i}", session_data2) camera1_sessions = cache.get_by_camera("camera_001") camera2_sessions = cache.get_by_camera("camera_002") assert len(camera1_sessions) == 2 assert len(camera2_sessions) == 2 for session_data in camera1_sessions: assert session_data.camera_id == "camera_001" for session_data in camera2_sessions: assert session_data.camera_id == "camera_002" def test_statistics_tracking(self): """Test cache statistics tracking.""" cache = SessionCache(CacheConfig(max_size=10)) session_data = SessionData("session_123", "camera_001", "display_001") cache.put("session_123", session_data) # Cache miss cache.get("nonexistent_session") # Cache hit cache.get("session_123") cache.get("session_123") # Another hit stats = cache.get_stats() assert stats.hits == 2 assert stats.misses == 1 assert stats.size == 1 assert stats.hit_rate() == 2/3 # 2 hits out of 3 total requests def test_memory_usage_estimation(self): """Test memory usage estimation.""" cache = SessionCache(CacheConfig(max_size=10)) initial_memory = cache.get_memory_usage() # Add large session session_data = SessionData("session_123", "camera_001", "display_001") session_data.add_detection_data("large_data", {"data": "x" * 1000}) cache.put("session_123", session_data) after_memory = cache.get_memory_usage() assert after_memory > initial_memory class TestSessionCacheManager: """Test session cache manager.""" def test_singleton_behavior(self): """Test that SessionCacheManager is a singleton.""" manager1 = SessionCacheManager() manager2 = SessionCacheManager() assert manager1 is manager2 def test_initialization(self): """Test session cache manager initialization.""" manager = SessionCacheManager() assert manager.detection_cache is not None assert manager.pipeline_cache is not None assert manager.session_cache is not None assert isinstance(manager.detection_cache, SessionCache) assert isinstance(manager.pipeline_cache, SessionCache) assert isinstance(manager.session_cache, SessionCache) def test_cache_detection_result(self): """Test caching detection results.""" manager = SessionCacheManager() manager.clear_all() # Start fresh detection_data = { "class": "car", "confidence": 0.95, "bbox": [100, 200, 300, 400], "track_id": 1001 } manager.cache_detection("camera_001", detection_data) cached_detection = manager.get_cached_detection("camera_001") assert cached_detection is not None assert cached_detection["class"] == "car" assert cached_detection["confidence"] == 0.95 assert cached_detection["track_id"] == 1001 def test_cache_pipeline_result(self): """Test caching pipeline results.""" manager = SessionCacheManager() manager.clear_all() pipeline_result = { "status": "success", "detections": [{"class": "car", "confidence": 0.9}], "execution_time": 0.15, "model_id": "yolo_v8" } manager.cache_pipeline_result("camera_001", pipeline_result) cached_result = manager.get_cached_pipeline_result("camera_001") assert cached_result is not None assert cached_result["status"] == "success" assert cached_result["execution_time"] == 0.15 assert len(cached_result["detections"]) == 1 def test_manage_session_data(self): """Test session data management.""" manager = SessionCacheManager() manager.clear_all() session_id = str(uuid.uuid4()) # Create session manager.create_session(session_id, "camera_001", {"initial": "data"}) # Update session manager.update_session_detection(session_id, {"car_brand": "Toyota"}) # Get session session_data = manager.get_session_detection(session_id) assert session_data is not None assert "initial" in session_data assert session_data["car_brand"] == "Toyota" def test_set_latest_frame(self): """Test setting and getting latest frame.""" manager = SessionCacheManager() manager.clear_all() frame_data = b"fake_frame_data" manager.set_latest_frame("camera_001", frame_data) retrieved_frame = manager.get_latest_frame("camera_001") assert retrieved_frame == frame_data def test_frame_skip_flag_management(self): """Test frame skip flag management.""" manager = SessionCacheManager() manager.clear_all() # Initially should be False assert manager.get_frame_skip_flag("camera_001") is False # Set to True manager.set_frame_skip_flag("camera_001", True) assert manager.get_frame_skip_flag("camera_001") is True # Set back to False manager.set_frame_skip_flag("camera_001", False) assert manager.get_frame_skip_flag("camera_001") is False def test_cleanup_expired_sessions(self): """Test cleanup of expired sessions.""" manager = SessionCacheManager() manager.clear_all() # Create sessions with short TTL manager.session_cache = SessionCache(CacheConfig(max_size=10, ttl_seconds=0.1)) # Add sessions for i in range(3): session_id = f"session_{i}" manager.create_session(session_id, "camera_001", {"test": "data"}) assert manager.session_cache.size() == 3 # Wait for expiration time.sleep(0.2) # Cleanup expired_count = manager.cleanup_expired_sessions() assert expired_count == 3 assert manager.session_cache.size() == 0 def test_clear_camera_cache(self): """Test clearing cache for specific camera.""" manager = SessionCacheManager() manager.clear_all() # Add data for multiple cameras manager.cache_detection("camera_001", {"class": "car"}) manager.cache_detection("camera_002", {"class": "truck"}) manager.cache_pipeline_result("camera_001", {"status": "success"}) manager.set_latest_frame("camera_001", b"frame1") manager.set_latest_frame("camera_002", b"frame2") # Clear camera_001 cache manager.clear_camera_cache("camera_001") # camera_001 data should be gone assert manager.get_cached_detection("camera_001") is None assert manager.get_cached_pipeline_result("camera_001") is None assert manager.get_latest_frame("camera_001") is None # camera_002 data should remain assert manager.get_cached_detection("camera_002") is not None assert manager.get_latest_frame("camera_002") is not None def test_get_cache_statistics(self): """Test getting cache statistics.""" manager = SessionCacheManager() manager.clear_all() # Add some data to generate statistics manager.cache_detection("camera_001", {"class": "car"}) manager.cache_pipeline_result("camera_001", {"status": "success"}) manager.create_session("session_123", "camera_001", {"initial": "data"}) # Access data to generate hits/misses manager.get_cached_detection("camera_001") # Hit manager.get_cached_detection("camera_002") # Miss stats = manager.get_cache_statistics() assert "detection_cache" in stats assert "pipeline_cache" in stats assert "session_cache" in stats assert "total_memory_usage" in stats detection_stats = stats["detection_cache"] assert detection_stats["size"] >= 1 assert detection_stats["hits"] >= 1 assert detection_stats["misses"] >= 1 def test_memory_pressure_handling(self): """Test handling memory pressure.""" # Create manager with small cache sizes config = CacheConfig(max_size=3) manager = SessionCacheManager() manager.detection_cache = SessionCache(config) manager.pipeline_cache = SessionCache(config) manager.session_cache = SessionCache(config) # Fill caches beyond capacity for i in range(5): manager.cache_detection(f"camera_{i}", {"class": "car", "data": "x" * 100}) manager.cache_pipeline_result(f"camera_{i}", {"status": "success", "data": "y" * 100}) manager.create_session(f"session_{i}", f"camera_{i}", {"data": "z" * 100}) # Caches should not exceed max size due to eviction assert manager.detection_cache.size() <= 3 assert manager.pipeline_cache.size() <= 3 assert manager.session_cache.size() <= 3 def test_concurrent_access_thread_safety(self): """Test thread safety of concurrent cache access.""" import threading import concurrent.futures manager = SessionCacheManager() manager.clear_all() results = [] errors = [] def cache_operation(thread_id): try: # Each thread performs multiple cache operations for i in range(10): session_id = f"thread_{thread_id}_session_{i}" # Create session manager.create_session(session_id, f"camera_{thread_id}", {"thread": thread_id, "index": i}) # Update session manager.update_session_detection(session_id, {"updated": True}) # Read session data = manager.get_session_detection(session_id) if data and data.get("thread") == thread_id: results.append((thread_id, i)) except Exception as e: errors.append((thread_id, str(e))) # Run operations concurrently with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: futures = [executor.submit(cache_operation, i) for i in range(5)] concurrent.futures.wait(futures) # Should have no errors and successful operations assert len(errors) == 0 assert len(results) >= 25 # At least some operations should succeed class TestSessionCacheIntegration: """Integration tests for session cache.""" def test_complete_detection_workflow(self): """Test complete detection workflow with caching.""" manager = SessionCacheManager() manager.clear_all() camera_id = "camera_001" session_id = str(uuid.uuid4()) # 1. Cache initial detection detection_data = { "class": "car", "confidence": 0.92, "bbox": [100, 200, 300, 400], "track_id": 1001, "timestamp": int(time.time() * 1000) } manager.cache_detection(camera_id, detection_data) # 2. Create session for tracking initial_session_data = { "detection_class": detection_data["class"], "confidence": detection_data["confidence"], "track_id": detection_data["track_id"] } manager.create_session(session_id, camera_id, initial_session_data) # 3. Cache pipeline processing result pipeline_result = { "status": "processing", "stage": "classification", "detections": [detection_data], "branches_completed": [], "branches_pending": ["car_brand_cls", "car_bodytype_cls"] } manager.cache_pipeline_result(camera_id, pipeline_result) # 4. Update session with classification results classification_updates = [ {"car_brand": "Toyota", "brand_confidence": 0.87}, {"car_body_type": "Sedan", "bodytype_confidence": 0.82} ] for update in classification_updates: manager.update_session_detection(session_id, update) # 5. Update pipeline result to completed final_pipeline_result = { "status": "completed", "stage": "finished", "detections": [detection_data], "branches_completed": ["car_brand_cls", "car_bodytype_cls"], "branches_pending": [], "execution_time": 0.25 } manager.cache_pipeline_result(camera_id, final_pipeline_result) # 6. Verify all cached data cached_detection = manager.get_cached_detection(camera_id) cached_pipeline = manager.get_cached_pipeline_result(camera_id) cached_session = manager.get_session_detection(session_id) # Assertions assert cached_detection["class"] == "car" assert cached_detection["track_id"] == 1001 assert cached_pipeline["status"] == "completed" assert len(cached_pipeline["branches_completed"]) == 2 assert cached_session["detection_class"] == "car" assert cached_session["car_brand"] == "Toyota" assert cached_session["car_body_type"] == "Sedan" assert cached_session["brand_confidence"] == 0.87 def test_cache_performance_under_load(self): """Test cache performance under load.""" manager = SessionCacheManager() manager.clear_all() import time # Measure performance of cache operations start_time = time.time() # Perform many cache operations for i in range(1000): camera_id = f"camera_{i % 10}" # 10 different cameras session_id = f"session_{i}" # Cache detection detection_data = { "class": "car", "confidence": 0.9 + (i % 10) * 0.01, "track_id": i, "bbox": [i % 100, i % 100, (i % 100) + 200, (i % 100) + 200] } manager.cache_detection(camera_id, detection_data) # Create session manager.create_session(session_id, camera_id, {"index": i}) # Read back (every 10th operation) if i % 10 == 0: manager.get_cached_detection(camera_id) manager.get_session_detection(session_id) end_time = time.time() total_time = end_time - start_time # Should complete in reasonable time (less than 1 second) assert total_time < 1.0 # Verify cache statistics stats = manager.get_cache_statistics() assert stats["detection_cache"]["size"] > 0 assert stats["session_cache"]["size"] > 0 assert stats["detection_cache"]["hits"] > 0 def test_cache_persistence_and_recovery(self): """Test cache persistence and recovery (if enabled).""" # This test would be more meaningful with actual persistence # For now, test the configuration and structure persistence_config = CacheConfig( max_size=100, enable_persistence=True, persistence_path="/tmp/detector_cache_test" ) cache = SessionCache(persistence_config) # Add some data session_data = SessionData("session_123", "camera_001", "display_001") session_data.add_detection_data("main", {"class": "car", "confidence": 0.95}) cache.put("session_123", session_data) # Verify data exists assert cache.contains("session_123") is True # In a real implementation, this would test: # 1. Saving cache to disk # 2. Loading cache from disk # 3. Verifying data integrity after reload