Refactor: PHASE 8: Testing & Integration

This commit is contained in:
ziesorx 2025-09-12 18:55:23 +07:00
parent af34f4fd08
commit 9e8c6804a7
32 changed files with 17128 additions and 0 deletions

View file

@ -0,0 +1,883 @@
"""
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