class ResourceReferenceTest(unittest.TestCase): # This is for tesing the behavior of resources in comparators. # The resource can be defined in file-level and message-level. # And it is referenced in field-level. So whether the removal of a resource is a breaking change # depends on the information from multiple levels (From File or Message to Field). # UnittestInvoker helps us to execute the protoc command to compile the proto file, # get a *_descriptor_set.pb file (by -o option) which contains the serialized data in protos, and # create a FileDescriptorSet (_PB_ORIGNAL and _PB_UPDATE) out of it. PROTO_DIR = os.path.join(os.getcwd(), "test/testdata/protos/example/") COMMON_PROTOS_DIR = os.path.join(os.getcwd(), "api-common-protos") def setUp(self): self.finding_container = FindingContainer() def test_resources_change(self): _INVOKER_ORIGNAL = Loader( proto_definition_dirs=[self.PROTO_DIR, self.COMMON_PROTOS_DIR], proto_files=[os.path.join(self.PROTO_DIR, "resource_database_v1.proto")], descriptor_set=None, ) _INVOKER_UPDATE = Loader( proto_definition_dirs=[self.PROTO_DIR, self.COMMON_PROTOS_DIR], proto_files=[ os.path.join(self.PROTO_DIR, "resource_database_v1beta1.proto") ], descriptor_set=None, ) FileSetComparator( FileSet(_INVOKER_ORIGNAL.get_descriptor_set()), FileSet(_INVOKER_UPDATE.get_descriptor_set()), self.finding_container, ).compare() addition_finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "RESOURCE_PATTERN_ADDITION" ) removal_finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "RESOURCE_PATTERN_REMOVAL" ) resource_definition_removal_finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "RESOURCE_DEFINITION_REMOVAL" ) self.assertEqual( addition_finding.location.proto_file_name, "resource_database_v1.proto", ) self.assertEqual(addition_finding.location.source_code_line, 13) self.assertEqual( removal_finding.location.proto_file_name, "resource_database_v1.proto", ) self.assertEqual( removal_finding.location.source_code_line, 13, ) self.assertEqual( resource_definition_removal_finding.location.proto_file_name, "resource_database_v1.proto", ) self.assertEqual( resource_definition_removal_finding.location.source_code_line, 34 ) self.assertEqual(resource_definition_removal_finding.change_type.value, 1) def test_resource_reference_change(self): _INVOKER_ORIGNAL = Loader( proto_definition_dirs=[self.PROTO_DIR, self.COMMON_PROTOS_DIR], proto_files=[os.path.join(self.PROTO_DIR, "resource_reference_v1.proto")], descriptor_set=None, ) _INVOKER_UPDATE = Loader( proto_definition_dirs=[self.PROTO_DIR, self.COMMON_PROTOS_DIR], proto_files=[ os.path.join(self.PROTO_DIR, "resource_reference_v1beta1.proto") ], descriptor_set=None, ) FileSetComparator( FileSet(_INVOKER_ORIGNAL.get_descriptor_set()), FileSet(_INVOKER_UPDATE.get_descriptor_set()), self.finding_container, ).compare() finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "RESOURCE_REFERENCE_CHANGE_CHILD_TYPE" ) self.assertEqual( finding.location.proto_file_name, "resource_reference_v1beta1.proto" ) self.assertEqual(finding.location.source_code_line, 25) # Find more details in comments of `resource_reference_v1beta1.proto` # 1. Resource_reference annotation is removed for `string name=1`, # but it is added in message-level. Non-breaking change. # 2. File-level resource definition `t2` is removed, but is added # to message-level resource. Non-breaking change. breaking_changes = self.finding_container.get_actionable_findings() self.assertEqual(len(breaking_changes), 1)
class FieldComparatorTest(unittest.TestCase): def setUp(self): self.finding_container = FindingContainer() 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_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_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_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_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_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_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_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_type_change_map_entry1(self): # Existing field is message_type, while the update field type is map. Breaking change. # Normally it will catch by type_name comparison. But in case we have a # message name as `{FieldName}Entry` which is the same as auto-generated nested message name, # we still consider the condition that the type of an existing field is changed from # normal message (`{FieldName}Entry`) to map entry message (`{FieldName}Entry`). field_no_map = make_field( proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry" ) # [Constructing] map<string, string> field key_field = make_field(proto_type="TYPE_STRING", number=1) value_field = make_field(proto_type="TYPE_STRING", number=2) field_map = make_field( proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry", map_entry={"key": key_field, "value": value_field}, ) FieldComparator( field_no_map, field_map, 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 test_type_change_map_entry2(self): # Existing field type is a map, while the update field type is a normal message. Breaking change. # Normally it will catch by type_name comparison. But in case we have a # message name as `{FieldName}Entry` which is the same as auto-generated nested message name, # we still consider the condition that the type of an existing field is changed from # map entry message (`{FieldName}Entry`) to normal message (`{FieldName}Entry`). field_no_map = make_field( proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry" ) # [Constructing] map<string, string> field key_field = make_field(proto_type="TYPE_STRING", number=1) value_field = make_field(proto_type="TYPE_STRING", number=2) field_map = make_field( proto_type="TYPE_MESSAGE", type_name=".exmaple.MapEntry", map_entry={"key": key_field, "value": value_field}, ) FieldComparator( field_map, field_no_map, 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 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 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_out_oneof(self): field_oneof = make_field(name="Foo", oneof_index=0, oneof_name="oneof_field") field_not_oneof = make_field(name="Foo") FieldComparator( field_oneof, field_not_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_OUT" ) self.assertTrue(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_optional_to_required(self): # Change an proto3 optional field to required. 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_optional, field_not_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_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_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_resource_reference_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_database = make_resource_database(resources=[resource]) # The update field has resource reference of type `example.v1/Foo`. field_options = desc.FieldOptions() field_options.Extensions[ resource_pb2.resource_reference ].type = "example.v1/Foo" 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_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_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_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_resource_reference_removal_breaking3(self): # Removed resource reference is defined by child type, which can not # be resolved to identical 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=True ) field_with_reference = make_field(name="Test", options=field_options) # Update field has no resource reference, and the removed resource child type # is not identical with the message resource option. message_resource = make_resource_descriptor( resource_type="example.v1/Bar", resource_patterns=["bar/{bar}"] ) field_resource = make_resource_descriptor( resource_type="example.v1/Foo", resource_patterns=["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", resource_database=resource_database, message_resource=message_resource, ) FieldComparator( field_with_reference, field_without_reference, self.finding_container, context="ctx", ).compare() # `bar/{bar}` is not parent resource of `foo/{foo}`. 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_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_resource_reference_change_same_type_non_breaking(self): # The field has the identical resource reference. 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_with_reference, field_with_reference, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings() # No breaking change should be detected. self.assertFalse(finding) def test_resource_reference_change_same_child_type_non_breaking(self): # The field has the identical resource reference. field_options = make_field_annotation_resource_reference( resource_type="example.v1/Foo", is_child_type=True ) field_with_reference = make_field(name="Test", options=field_options) FieldComparator( field_with_reference, field_with_reference, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings() # No breaking change should be detected. self.assertFalse(finding) 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_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_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")
class FileSetComparatorTest(unittest.TestCase): def setUp(self): self.finding_container = FindingContainer() def test_service_removal(self): file_set = make_file_set( files=[make_file_pb2(services=[make_service()], )]) FileSetComparator( file_set, make_file_set(), self.finding_container, ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.change_type.name, "MAJOR") def test_service_addition(self): file_set = make_file_set( files=[make_file_pb2(services=[make_service()], )]) FileSetComparator( make_file_set(), file_set, self.finding_container, ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "SERVICE_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") def test_service_change(self): input_message = make_message(name="request", full_name=".example.v1.request") output_message = make_message(name="response", full_name=".example.v1.response") service_original = make_service(methods=[ make_method( name="DoThing", input_message=input_message, output_message=output_message, ) ]) service_update = make_service() FileSetComparator( make_file_set(files=[ make_file_pb2( services=[service_original], messages=[input_message, output_message], ) ]), make_file_set(files=[make_file_pb2(services=[service_update])]), self.finding_container, ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "METHOD_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "my_proto.proto") 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_message_in_dependency_change_breaking(self): # Message "dep_message" is imported from dep.proto and referenced as a field type. field_type_original = make_message( name="dep_message", proto_file_name="dep.proto", ) message_original = make_message( fields=[make_field(type_name=".test.import.dep_message")], ) # Message "test_message" is defined in my_proto.proto referenced as a field type. field_type_update = make_message(name="test_message", ) message_update = make_message( fields=[make_field(type_name="test_message")]) FileSetComparator( make_file_set(files=[ make_file_pb2( name="original.proto", messages=[message_original], dependency=["test/import/dep.proto"], package="example.v1", ), make_file_pb2( name="test/import/dep.proto", messages=[field_type_original], package="test.import", ), ]), make_file_set(files=[ make_file_pb2( name="update.proto", messages=[field_type_update, message_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.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, "update.proto") def test_enum_change(self): enum_original = make_enum( name="Irrelevant", values=( ("RED", 1), ("GREEN", 2), ("BLUE", 3), ), ) enum_update = make_enum( name="Irrelevant", values=( ("RED", 1), ("GREEN", 2), ), ) FileSetComparator( make_file_set(files=[make_file_pb2(enums=[enum_original])]), make_file_set(files=[make_file_pb2(enums=[enum_update])]), self.finding_container, ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_VALUE_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "my_proto.proto") 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="original.proto", messages=[message_original], dependency=["test/import/dep.proto"], package="example.v1", ), make_file_pb2( name="test/import/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.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, "update.proto") def test_resources_existing_pattern_change(self): options_original = make_file_options_resource_definition( resource_type=".example.v1.Bar", resource_patterns=["foo/{foo}/bar/{bar}"]) file_pb2 = make_file_pb2(name="foo.proto", package=".example.v1", options=options_original) file_set_original = make_file_set(files=[file_pb2]) options_update = make_file_options_resource_definition( resource_type=".example.v1.Bar", resource_patterns=["foo/{foo}/bar/"]) file_pb2 = make_file_pb2(name="foo.proto", package=".example.v1", options=options_update) file_set_update = make_file_set(files=[file_pb2]) FileSetComparator(file_set_original, file_set_update, self.finding_container).compare() finding = next(f for f in self.finding_container.get_all_findings() if f.change_type.name == "MAJOR") self.assertEqual(finding.category.name, "RESOURCE_PATTERN_REMOVAL") self.assertEqual( finding.location.proto_file_name, "foo.proto", ) def test_resources_existing_pattern_removal(self): options_original = make_file_options_resource_definition( resource_type=".example.v1.Bar", resource_patterns=["bar/{bar}", "foo/{foo}/bar"], ) file_pb2 = make_file_pb2(name="foo.proto", package=".example.v1", options=options_original) file_set_original = make_file_set(files=[file_pb2]) options_update = make_file_options_resource_definition( resource_type=".example.v1.Bar", resource_patterns=["bar/{bar}"]) file_pb2 = make_file_pb2(name="foo.proto", package=".example.v1", options=options_update) file_set_update = make_file_set(files=[file_pb2]) FileSetComparator(file_set_original, file_set_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, "RESOURCE_PATTERN_REMOVAL") self.assertEqual( finding.location.proto_file_name, "foo.proto", ) def test_resources_addition(self): file_set_original = make_file_set( files=[make_file_pb2(name="foo.proto", package=".example.v1")]) options_update = make_file_options_resource_definition( resource_type=".example.v1.Bar", resource_patterns=["foo/{foo}/bar/{bar}"]) file_pb2 = make_file_pb2(name="foo.proto", package=".example.v1", options=options_update) file_set_update = make_file_set(files=[file_pb2]) FileSetComparator(file_set_original, file_set_update, self.finding_container).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual( finding.category.name, "RESOURCE_DEFINITION_ADDITION", ) self.assertEqual( finding.location.proto_file_name, "foo.proto", ) def test_resources_removal(self): # Create message with resource options. message_options = make_message_options_resource_definition( resource_type="example.v1/Bar", resource_patterns=["user/{user}", "user/{user}/bar/"], ) message = make_message("Test", options=message_options) # Original file set with one resource defined at message level. file_set_original = make_file_set(files=[ make_file_pb2( name="bar.proto", package=".example.v1", messages=[message]) ]) # Update file set without any resources. file_set_update = make_file_set( files=[make_file_pb2(name="foo.proto", package=".example.v1")]) FileSetComparator(file_set_original, file_set_update, self.finding_container).compare() file_resource_removal = next( f for f in self.finding_container.get_all_findings() if f.category.name == "RESOURCE_DEFINITION_REMOVAL") self.assertEqual( file_resource_removal.location.proto_file_name, "bar.proto", ) def test_java_multiple_files_removal(self): option1 = descriptor_pb2.FileOptions() option1.java_multiple_files = True file1 = make_file_pb2( name="file.proto", package="example.v1", options=option1, ) file_set_original = make_file_set(files=[file1]) option2 = descriptor_pb2.FileOptions() option2.java_multiple_files = False file2 = make_file_pb2( name="file.proto", package="example.v1", options=option2, ) file_set_update = make_file_set(files=[file2]) FileSetComparator(file_set_original, file_set_update, self.finding_container).compare() self.assertTrue(len(self.finding_container.get_all_findings())) finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "PACKAGING_OPTION_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") def test_java_multiple_files_addition(self): option1 = descriptor_pb2.FileOptions() option1.java_multiple_files = False file1 = make_file_pb2( name="file.proto", package="example.v1", options=option1, ) file_set_original = make_file_set(files=[file1]) option2 = descriptor_pb2.FileOptions() option2.java_multiple_files = True file2 = make_file_pb2( name="file.proto", package="example.v1", options=option2, ) file_set_update = make_file_set(files=[file2]) FileSetComparator(file_set_original, file_set_update, self.finding_container).compare() self.assertTrue(len(self.finding_container.get_all_findings())) finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "PACKAGING_OPTION_ADDITION") self.assertEqual(finding.change_type.name, "MAJOR") def test_java_outer_classname_removal(self): option1 = descriptor_pb2.FileOptions() option1.java_outer_classname = "Foo" file1 = make_file_pb2( name="fil1.proto", package="example.v1", options=option1, ) option2 = descriptor_pb2.FileOptions() option2.java_outer_classname = "Bar" file2 = make_file_pb2( name="fil2.proto", package="example.v1", options=option2, ) file_set_original = make_file_set(files=[file1, file2]) option3 = descriptor_pb2.FileOptions() option3.java_outer_classname = "Bar" file3 = make_file_pb2(name="file3.proto", package="example.v1beta", options=option3) file_set_update = make_file_set(files=[file3]) FileSetComparator(file_set_original, file_set_update, self.finding_container).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "PACKAGING_OPTION_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") def test_packaging_options_change(self): file_options_original = descriptor_pb2.FileOptions() file_options_original.php_namespace = "Google\\Cloud\\Service\\V1" file_options_original.csharp_namespace = "Google.Cloud.Service.V1" file_options_original.java_outer_classname = "ServiceProto" file_original = make_file_pb2( name="original.proto", package="google.cloud.service.v1", options=file_options_original, ) file_options_update = descriptor_pb2.FileOptions() # Breaking since version should be updated to `v1alpha` file_options_update.php_namespace = "Google\\Cloud\\Service\\V1beta" # No breaking change file_options_update.csharp_namespace = "Google.Cloud.Service.V1alpha" file_options_update.java_outer_classname = "ServiceUpdateProto" file_update = make_file_pb2( name="update.proto", package="google.cloud.service.v1alpha", options=file_options_update, ) FileSetComparator( make_file_set(files=[file_original]), make_file_set(files=[file_update]), self.finding_container, ).compare() java_classname_option_removal = next( f for f in self.finding_container.get_all_findings() if f.category.name == "PACKAGING_OPTION_REMOVAL" and f.subject == "java_outer_classname") php_namespace_option_removal = next( f for f in self.finding_container.get_all_findings() if f.category.name == "PACKAGING_OPTION_REMOVAL" and f.subject == "php_namespace") self.assertTrue(java_classname_option_removal) self.assertTrue(php_namespace_option_removal) def test_packaging_options_version_update(self): file_options_original = descriptor_pb2.FileOptions() file_options_original.java_outer_classname = "ServiceProto" file_options_original.java_package = "com.google.cloud.service.v1" file_options_original.csharp_namespace = "Google.Cloud.Service.V1" file_options_original.php_namespace = "Google\\Cloud\\Service\\V1" file_options_original.ruby_package = "Google::Cloud::Service::V1" file_original = make_file_pb2( name="original.proto", package="google.cloud.service.v1", options=file_options_original, ) file_options_update = descriptor_pb2.FileOptions() file_options_update.java_outer_classname = "ServiceProto" file_options_update.java_package = "com.google.cloud.service.v1alpha" file_options_update.csharp_namespace = "Google.Cloud.Service.V1alpha" file_options_update.php_namespace = "Google\\Cloud\\Service\\V1alpha" file_options_update.ruby_package = "Google::Cloud::Service::V1alpha" file_update = make_file_pb2( name="update.proto", package="google.cloud.service.v1alpha", options=file_options_update, ) FileSetComparator( make_file_set(files=[file_original]), make_file_set(files=[file_update]), self.finding_container, ).compare() self.assertEqual(self.finding_container.get_all_findings(), [])
class ServiceComparatorTest(unittest.TestCase): def setUp(self): self.service_foo = make_service(name="Foo") self.finding_container = FindingContainer() def test_service_removal(self): ServiceComparator(self.service_foo, None, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "SERVICE_REMOVAL") self.assertEqual(finding.location.proto_file_name, "foo") def test_service_addition(self): ServiceComparator(None, self.service_foo, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "SERVICE_ADDITION") self.assertEqual(finding.location.proto_file_name, "foo") def test_service_host_addition(self): service_without_host = make_service() service_with_host = make_service(host="api.google.com") ServiceComparator( service_without_host, service_with_host, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "SERVICE_HOST_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_service_host_removal(self): service_without_host = make_service() service_with_host = make_service(host="api.google.com") ServiceComparator( service_with_host, service_without_host, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "SERVICE_HOST_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_service_host_change(self): service_original = make_service(host="default.host") service_update = make_service(host="default.host.update") ServiceComparator(service_original, service_update, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "SERVICE_HOST_CHANGE") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_service_oauth_scopes_change(self): service_original = make_service(scopes=("https://foo/user/", "https://foo/admin/")) service_update = make_service( scopes=("https://www.googleapis.com/auth/cloud-platform")) ServiceComparator(service_original, service_update, self.finding_container, context="ctx").compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "OAUTH_SCOPE_REMOVAL" and f.subject == "https://foo/user/") self.assertEqual(finding.location.proto_file_name, "foo") finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "OAUTH_SCOPE_REMOVAL" and f.subject == "https://foo/admin/") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_method_removal(self): method_foo = make_method(name="foo") method_bar = make_method(name="bar") service_original = make_service(methods=(method_foo, method_bar)) service_update = make_service(methods=(method_foo, )) ServiceComparator(service_original, service_update, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "METHOD_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_method_addition(self): method_foo = make_method(name="foo") method_bar = make_method(name="bar") service_original = make_service(methods=(method_foo, )) service_update = make_service(methods=(method_foo, method_bar)) ServiceComparator(service_original, service_update, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "METHOD_ADDTION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_method_input_type_change(self): message_foo_request = make_message(name="FooRequest", full_name="FooRequest") message_bar_request = make_message(name="BarRequest", full_name="BarRequest") service_original = make_service(methods=( make_method(name="Foo", input_message=message_foo_request), )) service_update = make_service(methods=( make_method(name="Foo", input_message=message_bar_request), )) ServiceComparator(service_original, service_update, self.finding_container, context="ctx").compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "METHOD_INPUT_TYPE_CHANGE") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_method_output_type_change(self): service_original = make_service(methods=(make_method( name="Foo", output_message=make_message(name="FooResponse", full_name=".example.FooResponse"), ), )) service_update = make_service(methods=(make_method( name="Foo", output_message=make_message(name="BarResponse", full_name=".example.BarResponse"), ), )) ServiceComparator(service_original, service_update, self.finding_container, context="ctx").compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "METHOD_RESPONSE_TYPE_CHANGE") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_method_streaming_state_change(self): service_original = make_service( methods=(make_method(name="Bar", client_streaming=True), )) service_update = make_service( methods=(make_method(name="Bar", server_streaming=True), )) ServiceComparator(service_original, service_update, self.finding_container, context="ctx").compare() client_streaming_finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "METHOD_CLIENT_STREAMING_CHANGE") self.assertEqual(client_streaming_finding.location.proto_file_name, "foo") server_streaming_finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "METHOD_SERVER_STREAMING_CHANGE") self.assertEqual(server_streaming_finding.change_type.name, "MAJOR") self.assertEqual(server_streaming_finding.location.proto_file_name, "foo") def test_method_paginated_state_change(self): paginated_response_message = make_message( name="PaginatedResponseMessage", fields=[ make_field( name="repeated_field", proto_type="TYPE_STRING", repeated=True, number=1, ), make_field( name="next_page_token", proto_type="TYPE_STRING", number=2, ), ], full_name=".example.v1.PaginatedResponseMessage", ) paginated_request_message = make_message( name="PaginatedRequestMessage", fields=[ make_field( name="page_size", proto_type="TYPE_INT32", number=1, ), make_field( name="page_token", proto_type="TYPE_STRING", number=2, ), ], full_name=".example.v1.PaginatedRequestMessage", ) messages_map_original = { ".example.v1.PaginatedResponseMessage": paginated_response_message, ".example.v1.PaginatedRequestMessage": paginated_request_message, } paged_method = make_method( name="NotInteresting", input_message=paginated_request_message, output_message=paginated_response_message, ) messages_map_update = { ".example.v1alpha.MethodInput": make_message(name="MethodInput", full_name=".example.v1alpha.MethodInput"), ".example.v1alpha.MethodOutput": make_message(name="MethodOutput", full_name=".example.v1alpha.MethodOutput"), } non_paged_method = make_method(name="NotInteresting") service_original = make_service(methods=(paged_method, ), messages_map=messages_map_original) service_update = make_service(methods=(non_paged_method, ), messages_map=messages_map_update) ServiceComparator(service_original, service_update, self.finding_container, context="ctx").compare() finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "METHOD_PAGINATED_RESPONSE_CHANGE") self.assertEqual(finding.location.proto_file_name, "foo") def test_method_signature_removal(self): ServiceComparator( make_service(methods=(make_method(name="NotInteresting", signatures=["sig1", "sig2"]), )), make_service(methods=( make_method(name="NotInteresting", signatures=["sig1"]), )), self.finding_container, context="ctx", ).compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "METHOD_SIGNATURE_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_lro_annotation_addition(self): lro_output_msg = make_message( name=".google.longrunning.Operation", full_name=".google.longrunning.Operation", ) method_lro = make_method( name="Method", output_message=lro_output_msg, lro_response_type="response_type", lro_metadata_type="FooMetadata", ) method_not_lro = make_method(name="Method", ) ServiceComparator( make_service(methods=(method_not_lro, )), make_service(methods=(method_lro, )), self.finding_container, context="ctx", ).compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "LRO_ANNOTATION_ADDITION") self.assertTrue(finding) def test_lro_annotation_removal(self): lro_output_msg = make_message( name=".google.longrunning.Operation", full_name=".google.longrunning.Operation", ) method_lro = make_method( name="Method", output_message=lro_output_msg, lro_response_type="response_type", lro_metadata_type="FooMetadata", ) method_not_lro = make_method(name="Method", ) ServiceComparator( make_service(methods=(method_lro, )), make_service(methods=(method_not_lro, )), self.finding_container, context="ctx", ).compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "LRO_ANNOTATION_REMOVAL") self.assertTrue(finding) def test_lro_annotation_invalid(self): lro_output_msg = make_message( name=".google.longrunning.Operation", full_name=".google.longrunning.Operation", ) method_lro = make_method( name="Method", output_message=lro_output_msg, lro_response_type="response_type", lro_metadata_type="FooMetadata", ) method_not_lro = make_method( name="Method", output_message=lro_output_msg, ) # `method_not_lro` returns `google.longrunning.Operation` # but is missing a response type or metadata type, the definition # is invalid. We still compare the two methods and take it as # lro annotation removal. ServiceComparator( make_service(methods=(method_lro, )), make_service(methods=(method_not_lro, )), self.finding_container, context="ctx", ).compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "LRO_ANNOTATION_REMOVAL") self.assertTrue(finding) def test_lro_annotation_response_change(self): lro_output_msg = make_message( name=".google.longrunning.Operation", full_name=".google.longrunning.Operation", ) method_original = make_method( name="Method", output_message=lro_output_msg, lro_response_type="response_type", lro_metadata_type="FooMetadata", ) method_update = make_method( name="Method", output_message=lro_output_msg, lro_response_type="response_type_update", lro_metadata_type="FooMetadata", ) ServiceComparator( make_service(methods=(method_original, )), make_service(methods=(method_update, )), self.finding_container, context="ctx", ).compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "LRO_RESPONSE_CHANGE") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_lro_annotation_metadata_change(self): lro_output_msg = make_message( name=".google.longrunning.Operation", full_name=".google.longrunning.Operation", ) method_original = make_method( name="Method", output_message=lro_output_msg, lro_response_type="response_type", lro_metadata_type="FooMetadata", ) method_update = make_method( name="Method", output_message=lro_output_msg, lro_response_type="response_type", lro_metadata_type="FooMetadataUpdate", ) ServiceComparator( make_service(methods=(method_original, )), make_service(methods=(method_update, )), self.finding_container, context="ctx", ).compare() finding = next(f for f in self.finding_container.get_all_findings() if f.category.name == "LRO_METADATA_CHANGE") self.assertEqual(finding.location.proto_file_name, "foo") def test_http_annotation_addition(self): method_without_http_annotation = make_method(name="Method", ) method_with_http_annotation = make_method( name="Method", http_uri="http_uri_update", http_body="http_body", ) ServiceComparator( make_service(methods=(method_without_http_annotation, )), make_service(methods=(method_with_http_annotation, )), self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "HTTP_ANNOTATION_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") def test_http_annotation_removal(self): method_without_http_annotation = make_method(name="Method", ) method_with_http_annotation = make_method( name="Method", http_uri="http_uri_update", http_body="http_body", ) ServiceComparator( make_service(methods=(method_with_http_annotation, )), make_service(methods=(method_without_http_annotation, )), self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "HTTP_ANNOTATION_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") def test_http_annotation_change(self): method_original = make_method( name="Method", http_uri="http_uri", http_body="*", ) method_update = make_method( name="Method", http_uri="http_uri_update", http_body="http_body", ) ServiceComparator( make_service(methods=(method_original, )), make_service(methods=(method_update, )), self.finding_container, context="ctx", ).compare() uri_change_finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "HTTP_ANNOTATION_CHANGE" and f.type == "http_uri") body_change_finding = next( f for f in self.finding_container.get_all_findings() if f.category.name == "HTTP_ANNOTATION_CHANGE" and f.type == "http_body") self.assertEqual( uri_change_finding.location.proto_file_name, "foo", ) self.assertEqual( body_change_finding.location.proto_file_name, "foo", ) def test_http_annotation_minor_version_update(self): method_original = make_method( name="Method", http_uri="/v1/{name=projects/*}", http_body="*", ) method_update = make_method( name="Method", http_uri="/v1alpha/{name=projects/*}", http_body="*", ) ServiceComparator( make_service(methods=(method_original, ), api_version="v1"), make_service(methods=(method_update, ), api_version="v1alpha"), self.finding_container, context="ctx", ).compare() findings_map = { f.message: f for f in self.finding_container.get_all_findings() } # No breaking changes, since only minor version update in the http URI. self.assertFalse(findings_map)
class DescriptorComparatorTest(unittest.TestCase): def setUp(self): self.message_foo = make_message("Message") self.finding_container = FindingContainer() def test_message_removal(self): DescriptorComparator(self.message_foo, None, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "MESSAGE_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_message_addition(self): DescriptorComparator(None, self.message_foo, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "MESSAGE_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "foo") 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_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, context="ctx").compare() finding = self.finding_container.get_all_findings()[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_nested_message_removal(self): DescriptorComparator( make_message(nested_messages=[make_message( name="nested_message")]), make_message(), self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "MESSAGE_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_nested_message_addition(self): DescriptorComparator( make_message(), make_message(nested_messages=[make_message( name="nested_message")]), self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "MESSAGE_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_nested_enum_addition(self): DescriptorComparator( make_message(), make_message(nested_enums=[make_enum(name="nested_message")]), self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_nested_enum_removal(self): DescriptorComparator( make_message(nested_enums=[make_enum(name="nested_message")]), make_message(), self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_nested_message_change(self): nested_field = make_field(name="nested_field") nested_message_with_fields = make_message(name="nested_message", fields=[nested_field]) nested_message_without_fields = make_message(name="nested_message") message1 = make_message(nested_messages=[nested_message_with_fields]) message2 = make_message( nested_messages=[nested_message_without_fields]) 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_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "foo") def test_nested_enum_change(self): nested_enum1 = make_enum( name="Foo", values=( ("RED", 1), ("GREEN", 2), ), ) nested_enum2 = make_enum( name="Foo", values=( ("RED", 1), ("GREEN", 2), ("BLUE", 3), ), ) message1 = make_message(nested_enums=[nested_enum1]) message2 = make_message(nested_enums=[nested_enum2]) 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 == "ENUM_VALUE_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "foo")
class EnumValueComparatorTest(unittest.TestCase): def setUp(self): L = descriptor_pb2.SourceCodeInfo.Location locations = [L(path=(2, 1), span=(1, 2))] self.enum_foo = make_enum_value( name="FOO", number=1, proto_file_name="test.proto", locations=locations, path=(2, 1), ) self.enum_bar = make_enum_value( name="BAR", number=1, proto_file_name="test_update.proto", locations=locations, path=(2, 1), ) self.finding_container = FindingContainer() def test_enum_value_removal(self): EnumValueComparator( self.enum_foo, None, self.finding_container, context="ctx", ).compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_VALUE_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "test.proto") self.assertEqual(finding.location.source_code_line, 2) def test_enum_value_addition(self): EnumValueComparator(None, self.enum_foo, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_VALUE_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "test.proto") self.assertEqual(finding.location.source_code_line, 2) def test_name_change(self): EnumValueComparator(self.enum_foo, self.enum_bar, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_VALUE_NAME_CHANGE") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "test_update.proto") self.assertEqual(finding.location.source_code_line, 2) def test_no_api_change(self): EnumValueComparator(self.enum_foo, self.enum_foo, self.finding_container, context="ctx").compare() self.assertEqual(len(self.finding_container.get_all_findings()), 0)
class EnumComparatorTest(unittest.TestCase): def setUp(self): L = descriptor_pb2.SourceCodeInfo.Location # fmt: off locations = [ L(path=( 4, 0, ), span=(1, 2, 3, 4)), # Enum will add (2, 1) for each EnumValue in the path. L(path=(4, 0, 2, 1), span=(2, 3, 4, 5)), ] self.enum_foo = make_enum( name="Foo", values=(("VALUE1", 1), ), proto_file_name="test.proto", locations=locations, path=( 4, 0, ), ) self.enum_bar = make_enum( name="Bar", values=( ("VALUE1", 1), ("VALUE2", 2), ), proto_file_name="test_update.proto", locations=locations, path=( 4, 0, ), ) self.finding_container = FindingContainer() # fmt: on def test_enum_removal(self): EnumComparator(self.enum_foo, None, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "test.proto") self.assertEqual(finding.location.source_code_line, 2) def test_enum_addition(self): EnumComparator(None, self.enum_bar, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "test_update.proto") self.assertEqual(finding.location.source_code_line, 2) def test_enum_value_change(self): EnumComparator(self.enum_foo, self.enum_bar, self.finding_container, context="ctx").compare() finding = self.finding_container.get_all_findings()[0] self.assertEqual(finding.category.name, "ENUM_VALUE_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") self.assertEqual(finding.location.proto_file_name, "test_update.proto") self.assertEqual(finding.location.source_code_line, 3) def test_no_api_change(self): EnumComparator(self.enum_foo, self.enum_foo, self.finding_container, context="ctx").compare() self.assertEqual(len(self.finding_container.get_all_findings()), 0)