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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_properties(self): service = make_service(name="ThingDoer") self.assertEqual(service.name, "ThingDoer") self.assertEqual(service.proto_file_name, "foo") self.assertEqual(service.path, ()) self.assertFalse(service.api_version) self.assertEqual( service.source_code_line, -1, )
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_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_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.getAllFindings()[0] self.assertEqual(finding.message, "A new service `Placeholder` is added.") self.assertEqual(finding.change_type.name, "MINOR")
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.getAllFindings()[0] self.assertEqual(finding.message, "An existing service `Placeholder` is removed.") self.assertEqual(finding.change_type.name, "MAJOR")
def test_source_code_line(self): L = descriptor_pb2.SourceCodeInfo.Location locations = [ L(path=(4, 0, 2, 1), span=(1, 2, 3, 4)), ] service = make_service( proto_file_name="test.proto", locations=locations, path=(4, 0, 2, 1), ) self.assertEqual(service.source_code_line, 2) self.assertEqual(service.proto_file_name, "test.proto")
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_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)
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_service_no_scopes(self): service = make_service() self.assertEqual(len(service.oauth_scopes), 0)
def test_service_scopes(self): service = make_service(scopes=("https://foo/user/", "https://foo/admin/")) oauth_scopes = [scope.value for scope in service.oauth_scopes] self.assertIn("https://foo/user/", oauth_scopes) self.assertIn("https://foo/admin/", oauth_scopes)
def test_service_no_host(self): service = make_service() self.assertFalse(service.host)
def test_service_host(self): service = make_service(host="thingdoer.googleapis.com") self.assertEqual(service.host.value, "thingdoer.googleapis.com")
def test_service_api_version(self): service = make_service(api_version="v1alpha") self.assertEqual(service.api_version, "v1alpha")
def test_file_set_properties(self): input_message = make_message(name="request", full_name=".example.v1.request") output_message = make_message(name="response", full_name=".example.v1.response") messages = [ make_message( "InnerMessage", fields=(make_field(name="hidden_message", number=1, type_name=".example.v1.request"), ), full_name=".example.v1.InnerMessage", ), input_message, output_message, ] services = [ make_service( name="ThingDoer", methods=(make_method( name="DoThing", input_message=input_message, output_message=output_message, ), ), ) ] enums = [ make_enum( name="Irrelevant", values=( ("RED", 1), ("GREEN", 2), ("BLUE", 3), ), ) ] file_foo = make_file_pb2( name="foo.proto", package="example.v1", services=services, enums=enums, dependency=["bar.proto"], ) file_bar = make_file_pb2(name="bar.proto", package="example.v1", messages=messages) file_set = make_file_set(files=[file_bar, file_foo]) # Default to be empty. self.assertFalse(file_set.packaging_options_map) self.assertEqual( list(file_set.messages_map.keys()), [ ".example.v1.InnerMessage", ".example.v1.request", ".example.v1.response" ], ) self.assertEqual(list(file_set.enums_map.keys()), [".example.v1.Irrelevant"]) self.assertEqual(list(file_set.services_map.keys()), ["ThingDoer"]) self.assertEqual( file_set.messages_map[".example.v1.InnerMessage"].fields[1].name, "hidden_message", ) self.assertEqual( file_set.enums_map[".example.v1.Irrelevant"].values[2].name, "GREEN") self.assertEqual( list(file_set.services_map["ThingDoer"].methods.keys()), ["DoThing"], ) self.assertEqual(list(file_set.global_enums_map.keys()), [".example.v1.Irrelevant"]) self.assertEqual( list(file_set.global_messages_map.keys()), [ ".example.v1.response", ".example.v1.request", ".example.v1.InnerMessage" ], )