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.getAllFindings()[0] self.assertEqual(finding.message, "An existing rpc method `DoThing` is removed.") 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_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).compare() findings_map = { f.message: f for f in self.finding_container.getAllFindings() } finding = findings_map["A new EnumValue `BLUE` is added."] self.assertEqual(finding.category.name, "ENUM_VALUE_ADDITION") self.assertEqual(finding.change_type.name, "MINOR") 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")
def test_method_types(self): input_msg = make_message(name="Input", full_name=".example.v1.input") output_msg = make_message(name="Output", full_name=".example.v1.output") method = make_method("DoSomething", input_msg, output_msg) self.assertEqual(method.name, "DoSomething") self.assertEqual(method.input.value, ".example.v1.input") self.assertEqual(method.output.value, ".example.v1.output")
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_service_methods(self): input_message = make_message("InputRequest") output_message = make_message("OutputResponse") service = make_service( name="ThingDoer", methods=( make_method( name="DoThing", input_message=input_message, output_message=output_message, ), make_method( name="Jump", input_message=input_message, output_message=output_message, ), make_method( name="Yawn", input_message=input_message, output_message=output_message, ), ), ) expected_names = ["DoThing", "Jump", "Yawn"] self.assertEqual(list(service.methods.keys()), expected_names)
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_detector_basic(self): # Mock original and updated FileDescriptorSet. L = desc.SourceCodeInfo.Location # fmt: off locations = [ L(path=(6, 0, 2, 0), span=(1, 2, 3, 4)), L( path=( 4, 0, ), span=(5, 6, 7, 8), ), L(path=(4, 1), span=(11, 12, 13, 14)), ] # fmt: on input_msg = make_message(name="input", full_name=".example.v1.input") output_msg = make_message(name="output", full_name=".example.v1.output") service_original = make_service(methods=(make_method( name="DoThing", input_message=input_msg, output_message=output_msg), )) service_update = make_service() file_set_original = desc.FileDescriptorSet(file=[ make_file_pb2( services=[service_original], locations=locations, messages=[input_msg, output_msg], ) ]) file_set_update = desc.FileDescriptorSet( file=[make_file_pb2(services=[service_update])]) with mock.patch("os.path.isdir") as mocked_isdir: with mock.patch("os.path.isfile") as mocked_isfile: mocked_isdir.return_value = True mocked_isfile.return_value = True opts = Options( original_api_definition_dirs="c,d", update_api_definition_dirs="a,b", original_proto_files="pf1, pf2", update_proto_files="pf3, pf4", original_descriptor_set_file_path=None, update_descriptor_set_file_path=None, human_readable_message=True, ) with mock.patch("sys.stdout", new=StringIO()) as fake_output: result = Detector(file_set_original, file_set_update, opts).detect_breaking_changes() self.assertEqual( fake_output.getvalue(), "my_proto.proto L2: An existing method `DoThing` is removed from service `Placeholder`.\n" + "my_proto.proto L6: An existing message `input` is removed.\n" + "my_proto.proto L12: An existing message `output` is removed.\n", ) self.assertEqual(len(result), 3)
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_nested_enum_addition(self): DescriptorComparator( make_message(), make_message(nested_enums=[make_enum(name="nested_message")]), self.finding_container, ).compare() finding = self.finding_container.getAllFindings()[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_file_set_resources(self): options = descriptor_pb2.FileOptions() options.Extensions[resource_pb2.resource_definition].append( resource_pb2.ResourceDescriptor( type="example.v1/Foo", pattern=["foo/{foo}"], )) # File1 with file-level resource definition `example.v1/Foo`. file1 = make_file_pb2( name="foo.proto", package=".example.v1", dependency=["bar.proto"], options=options, ) # File2 with resource definition in message and nested message. message_options = descriptor_pb2.MessageOptions() resource = message_options.Extensions[resource_pb2.resource] resource.pattern.append("user/{user}") resource.pattern.append("user/{user}/bar/") resource.type = "example.v1/Bar" nested_message_options = descriptor_pb2.MessageOptions() resource = nested_message_options.Extensions[resource_pb2.resource] resource.pattern.append("tests/{test}/") resource.type = "example.v1/Test" nesed_message = make_message("nested_message", options=nested_message_options) message = make_message( name="outer_message", nested_messages=[nesed_message], options=message_options, ) file2 = make_file_pb2(name="bar.proto", package=".example.bar", messages=[message]) file_set = make_file_set(files=[file1, file2]) # All resources should be registered in the database. resource_types = file_set.resources_database.types resource_patterns = file_set.resources_database.patterns self.assertEqual( list(resource_types.keys()), ["example.v1/Foo", "example.v1/Bar", "example.v1/Test"], ) self.assertEqual( list(resource_patterns.keys()), ["foo/{foo}", "user/{user}", "user/{user}/bar/", "tests/{test}/"], ) # Check used resources database. # File1 depends on file2, but they are not in the same package, only file1 # is API definition file. So only resources in file1 are in used_resource_database. self.assertEqual(list(file_set.used_resources_database.types.keys()), ["example.v1/Foo"]) self.assertEqual( list(file_set.used_resources_database.patterns.keys()), ["foo/{foo}"])
def test_nested_message_removal(self): DescriptorComparator( make_message(nested_messages=[make_message( name="nested_message")]), make_message(), self.finding_container, ).compare() finding = self.finding_container.getAllFindings()[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_method_longrunning(self): input_msg = make_message(name="Input", full_name=".example.v1.input") output_msg = make_message( name=".google.longrunning.Operation", full_name=".google.longrunning.Operation", ) method = make_method("DoSomething", input_msg, output_msg) self.assertEqual(method.name, "DoSomething") self.assertEqual(method.input.value, ".example.v1.input") self.assertEqual(method.output.value, ".google.longrunning.Operation") self.assertEqual(method.longrunning, True)
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_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_resource_annotation(self): options = descriptor_pb2.MessageOptions() resource = options.Extensions[resource_pb2.resource] resource.pattern.append("foo/{foo}/bar") resource.pattern.append("foo/{foo}/bar/{bar}") resource.type = "example/Bar" message = make_message("Message", options=options) message_without_resource = make_message("Message") self.assertEqual(message_without_resource.resource, None) self.assertEqual(message.resource.value.type, "example/Bar") self.assertEqual(message.resource.value.pattern, ["foo/{foo}/bar", "foo/{foo}/bar/{bar}"])
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_file_set_source_code_location(self): L = descriptor_pb2.SourceCodeInfo.Location locations = [ L(path=(4, 0, 2, 0, 5), span=(5, 2, 3, 4)), L(path=(4, 0, 3, 0), span=(6, 1, 2, 4)), L(path=(4, 0, 4, 0), span=(7, 1, 2, 4)), L(path=(4, 0, 4, 0, 2, 1), span=(9, 1, 2, 4)), ] messages = [ make_message( "OuterMessage", fields=(make_field( name="hidden_message", number=1, ), ), nested_messages=(make_message(name="InterMessage"), ), nested_enums=(make_enum( name="InterEnum", values=( ("RED", 1), ("GREEN", 2), ("BLUE", 3), ), ), ), ) ] file_pb2 = make_file_pb2(messages=messages, locations=locations) file_set = make_file_set(files=[file_pb2]) self.assertEqual( file_set.messages_map[".example.v1.OuterMessage"].fields[1]. proto_type.source_code_line, 6, ) self.assertEqual( file_set.messages_map[".example.v1.OuterMessage"]. nested_messages["InterMessage"].source_code_line, 7, ) self.assertEqual( file_set.messages_map[".example.v1.OuterMessage"]. nested_enums["InterEnum"].source_code_line, 8, ) self.assertEqual( file_set.messages_map[".example.v1.OuterMessage"]. nested_enums["InterEnum"].values[2].source_code_line, 10, )
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_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_method_lro_annotationn(self): input_msg = make_message(name="Input", full_name=".example.v1.input") output_msg = make_message( name=".google.longrunning.Operation", full_name=".google.longrunning.Operation", ) method = make_method( name="Method", input_message=input_msg, output_message=output_msg, lro_response_type="response_type", lro_metadata_type="metadata_type", ) lro_annotation = method.lro_annotation.value self.assertEqual(lro_annotation["response_type"], "response_type") self.assertEqual(lro_annotation["metadata_type"], "metadata_type")
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_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() findings_map = { f.message: f for f in self.finding_container.getAllFindings() } file_resource_removal = findings_map[ "An existing resource definition `example.v1/Bar` has been removed."] self.assertEqual( file_resource_removal.category.name, "RESOURCE_DEFINITION_REMOVAL", ) self.assertEqual( file_resource_removal.location.proto_file_name, "bar.proto", )
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_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_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_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).compare() findings_map = { f.message: f for f in self.finding_container.getAllFindings() } finding = findings_map["An existing field `nested_field` is removed."] self.assertEqual(finding.category.name, "FIELD_REMOVAL") self.assertEqual(finding.change_type.name, "MAJOR") self.assertEqual(finding.location.proto_file_name, "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")