python-detector-worker/tests/unit/storage/test_session_cache.py
2025-09-12 18:55:23 +07:00

883 lines
No EOL
31 KiB
Python

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