Skip to content

Commit da4d887

Browse files
committed
fix(vehicle components data model): Increase test coverage
1 parent 5cfb1a8 commit da4d887

File tree

1 file changed

+277
-2
lines changed

1 file changed

+277
-2
lines changed

tests/test_data_model_vehicle_components_base.py

Lines changed: 277 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,8 @@ def test_data_structure_validation(self, basic_model, empty_model) -> None:
444444
assert "Components" in empty_model._data
445445

446446
# Test with corrupted data structure
447-
corrupted_model = ComponentDataModelBase({}, {}, {})
447+
mock_schema = Mock(spec=VehicleComponentsJsonSchema)
448+
corrupted_model = ComponentDataModelBase({}, {}, mock_schema)
448449
assert corrupted_model._data == {"Components": {}, "Format version": 1}
449450

450451
def test_nested_path_creation(self, empty_model) -> None:
@@ -567,7 +568,8 @@ def test_component_access_patterns(self, realistic_model) -> None:
567568
assert realistic_model.has_components() is True
568569

569570
# Test with model that has no components
570-
empty_components = ComponentDataModelBase({"Components": {}, "Format version": 1}, {}, {})
571+
mock_schema = Mock(spec=VehicleComponentsJsonSchema)
572+
empty_components = ComponentDataModelBase({"Components": {}, "Format version": 1}, {}, mock_schema)
571573
assert empty_components.has_components() is False
572574

573575
def test_version_field_special_handling(self, basic_model) -> None:
@@ -716,3 +718,276 @@ def test_data_consistency_after_operations(self, basic_model) -> None:
716718
assert basic_model.get_component_value(("Battery", "Specifications", "Capacity mAh")) == 2500
717719
assert basic_model.get_component_value(("Frame", "Specifications", "TOW min Kg")) == 1.2
718720
assert basic_model.get_component_value(("New Component", "Type")) == "Test"
721+
722+
723+
class TestComponentDataModelEdgeCases:
724+
"""Test edge cases and error scenarios for ComponentDataModelBase."""
725+
726+
@pytest.fixture
727+
def model_with_datatypes(self) -> ComponentDataModelBase:
728+
"""Create a ComponentDataModelBase with component datatypes for type casting tests."""
729+
component_datatypes = {
730+
"Battery": {
731+
"Specifications": {
732+
"Capacity mAh": int,
733+
"Voltage V": float,
734+
"Chemistry": str,
735+
"Has BMS": bool,
736+
"Tags": list,
737+
"Metadata": dict,
738+
}
739+
}
740+
}
741+
742+
schema = Mock(spec=VehicleComponentsJsonSchema)
743+
initial_data = {"Components": {}, "Format version": 1}
744+
745+
return ComponentDataModelBase(initial_data, component_datatypes, schema)
746+
747+
def test_get_component_datatype_with_invalid_paths(self, model_with_datatypes) -> None:
748+
"""
749+
Test _get_component_datatype with various invalid path scenarios.
750+
751+
GIVEN: A model with defined component datatypes
752+
WHEN: Requesting datatypes for invalid paths
753+
THEN: Should return None gracefully
754+
"""
755+
# Test with short path (missing lines 113-114)
756+
assert model_with_datatypes._get_component_datatype(("Battery",)) is None
757+
assert model_with_datatypes._get_component_datatype(("Battery", "Specifications")) is None
758+
759+
# Test with empty datatypes
760+
model_with_datatypes._component_datatypes = {}
761+
assert model_with_datatypes._get_component_datatype(("Battery", "Specifications", "Capacity mAh")) is None
762+
763+
# Test with missing component type
764+
model_with_datatypes._component_datatypes = {"Other": {}}
765+
assert model_with_datatypes._get_component_datatype(("Battery", "Specifications", "Capacity mAh")) is None
766+
767+
def test_safe_cast_value_none_handling(self, model_with_datatypes) -> None:
768+
"""
769+
Test _safe_cast_value handling of None values for all datatypes.
770+
771+
GIVEN: A model with type casting capability
772+
WHEN: Casting None values to different datatypes
773+
THEN: Should return appropriate default values (missing lines 134, 137-143)
774+
"""
775+
path = ("Battery", "Specifications", "Test")
776+
777+
# Test None to string
778+
result = model_with_datatypes._safe_cast_value(None, str, path)
779+
assert result == ""
780+
781+
# Test None to int
782+
result = model_with_datatypes._safe_cast_value(None, int, path)
783+
assert result == 0
784+
785+
# Test None to float
786+
result = model_with_datatypes._safe_cast_value(None, float, path)
787+
assert result == 0.0
788+
789+
# Test None to bool
790+
result = model_with_datatypes._safe_cast_value(None, bool, path)
791+
assert result is False
792+
793+
# Test None to list
794+
result = model_with_datatypes._safe_cast_value(None, list, path)
795+
assert result == []
796+
797+
def test_safe_cast_value_none_handling_edge_cases(self, model_with_datatypes) -> None:
798+
"""
799+
Test _safe_cast_value None handling for edge cases.
800+
801+
GIVEN: A model with type casting capability
802+
WHEN: Casting None values to unusual datatypes
803+
THEN: Should handle gracefully (covers lines 141-143)
804+
"""
805+
path = ("Battery", "Specifications", "Test")
806+
807+
# Test None to dict - covers line 141
808+
result = model_with_datatypes._safe_cast_value(None, dict, path)
809+
assert result == {}
810+
811+
# Test None with unknown datatype - covers line 143 (fallback case)
812+
class CustomType:
813+
pass
814+
815+
result = model_with_datatypes._safe_cast_value(None, CustomType, path)
816+
assert result == "" # Should return empty string for unknown types
817+
818+
def test_get_component_datatype_isinstance_comprehensive_coverage(self, model_with_datatypes) -> None:
819+
"""
820+
Test _get_component_datatype isinstance check with comprehensive scenarios.
821+
822+
GIVEN: A model with component datatypes
823+
WHEN: Accessing datatypes with various path scenarios
824+
THEN: Should execute isinstance check paths (covers lines 113-114)
825+
"""
826+
# Set up component datatypes with both type objects and non-type values
827+
model_with_datatypes._component_datatypes = {
828+
"Battery": {
829+
"Specifications": {
830+
"ValidType": int, # This is a type - line 113
831+
"InvalidType": "not_a_type", # This is not a type - line 114
832+
"AnotherValidType": str, # Another type - line 113
833+
"NonCallable": 42, # Not callable - line 114
834+
}
835+
}
836+
}
837+
838+
# Test valid type (covers line 113: if isinstance(datatype, type))
839+
datatype = model_with_datatypes._get_component_datatype(("Battery", "Specifications", "ValidType"))
840+
assert datatype is int
841+
842+
# Test another valid type (covers line 113 again)
843+
datatype = model_with_datatypes._get_component_datatype(("Battery", "Specifications", "AnotherValidType"))
844+
assert datatype is str
845+
846+
# Test invalid type - string (covers line 114: else case)
847+
datatype = model_with_datatypes._get_component_datatype(("Battery", "Specifications", "InvalidType"))
848+
assert datatype is None
849+
850+
# Test invalid type - number (covers line 114: else case)
851+
datatype = model_with_datatypes._get_component_datatype(("Battery", "Specifications", "NonCallable"))
852+
assert datatype is None
853+
854+
def test_safe_cast_value_list_dict_special_handling_direct(self, model_with_datatypes, caplog) -> None:
855+
"""
856+
Test direct path to list/dict special handling in _safe_cast_value.
857+
858+
GIVEN: A model with type casting capability
859+
WHEN: Attempting to cast to list or dict types
860+
THEN: Should execute special handling code and log error (covers lines 152-154)
861+
"""
862+
path = ("Battery", "Specifications", "TestField")
863+
864+
# Test list datatype - should hit line 152 check, log error, and fall back to _process_value
865+
result = model_with_datatypes._safe_cast_value("some_value", list, path)
866+
assert result == "some_value" # Falls back to _process_value which returns the original value
867+
assert "Failed to cast value" in caplog.text
868+
869+
caplog.clear()
870+
871+
# Test dict datatype - should hit line 152 check, log error, and fall back to _process_value
872+
result = model_with_datatypes._safe_cast_value("another_value", dict, path)
873+
assert result == "another_value" # Falls back to _process_value which returns the original value
874+
assert "Failed to cast value" in caplog.text
875+
876+
def test_safe_cast_value_attribute_error_handling(self, model_with_datatypes, caplog) -> None:
877+
"""
878+
Test AttributeError handling in _safe_cast_value.
879+
880+
GIVEN: A model with type casting capability
881+
WHEN: A callable datatype raises AttributeError during instantiation
882+
THEN: Should catch AttributeError and fall back to _process_value (covers line 159)
883+
"""
884+
path = ("Battery", "Specifications", "TestField")
885+
886+
# Create a mock class that raises AttributeError when called
887+
class AttributeErrorType(type):
888+
def __call__(cls, *args, **kwargs) -> None:
889+
msg = "Mock AttributeError for testing"
890+
raise AttributeError(msg)
891+
892+
class MockDatatype(metaclass=AttributeErrorType):
893+
pass
894+
895+
# This should trigger the AttributeError handling at line 159
896+
result = model_with_datatypes._safe_cast_value("test_value", MockDatatype, path)
897+
898+
# Should log the error and fall back to _process_value
899+
assert "Failed to cast value" in caplog.text
900+
assert "AttributeError" in caplog.text
901+
assert result == "test_value" # Fallback to _process_value result
902+
903+
def test_deep_merge_dicts_recursive_comprehensive(self, model_with_datatypes) -> None:
904+
"""
905+
Test _deep_merge_dicts recursive merging comprehensively.
906+
907+
GIVEN: A model instance with _deep_merge_dicts method
908+
WHEN: Merging nested dictionaries with various structures
909+
THEN: Should handle all recursive paths (covers lines 259-260)
910+
"""
911+
# Test deep recursive merging (covers line 259: recursive call)
912+
default = {
913+
"level1": {
914+
"level2": {
915+
"level3": {"default_value": "from_default", "shared_key": "default_shared"},
916+
"default_level2": "default",
917+
},
918+
"simple_default": "default",
919+
},
920+
"top_level_default": "default",
921+
}
922+
923+
existing = {
924+
"level1": {
925+
"level2": {
926+
"level3": {
927+
"existing_value": "from_existing",
928+
"shared_key": "existing_shared", # Should override default
929+
},
930+
"existing_level2": "existing",
931+
},
932+
"simple_existing": "existing",
933+
},
934+
"top_level_existing": "existing",
935+
}
936+
937+
result = model_with_datatypes._deep_merge_dicts(default, existing)
938+
939+
# Verify deep recursive merging occurred (line 259)
940+
assert result["level1"]["level2"]["level3"]["default_value"] == "from_default"
941+
assert result["level1"]["level2"]["level3"]["existing_value"] == "from_existing"
942+
assert result["level1"]["level2"]["level3"]["shared_key"] == "existing_shared"
943+
assert result["level1"]["level2"]["default_level2"] == "default"
944+
assert result["level1"]["level2"]["existing_level2"] == "existing"
945+
assert result["level1"]["simple_default"] == "default"
946+
assert result["level1"]["simple_existing"] == "existing"
947+
assert result["top_level_default"] == "default"
948+
assert result["top_level_existing"] == "existing"
949+
950+
# Test case where existing value is not a dict (covers line 260: else case)
951+
default_with_dict = {"mixed_key": {"nested": "should_not_appear"}}
952+
953+
existing_with_string = {
954+
"mixed_key": "string_value" # Not a dict
955+
}
956+
957+
result = model_with_datatypes._deep_merge_dicts(default_with_dict, existing_with_string)
958+
959+
# existing value should be preserved (line 260: result[key] = existing[key])
960+
assert result["mixed_key"] == "string_value"
961+
assert not isinstance(result["mixed_key"], dict)
962+
963+
def test_process_value_none_and_version_handling(self, model_with_datatypes) -> None:
964+
"""
965+
Test _process_value None handling and Version field special case.
966+
967+
GIVEN: A model instance
968+
WHEN: Processing None values and Version fields
969+
THEN: Should handle appropriately (covers line 173)
970+
"""
971+
# Test None value handling (covers line 173: if value is None)
972+
path = ("Battery", "Specifications", "TestField")
973+
result = model_with_datatypes._process_value(path, None)
974+
assert result == ""
975+
976+
# Test Version field special handling (covers version check)
977+
version_path = ("Battery", "Product", "Version")
978+
result = model_with_datatypes._process_value(version_path, "1.2.3-beta")
979+
assert result == "1.2.3-beta"
980+
assert isinstance(result, str)
981+
982+
# Test that non-Version fields still attempt numeric conversion
983+
numeric_path = ("Battery", "Specifications", "Capacity")
984+
result = model_with_datatypes._process_value(numeric_path, "1500")
985+
assert result == 1500
986+
assert isinstance(result, int)
987+
988+
# Verify None handling for different path types
989+
result = model_with_datatypes._process_value(version_path, None)
990+
assert result == ""
991+
992+
result = model_with_datatypes._process_value(numeric_path, None)
993+
assert result == ""

0 commit comments

Comments
 (0)