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 test_nested_map_entries(self): # Create the inner message. inner_msg = [ make_message( "FieldEntry", fields=( make_field( name="key", number=1, ), make_field( name="value", number=2, ), ), map_entry=True, ), ] field = make_field("field", type_name="FieldEntry", repeated=True, number=1) outer_msg = make_message(fields=[field], nested_messages=inner_msg) self.assertEqual(list(outer_msg.map_entries.keys()), ["FieldEntry"]) self.assertTrue(outer_msg.map_entries["FieldEntry"]["key"]) self.assertTrue(outer_msg.map_entries["FieldEntry"]["value"]) self.assertFalse(outer_msg.nested_messages) self.assertTrue(outer_msg.fields[1].map_entry)
def test_nested_types(self): # Create the inner message. inner_msg = [ make_message( "InnerMessage", fields=(make_field( name="hidden_message", number=1, ), ), ) ] inner_enums = [make_enum(name="InnerEnum")] inner_fields = [make_field("not_interesting")] # Create the outer message, which contains inner fields, enums, messages. outer_msg = make_message( "Outer", fields=inner_fields, nested_enums=inner_enums, nested_messages=inner_msg, ) self.assertEqual(len(outer_msg.fields.keys()), 1) self.assertEqual(outer_msg.fields[1].name, "not_interesting") self.assertEqual(len(outer_msg.nested_enums.keys()), 1) self.assertEqual( outer_msg.nested_messages["InnerMessage"].fields[1].name, "hidden_message")
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_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_method_no_page_field(self): # No repeated field in the response message. field_next_page_token = make_field( name="next_page_token", proto_type="TYPE_STRING", number=2, ) response_message = make_message( name="ResponseMessage", fields=[field_next_page_token], ) field_page_size = make_field( name="page_size", proto_type="TYPE_INT32", number=1, ) field_page_token = make_field( name="page_token", proto_type="TYPE_STRING", number=2, ) request_message = make_message( name="RequestMessage", fields=[field_page_size, field_page_token], ) messages_map = { "ResponseMessage": response_message, "RequestMessage": request_message, } method = make_method( name="PagedMethod", input_message=request_message, output_message=response_message, messages_map=messages_map, ) self.assertEqual(method.paged_result_field, None) # Missing `page_size` in request message. request_message_without_page_size = make_message( name="RequestMessage", fields=[field_page_size], ) method = make_method( name="NoPagedMethod", input_message=request_message_without_page_size, output_message=response_message, messages_map=messages_map, ) self.assertEqual(method.paged_result_field, None) # Missing `next_page_token` in response message. response_message_without_next_page_token = make_message( name="ResponseMessage", ) method = make_method( name="NoPagedMethod", input_message=request_message, output_message=response_message_without_next_page_token, messages_map=messages_map, ) self.assertEqual(method.paged_result_field, None)
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_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_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_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 test_get_field(self): fields = ( make_field(name="field_one", number=1), make_field(name="field_two", number=2), ) message = make_message("Message", fields=fields) field_one = message.fields[1] self.assertIsInstance(field_one, wrappers.Field) self.assertEqual(field_one.name, "field_one")
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 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 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 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")
def test_nested_field_type_change(self): field_int = make_field(proto_type="TYPE_INT32") field_string = make_field(proto_type="TYPE_STRING") message1 = make_message(fields=[field_int]) message2 = make_message(fields=[field_string]) DescriptorComparator(message1, message2, self.finding_container, context="ctx").compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "FIELD_TYPE_CHANGE") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo")
def test_message_change_breaking(self): message_original = make_message( fields=(make_field(name="field_one", number=1), )) message_update = make_message( fields=(make_field(name="field_two", number=1), )) FileSetComparator( make_file_set(files=[make_file_pb2(messages=[message_original])]), make_file_set(files=[make_file_pb2(messages=[message_update])]), self.finding_container, ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.category.name, "FIELD_NAME_CHANGE") self.assertEqual(finding.location.proto_file_name, "my_proto.proto")
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_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_nested_field_type_change(self): field_int = make_field(proto_type="TYPE_INT32") field_string = make_field(proto_type="TYPE_STRING") message1 = make_message(fields=[field_int]) message2 = make_message(fields=[field_string]) DescriptorComparator(message1, message2, self.finding_container).compare() findings_map = { f.message: f for f in self.finding_container.getAllFindings() } finding = findings_map[ "Type of an existing field `my_field` is changed from `int32` to `string`."] self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo")
def test_enum_in_dependency_change_breaking(self): # Enum "dep_message" is imported from dep.proto and referenced as a field type. field_type_original = make_enum( name="dep_enum", proto_file_name="dep.proto", ) message_original = make_message( fields=[make_field(type_name=".test.import.dep_enum")], ) # Message "test_enum" is defined in update.proto referenced as a field type. field_type_update = make_enum(name="test_enum", ) message_update = make_message( fields=[make_field(type_name="test_enum")]) FileSetComparator( make_file_set(files=[ make_file_pb2( name="orignal.proto", messages=[message_original], dependency="test/import/dep.proto", package="example.v1", ), make_file_pb2( name="dep.proto", enums=[field_type_original], package="test.import", ), ]), make_file_set(files=[ make_file_pb2( name="update.proto", messages=[message_update], enums=[field_type_update], package="example.v1beta1", ) ]), self.finding_container, ).compare() # The breaking change should be in field level, instead of message removal, # since the message is imported from dependency file. finding = self.finding_container.getAllFindings()[0] self.assertEqual( finding.message, "Type of an existing field `my_field` is changed from `.test.import.dep_enum` to `test_enum`.", ) self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.category.name, "FIELD_TYPE_CHANGE") self.assertEqual(finding.location.proto_file_name, "update.proto")
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_resource_reference_addition_breaking(self): # The added resource reference is not in the database. Breaking change. # The original field is without resource reference. field_without_reference = make_field(name="Test") # The update field has resource reference, but it does not exist # in the global database. 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) 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")
def test_nested_field_addition(self): field_int = make_field(proto_type="TYPE_INT32") message_update = make_message(fields=[field_int]) DescriptorComparator(make_message(), message_update, self.finding_container).compare() finding = self.finding_container.getAllFindings()[0] self.assertEqual(finding.category.name, "FIELD_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "foo")
def test_method_paged(self): field_next_page_token = make_field( name="next_page_token", proto_type="TYPE_STRING", number=2, ) field_repeated = make_field( name="repeated_field", proto_type="TYPE_STRING", repeated=True, number=1, ) response_message = make_message( name="ResponseMessage", fields=[field_repeated, field_next_page_token], full_name=".example.v1.ResponseMessage", ) field_page_size = make_field( name="page_size", proto_type="TYPE_INT32", number=1, ) field_page_token = make_field( name="page_token", proto_type="TYPE_STRING", number=2, ) request_message = make_message( name="RequestMessage", fields=[field_page_size, field_page_token], full_name=".example.v1.RequestMessage", ) messages_map = { ".example.v1.ResponseMessage": response_message, ".example.v1.RequestMessage": request_message, } method = make_method( name="PagedMethod", input_message=request_message, output_message=response_message, messages_map=messages_map, ) self.assertEqual(method.paged_result_field.name, "repeated_field") self.assertEqual(method.longrunning, False)
def test_resource_reference_removal_breaking1(self): # Removed resource reference is not added in message options. Breaking. # 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) # The update field has no resource reference, and no resource reference is # defined in the message. field_without_reference = make_field(name="Test") 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_resource_reference_change_same_type_breaking(self): # Both fields have resource reference identified by type. # But the type value is different. Breaking change. field_options_foo = make_field_annotation_resource_reference( resource_type="example.v1/Foo", is_child_type=False ) field_options_bar = make_field_annotation_resource_reference( resource_type="example.v1/Bar", is_child_type=False ) field_with_reference_foo = make_field(name="Test", options=field_options_foo) field_with_reference_bar = make_field(name="Test", options=field_options_bar) FieldComparator( field_with_reference_foo, field_with_reference_bar, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.change_type.name, "MAJOR")
def test_oneofs(self): # Oneof with only one field. oneof = make_oneof(name="not_interesting") oneof_field = make_field(name="first_field", number=1, oneof_index=0, oneof_name="not_interesting") non_oneof_field = make_field(name="second_field", number=2) message = make_message( name="Message", fields=( oneof_field, non_oneof_field, ), oneofs=(oneof, ), ) self.assertEqual(list(message.oneofs.keys()), ["not_interesting"]) self.assertEqual({x.name for x in message.fields.values() if x.oneof}, {"first_field"})
def test_resource_reference_change_type_conversion_breaking(self): resource_bar = make_resource_descriptor( resource_type="example.v1/Bar", resource_patterns=["bar/{bar}/foo/{foo}", "bar/{bar}/foo"], ) resource_foo = make_resource_descriptor( resource_type="example.v1/Foo", resource_patterns=["foo/{foo}"] ) # Register two resources in database. resource_database = make_resource_database( resources=[resource_bar, resource_foo] ) # The original field is defined by child type. field_options_child = make_field_annotation_resource_reference( resource_type="example.v1/Bar", 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/Foo", is_child_type=False ) field_with_reference_parent = make_field( name="Test", options=field_options_parent, resource_database=resource_database, ) # The two resources can nnot 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()[0] self.assertEqual(finding.category.name, "RESOURCE_REFERENCE_CHANGE_CHILD_TYPE") self.assertEqual(finding.change_type.name, "MAJOR")