@@ -444,7 +444,8 @@ def test_data_structure_validation(self, basic_model, empty_model) -> None:
444
444
assert "Components" in empty_model ._data
445
445
446
446
# Test with corrupted data structure
447
- corrupted_model = ComponentDataModelBase ({}, {}, {})
447
+ mock_schema = Mock (spec = VehicleComponentsJsonSchema )
448
+ corrupted_model = ComponentDataModelBase ({}, {}, mock_schema )
448
449
assert corrupted_model ._data == {"Components" : {}, "Format version" : 1 }
449
450
450
451
def test_nested_path_creation (self , empty_model ) -> None :
@@ -567,7 +568,8 @@ def test_component_access_patterns(self, realistic_model) -> None:
567
568
assert realistic_model .has_components () is True
568
569
569
570
# 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 )
571
573
assert empty_components .has_components () is False
572
574
573
575
def test_version_field_special_handling (self , basic_model ) -> None :
@@ -716,3 +718,276 @@ def test_data_consistency_after_operations(self, basic_model) -> None:
716
718
assert basic_model .get_component_value (("Battery" , "Specifications" , "Capacity mAh" )) == 2500
717
719
assert basic_model .get_component_value (("Frame" , "Specifications" , "TOW min Kg" )) == 1.2
718
720
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