786 lines
No EOL
29 KiB
Python
786 lines
No EOL
29 KiB
Python
"""
|
|
Unit tests for field mapping and template resolution.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
from datetime import datetime
|
|
import json
|
|
|
|
from detector_worker.pipeline.field_mapper import (
|
|
FieldMapper,
|
|
MappingContext,
|
|
TemplateResolver,
|
|
FieldMappingError,
|
|
NestedFieldAccessor
|
|
)
|
|
from detector_worker.detection.detection_result import DetectionResult, BoundingBox
|
|
|
|
|
|
class TestNestedFieldAccessor:
|
|
"""Test nested field access functionality."""
|
|
|
|
def test_get_nested_value_simple(self):
|
|
"""Test getting simple nested values."""
|
|
data = {
|
|
"user": {
|
|
"name": "John",
|
|
"age": 30,
|
|
"address": {
|
|
"city": "New York",
|
|
"zip": "10001"
|
|
}
|
|
}
|
|
}
|
|
|
|
accessor = NestedFieldAccessor()
|
|
|
|
assert accessor.get_nested_value(data, "user.name") == "John"
|
|
assert accessor.get_nested_value(data, "user.age") == 30
|
|
assert accessor.get_nested_value(data, "user.address.city") == "New York"
|
|
assert accessor.get_nested_value(data, "user.address.zip") == "10001"
|
|
|
|
def test_get_nested_value_array_access(self):
|
|
"""Test accessing array elements."""
|
|
data = {
|
|
"results": [
|
|
{"score": 0.9, "label": "car"},
|
|
{"score": 0.8, "label": "truck"}
|
|
],
|
|
"bbox": [100, 200, 300, 400]
|
|
}
|
|
|
|
accessor = NestedFieldAccessor()
|
|
|
|
assert accessor.get_nested_value(data, "results[0].score") == 0.9
|
|
assert accessor.get_nested_value(data, "results[0].label") == "car"
|
|
assert accessor.get_nested_value(data, "results[1].score") == 0.8
|
|
assert accessor.get_nested_value(data, "bbox[0]") == 100
|
|
assert accessor.get_nested_value(data, "bbox[3]") == 400
|
|
|
|
def test_get_nested_value_nonexistent_path(self):
|
|
"""Test accessing non-existent paths."""
|
|
data = {"user": {"name": "John"}}
|
|
accessor = NestedFieldAccessor()
|
|
|
|
assert accessor.get_nested_value(data, "user.nonexistent") is None
|
|
assert accessor.get_nested_value(data, "nonexistent.field") is None
|
|
assert accessor.get_nested_value(data, "user.address.city") is None
|
|
|
|
def test_get_nested_value_with_default(self):
|
|
"""Test getting nested values with default fallback."""
|
|
data = {"user": {"name": "John"}}
|
|
accessor = NestedFieldAccessor()
|
|
|
|
assert accessor.get_nested_value(data, "user.age", default=25) == 25
|
|
assert accessor.get_nested_value(data, "user.name", default="Unknown") == "John"
|
|
|
|
def test_set_nested_value(self):
|
|
"""Test setting nested values."""
|
|
data = {}
|
|
accessor = NestedFieldAccessor()
|
|
|
|
accessor.set_nested_value(data, "user.name", "John")
|
|
assert data["user"]["name"] == "John"
|
|
|
|
accessor.set_nested_value(data, "user.address.city", "New York")
|
|
assert data["user"]["address"]["city"] == "New York"
|
|
|
|
accessor.set_nested_value(data, "scores[0]", 0.95)
|
|
assert data["scores"][0] == 0.95
|
|
|
|
def test_set_nested_value_overwrite(self):
|
|
"""Test overwriting existing nested values."""
|
|
data = {"user": {"name": "John", "age": 30}}
|
|
accessor = NestedFieldAccessor()
|
|
|
|
accessor.set_nested_value(data, "user.name", "Jane")
|
|
assert data["user"]["name"] == "Jane"
|
|
assert data["user"]["age"] == 30 # Should not affect other fields
|
|
|
|
|
|
class TestTemplateResolver:
|
|
"""Test template string resolution."""
|
|
|
|
def test_resolve_simple_template(self):
|
|
"""Test resolving simple template variables."""
|
|
resolver = TemplateResolver()
|
|
|
|
template = "Hello {name}, you are {age} years old"
|
|
context = {"name": "John", "age": 30}
|
|
|
|
result = resolver.resolve(template, context)
|
|
assert result == "Hello John, you are 30 years old"
|
|
|
|
def test_resolve_nested_template(self):
|
|
"""Test resolving nested field templates."""
|
|
resolver = TemplateResolver()
|
|
|
|
template = "User: {user.name} from {user.address.city}"
|
|
context = {
|
|
"user": {
|
|
"name": "John",
|
|
"address": {"city": "New York", "zip": "10001"}
|
|
}
|
|
}
|
|
|
|
result = resolver.resolve(template, context)
|
|
assert result == "User: John from New York"
|
|
|
|
def test_resolve_array_template(self):
|
|
"""Test resolving array element templates."""
|
|
resolver = TemplateResolver()
|
|
|
|
template = "First result: {results[0].label} ({results[0].score})"
|
|
context = {
|
|
"results": [
|
|
{"label": "car", "score": 0.95},
|
|
{"label": "truck", "score": 0.87}
|
|
]
|
|
}
|
|
|
|
result = resolver.resolve(template, context)
|
|
assert result == "First result: car (0.95)"
|
|
|
|
def test_resolve_missing_variables(self):
|
|
"""Test resolving templates with missing variables."""
|
|
resolver = TemplateResolver()
|
|
|
|
template = "Hello {name}, you are {age} years old"
|
|
context = {"name": "John"} # Missing age
|
|
|
|
with pytest.raises(FieldMappingError) as exc_info:
|
|
resolver.resolve(template, context)
|
|
|
|
assert "Variable 'age' not found" in str(exc_info.value)
|
|
|
|
def test_resolve_with_defaults(self):
|
|
"""Test resolving templates with default values."""
|
|
resolver = TemplateResolver(allow_missing=True)
|
|
|
|
template = "Hello {name}, you are {age|25} years old"
|
|
context = {"name": "John"} # Missing age, should use default
|
|
|
|
result = resolver.resolve(template, context)
|
|
assert result == "Hello John, you are 25 years old"
|
|
|
|
def test_resolve_complex_template(self):
|
|
"""Test resolving complex templates with multiple variable types."""
|
|
resolver = TemplateResolver()
|
|
|
|
template = "{camera_id}:{timestamp}:{session_id}:{results[0].class}_{bbox[0]}_{bbox[1]}"
|
|
context = {
|
|
"camera_id": "cam001",
|
|
"timestamp": 1640995200000,
|
|
"session_id": "sess123",
|
|
"results": [{"class": "car", "confidence": 0.95}],
|
|
"bbox": [100, 200, 300, 400]
|
|
}
|
|
|
|
result = resolver.resolve(template, context)
|
|
assert result == "cam001:1640995200000:sess123:car_100_200"
|
|
|
|
def test_resolve_conditional_template(self):
|
|
"""Test resolving conditional templates."""
|
|
resolver = TemplateResolver()
|
|
|
|
# Simple conditional
|
|
template = "{name} is {age > 18 ? 'adult' : 'minor'}"
|
|
|
|
context_adult = {"name": "John", "age": 25}
|
|
result_adult = resolver.resolve(template, context_adult)
|
|
assert result_adult == "John is adult"
|
|
|
|
context_minor = {"name": "Jane", "age": 16}
|
|
result_minor = resolver.resolve(template, context_minor)
|
|
assert result_minor == "Jane is minor"
|
|
|
|
def test_escape_braces(self):
|
|
"""Test escaping braces in templates."""
|
|
resolver = TemplateResolver()
|
|
|
|
template = "Literal {{braces}} and variable {name}"
|
|
context = {"name": "John"}
|
|
|
|
result = resolver.resolve(template, context)
|
|
assert result == "Literal {braces} and variable John"
|
|
|
|
|
|
class TestMappingContext:
|
|
"""Test mapping context data structure."""
|
|
|
|
def test_creation(self):
|
|
"""Test mapping context creation."""
|
|
detection = DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001, 1640995200000)
|
|
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123",
|
|
detection=detection,
|
|
timestamp=1640995200000
|
|
)
|
|
|
|
assert context.camera_id == "camera_001"
|
|
assert context.display_id == "display_001"
|
|
assert context.session_id == "session_123"
|
|
assert context.detection == detection
|
|
assert context.timestamp == 1640995200000
|
|
assert context.branch_results == {}
|
|
assert context.metadata == {}
|
|
|
|
def test_add_branch_result(self):
|
|
"""Test adding branch results to context."""
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123"
|
|
)
|
|
|
|
context.add_branch_result("car_brand_cls", {"brand": "Toyota", "confidence": 0.87})
|
|
context.add_branch_result("car_bodytype_cls", {"body_type": "Sedan", "confidence": 0.82})
|
|
|
|
assert len(context.branch_results) == 2
|
|
assert context.branch_results["car_brand_cls"]["brand"] == "Toyota"
|
|
assert context.branch_results["car_bodytype_cls"]["body_type"] == "Sedan"
|
|
|
|
def test_to_dict(self):
|
|
"""Test converting context to dictionary."""
|
|
detection = DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001, 1640995200000)
|
|
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123",
|
|
detection=detection,
|
|
timestamp=1640995200000
|
|
)
|
|
|
|
context.add_branch_result("car_brand_cls", {"brand": "Toyota"})
|
|
context.add_metadata("model_id", "yolo_v8")
|
|
|
|
context_dict = context.to_dict()
|
|
|
|
assert context_dict["camera_id"] == "camera_001"
|
|
assert context_dict["display_id"] == "display_001"
|
|
assert context_dict["session_id"] == "session_123"
|
|
assert context_dict["timestamp"] == 1640995200000
|
|
assert context_dict["class"] == "car"
|
|
assert context_dict["confidence"] == 0.9
|
|
assert context_dict["track_id"] == 1001
|
|
assert context_dict["bbox"]["x1"] == 100
|
|
assert context_dict["car_brand_cls"]["brand"] == "Toyota"
|
|
assert context_dict["model_id"] == "yolo_v8"
|
|
|
|
def test_add_metadata(self):
|
|
"""Test adding metadata to context."""
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123"
|
|
)
|
|
|
|
context.add_metadata("model_version", "v2.1")
|
|
context.add_metadata("inference_time", 0.15)
|
|
|
|
assert context.metadata["model_version"] == "v2.1"
|
|
assert context.metadata["inference_time"] == 0.15
|
|
|
|
|
|
class TestFieldMapper:
|
|
"""Test field mapping functionality."""
|
|
|
|
def test_initialization(self):
|
|
"""Test field mapper initialization."""
|
|
mapper = FieldMapper()
|
|
|
|
assert isinstance(mapper.template_resolver, TemplateResolver)
|
|
assert isinstance(mapper.field_accessor, NestedFieldAccessor)
|
|
|
|
def test_map_fields_simple(self):
|
|
"""Test simple field mapping."""
|
|
mapper = FieldMapper()
|
|
|
|
field_mappings = {
|
|
"camera_id": "{camera_id}",
|
|
"detection_class": "{class}",
|
|
"confidence_score": "{confidence}",
|
|
"track_identifier": "{track_id}"
|
|
}
|
|
|
|
detection = DetectionResult("car", 0.92, BoundingBox(100, 200, 300, 400), 1001, 1640995200000)
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123",
|
|
detection=detection,
|
|
timestamp=1640995200000
|
|
)
|
|
|
|
mapped_fields = mapper.map_fields(field_mappings, context)
|
|
|
|
assert mapped_fields["camera_id"] == "camera_001"
|
|
assert mapped_fields["detection_class"] == "car"
|
|
assert mapped_fields["confidence_score"] == 0.92
|
|
assert mapped_fields["track_identifier"] == 1001
|
|
|
|
def test_map_fields_with_branch_results(self):
|
|
"""Test field mapping with branch results."""
|
|
mapper = FieldMapper()
|
|
|
|
field_mappings = {
|
|
"car_brand": "{car_brand_cls.brand}",
|
|
"car_model": "{car_brand_cls.model}",
|
|
"body_type": "{car_bodytype_cls.body_type}",
|
|
"brand_confidence": "{car_brand_cls.confidence}",
|
|
"combined_info": "{car_brand_cls.brand} {car_bodytype_cls.body_type}"
|
|
}
|
|
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123"
|
|
)
|
|
|
|
context.add_branch_result("car_brand_cls", {
|
|
"brand": "Toyota",
|
|
"model": "Camry",
|
|
"confidence": 0.87
|
|
})
|
|
context.add_branch_result("car_bodytype_cls", {
|
|
"body_type": "Sedan",
|
|
"confidence": 0.82
|
|
})
|
|
|
|
mapped_fields = mapper.map_fields(field_mappings, context)
|
|
|
|
assert mapped_fields["car_brand"] == "Toyota"
|
|
assert mapped_fields["car_model"] == "Camry"
|
|
assert mapped_fields["body_type"] == "Sedan"
|
|
assert mapped_fields["brand_confidence"] == 0.87
|
|
assert mapped_fields["combined_info"] == "Toyota Sedan"
|
|
|
|
def test_map_fields_bbox_access(self):
|
|
"""Test field mapping with bounding box access."""
|
|
mapper = FieldMapper()
|
|
|
|
field_mappings = {
|
|
"bbox_x1": "{bbox.x1}",
|
|
"bbox_y1": "{bbox.y1}",
|
|
"bbox_x2": "{bbox.x2}",
|
|
"bbox_y2": "{bbox.y2}",
|
|
"bbox_width": "{bbox.width}",
|
|
"bbox_height": "{bbox.height}",
|
|
"bbox_area": "{bbox.area}",
|
|
"bbox_center_x": "{bbox.center_x}",
|
|
"bbox_center_y": "{bbox.center_y}"
|
|
}
|
|
|
|
detection = DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123",
|
|
detection=detection
|
|
)
|
|
|
|
mapped_fields = mapper.map_fields(field_mappings, context)
|
|
|
|
assert mapped_fields["bbox_x1"] == 100
|
|
assert mapped_fields["bbox_y1"] == 200
|
|
assert mapped_fields["bbox_x2"] == 300
|
|
assert mapped_fields["bbox_y2"] == 400
|
|
assert mapped_fields["bbox_width"] == 200 # 300 - 100
|
|
assert mapped_fields["bbox_height"] == 200 # 400 - 200
|
|
assert mapped_fields["bbox_area"] == 40000 # 200 * 200
|
|
assert mapped_fields["bbox_center_x"] == 200 # (100 + 300) / 2
|
|
assert mapped_fields["bbox_center_y"] == 300 # (200 + 400) / 2
|
|
|
|
def test_map_fields_with_sql_functions(self):
|
|
"""Test field mapping with SQL function templates."""
|
|
mapper = FieldMapper()
|
|
|
|
field_mappings = {
|
|
"created_at": "NOW()",
|
|
"updated_at": "CURRENT_TIMESTAMP",
|
|
"uuid_field": "UUID()",
|
|
"json_data": "JSON_OBJECT('class', '{class}', 'confidence', {confidence})"
|
|
}
|
|
|
|
detection = DetectionResult("car", 0.9, BoundingBox(100, 200, 300, 400), 1001)
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123",
|
|
detection=detection
|
|
)
|
|
|
|
mapped_fields = mapper.map_fields(field_mappings, context)
|
|
|
|
# SQL functions should pass through unchanged
|
|
assert mapped_fields["created_at"] == "NOW()"
|
|
assert mapped_fields["updated_at"] == "CURRENT_TIMESTAMP"
|
|
assert mapped_fields["uuid_field"] == "UUID()"
|
|
assert mapped_fields["json_data"] == "JSON_OBJECT('class', 'car', 'confidence', 0.9)"
|
|
|
|
def test_map_fields_missing_branch_data(self):
|
|
"""Test field mapping with missing branch data."""
|
|
mapper = FieldMapper()
|
|
|
|
field_mappings = {
|
|
"car_brand": "{car_brand_cls.brand}",
|
|
"car_model": "{nonexistent_branch.model}"
|
|
}
|
|
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123"
|
|
)
|
|
|
|
# Only add one branch result
|
|
context.add_branch_result("car_brand_cls", {"brand": "Toyota"})
|
|
|
|
with pytest.raises(FieldMappingError) as exc_info:
|
|
mapper.map_fields(field_mappings, context)
|
|
|
|
assert "nonexistent_branch.model" in str(exc_info.value)
|
|
|
|
def test_map_fields_with_defaults(self):
|
|
"""Test field mapping with default values."""
|
|
mapper = FieldMapper(allow_missing=True)
|
|
|
|
field_mappings = {
|
|
"car_brand": "{car_brand_cls.brand|Unknown}",
|
|
"car_model": "{car_brand_cls.model|N/A}",
|
|
"confidence": "{confidence|0.0}"
|
|
}
|
|
|
|
context = MappingContext(
|
|
camera_id="camera_001",
|
|
display_id="display_001",
|
|
session_id="session_123"
|
|
)
|
|
|
|
# Don't add any branch results
|
|
mapped_fields = mapper.map_fields(field_mappings, context)
|
|
|
|
assert mapped_fields["car_brand"] == "Unknown"
|
|
assert mapped_fields["car_model"] == "N/A"
|
|
assert mapped_fields["confidence"] == "0.0"
|
|
|
|
def test_map_database_fields(self):
|
|
"""Test mapping fields for database operations."""
|
|
mapper = FieldMapper()
|
|
|
|
# Database field mapping
|
|
db_field_mappings = {
|
|
"camera_id": "{camera_id}",
|
|
"session_id": "{session_id}",
|
|
"detection_timestamp": "{timestamp}",
|
|
"object_class": "{class}",
|
|
"detection_confidence": "{confidence}",
|
|
"track_id": "{track_id}",
|
|
"bbox_json": "JSON_OBJECT('x1', {bbox.x1}, 'y1', {bbox.y1}, 'x2', {bbox.x2}, 'y2', {bbox.y2})",
|
|
"car_brand": "{car_brand_cls.brand}",
|
|
"car_body_type": "{car_bodytype_cls.body_type}",
|
|
"license_plate": "{license_ocr.text}",
|
|
"created_at": "NOW()",
|
|
"updated_at": "NOW()"
|
|
}
|
|
|
|
detection = DetectionResult("car", 0.93, BoundingBox(150, 250, 350, 450), 2001, 1640995300000)
|
|
context = MappingContext(
|
|
camera_id="camera_002",
|
|
display_id="display_002",
|
|
session_id="session_456",
|
|
detection=detection,
|
|
timestamp=1640995300000
|
|
)
|
|
|
|
# Add branch results
|
|
context.add_branch_result("car_brand_cls", {"brand": "Honda", "confidence": 0.89})
|
|
context.add_branch_result("car_bodytype_cls", {"body_type": "SUV", "confidence": 0.85})
|
|
context.add_branch_result("license_ocr", {"text": "ABC-123", "confidence": 0.76})
|
|
|
|
mapped_fields = mapper.map_fields(db_field_mappings, context)
|
|
|
|
assert mapped_fields["camera_id"] == "camera_002"
|
|
assert mapped_fields["session_id"] == "session_456"
|
|
assert mapped_fields["detection_timestamp"] == 1640995300000
|
|
assert mapped_fields["object_class"] == "car"
|
|
assert mapped_fields["detection_confidence"] == 0.93
|
|
assert mapped_fields["track_id"] == 2001
|
|
assert mapped_fields["bbox_json"] == "JSON_OBJECT('x1', 150, 'y1', 250, 'x2', 350, 'y2', 450)"
|
|
assert mapped_fields["car_brand"] == "Honda"
|
|
assert mapped_fields["car_body_type"] == "SUV"
|
|
assert mapped_fields["license_plate"] == "ABC-123"
|
|
assert mapped_fields["created_at"] == "NOW()"
|
|
assert mapped_fields["updated_at"] == "NOW()"
|
|
|
|
def test_map_redis_keys(self):
|
|
"""Test mapping Redis key templates."""
|
|
mapper = FieldMapper()
|
|
|
|
key_templates = [
|
|
"inference:{camera_id}:{timestamp}:{session_id}:car",
|
|
"detection:{display_id}:{track_id}",
|
|
"cropped_image:{camera_id}:{session_id}:{class}",
|
|
"metadata:{session_id}:brands:{car_brand_cls.brand}",
|
|
"tracking:{camera_id}:active_tracks"
|
|
]
|
|
|
|
detection = DetectionResult("car", 0.88, BoundingBox(200, 300, 400, 500), 3001, 1640995400000)
|
|
context = MappingContext(
|
|
camera_id="camera_003",
|
|
display_id="display_003",
|
|
session_id="session_789",
|
|
detection=detection,
|
|
timestamp=1640995400000
|
|
)
|
|
|
|
context.add_branch_result("car_brand_cls", {"brand": "Ford"})
|
|
|
|
mapped_keys = [mapper.map_template(template, context) for template in key_templates]
|
|
|
|
expected_keys = [
|
|
"inference:camera_003:1640995400000:session_789:car",
|
|
"detection:display_003:3001",
|
|
"cropped_image:camera_003:session_789:car",
|
|
"metadata:session_789:brands:Ford",
|
|
"tracking:camera_003:active_tracks"
|
|
]
|
|
|
|
assert mapped_keys == expected_keys
|
|
|
|
def test_map_template(self):
|
|
"""Test single template mapping."""
|
|
mapper = FieldMapper()
|
|
|
|
template = "Camera {camera_id} detected {class} with {confidence:.2f} confidence at {timestamp}"
|
|
|
|
detection = DetectionResult("truck", 0.876, BoundingBox(100, 150, 300, 350), 4001, 1640995500000)
|
|
context = MappingContext(
|
|
camera_id="camera_004",
|
|
display_id="display_004",
|
|
session_id="session_101",
|
|
detection=detection,
|
|
timestamp=1640995500000
|
|
)
|
|
|
|
result = mapper.map_template(template, context)
|
|
expected = "Camera camera_004 detected truck with 0.88 confidence at 1640995500000"
|
|
|
|
assert result == expected
|
|
|
|
def test_validate_field_mappings(self):
|
|
"""Test field mapping validation."""
|
|
mapper = FieldMapper()
|
|
|
|
# Valid mappings
|
|
valid_mappings = {
|
|
"camera_id": "{camera_id}",
|
|
"class": "{class}",
|
|
"confidence": "{confidence}",
|
|
"created_at": "NOW()"
|
|
}
|
|
|
|
assert mapper.validate_field_mappings(valid_mappings) is True
|
|
|
|
# Invalid mappings (malformed templates)
|
|
invalid_mappings = {
|
|
"camera_id": "{camera_id", # Missing closing brace
|
|
"class": "class}", # Missing opening brace
|
|
"confidence": "{nonexistent_field}" # This might be valid depending on context
|
|
}
|
|
|
|
with pytest.raises(FieldMappingError):
|
|
mapper.validate_field_mappings(invalid_mappings)
|
|
|
|
def test_create_context_from_detection(self):
|
|
"""Test creating mapping context from detection result."""
|
|
mapper = FieldMapper()
|
|
|
|
detection = DetectionResult("car", 0.95, BoundingBox(50, 100, 250, 300), 5001, 1640995600000)
|
|
|
|
context = mapper.create_context_from_detection(
|
|
detection,
|
|
camera_id="camera_005",
|
|
display_id="display_005",
|
|
session_id="session_202"
|
|
)
|
|
|
|
assert context.camera_id == "camera_005"
|
|
assert context.display_id == "display_005"
|
|
assert context.session_id == "session_202"
|
|
assert context.detection == detection
|
|
assert context.timestamp == 1640995600000
|
|
|
|
def test_format_sql_value(self):
|
|
"""Test SQL value formatting."""
|
|
mapper = FieldMapper()
|
|
|
|
# String values should be quoted
|
|
assert mapper.format_sql_value("test_string") == "'test_string'"
|
|
assert mapper.format_sql_value("John's car") == "'John''s car'" # Escape quotes
|
|
|
|
# Numeric values should not be quoted
|
|
assert mapper.format_sql_value(42) == "42"
|
|
assert mapper.format_sql_value(3.14) == "3.14"
|
|
assert mapper.format_sql_value(0.95) == "0.95"
|
|
|
|
# Boolean values
|
|
assert mapper.format_sql_value(True) == "TRUE"
|
|
assert mapper.format_sql_value(False) == "FALSE"
|
|
|
|
# None/NULL values
|
|
assert mapper.format_sql_value(None) == "NULL"
|
|
|
|
# SQL functions should pass through
|
|
assert mapper.format_sql_value("NOW()") == "NOW()"
|
|
assert mapper.format_sql_value("CURRENT_TIMESTAMP") == "CURRENT_TIMESTAMP"
|
|
|
|
|
|
class TestFieldMapperIntegration:
|
|
"""Integration tests for field mapping."""
|
|
|
|
def test_complete_mapping_workflow(self):
|
|
"""Test complete field mapping workflow."""
|
|
mapper = FieldMapper()
|
|
|
|
# Simulate complete detection workflow
|
|
detection = DetectionResult("car", 0.91, BoundingBox(120, 180, 320, 380), 6001, 1640995700000)
|
|
context = MappingContext(
|
|
camera_id="camera_006",
|
|
display_id="display_006",
|
|
session_id="session_303",
|
|
detection=detection,
|
|
timestamp=1640995700000
|
|
)
|
|
|
|
# Add comprehensive branch results
|
|
context.add_branch_result("car_brand_cls", {
|
|
"brand": "BMW",
|
|
"model": "X5",
|
|
"confidence": 0.84,
|
|
"top3_brands": ["BMW", "Audi", "Mercedes"]
|
|
})
|
|
|
|
context.add_branch_result("car_bodytype_cls", {
|
|
"body_type": "SUV",
|
|
"confidence": 0.79,
|
|
"features": ["tall", "4_doors", "roof_rails"]
|
|
})
|
|
|
|
context.add_branch_result("car_color_cls", {
|
|
"color": "Black",
|
|
"confidence": 0.73,
|
|
"rgb_values": [20, 25, 30]
|
|
})
|
|
|
|
context.add_branch_result("license_ocr", {
|
|
"text": "XYZ-789",
|
|
"confidence": 0.68,
|
|
"region_bbox": [150, 320, 290, 360]
|
|
})
|
|
|
|
# Database field mapping
|
|
db_mappings = {
|
|
"camera_id": "{camera_id}",
|
|
"display_id": "{display_id}",
|
|
"session_id": "{session_id}",
|
|
"detection_timestamp": "{timestamp}",
|
|
"object_class": "{class}",
|
|
"detection_confidence": "{confidence}",
|
|
"track_id": "{track_id}",
|
|
"bbox_x1": "{bbox.x1}",
|
|
"bbox_y1": "{bbox.y1}",
|
|
"bbox_x2": "{bbox.x2}",
|
|
"bbox_y2": "{bbox.y2}",
|
|
"bbox_area": "{bbox.area}",
|
|
"car_brand": "{car_brand_cls.brand}",
|
|
"car_model": "{car_brand_cls.model}",
|
|
"car_body_type": "{car_bodytype_cls.body_type}",
|
|
"car_color": "{car_color_cls.color}",
|
|
"license_plate": "{license_ocr.text}",
|
|
"brand_confidence": "{car_brand_cls.confidence}",
|
|
"bodytype_confidence": "{car_bodytype_cls.confidence}",
|
|
"color_confidence": "{car_color_cls.confidence}",
|
|
"license_confidence": "{license_ocr.confidence}",
|
|
"detection_summary": "{car_brand_cls.brand} {car_bodytype_cls.body_type} ({car_color_cls.color})",
|
|
"created_at": "NOW()",
|
|
"updated_at": "NOW()"
|
|
}
|
|
|
|
mapped_db_fields = mapper.map_fields(db_mappings, context)
|
|
|
|
# Verify all mappings
|
|
assert mapped_db_fields["camera_id"] == "camera_006"
|
|
assert mapped_db_fields["session_id"] == "session_303"
|
|
assert mapped_db_fields["object_class"] == "car"
|
|
assert mapped_db_fields["detection_confidence"] == 0.91
|
|
assert mapped_db_fields["track_id"] == 6001
|
|
assert mapped_db_fields["bbox_area"] == 40000 # 200 * 200
|
|
assert mapped_db_fields["car_brand"] == "BMW"
|
|
assert mapped_db_fields["car_model"] == "X5"
|
|
assert mapped_db_fields["car_body_type"] == "SUV"
|
|
assert mapped_db_fields["car_color"] == "Black"
|
|
assert mapped_db_fields["license_plate"] == "XYZ-789"
|
|
assert mapped_db_fields["detection_summary"] == "BMW SUV (Black)"
|
|
|
|
# Redis key mapping
|
|
redis_key_templates = [
|
|
"detection:{camera_id}:{session_id}:main",
|
|
"cropped:{camera_id}:{session_id}:car_image",
|
|
"metadata:{session_id}:brand:{car_brand_cls.brand}",
|
|
"tracking:{camera_id}:track_{track_id}",
|
|
"classification:{session_id}:results"
|
|
]
|
|
|
|
mapped_redis_keys = [
|
|
mapper.map_template(template, context)
|
|
for template in redis_key_templates
|
|
]
|
|
|
|
expected_redis_keys = [
|
|
"detection:camera_006:session_303:main",
|
|
"cropped:camera_006:session_303:car_image",
|
|
"metadata:session_303:brand:BMW",
|
|
"tracking:camera_006:track_6001",
|
|
"classification:session_303:results"
|
|
]
|
|
|
|
assert mapped_redis_keys == expected_redis_keys
|
|
|
|
def test_error_handling_and_recovery(self):
|
|
"""Test error handling and recovery in field mapping."""
|
|
mapper = FieldMapper(allow_missing=True)
|
|
|
|
# Context with missing detection
|
|
context = MappingContext(
|
|
camera_id="camera_007",
|
|
display_id="display_007",
|
|
session_id="session_404"
|
|
)
|
|
|
|
# Partial branch results
|
|
context.add_branch_result("car_brand_cls", {"brand": "Unknown"})
|
|
# Missing car_bodytype_cls branch
|
|
|
|
# Field mappings with some missing data
|
|
mappings = {
|
|
"camera_id": "{camera_id}",
|
|
"detection_class": "{class|Unknown}",
|
|
"confidence": "{confidence|0.0}",
|
|
"car_brand": "{car_brand_cls.brand|N/A}",
|
|
"car_body_type": "{car_bodytype_cls.body_type|Unknown}",
|
|
"car_model": "{car_brand_cls.model|N/A}"
|
|
}
|
|
|
|
mapped_fields = mapper.map_fields(mappings, context)
|
|
|
|
assert mapped_fields["camera_id"] == "camera_007"
|
|
assert mapped_fields["detection_class"] == "Unknown"
|
|
assert mapped_fields["confidence"] == "0.0"
|
|
assert mapped_fields["car_brand"] == "Unknown"
|
|
assert mapped_fields["car_body_type"] == "Unknown"
|
|
assert mapped_fields["car_model"] == "N/A" |