From 984d75c3ffe43da25db23c6e29941f8b763dceb2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 7 Feb 2025 11:18:56 +1000 Subject: [PATCH] Add provider conformance tests for adding/editing features with unset attribute values --- tests/src/python/providertestbase.py | 232 +++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/tests/src/python/providertestbase.py b/tests/src/python/providertestbase.py index 6355499210a..bf154164af6 100644 --- a/tests/src/python/providertestbase.py +++ b/tests/src/python/providertestbase.py @@ -31,6 +31,7 @@ from qgis.core import ( QgsVectorDataProvider, QgsVectorLayerFeatureSource, QgsVectorLayerUtils, + QgsUnsetAttributeValue, ) from featuresourcetestbase import FeatureSourceTestCase @@ -1003,6 +1004,86 @@ class ProviderTestCase(FeatureSourceTestCase): ) self.assertEqual(l.dataProvider().featureCount(), 7) + def testAddFeatureUnsetAttributes(self): + if not getattr(self, "getEditableLayer", None): + return + + l = self.getEditableLayer() + self.assertTrue(l.isValid()) + + f1 = QgsFeature() + f1.setAttributes( + [ + 6, + -220, + QgsUnsetAttributeValue(), + "String", + "15", + NULL, + NULL, + NULL, + ] + ) + f1.setGeometry(QgsGeometry.fromWkt("Point (-72.345 71.987)")) + + f2 = QgsFeature() + f2.setAttributes( + [ + 7, + QgsUnsetAttributeValue(), + "Coconut", + "CoCoNut", + "13", + NULL, + NULL, + NULL, + ] + ) + + if ( + l.dataProvider().capabilities() + & QgsVectorDataProvider.Capability.AddFeatures + ): + # expect success + result, added = l.dataProvider().addFeatures( + [f1, f2], QgsFeatureSink.Flag.FastInsert + ) + self.assertTrue( + result, + "Provider reported AddFeatures capability, but returned False to addFeatures using QgsUnsetAttributeValues", + ) + self.assertEqual(l.dataProvider().featureCount(), 7) + + features = [f for f in l.dataProvider().getFeatures()] + self.assertEqual(len(features), 7) + f6 = [f for f in features if f[0] == 6][0] + f7 = [f for f in features if f[0] == 7][0] + self.assertEqual(f6[1], -220) + self.assertTrue( + isinstance(f6[2], QgsUnsetAttributeValue) + or f6[2] == NULL + or str(f6[2]) == "", + f"Expected null/unset value, got {f6[2]}", + ) + self.assertEqual(f6[3], "String") + self.assertEqual(f6[4], "15") + self.assertEqual(f6[5], NULL) + self.assertEqual(f6[6], NULL) + self.assertEqual(f6[7], NULL) + + self.assertTrue( + isinstance(f7[1], QgsUnsetAttributeValue) + or f7[1] == NULL + or str(f7[1]) == "", + f"Expected null/unset value, got {f7[1]}", + ) + self.assertEqual(f7[2], "Coconut") + self.assertEqual(f7[3], "CoCoNut") + self.assertEqual(f7[4], "13") + self.assertEqual(f7[5], NULL) + self.assertEqual(f7[6], NULL) + self.assertEqual(f7[7], NULL) + def testAddFeatureMissingAttributes(self): if not getattr(self, "getEditableLayer", None): return @@ -1386,6 +1467,80 @@ class ProviderTestCase(FeatureSourceTestCase): "Provider reported no ChangeAttributeValues capability, but returned true to changeAttributeValues", ) + def testChangeAttributesUnsetValue(self): + if not getattr(self, "getEditableLayer", None): + return + + l = self.getEditableLayer() + self.assertTrue(l.isValid()) + + # find 2 features to change + features = [f for f in l.dataProvider().getFeatures()] + # need to keep order here + to_change = [f for f in features if f.attributes()[0] == 1] + to_change.extend([f for f in features if f.attributes()[0] == 3]) + # changes by feature id, for changeAttributeValues call + changes = { + to_change[0].id(): {1: QgsUnsetAttributeValue(), 3: "new string"}, + to_change[1].id(): {1: 502, 4: QgsUnsetAttributeValue()}, + } + # changes by pk, for testing after retrieving changed features + new_attr_map = {1: {3: "new string"}, 3: {1: 502}} + + if ( + l.dataProvider().capabilities() + & QgsVectorDataProvider.Capability.ChangeAttributeValues + ): + # expect success + result = l.dataProvider().changeAttributeValues(changes) + self.assertTrue( + result, + "Provider reported ChangeAttributeValues capability, but returned False to changeAttributeValues", + ) + + # check result + self.testGetFeatures(l.dataProvider(), changed_attributes=new_attr_map) + + else: + # expect fail + self.assertFalse( + l.dataProvider().changeAttributeValues(changes), + "Provider reported no ChangeAttributeValues capability, but returned true to changeAttributeValues", + ) + + def testChangeAttributesOnlyUnsetValue(self): + if not getattr(self, "getEditableLayer", None): + return + + l = self.getEditableLayer() + self.assertTrue(l.isValid()) + + # find 2 features to change + features = [f for f in l.dataProvider().getFeatures()] + # need to keep order here + to_change = [f for f in features if f.attributes()[0] == 1] + to_change.extend([f for f in features if f.attributes()[0] == 3]) + # changes by feature id, for changeAttributeValues call + changes = { + to_change[0].id(): {1: QgsUnsetAttributeValue()}, + to_change[1].id(): {4: QgsUnsetAttributeValue()}, + } + if ( + l.dataProvider().capabilities() + & QgsVectorDataProvider.Capability.ChangeAttributeValues + ): + l.dataProvider().changeAttributeValues(changes) + + # check result + self.testGetFeatures(l.dataProvider()) + + else: + # expect fail + self.assertFalse( + l.dataProvider().changeAttributeValues(changes), + "Provider reported no ChangeAttributeValues capability, but returned true to changeAttributeValues", + ) + def testChangeAttributesConstraintViolation(self): """Checks that changing attributes violating a DB-level CHECK constraint returns false the provider test case must provide an editable layer with a text field @@ -1591,6 +1746,83 @@ class ProviderTestCase(FeatureSourceTestCase): "Provider reported no ChangeAttributeValues capability, but returned true to changeFeatures", ) + def testChangeFeaturesUnsetAttribute(self): + if not getattr(self, "getEditableLayer", None): + return + + l = self.getEditableLayer() + self.assertTrue(l.isValid()) + + features = [f for f in l.dataProvider().getFeatures()] + + # find 2 features to change attributes for + features = [f for f in l.dataProvider().getFeatures()] + # need to keep order here + to_change = [f for f in features if f.attributes()[0] == 1] + to_change.extend([f for f in features if f.attributes()[0] == 2]) + # changes by feature id, for changeAttributeValues call + attribute_changes = { + to_change[0].id(): {1: QgsUnsetAttributeValue(), 3: "new string"}, + to_change[1].id(): {1: 502, 4: QgsUnsetAttributeValue()}, + } + # changes by pk, for testing after retrieving changed features + new_attr_map = {1: {3: "new string"}, 2: {1: 502}} + + # find 2 features to change geometries for + to_change = [f for f in features if f.attributes()[0] == 1] + to_change.extend([f for f in features if f.attributes()[0] == 3]) + # changes by feature id, for changeGeometryValues call + geometry_changes = { + to_change[0].id(): QgsGeometry.fromWkt("Point (10 20)"), + to_change[1].id(): QgsGeometry(), + } + # changes by pk, for testing after retrieving changed features + new_geom_map = {1: QgsGeometry.fromWkt("Point ( 10 20 )"), 3: QgsGeometry()} + + if ( + l.dataProvider().capabilities() + & QgsVectorDataProvider.Capability.ChangeGeometries + and l.dataProvider().capabilities() + & QgsVectorDataProvider.Capability.ChangeAttributeValues + ): + # expect success + result = l.dataProvider().changeFeatures( + attribute_changes, geometry_changes + ) + self.assertTrue( + result, + "Provider reported ChangeGeometries and ChangeAttributeValues capability, but returned False to changeFeatures", + ) + + # check result + self.testGetFeatures( + l.dataProvider(), + changed_attributes=new_attr_map, + changed_geometries=new_geom_map, + ) + + # change empty list, should return true for consistency + self.assertTrue(l.dataProvider().changeFeatures({}, {})) + + elif ( + not l.dataProvider().capabilities() + & QgsVectorDataProvider.Capability.ChangeGeometries + ): + # expect fail + self.assertFalse( + l.dataProvider().changeFeatures(attribute_changes, geometry_changes), + "Provider reported no ChangeGeometries capability, but returned true to changeFeatures", + ) + elif ( + not l.dataProvider().capabilities() + & QgsVectorDataProvider.Capability.ChangeAttributeValues + ): + # expect fail + self.assertFalse( + l.dataProvider().changeFeatures(attribute_changes, geometry_changes), + "Provider reported no ChangeAttributeValues capability, but returned true to changeFeatures", + ) + def testMinMaxAfterChanges(self): """ Tests retrieving field min and max value after making changes to the provider's features