def _compare_nested_fields(self, fields_dict_original, fields_dict_update): fields_number_original = set(fields_dict_original.keys()) fields_number_update = set(fields_dict_update.keys()) for field_number in fields_number_original - fields_number_update: FieldComparator( fields_dict_original[field_number], None, self.finding_container, context=self.context, ).compare() for field_number in fields_number_update - fields_number_original: FieldComparator( None, fields_dict_update[field_number], self.finding_container, context=self.context, ).compare() for field_number in fields_number_original & fields_number_update: FieldComparator( fields_dict_original[field_number], fields_dict_update[field_number], self.finding_container, context=self.context, ).compare()
def test_resource_reference_change_type_conversion_non_breaking(self): child_resource = make_resource_descriptor( resource_type="example.v1/Foo", resource_patterns=["bar/{bar}/foo/{foo}", "bar/{bar}/foo"], ) parent_resource = make_resource_descriptor( resource_type="example.v1/Bar", resource_patterns=["bar/{bar}"] ) # Register two resources in database. resource_database = make_resource_database( resources=[child_resource, parent_resource] ) # The original field is defined by child type. field_options_child = make_field_annotation_resource_reference( resource_type="example.v1/Foo", is_child_type=True ) field_with_reference_child = make_field( name="Test", options=field_options_child, resource_database=resource_database, ) # The update field is defined by parent type. field_options_parent = make_field_annotation_resource_reference( resource_type="example.v1/Bar", is_child_type=False ) field_with_reference_parent = make_field( name="Test", options=field_options_parent, resource_database=resource_database, ) # The two resources can be resolved to the identical resource. FieldComparator( field_with_reference_child, field_with_reference_parent, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings() # No breaking change should be detected. self.assertFalse(finding) # Reverse should be same since the two resources can # be resolved to the identical resource. FieldComparator( field_with_reference_parent, field_with_reference_child, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings() # No breaking change should be detected. self.assertFalse(finding)
def _compare_nested_fields(self, fields_dict_original, fields_dict_update): fields_number_original = set(fields_dict_original.keys()) fields_number_update = set(fields_dict_update.keys()) for fieldNumber in fields_number_original - fields_number_update: FieldComparator(fields_dict_original[fieldNumber], None).compare() for fieldNumber in fields_number_update - fields_number_original: FieldComparator(None, fields_dict_update[fieldNumber]).compare() for fieldNumber in fields_number_original & fields_number_update: FieldComparator( fields_dict_original[fieldNumber], fields_dict_update[fieldNumber], ).compare()
def _compareNestedFields(self, fieldsDict_original, fieldsDict_update): fieldsUnique_original = list( set(fieldsDict_original.keys()) - set(fieldsDict_update.keys())) fieldsUnique_update = list( set(fieldsDict_update.keys()) - set(fieldsDict_original.keys())) fieldsIntersaction = list( set(fieldsDict_original.keys()) & set(fieldsDict_update.keys())) for fieldNumber in fieldsUnique_original: FieldComparator(fieldsDict_original[fieldNumber], None).compare() for fieldNumber in fieldsUnique_update: FieldComparator(None, fieldsDict_update[fieldNumber]).compare() for fieldNumber in fieldsIntersaction: FieldComparator(fieldsDict_original[fieldNumber], fieldsDict_update[fieldNumber]).compare()
def test_field_behavior_change(self): field_required = make_field(required=True) field_non_required = make_field(required=False) # Required to optional, non-breaking change. FieldComparator( field_required, field_non_required, self.finding_container, context="ctx" ).compare() findings = self.finding_container.get_all_findings() self.assertFalse(findings) # Required to optional, non-breaking change. FieldComparator( field_non_required, field_required, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "FIELD_BEHAVIOR_CHANGE")
def test_type_change_map_entry4(self): # Both fields are map type. But the key, value types are not identical. # But only the versions pare are different. Non-breaking change. # [Constructing] map<string, .example.v1.value> field key_original = make_field(proto_type="TYPE_STRING", number=1) value_original = make_field( proto_type="TYPE_MESSAGE", type_name=".example.v1.value", number=2 ) field_original = make_field( proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry", map_entry={"key": key_original, "value": value_original}, api_version="v1", ) # [Constructing] map<string, .example.v1beta1.value> field key_update = make_field(proto_type="TYPE_STRING", number=1) value_update = make_field( proto_type="TYPE_MESSAGE", number=2, type_name=".example.v1beta1.value" ) field_update = make_field( proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry", map_entry={"key": key_update, "value": value_update}, api_version="v1beta1", ) FieldComparator( field_original, field_update, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings() self.assertFalse(finding)
def test_type_change_map_entry3(self): # Both fields are map type. But the key, value types are not identical. Breaking change. # [Constructing] map<string, string> field key_original = make_field(proto_type="TYPE_STRING", number=1) value_original = make_field(proto_type="TYPE_STRING", number=2) field_original = make_field( proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry", map_entry={"key": key_original, "value": value_original}, ) # [Constructing] map<key, value> field key_update = make_field( proto_type="TYPE_MESSAGE", number=1, type_name=".example.key" ) value_update = make_field( proto_type="TYPE_MESSAGE", number=2, type_name=".example.value" ) field_update = make_field( proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry", map_entry={"key": key_update, "value": value_update}, ) FieldComparator( field_original, field_update, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE") self.assertEqual(finding.location.proto_file_name, "foo")
def fieldAddition(self): field_married = update_version.DESCRIPTOR.message_types_by_name[ "Person"].fields_by_name['married'] FieldComparator(None, field_married).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual(finding.message, 'A new Field married is added.') self.assertEqual(finding.category.name, 'FIELD_ADDITION')
def test_resource_reference_removal_breaking2(self): # Removed resource reference is defined by type, which is not identical # with the message options. # Original field has resource reference `example.v1/Foo`. field_options = make_field_annotation_resource_reference( resource_type="example.v1/Foo", is_child_type=False ) field_with_reference = make_field(name="Test", options=field_options) # Update field has no resource reference, and the resource type # is different from the message options type. message_resource = make_resource_descriptor( resource_type="NotInteresting", resource_patterns=["NotInteresting"] ) field_without_reference = make_field( name="Test", message_resource=message_resource ) FieldComparator( field_with_reference, field_without_reference, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_actionable_findings()[0] self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR")
def test_field_addition(self): field_foo = make_field("Foo") FieldComparator( None, field_foo, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "FIELD_ADDITION")
def test_resource_reference_removal_non_breaking1(self): # Removed resource reference is defined by type, and it is # added back to the message options. # Original field has resource reference `example.v1/Foo`. field_options = make_field_annotation_resource_reference( resource_type="example.v1/Foo", is_child_type=False ) field_with_reference = make_field(name="Test", options=field_options) # Update field has no resource reference. But the message has # resource options `example.v1/Foo`. message_resource = make_resource_descriptor( resource_type="example.v1/Foo", resource_patterns=["bar/{bar}"] ) field_without_reference = make_field( name="Test", message_resource=message_resource ) FieldComparator( field_with_reference, field_without_reference, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_MOVED") self.assertEqual(finding.change_type.name, "MINOR")
def test_resource_reference_removal_non_breaking2(self): # Removed resource reference is defined by child type, and it # can be resolved to the same resource with the message options. # Original field has resource reference `example.v1/Foo`. field_options = make_field_annotation_resource_reference( resource_type="example.v1/Foo", is_child_type=False ) field_with_reference = make_field(name="Test", options=field_options) # Update field has no resource reference. But the message has # resource options `example.v1/Foo`. message_resource = make_resource_descriptor( resource_type="example.v1/Foo", resource_patterns=["bar/{bar}"] ) field_resource = make_resource_descriptor( resource_type="example.v1/Foo", resource_patterns=["bar/{bar}/foo/{foo}"] ) # Register the two resources in the database. resource_database = make_resource_database( resources=[message_resource, field_resource] ) field_without_reference = make_field( name="Test", message_resource=message_resource, resource_database=resource_database, ) # `bar/{bar}` is the parent resource of `bar/{bar}/foo/{foo}`. FieldComparator( field_with_reference, field_without_reference, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_MOVED") self.assertEqual(finding.change_type.name, "MINOR")
def fieldRemoval(self): field_company_address = update_version.DESCRIPTOR.message_types_by_name[ "Person"].fields_by_name['company_address'] FieldComparator(field_company_address, None).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual(finding.message, 'A Field company_address is removed') self.assertEqual(finding.category.name, 'FIELD_REMOVAL')
def test_resource_reference_child_type_addition_non_breaking(self): # The added resource reference is in the database. Non-breaking change. # The original field is without resource reference. field_without_reference = make_field(name="Test") # Create a database with resource `example.v1/Foo` registered. resource = make_resource_descriptor( resource_type="example.v1/Foo", resource_patterns=["foo/{foo}"] ) resource_child = make_resource_descriptor( resource_type="example.v1/Bar", resource_patterns=["foo/{foo}/bar/{bar}"], ) resource_database = make_resource_database(resources=[resource, resource_child]) # The update field has resource reference of child_type `example.v1/Bar`. field_options = desc.FieldOptions() field_options.Extensions[ resource_pb2.resource_reference ].child_type = "example.v1/Bar" field_with_reference = make_field( name="Test", options=field_options, resource_database=resource_database ) FieldComparator( field_without_reference, field_with_reference, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_ADDITION") self.assertEqual(finding.change_type.name, "MINOR")
def test_message_type_change(self): field_message = make_field(type_name=".example.v1.Enum") field_message_update = make_field(type_name=".example.v1beta1.EnumUpdate") FieldComparator( field_message, field_message_update, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")
def test_primitive_type_change(self): field_int = make_field(proto_type="TYPE_INT32") field_string = make_field(proto_type="TYPE_STRING") FieldComparator( field_int, field_string, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")
def test_field_removal(self): field_foo = make_field("Foo") FieldComparator( field_foo, None, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "FIELD_REMOVAL") self.assertEqual(finding.location.proto_file_name, "foo")
def test_repeated_label_change(self): field_repeated = make_field(repeated=True) field_non_repeated = make_field(repeated=False) FieldComparator( field_repeated, field_non_repeated, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "FIELD_REPEATED_CHANGE")
def test_name_change(self): field_foo = make_field("Foo", nested_path=["foo"]) field_bar = make_field("Bar", nested_path=["bar"]) FieldComparator( field_foo, field_bar, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "FIELD_NAME_CHANGE") self.assertEqual(finding.extra_info[0], "foo")
def moveExistingFieldOutofOneof(self): field_email_original = original_version.DESCRIPTOR.message_types_by_name[ "AddressBook"].fields_by_name['deprecated'] field_email_update = update_version.DESCRIPTOR.message_types_by_name[ "AddressBook"].fields_by_name['deprecated'] FieldComparator(field_email_original, field_email_update).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual(finding.message, 'The Field deprecated is moved out of one-of') self.assertEqual(finding.category.name, 'FIELD_ONEOF_REMOVAL')
def test_message_type_change_minor_version_update(self): field_message = make_field(type_name=".example.v1.Enum", api_version="v1") field_message_update = make_field( type_name=".example.v1beta1.Enum", api_version="v1beta1" ) FieldComparator( field_message, field_message_update, self.finding_container, context="ctx" ).compare() findings = self.finding_container.get_all_findings() self.assertFalse(findings)
def moveExistingFieldIntoOneof(self): field_email_original = original_version.DESCRIPTOR.message_types_by_name[ "Person"].fields_by_name['home_address'] field_email_update = update_version.DESCRIPTOR.message_types_by_name[ "Person"].fields_by_name['home_address'] FieldComparator(field_email_original, field_email_update).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual(finding.message, 'The Field home_address is moved into one-of') self.assertEqual(finding.category.name, 'FIELD_ONEOF_ADDITION')
def typeChange(self): field_id_original = original_version.DESCRIPTOR.message_types_by_name[ "Person"].fields[1] field_id_update = update_version.DESCRIPTOR.message_types_by_name[ "Person"].fields[1] FieldComparator(field_id_original, field_id_update).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual( finding.message, 'Type of the Field is changed, the original is TYPE_INT32, but the updated is TYPE_STRING' ) self.assertEqual(finding.category.name, 'FIELD_TYPE_CHANGE')
def test_oneof_change(self): # Field `single = 5` in `message_v1.proto` is moved out of One-of. FieldComparator(self.person_fields_v1[5], self.person_fields_v1beta1[5]).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual( finding.message, "The existing field single is moved out of One-of.", ) self.assertEqual(finding.category.name, "FIELD_ONEOF_REMOVAL") self.assertEqual(finding.location.path, "message_v1beta1.proto Line: 22")
def test_type_change(self): # Field `id` is `int32` type in `message_v1.proto`, # but updated to `string` in `message_v1beta1.proto`. FieldComparator(self.person_fields_v1[2], self.person_fields_v1beta1[2]).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual( finding.message, "Type of the field is changed, the original is TYPE_INT32," " but the updated is TYPE_STRING", ) self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE")
def test_repeated_label_change(self): # Field `phones` in `message_v1.proto` has `repeated` label, # but it's removed in the `message_v1beta1.proto`. FieldComparator(self.person_fields_v1[4], self.person_fields_v1beta1[4]).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual( finding.message, "Repeated state of the Field is changed, the original is LABEL_REPEATED," " but the updated is LABEL_OPTIONAL", ) self.assertEqual(finding.category.name, "FIELD_REPEATED_CHANGE")
def nameChange(self): field_email_original = original_version.DESCRIPTOR.message_types_by_name[ "Person"].fields_by_name['email'] field_email_update = update_version.DESCRIPTOR.message_types_by_name[ "Person"].fields_by_name['email_address'] FieldComparator(field_email_original, field_email_update).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual( finding.message, 'Name of the Field is changed, the original is email, but the updated is email_address' ) self.assertEqual(finding.category.name, 'FIELD_NAME_CHANGE')
def test_into_oneof(self): field_oneof = make_field(name="Foo", oneof_index=0, oneof_name="oneof_field") field_not_oneof = make_field(name="Foo") FieldComparator( field_not_oneof, field_oneof, self.finding_container, context="ctx" ).compare() finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "FIELD_ONEOF_MOVE_IN" ) self.assertTrue(finding)
def repeatedLabelChange(self): field_phones_original = original_version.DESCRIPTOR.message_types_by_name[ "Person"].fields_by_name['phones'] field_phones_update = update_version.DESCRIPTOR.message_types_by_name[ "Person"].fields_by_name['phones'] FieldComparator(field_phones_original, field_phones_update).compare() finding = FindingContainer.getAllFindings()[0] self.assertEqual( finding.message, 'Repeated state of the Field is changed, the original is LABEL_REPEATED, but the updated is LABEL_OPTIONAL' ) self.assertEqual(finding.category.name, 'FIELD_REPEATED_CHANGE')
def test_proto3_required_to_optional(self): # Change required field to be proto3 optional. Non-breaking change. field_optional = make_field( name="Foo", oneof_index=0, oneof_name="oneof_field", proto3_optional=True ) field_not_optional = make_field( name="Foo", oneof_index=0, oneof_name="oneof_field" ) FieldComparator( field_not_optional, field_optional, self.finding_container, context="ctx" ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "FIELD_PROTO3_OPTIONAL_CHANGE")