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