Files
motovaultpro/mvp-platform-services/vehicles/etl/tests/test_engine_spec_parser.py
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

381 lines
16 KiB
Python

"""
Unit Tests for EngineSpecParser
Tests the engine specification parsing functionality including:
- Standard engine format parsing (displacement, configuration, cylinders)
- CRITICAL: L→I normalization (L3 becomes I3)
- Hybrid and electric vehicle detection
- Fuel type and aspiration parsing
- Electric motor creation for empty engines arrays
- Error handling for unparseable engines
"""
import unittest
from unittest.mock import patch
# Import the class we're testing
from ..utils.engine_spec_parser import EngineSpecParser, EngineSpec
class TestEngineSpecParser(unittest.TestCase):
"""Test cases for EngineSpecParser utility"""
def setUp(self):
"""Set up test environment before each test"""
self.parser = EngineSpecParser()
def test_parse_standard_engines(self):
"""Test parsing of standard engine formats"""
test_cases = [
# Format: (input, expected_displacement, expected_config, expected_cylinders)
("2.0L I4", 2.0, "I", 4),
("3.5L V6", 3.5, "V", 6),
("5.6L V8", 5.6, "V", 8),
("1.6L I4", 1.6, "I", 4),
("6.2L V8", 6.2, "V", 8),
]
for engine_str, expected_disp, expected_config, expected_cyl in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.displacement_l, expected_disp,
f"Displacement: expected {expected_disp}, got {spec.displacement_l}")
self.assertEqual(spec.configuration, expected_config,
f"Configuration: expected {expected_config}, got {spec.configuration}")
self.assertEqual(spec.cylinders, expected_cyl,
f"Cylinders: expected {expected_cyl}, got {spec.cylinders}")
self.assertEqual(spec.fuel_type, "Gasoline") # Default
self.assertEqual(spec.aspiration, "Natural") # Default
def test_l_to_i_normalization(self):
"""Test CRITICAL L→I configuration normalization"""
test_cases = [
# L-configuration should become I (Inline)
("1.5L L3", "I", 3),
("2.0L L4", "I", 4),
("1.2L L3", "I", 3),
]
for engine_str, expected_config, expected_cyl in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.configuration, expected_config,
f"L→I normalization failed: '{engine_str}' should become I{expected_cyl}")
self.assertEqual(spec.cylinders, expected_cyl)
self.assertEqual(spec.raw_string, engine_str) # Original preserved
def test_subaru_boxer_engines(self):
"""Test Subaru Boxer (H-configuration) engines"""
test_cases = [
("2.4L H4", 2.4, "H", 4),
("2.0L H4", 2.0, "H", 4),
("2.5L H4", 2.5, "H", 4),
]
for engine_str, expected_disp, expected_config, expected_cyl in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.displacement_l, expected_disp)
self.assertEqual(spec.configuration, expected_config) # Should remain H
self.assertEqual(spec.cylinders, expected_cyl)
def test_w_configuration_engines(self):
"""Test W-configuration engines (VW Group, Bentley)"""
test_cases = [
("6.0L W12", 6.0, "W", 12),
("4.0L W8", 4.0, "W", 8),
("8.0L W16", 8.0, "W", 16), # Theoretical case
]
for engine_str, expected_disp, expected_config, expected_cyl in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.displacement_l, expected_disp)
self.assertEqual(spec.configuration, expected_config) # Should remain W
self.assertEqual(spec.cylinders, expected_cyl)
def test_hybrid_detection(self):
"""Test hybrid engine detection patterns"""
test_cases = [
# Format: (input, expected_fuel_type)
("2.5L I4 FULL HYBRID EV- (FHEV)", "Full Hybrid"),
("1.5L L3 PLUG-IN HYBRID EV- (PHEV)", "Plug-in Hybrid"),
("2.0L I4 FULL HYBRID EV- (FHEV)", "Full Hybrid"),
("1.8L I4 HYBRID", "Hybrid"), # Generic hybrid
]
for engine_str, expected_fuel_type in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.fuel_type, expected_fuel_type,
f"Expected fuel type '{expected_fuel_type}' for '{engine_str}'")
def test_l_to_i_with_hybrid(self):
"""Test L→I normalization combined with hybrid detection"""
test_cases = [
("1.5L L3 PLUG-IN HYBRID EV- (PHEV)", "I", 3, "Plug-in Hybrid"),
("1.2L L3 FULL HYBRID EV- (FHEV)", "I", 3, "Full Hybrid"),
]
for engine_str, expected_config, expected_cyl, expected_fuel in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
# Test both L→I normalization AND hybrid detection
self.assertEqual(spec.configuration, expected_config,
f"L→I normalization failed for hybrid: '{engine_str}'")
self.assertEqual(spec.cylinders, expected_cyl)
self.assertEqual(spec.fuel_type, expected_fuel,
f"Hybrid detection failed: '{engine_str}'")
def test_flex_fuel_detection(self):
"""Test flex fuel detection"""
test_cases = [
("5.6L V8 FLEX", "Flex Fuel"),
("4.0L V6 FLEX", "Flex Fuel"),
]
for engine_str, expected_fuel_type in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.fuel_type, expected_fuel_type)
def test_electric_detection(self):
"""Test electric engine detection"""
test_cases = [
("1.8L I4 ELECTRIC", "Electric"),
]
for engine_str, expected_fuel_type in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.fuel_type, expected_fuel_type)
def test_aspiration_detection(self):
"""Test turbo/supercharged detection"""
# Note: These patterns are less common in current JSON data
test_cases = [
("2.0L I4 TURBO", "Turbocharged"),
("6.2L V8 SUPERCHARGED", "Supercharged"),
("3.0L V6 SC", "Supercharged"), # SC abbreviation
]
for engine_str, expected_aspiration in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.aspiration, expected_aspiration)
def test_create_electric_motor(self):
"""Test electric motor creation for empty engines arrays"""
spec = self.parser.create_electric_motor()
self.assertIsNone(spec.displacement_l)
self.assertEqual(spec.configuration, "Electric")
self.assertIsNone(spec.cylinders)
self.assertEqual(spec.fuel_type, "Electric")
self.assertIsNone(spec.aspiration)
self.assertEqual(spec.raw_string, "Electric Motor")
def test_parse_multiple_engines_empty_array(self):
"""Test handling of empty engines array (electric vehicles)"""
# Empty array should create electric motor
specs = self.parser.parse_multiple_engines([])
self.assertEqual(len(specs), 1)
self.assertEqual(specs[0].raw_string, "Electric Motor")
self.assertEqual(specs[0].fuel_type, "Electric")
def test_parse_multiple_engines_normal(self):
"""Test parsing multiple engine specifications"""
engine_strings = [
"2.0L I4",
"3.5L V6",
"1.5L L3 PLUG-IN HYBRID EV- (PHEV)" # Includes L→I normalization
]
specs = self.parser.parse_multiple_engines(engine_strings)
self.assertEqual(len(specs), 3)
# Check first engine
self.assertEqual(specs[0].displacement_l, 2.0)
self.assertEqual(specs[0].configuration, "I")
# Check third engine (L→I normalization + hybrid)
self.assertEqual(specs[2].configuration, "I") # L normalized to I
self.assertEqual(specs[2].fuel_type, "Plug-in Hybrid")
def test_unparseable_engines(self):
"""Test handling of unparseable engine strings"""
unparseable_cases = [
"Custom Hybrid System",
"V12 Twin-Turbo Custom",
"V10 Plus",
"Unknown Engine Type",
"", # Empty string
]
for engine_str in unparseable_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
# Should create fallback engine
self.assertEqual(spec.configuration, "Unknown")
self.assertEqual(spec.fuel_type, "Unknown")
self.assertEqual(spec.aspiration, "Natural")
self.assertIsNone(spec.displacement_l)
self.assertIsNone(spec.cylinders)
self.assertEqual(spec.raw_string, engine_str or "Empty Engine String")
def test_case_insensitive_parsing(self):
"""Test that parsing is case insensitive"""
test_cases = [
("2.0l i4", 2.0, "I", 4), # Lowercase
("3.5L v6", 3.5, "V", 6), # Mixed case
("1.5L l3 plug-in hybrid ev- (phev)", "I", 3, "Plug-in Hybrid"), # Lowercase with hybrid
]
for engine_str, expected_disp, expected_config, expected_cyl in test_cases[:2]:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.displacement_l, expected_disp)
self.assertEqual(spec.configuration, expected_config)
self.assertEqual(spec.cylinders, expected_cyl)
# Test hybrid case separately
spec = self.parser.parse_engine_string("1.5L l3 plug-in hybrid ev- (phev)")
self.assertEqual(spec.configuration, "I") # L→I normalization
self.assertEqual(spec.fuel_type, "Plug-in Hybrid")
def test_get_unique_engines(self):
"""Test deduplication of engine specifications"""
# Create list with duplicates
engine_specs = [
self.parser.parse_engine_string("2.0L I4"),
self.parser.parse_engine_string("2.0L I4"), # Duplicate
self.parser.parse_engine_string("3.5L V6"),
self.parser.parse_engine_string("2.0L I4"), # Another duplicate
]
unique_specs = self.parser.get_unique_engines(engine_specs)
# Should have only 2 unique engines
self.assertEqual(len(unique_specs), 2)
# Check that we have the expected unique engines
displacement_configs = [(spec.displacement_l, spec.configuration, spec.cylinders)
for spec in unique_specs]
self.assertIn((2.0, "I", 4), displacement_configs)
self.assertIn((3.5, "V", 6), displacement_configs)
def test_validate_engine_spec(self):
"""Test engine specification validation"""
# Valid engine
valid_spec = self.parser.parse_engine_string("2.0L I4")
warnings = self.parser.validate_engine_spec(valid_spec)
self.assertEqual(len(warnings), 0)
# Invalid displacement
invalid_spec = EngineSpec(
displacement_l=-1.0, # Invalid
configuration="I",
cylinders=4,
fuel_type="Gasoline",
aspiration="Natural",
raw_string="Invalid Engine"
)
warnings = self.parser.validate_engine_spec(invalid_spec)
self.assertGreater(len(warnings), 0)
self.assertTrue(any("displacement" in w for w in warnings))
# Electric motor should be valid
electric_spec = self.parser.create_electric_motor()
warnings = self.parser.validate_engine_spec(electric_spec)
self.assertEqual(len(warnings), 0)
def test_normalize_configuration_directly(self):
"""Test the normalize_configuration method directly"""
# Test L→I normalization
self.assertEqual(self.parser.normalize_configuration('L'), 'I')
self.assertEqual(self.parser.normalize_configuration('l'), 'I') # Case insensitive
# Test other configurations remain unchanged
self.assertEqual(self.parser.normalize_configuration('I'), 'I')
self.assertEqual(self.parser.normalize_configuration('V'), 'V')
self.assertEqual(self.parser.normalize_configuration('H'), 'H')
self.assertEqual(self.parser.normalize_configuration('i'), 'I') # Uppercase
def test_decimal_displacement(self):
"""Test engines with decimal displacements"""
test_cases = [
("1.5L I4", 1.5),
("2.3L I4", 2.3),
("3.7L V6", 3.7),
]
for engine_str, expected_disp in test_cases:
with self.subTest(engine_str=engine_str):
spec = self.parser.parse_engine_string(engine_str)
self.assertEqual(spec.displacement_l, expected_disp)
def test_edge_case_patterns(self):
"""Test edge cases and boundary conditions"""
# Large engines
spec = self.parser.parse_engine_string("8.4L V10")
self.assertEqual(spec.displacement_l, 8.4)
self.assertEqual(spec.cylinders, 10)
# Small engines
spec = self.parser.parse_engine_string("1.0L I3")
self.assertEqual(spec.displacement_l, 1.0)
self.assertEqual(spec.cylinders, 3)
# Very small displacement
spec = self.parser.parse_engine_string("0.7L I3")
self.assertEqual(spec.displacement_l, 0.7)
class TestEngineSpec(unittest.TestCase):
"""Test cases for EngineSpec dataclass"""
def test_engine_spec_creation(self):
"""Test EngineSpec creation and string representation"""
spec = EngineSpec(
displacement_l=2.0,
configuration="I",
cylinders=4,
fuel_type="Gasoline",
aspiration="Natural",
raw_string="2.0L I4"
)
self.assertEqual(spec.displacement_l, 2.0)
self.assertEqual(spec.configuration, "I")
self.assertEqual(spec.cylinders, 4)
self.assertEqual(spec.fuel_type, "Gasoline")
self.assertEqual(spec.aspiration, "Natural")
self.assertEqual(spec.raw_string, "2.0L I4")
# Test string representation
str_repr = str(spec)
self.assertIn("2.0L I4", str_repr)
self.assertIn("Gasoline", str_repr)
if __name__ == '__main__':
# Run specific test for L→I normalization if needed
if len(unittest.sys.argv) > 1 and 'l_to_i' in unittest.sys.argv[1].lower():
suite = unittest.TestSuite()
suite.addTest(TestEngineSpecParser('test_l_to_i_normalization'))
suite.addTest(TestEngineSpecParser('test_l_to_i_with_hybrid'))
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
else:
unittest.main(verbosity=2)