959 lines
No EOL
32 KiB
Python
959 lines
No EOL
32 KiB
Python
"""
|
|
Unit tests for action execution functionality.
|
|
"""
|
|
import pytest
|
|
import asyncio
|
|
import json
|
|
import base64
|
|
import numpy as np
|
|
from unittest.mock import Mock, MagicMock, patch, AsyncMock
|
|
from datetime import datetime, timedelta
|
|
|
|
from detector_worker.pipeline.action_executor import (
|
|
ActionExecutor,
|
|
ActionResult,
|
|
ActionType,
|
|
RedisAction,
|
|
PostgreSQLAction,
|
|
FileAction
|
|
)
|
|
from detector_worker.detection.detection_result import DetectionResult, BoundingBox
|
|
from detector_worker.core.exceptions import ActionError, RedisError, DatabaseError
|
|
|
|
|
|
class TestActionResult:
|
|
"""Test action execution result."""
|
|
|
|
def test_creation_success(self):
|
|
"""Test successful action result creation."""
|
|
result = ActionResult(
|
|
action_type=ActionType.REDIS_SAVE,
|
|
success=True,
|
|
execution_time=0.05,
|
|
metadata={"key": "saved_image_key", "expiry": 600}
|
|
)
|
|
|
|
assert result.action_type == ActionType.REDIS_SAVE
|
|
assert result.success is True
|
|
assert result.execution_time == 0.05
|
|
assert result.metadata["key"] == "saved_image_key"
|
|
assert result.error is None
|
|
|
|
def test_creation_failure(self):
|
|
"""Test failed action result creation."""
|
|
result = ActionResult(
|
|
action_type=ActionType.POSTGRESQL_INSERT,
|
|
success=False,
|
|
error="Database connection failed",
|
|
execution_time=0.02
|
|
)
|
|
|
|
assert result.action_type == ActionType.POSTGRESQL_INSERT
|
|
assert result.success is False
|
|
assert result.error == "Database connection failed"
|
|
assert result.metadata == {}
|
|
|
|
|
|
class TestRedisAction:
|
|
"""Test Redis action implementations."""
|
|
|
|
def test_creation(self):
|
|
"""Test Redis action creation."""
|
|
action_config = {
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:{display_id}:{timestamp}:{session_id}",
|
|
"expire_seconds": 600
|
|
}
|
|
|
|
action = RedisAction(action_config)
|
|
|
|
assert action.action_type == ActionType.REDIS_SAVE
|
|
assert action.region == "car"
|
|
assert action.key_template == "inference:{display_id}:{timestamp}:{session_id}"
|
|
assert action.expire_seconds == 600
|
|
|
|
def test_resolve_key_template(self):
|
|
"""Test key template resolution."""
|
|
action_config = {
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:{display_id}:{timestamp}:{session_id}:{filename}",
|
|
"expire_seconds": 600
|
|
}
|
|
|
|
action = RedisAction(action_config)
|
|
|
|
context = {
|
|
"display_id": "display_001",
|
|
"timestamp": "1640995200000",
|
|
"session_id": "session_123",
|
|
"filename": "detection.jpg"
|
|
}
|
|
|
|
resolved_key = action.resolve_key(context)
|
|
expected_key = "inference:display_001:1640995200000:session_123:detection.jpg"
|
|
|
|
assert resolved_key == expected_key
|
|
|
|
def test_resolve_key_missing_variable(self):
|
|
"""Test key resolution with missing variable."""
|
|
action_config = {
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:{display_id}:{missing_var}",
|
|
"expire_seconds": 600
|
|
}
|
|
|
|
action = RedisAction(action_config)
|
|
|
|
context = {"display_id": "display_001"}
|
|
|
|
with pytest.raises(ActionError):
|
|
action.resolve_key(context)
|
|
|
|
|
|
class TestPostgreSQLAction:
|
|
"""Test PostgreSQL action implementations."""
|
|
|
|
def test_creation_insert(self):
|
|
"""Test PostgreSQL insert action creation."""
|
|
action_config = {
|
|
"type": "postgresql_insert",
|
|
"table": "detections",
|
|
"fields": {
|
|
"camera_id": "{camera_id}",
|
|
"session_id": "{session_id}",
|
|
"detection_class": "{class}",
|
|
"confidence": "{confidence}",
|
|
"bbox_x1": "{bbox.x1}",
|
|
"created_at": "NOW()"
|
|
}
|
|
}
|
|
|
|
action = PostgreSQLAction(action_config)
|
|
|
|
assert action.action_type == ActionType.POSTGRESQL_INSERT
|
|
assert action.table == "detections"
|
|
assert len(action.fields) == 6
|
|
assert action.key_field is None
|
|
|
|
def test_creation_update(self):
|
|
"""Test PostgreSQL update action creation."""
|
|
action_config = {
|
|
"type": "postgresql_update_combined",
|
|
"table": "car_info",
|
|
"key_field": "session_id",
|
|
"fields": {
|
|
"car_brand": "{car_brand_cls.brand}",
|
|
"car_body_type": "{car_bodytype_cls.body_type}",
|
|
"updated_at": "NOW()"
|
|
},
|
|
"waitForBranches": ["car_brand_cls", "car_bodytype_cls"]
|
|
}
|
|
|
|
action = PostgreSQLAction(action_config)
|
|
|
|
assert action.action_type == ActionType.POSTGRESQL_UPDATE
|
|
assert action.table == "car_info"
|
|
assert action.key_field == "session_id"
|
|
assert action.wait_for_branches == ["car_brand_cls", "car_bodytype_cls"]
|
|
|
|
def test_resolve_field_values(self):
|
|
"""Test field value resolution."""
|
|
action_config = {
|
|
"type": "postgresql_insert",
|
|
"table": "detections",
|
|
"fields": {
|
|
"camera_id": "{camera_id}",
|
|
"detection_class": "{class}",
|
|
"confidence": "{confidence}",
|
|
"brand": "{car_brand_cls.brand}"
|
|
}
|
|
}
|
|
|
|
action = PostgreSQLAction(action_config)
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"class": "car",
|
|
"confidence": 0.85
|
|
}
|
|
|
|
branch_results = {
|
|
"car_brand_cls": {"brand": "Toyota", "confidence": 0.78}
|
|
}
|
|
|
|
resolved_fields = action.resolve_field_values(context, branch_results)
|
|
|
|
assert resolved_fields["camera_id"] == "camera_001"
|
|
assert resolved_fields["detection_class"] == "car"
|
|
assert resolved_fields["confidence"] == 0.85
|
|
assert resolved_fields["brand"] == "Toyota"
|
|
|
|
|
|
class TestFileAction:
|
|
"""Test file action implementations."""
|
|
|
|
def test_creation(self):
|
|
"""Test file action creation."""
|
|
action_config = {
|
|
"type": "save_image",
|
|
"path": "/tmp/detections/{camera_id}_{timestamp}.jpg",
|
|
"region": "car",
|
|
"format": "jpeg",
|
|
"quality": 85
|
|
}
|
|
|
|
action = FileAction(action_config)
|
|
|
|
assert action.action_type == ActionType.SAVE_IMAGE
|
|
assert action.path_template == "/tmp/detections/{camera_id}_{timestamp}.jpg"
|
|
assert action.region == "car"
|
|
assert action.format == "jpeg"
|
|
assert action.quality == 85
|
|
|
|
def test_resolve_path_template(self):
|
|
"""Test path template resolution."""
|
|
action_config = {
|
|
"type": "save_image",
|
|
"path": "/tmp/detections/{camera_id}/{date}/{timestamp}.jpg"
|
|
}
|
|
|
|
action = FileAction(action_config)
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"timestamp": "1640995200000",
|
|
"date": "2022-01-01"
|
|
}
|
|
|
|
resolved_path = action.resolve_path(context)
|
|
expected_path = "/tmp/detections/camera_001/2022-01-01/1640995200000.jpg"
|
|
|
|
assert resolved_path == expected_path
|
|
|
|
|
|
class TestActionExecutor:
|
|
"""Test action execution functionality."""
|
|
|
|
def test_initialization(self):
|
|
"""Test action executor initialization."""
|
|
executor = ActionExecutor()
|
|
|
|
assert executor.redis_client is None
|
|
assert executor.db_manager is None
|
|
assert executor.max_concurrent_actions == 10
|
|
assert executor.action_timeout == 30.0
|
|
|
|
def test_initialization_with_clients(self, mock_redis_client, mock_database_connection):
|
|
"""Test initialization with client instances."""
|
|
executor = ActionExecutor(
|
|
redis_client=mock_redis_client,
|
|
db_manager=mock_database_connection
|
|
)
|
|
|
|
assert executor.redis_client is mock_redis_client
|
|
assert executor.db_manager is mock_database_connection
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_actions_empty_list(self):
|
|
"""Test executing empty action list."""
|
|
executor = ActionExecutor()
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"session_id": "session_123"
|
|
}
|
|
|
|
results = await executor.execute_actions([], {}, context)
|
|
|
|
assert results == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_redis_save_action(self, mock_redis_client, mock_frame):
|
|
"""Test executing Redis save image action."""
|
|
executor = ActionExecutor(redis_client=mock_redis_client)
|
|
|
|
actions = [
|
|
{
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:{camera_id}:{session_id}",
|
|
"expire_seconds": 600
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.9,
|
|
"detection": DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
}
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"session_id": "session_123",
|
|
"frame_data": mock_frame
|
|
}
|
|
|
|
# Mock successful Redis operations
|
|
mock_redis_client.set.return_value = True
|
|
mock_redis_client.expire.return_value = True
|
|
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].action_type == ActionType.REDIS_SAVE
|
|
|
|
# Verify Redis calls
|
|
mock_redis_client.set.assert_called_once()
|
|
mock_redis_client.expire.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_postgresql_insert_action(self, mock_database_connection):
|
|
"""Test executing PostgreSQL insert action."""
|
|
# Mock database manager
|
|
mock_db_manager = Mock()
|
|
mock_db_manager.execute_query = AsyncMock(return_value=True)
|
|
|
|
executor = ActionExecutor(db_manager=mock_db_manager)
|
|
|
|
actions = [
|
|
{
|
|
"type": "postgresql_insert",
|
|
"table": "detections",
|
|
"fields": {
|
|
"camera_id": "{camera_id}",
|
|
"session_id": "{session_id}",
|
|
"detection_class": "{class}",
|
|
"confidence": "{confidence}",
|
|
"created_at": "NOW()"
|
|
}
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.9,
|
|
"detection": DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
}
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"session_id": "session_123",
|
|
"class": "car",
|
|
"confidence": 0.9
|
|
}
|
|
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].action_type == ActionType.POSTGRESQL_INSERT
|
|
|
|
# Verify database call
|
|
mock_db_manager.execute_query.assert_called_once()
|
|
call_args = mock_db_manager.execute_query.call_args[0]
|
|
assert "INSERT INTO detections" in call_args[0]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_postgresql_update_action(self, mock_database_connection):
|
|
"""Test executing PostgreSQL update action."""
|
|
mock_db_manager = Mock()
|
|
mock_db_manager.execute_query = AsyncMock(return_value=True)
|
|
|
|
executor = ActionExecutor(db_manager=mock_db_manager)
|
|
|
|
actions = [
|
|
{
|
|
"type": "postgresql_update_combined",
|
|
"table": "car_info",
|
|
"key_field": "session_id",
|
|
"fields": {
|
|
"car_brand": "{car_brand_cls.brand}",
|
|
"car_body_type": "{car_bodytype_cls.body_type}",
|
|
"updated_at": "NOW()"
|
|
},
|
|
"waitForBranches": ["car_brand_cls", "car_bodytype_cls"]
|
|
}
|
|
]
|
|
|
|
regions = {}
|
|
|
|
context = {
|
|
"session_id": "session_123"
|
|
}
|
|
|
|
branch_results = {
|
|
"car_brand_cls": {"brand": "Toyota"},
|
|
"car_bodytype_cls": {"body_type": "Sedan"}
|
|
}
|
|
|
|
results = await executor.execute_actions(actions, regions, context, branch_results)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].action_type == ActionType.POSTGRESQL_UPDATE
|
|
|
|
# Verify database call
|
|
mock_db_manager.execute_query.assert_called_once()
|
|
call_args = mock_db_manager.execute_query.call_args[0]
|
|
assert "UPDATE car_info SET" in call_args[0]
|
|
assert "WHERE session_id" in call_args[0]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_file_save_action(self, mock_frame):
|
|
"""Test executing file save action."""
|
|
executor = ActionExecutor()
|
|
|
|
actions = [
|
|
{
|
|
"type": "save_image",
|
|
"path": "/tmp/test_{camera_id}_{timestamp}.jpg",
|
|
"region": "car",
|
|
"format": "jpeg",
|
|
"quality": 85
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.9,
|
|
"detection": DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
}
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"timestamp": "1640995200000",
|
|
"frame_data": mock_frame
|
|
}
|
|
|
|
with patch('cv2.imwrite') as mock_imwrite:
|
|
mock_imwrite.return_value = True
|
|
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].action_type == ActionType.SAVE_IMAGE
|
|
|
|
# Verify file save call
|
|
mock_imwrite.assert_called_once()
|
|
call_args = mock_imwrite.call_args
|
|
assert "/tmp/test_camera_001_1640995200000.jpg" in call_args[0][0]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_actions_parallel(self, mock_redis_client):
|
|
"""Test parallel execution of multiple actions."""
|
|
executor = ActionExecutor(redis_client=mock_redis_client)
|
|
|
|
# Multiple Redis actions
|
|
actions = [
|
|
{
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:car:{session_id}",
|
|
"expire_seconds": 600
|
|
},
|
|
{
|
|
"type": "redis_publish",
|
|
"channel": "detections",
|
|
"message": "{camera_id}:car_detected"
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.9,
|
|
"detection": DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
}
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"session_id": "session_123",
|
|
"frame_data": np.zeros((480, 640, 3), dtype=np.uint8)
|
|
}
|
|
|
|
# Mock Redis operations
|
|
mock_redis_client.set.return_value = True
|
|
mock_redis_client.expire.return_value = True
|
|
mock_redis_client.publish.return_value = 1
|
|
|
|
import time
|
|
start_time = time.time()
|
|
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
execution_time = time.time() - start_time
|
|
|
|
assert len(results) == 2
|
|
assert all(result.success for result in results)
|
|
|
|
# Should execute in parallel (faster than sequential)
|
|
assert execution_time < 0.1 # Allow some overhead
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_actions_error_handling(self, mock_redis_client):
|
|
"""Test error handling in action execution."""
|
|
executor = ActionExecutor(redis_client=mock_redis_client)
|
|
|
|
actions = [
|
|
{
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:{session_id}",
|
|
"expire_seconds": 600
|
|
},
|
|
{
|
|
"type": "redis_save_image", # This one will fail
|
|
"region": "truck", # Region not detected
|
|
"key": "inference:truck:{session_id}",
|
|
"expire_seconds": 600
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.9,
|
|
"detection": DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
# No truck region
|
|
}
|
|
|
|
context = {
|
|
"session_id": "session_123",
|
|
"frame_data": np.zeros((480, 640, 3), dtype=np.uint8)
|
|
}
|
|
|
|
# Mock Redis operations
|
|
mock_redis_client.set.return_value = True
|
|
mock_redis_client.expire.return_value = True
|
|
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
assert len(results) == 2
|
|
assert results[0].success is True # Car action succeeds
|
|
assert results[1].success is False # Truck action fails
|
|
assert "Region 'truck' not found" in results[1].error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_actions_timeout(self, mock_redis_client):
|
|
"""Test action execution timeout."""
|
|
config = {"action_timeout": 0.001} # Very short timeout
|
|
executor = ActionExecutor(redis_client=mock_redis_client, config=config)
|
|
|
|
def slow_redis_operation(*args, **kwargs):
|
|
import time
|
|
time.sleep(1) # Longer than timeout
|
|
return True
|
|
|
|
mock_redis_client.set.side_effect = slow_redis_operation
|
|
|
|
actions = [
|
|
{
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:{session_id}",
|
|
"expire_seconds": 600
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.9,
|
|
"detection": DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
}
|
|
|
|
context = {
|
|
"session_id": "session_123",
|
|
"frame_data": np.zeros((480, 640, 3), dtype=np.uint8)
|
|
}
|
|
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is False
|
|
assert "timeout" in results[0].error.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_redis_publish_action(self, mock_redis_client):
|
|
"""Test executing Redis publish action."""
|
|
executor = ActionExecutor(redis_client=mock_redis_client)
|
|
|
|
actions = [
|
|
{
|
|
"type": "redis_publish",
|
|
"channel": "detections:{camera_id}",
|
|
"message": {
|
|
"camera_id": "{camera_id}",
|
|
"detection_class": "{class}",
|
|
"confidence": "{confidence}",
|
|
"timestamp": "{timestamp}"
|
|
}
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.9,
|
|
"detection": DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
}
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"class": "car",
|
|
"confidence": 0.9,
|
|
"timestamp": "1640995200000"
|
|
}
|
|
|
|
mock_redis_client.publish.return_value = 1
|
|
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].action_type == ActionType.REDIS_PUBLISH
|
|
|
|
# Verify publish call
|
|
mock_redis_client.publish.assert_called_once()
|
|
call_args = mock_redis_client.publish.call_args
|
|
assert call_args[0][0] == "detections:camera_001" # Channel
|
|
|
|
# Message should be JSON
|
|
message = call_args[0][1]
|
|
parsed_message = json.loads(message)
|
|
assert parsed_message["camera_id"] == "camera_001"
|
|
assert parsed_message["detection_class"] == "car"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_execute_conditional_action(self):
|
|
"""Test executing conditional actions."""
|
|
executor = ActionExecutor()
|
|
|
|
actions = [
|
|
{
|
|
"type": "conditional",
|
|
"condition": "{confidence} > 0.8",
|
|
"actions": [
|
|
{
|
|
"type": "log",
|
|
"message": "High confidence detection: {class} ({confidence})"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.95, # High confidence
|
|
"detection": DetectionResult("car", 0.95, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
}
|
|
|
|
context = {
|
|
"class": "car",
|
|
"confidence": 0.95
|
|
}
|
|
|
|
with patch('logging.info') as mock_log:
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
|
|
# Should have logged the message
|
|
mock_log.assert_called_once()
|
|
log_message = mock_log.call_args[0][0]
|
|
assert "High confidence detection: car (0.95)" in log_message
|
|
|
|
def test_crop_region_from_frame(self, mock_frame):
|
|
"""Test cropping region from frame."""
|
|
executor = ActionExecutor()
|
|
|
|
detection = DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
|
|
cropped = executor._crop_region_from_frame(mock_frame, detection.bbox)
|
|
|
|
assert cropped.shape == (200, 200, 3) # 400-200, 300-100
|
|
|
|
def test_encode_image_base64(self, mock_frame):
|
|
"""Test encoding image to base64."""
|
|
executor = ActionExecutor()
|
|
|
|
# Crop a small region
|
|
cropped_frame = mock_frame[200:400, 100:300] # 200x200 region
|
|
|
|
with patch('cv2.imencode') as mock_imencode:
|
|
# Mock successful encoding
|
|
mock_imencode.return_value = (True, np.array([1, 2, 3, 4], dtype=np.uint8))
|
|
|
|
encoded = executor._encode_image_base64(cropped_frame, format="jpeg")
|
|
|
|
# Should return base64 string
|
|
assert isinstance(encoded, str)
|
|
assert len(encoded) > 0
|
|
|
|
# Verify encoding call
|
|
mock_imencode.assert_called_once()
|
|
assert mock_imencode.call_args[0][0] == '.jpg'
|
|
|
|
def test_build_insert_query(self):
|
|
"""Test building INSERT SQL query."""
|
|
executor = ActionExecutor()
|
|
|
|
table = "detections"
|
|
fields = {
|
|
"camera_id": "camera_001",
|
|
"detection_class": "car",
|
|
"confidence": 0.9,
|
|
"created_at": "NOW()"
|
|
}
|
|
|
|
query, values = executor._build_insert_query(table, fields)
|
|
|
|
assert "INSERT INTO detections" in query
|
|
assert "camera_id, detection_class, confidence, created_at" in query
|
|
assert "VALUES (%s, %s, %s, NOW())" in query
|
|
assert values == ["camera_001", "car", 0.9]
|
|
|
|
def test_build_update_query(self):
|
|
"""Test building UPDATE SQL query."""
|
|
executor = ActionExecutor()
|
|
|
|
table = "car_info"
|
|
fields = {
|
|
"car_brand": "Toyota",
|
|
"car_body_type": "Sedan",
|
|
"updated_at": "NOW()"
|
|
}
|
|
key_field = "session_id"
|
|
key_value = "session_123"
|
|
|
|
query, values = executor._build_update_query(table, fields, key_field, key_value)
|
|
|
|
assert "UPDATE car_info SET" in query
|
|
assert "car_brand = %s" in query
|
|
assert "car_body_type = %s" in query
|
|
assert "updated_at = NOW()" in query
|
|
assert "WHERE session_id = %s" in query
|
|
assert values == ["Toyota", "Sedan", "session_123"]
|
|
|
|
def test_evaluate_condition(self):
|
|
"""Test evaluating conditional expressions."""
|
|
executor = ActionExecutor()
|
|
|
|
context = {
|
|
"confidence": 0.85,
|
|
"class": "car",
|
|
"area": 40000
|
|
}
|
|
|
|
# Simple comparisons
|
|
assert executor._evaluate_condition("{confidence} > 0.8", context) is True
|
|
assert executor._evaluate_condition("{confidence} < 0.8", context) is False
|
|
assert executor._evaluate_condition("{confidence} >= 0.85", context) is True
|
|
assert executor._evaluate_condition("{confidence} == 0.85", context) is True
|
|
|
|
# String comparisons
|
|
assert executor._evaluate_condition("{class} == 'car'", context) is True
|
|
assert executor._evaluate_condition("{class} != 'truck'", context) is True
|
|
|
|
# Complex conditions
|
|
assert executor._evaluate_condition("{confidence} > 0.8 and {area} > 30000", context) is True
|
|
assert executor._evaluate_condition("{confidence} > 0.9 or {area} > 30000", context) is True
|
|
assert executor._evaluate_condition("{confidence} > 0.9 and {area} < 30000", context) is False
|
|
|
|
def test_validate_action_config(self):
|
|
"""Test action configuration validation."""
|
|
executor = ActionExecutor()
|
|
|
|
# Valid Redis action
|
|
valid_redis = {
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:{session_id}",
|
|
"expire_seconds": 600
|
|
}
|
|
assert executor._validate_action_config(valid_redis) is True
|
|
|
|
# Invalid action (missing required fields)
|
|
invalid_action = {
|
|
"type": "redis_save_image"
|
|
# Missing region and key
|
|
}
|
|
with pytest.raises(ActionError):
|
|
executor._validate_action_config(invalid_action)
|
|
|
|
# Unknown action type
|
|
unknown_action = {
|
|
"type": "unknown_action_type",
|
|
"some_field": "value"
|
|
}
|
|
with pytest.raises(ActionError):
|
|
executor._validate_action_config(unknown_action)
|
|
|
|
|
|
class TestActionExecutorIntegration:
|
|
"""Integration tests for action execution."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_complete_detection_workflow(self, mock_redis_client, mock_frame):
|
|
"""Test complete detection workflow with multiple actions."""
|
|
# Mock database manager
|
|
mock_db_manager = Mock()
|
|
mock_db_manager.execute_query = AsyncMock(return_value=True)
|
|
|
|
executor = ActionExecutor(
|
|
redis_client=mock_redis_client,
|
|
db_manager=mock_db_manager
|
|
)
|
|
|
|
# Complete action workflow
|
|
actions = [
|
|
# Save cropped image to Redis
|
|
{
|
|
"type": "redis_save_image",
|
|
"region": "car",
|
|
"key": "inference:{camera_id}:{timestamp}:{session_id}:car",
|
|
"expire_seconds": 600
|
|
},
|
|
# Insert initial detection record
|
|
{
|
|
"type": "postgresql_insert",
|
|
"table": "car_detections",
|
|
"fields": {
|
|
"camera_id": "{camera_id}",
|
|
"session_id": "{session_id}",
|
|
"detection_class": "{class}",
|
|
"confidence": "{confidence}",
|
|
"bbox_x1": "{bbox.x1}",
|
|
"bbox_y1": "{bbox.y1}",
|
|
"bbox_x2": "{bbox.x2}",
|
|
"bbox_y2": "{bbox.y2}",
|
|
"created_at": "NOW()"
|
|
}
|
|
},
|
|
# Publish detection event
|
|
{
|
|
"type": "redis_publish",
|
|
"channel": "detections:{camera_id}",
|
|
"message": {
|
|
"event": "car_detected",
|
|
"camera_id": "{camera_id}",
|
|
"session_id": "{session_id}",
|
|
"timestamp": "{timestamp}"
|
|
}
|
|
}
|
|
]
|
|
|
|
regions = {
|
|
"car": {
|
|
"bbox": [100, 200, 300, 400],
|
|
"confidence": 0.92,
|
|
"detection": DetectionResult("car", 0.92, BoundingBox(100, 200, 300, 400), 1001)
|
|
}
|
|
}
|
|
|
|
context = {
|
|
"camera_id": "camera_001",
|
|
"session_id": "session_123",
|
|
"timestamp": "1640995200000",
|
|
"class": "car",
|
|
"confidence": 0.92,
|
|
"bbox": {"x1": 100, "y1": 200, "x2": 300, "y2": 400},
|
|
"frame_data": mock_frame
|
|
}
|
|
|
|
# Mock all Redis operations
|
|
mock_redis_client.set.return_value = True
|
|
mock_redis_client.expire.return_value = True
|
|
mock_redis_client.publish.return_value = 1
|
|
|
|
results = await executor.execute_actions(actions, regions, context)
|
|
|
|
# All actions should succeed
|
|
assert len(results) == 3
|
|
assert all(result.success for result in results)
|
|
|
|
# Verify all operations were called
|
|
mock_redis_client.set.assert_called_once() # Image save
|
|
mock_redis_client.expire.assert_called_once() # Set expiry
|
|
mock_redis_client.publish.assert_called_once() # Publish event
|
|
mock_db_manager.execute_query.assert_called_once() # Database insert
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_branch_dependent_actions(self, mock_database_connection):
|
|
"""Test actions that depend on branch results."""
|
|
mock_db_manager = Mock()
|
|
mock_db_manager.execute_query = AsyncMock(return_value=True)
|
|
|
|
executor = ActionExecutor(db_manager=mock_db_manager)
|
|
|
|
# Action that waits for classification branches
|
|
actions = [
|
|
{
|
|
"type": "postgresql_update_combined",
|
|
"table": "car_info",
|
|
"key_field": "session_id",
|
|
"fields": {
|
|
"car_brand": "{car_brand_cls.brand}",
|
|
"car_body_type": "{car_bodytype_cls.body_type}",
|
|
"car_color": "{car_color_cls.color}",
|
|
"confidence_brand": "{car_brand_cls.confidence}",
|
|
"confidence_bodytype": "{car_bodytype_cls.confidence}",
|
|
"updated_at": "NOW()"
|
|
},
|
|
"waitForBranches": ["car_brand_cls", "car_bodytype_cls", "car_color_cls"]
|
|
}
|
|
]
|
|
|
|
regions = {}
|
|
|
|
context = {
|
|
"session_id": "session_123"
|
|
}
|
|
|
|
# Simulated branch results
|
|
branch_results = {
|
|
"car_brand_cls": {"brand": "Toyota", "confidence": 0.87},
|
|
"car_bodytype_cls": {"body_type": "Sedan", "confidence": 0.82},
|
|
"car_color_cls": {"color": "Red", "confidence": 0.79}
|
|
}
|
|
|
|
results = await executor.execute_actions(actions, regions, context, branch_results)
|
|
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].action_type == ActionType.POSTGRESQL_UPDATE
|
|
|
|
# Verify database call with all branch data
|
|
mock_db_manager.execute_query.assert_called_once()
|
|
call_args = mock_db_manager.execute_query.call_args
|
|
query = call_args[0][0]
|
|
values = call_args[0][1]
|
|
|
|
assert "UPDATE car_info SET" in query
|
|
assert "car_brand = %s" in query
|
|
assert "car_body_type = %s" in query
|
|
assert "car_color = %s" in query
|
|
assert "WHERE session_id = %s" in query
|
|
|
|
assert "Toyota" in values
|
|
assert "Sedan" in values
|
|
assert "Red" in values
|
|
assert "session_123" in values |