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