1# Copyright 2018 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import collections 16 17from typing import Sequence 18from unittest import mock 19 20import pytest 21 22from google.api import client_pb2 23from google.api import resource_pb2 24from google.api_core import exceptions 25from google.longrunning import operations_pb2 26from google.protobuf import descriptor_pb2 27 28from gapic.schema import api 29from gapic.schema import imp 30from gapic.schema import naming 31from gapic.schema import wrappers 32from gapic.utils import Options 33 34from test_utils.test_utils import ( 35 make_enum_pb2, 36 make_field_pb2, 37 make_file_pb2, 38 make_message_pb2, 39 make_naming, 40 make_oneof_pb2, 41) 42 43 44def test_api_build(): 45 # Put together a couple of minimal protos. 46 fd = ( 47 make_file_pb2( 48 name='dep.proto', 49 package='google.dep', 50 messages=(make_message_pb2(name='ImportedMessage', fields=()),), 51 ), 52 make_file_pb2( 53 name='common.proto', 54 package='google.example.v1.common', 55 messages=(make_message_pb2(name='Bar'),), 56 ), 57 make_file_pb2( 58 name='foo.proto', 59 package='google.example.v1', 60 messages=( 61 make_message_pb2(name='Foo', fields=()), 62 make_message_pb2(name='GetFooRequest', fields=( 63 make_field_pb2(name='imported_message', number=1, 64 type_name='.google.dep.ImportedMessage'), 65 make_field_pb2(name='primitive', number=2, type=1), 66 )), 67 make_message_pb2(name='GetFooResponse', fields=( 68 make_field_pb2(name='foo', number=1, 69 type_name='.google.example.v1.Foo'), 70 )), 71 ), 72 services=(descriptor_pb2.ServiceDescriptorProto( 73 name='FooService', 74 method=( 75 descriptor_pb2.MethodDescriptorProto( 76 name='GetFoo', 77 input_type='google.example.v1.GetFooRequest', 78 output_type='google.example.v1.GetFooResponse', 79 ), 80 ), 81 ),), 82 ), 83 ) 84 85 # Create an API with those protos. 86 api_schema = api.API.build(fd, package='google.example.v1') 87 88 # Establish that the API has the data expected. 89 assert isinstance(api_schema, api.API) 90 assert len(api_schema.all_protos) == 3 91 assert len(api_schema.protos) == 2 92 assert 'google.dep.ImportedMessage' not in api_schema.messages 93 assert 'google.example.v1.common.Bar' in api_schema.messages 94 assert 'google.example.v1.Foo' in api_schema.messages 95 assert 'google.example.v1.GetFooRequest' in api_schema.messages 96 assert 'google.example.v1.GetFooResponse' in api_schema.messages 97 assert 'google.example.v1.FooService' in api_schema.services 98 assert len(api_schema.enums) == 0 99 assert api_schema.protos['foo.proto'].python_modules == ( 100 imp.Import(package=('google', 'dep'), module='dep_pb2'), 101 ) 102 103 assert api_schema.requires_package(('google', 'example', 'v1')) 104 assert not api_schema.requires_package(('elgoog', 'example', 'v1')) 105 106 # Establish that the subpackages work. 107 assert 'common' in api_schema.subpackages 108 sub = api_schema.subpackages['common'] 109 assert len(sub.protos) == 1 110 assert 'google.example.v1.common.Bar' in sub.messages 111 assert 'google.example.v1.Foo' not in sub.messages 112 113 114def test_top_level_messages(): 115 message_pbs = ( 116 make_message_pb2(name='Mollusc', nested_type=( 117 make_message_pb2(name='Squid'), 118 )), 119 ) 120 fds = ( 121 make_file_pb2( 122 messages=message_pbs, 123 package='google.example.v3', 124 ), 125 ) 126 api_schema = api.API.build(fds, package='google.example.v3') 127 actual = [m.name for m in api_schema.top_level_messages.values()] 128 expected = ['Mollusc'] 129 assert expected == actual 130 131 132def test_top_level_enum(): 133 # Test that a nested enum works properly. 134 message_pbs = ( 135 make_message_pb2(name='Coleoidea', enum_type=( 136 make_enum_pb2( 137 'Superorder', 138 'Decapodiformes', 139 'Octopodiformes', 140 'Palaeoteuthomorpha', 141 ), 142 )), 143 ) 144 enum_pbs = ( 145 make_enum_pb2( 146 'Order', 147 'Gastropoda', 148 'Bivalvia', 149 'Cephalopoda', 150 ), 151 ) 152 fds = ( 153 make_file_pb2( 154 messages=message_pbs, 155 enums=enum_pbs, 156 package='google.example.v3', 157 ), 158 ) 159 api_schema = api.API.build(fds, package='google.example.v3') 160 actual = [e.name for e in api_schema.top_level_enums.values()] 161 expected = ['Order'] 162 assert expected == actual 163 164 165def test_proto_build(): 166 fdp = descriptor_pb2.FileDescriptorProto( 167 name='my_proto_file.proto', 168 package='google.example.v1', 169 ) 170 proto = api.Proto.build(fdp, file_to_generate=True, naming=make_naming()) 171 assert isinstance(proto, api.Proto) 172 173 174def test_proto_names(): 175 # Put together a couple of minimal protos. 176 fd = ( 177 make_file_pb2( 178 name='dep.proto', 179 package='google.dep', 180 messages=(make_message_pb2(name='ImportedMessage', fields=()),), 181 ), 182 make_file_pb2( 183 name='foo.proto', 184 package='google.example.v1', 185 messages=( 186 make_message_pb2(name='Foo', fields=()), 187 make_message_pb2(name='Bar', fields=( 188 make_field_pb2(name='imported_message', number=1, 189 type_name='.google.dep.ImportedMessage'), 190 make_field_pb2(name='primitive', number=2, type=1), 191 )), 192 make_message_pb2(name='Baz', fields=( 193 make_field_pb2(name='foo', number=1, 194 type_name='.google.example.v1.Foo'), 195 )), 196 ), 197 ), 198 ) 199 200 # Create an API with those protos. 201 api_schema = api.API.build(fd, package='google.example.v1') 202 proto = api_schema.protos['foo.proto'] 203 assert proto.names == {'Foo', 'Bar', 'Baz', 'foo', 'imported_message', 204 'primitive'} 205 assert proto.disambiguate('enum') == 'enum' 206 assert proto.disambiguate('foo') == '_foo' 207 208 209def test_proto_keyword_fname(): 210 # Protos with filenames that happen to be python keywords 211 # cannot be directly imported. 212 # Check that the file names are unspecialized when building the API object. 213 fd = ( 214 make_file_pb2( 215 name='import.proto', 216 package='google.keywords.v1', 217 messages=(make_message_pb2(name='ImportRequest', fields=()),), 218 ), 219 make_file_pb2( 220 name='import_.proto', 221 package='google.keywords.v1', 222 messages=(make_message_pb2(name='ImportUnderRequest', fields=()),), 223 ), 224 make_file_pb2( 225 name='class_.proto', 226 package='google.keywords.v1', 227 messages=(make_message_pb2(name='ClassUnderRequest', fields=()),), 228 ), 229 make_file_pb2( 230 name='class.proto', 231 package='google.keywords.v1', 232 messages=(make_message_pb2(name='ClassRequest', fields=()),), 233 ) 234 ) 235 236 # We can't create new collisions, so check that renames cascade. 237 api_schema = api.API.build(fd, package='google.keywords.v1') 238 assert set(api_schema.protos.keys()) == { 239 'import_.proto', 240 'import__.proto', 241 'class_.proto', 242 'class__.proto', 243 } 244 245 246def test_proto_oneof(): 247 # Put together a couple of minimal protos. 248 fd = ( 249 make_file_pb2( 250 name='dep.proto', 251 package='google.dep', 252 messages=(make_message_pb2(name='ImportedMessage', fields=()),), 253 ), 254 make_file_pb2( 255 name='foo.proto', 256 package='google.example.v1', 257 messages=( 258 make_message_pb2(name='Foo', fields=()), 259 make_message_pb2( 260 name='Bar', 261 fields=( 262 make_field_pb2(name='imported_message', number=1, 263 type_name='.google.dep.ImportedMessage', 264 oneof_index=0), 265 make_field_pb2( 266 name='primitive', number=2, type=1, oneof_index=0), 267 ), 268 oneof_decl=( 269 make_oneof_pb2(name="value_type"), 270 ) 271 ) 272 ) 273 ) 274 ) 275 276 # Create an API with those protos. 277 api_schema = api.API.build(fd, package='google.example.v1') 278 proto = api_schema.protos['foo.proto'] 279 assert proto.names == {'imported_message', 'Bar', 'primitive', 'Foo'} 280 oneofs = proto.messages["google.example.v1.Bar"].oneofs 281 assert len(oneofs) == 1 282 assert "value_type" in oneofs.keys() 283 284 285def test_proto_names_import_collision(): 286 # Put together a couple of minimal protos. 287 fd = ( 288 make_file_pb2( 289 name='a/b/c/spam.proto', 290 package='a.b.c', 291 messages=(make_message_pb2(name='ImportedMessage', fields=()),), 292 ), 293 make_file_pb2( 294 name='x/y/z/spam.proto', 295 package='x.y.z', 296 messages=(make_message_pb2(name='OtherMessage', fields=()),), 297 ), 298 make_file_pb2( 299 name='foo.proto', 300 package='google.example.v1', 301 messages=( 302 make_message_pb2(name='Foo', fields=()), 303 make_message_pb2(name='Bar', fields=( 304 make_field_pb2(name='imported_message', number=1, 305 type_name='.a.b.c.ImportedMessage'), 306 make_field_pb2(name='other_message', number=2, 307 type_name='.x.y.z.OtherMessage'), 308 make_field_pb2(name='primitive', number=3, type=1), 309 )), 310 make_message_pb2(name='Baz', fields=( 311 make_field_pb2(name='foo', number=1, 312 type_name='.google.example.v1.Foo'), 313 )), 314 ), 315 ), 316 ) 317 318 # Create an API with those protos. 319 api_schema = api.API.build(fd, package='google.example.v1') 320 proto = api_schema.protos['foo.proto'] 321 assert proto.names == {'Foo', 'Bar', 'Baz', 'foo', 'imported_message', 322 'other_message', 'primitive', 'spam'} 323 324 325def test_proto_names_import_collision_flattening(): 326 lro_proto = api.Proto.build(make_file_pb2( 327 name='operations.proto', package='google.longrunning', 328 messages=(make_message_pb2(name='Operation'),), 329 ), file_to_generate=False, naming=make_naming()) 330 331 fd = ( 332 make_file_pb2( 333 name='mollusc.proto', 334 package='google.animalia.mollusca', 335 messages=( 336 make_message_pb2(name='Mollusc',), 337 make_message_pb2(name='MolluscResponse',), 338 make_message_pb2(name='MolluscMetadata',), 339 ), 340 ), 341 make_file_pb2( 342 name='squid.proto', 343 package='google.animalia.mollusca', 344 messages=( 345 make_message_pb2( 346 name='IdentifySquidRequest', 347 fields=( 348 make_field_pb2( 349 name='mollusc', 350 number=1, 351 type_name='.google.animalia.mollusca.Mollusc' 352 ), 353 ), 354 ), 355 make_message_pb2( 356 name='IdentifySquidResponse', 357 fields=(), 358 ), 359 ), 360 services=( 361 descriptor_pb2.ServiceDescriptorProto( 362 name='SquidIdentificationService', 363 method=( 364 descriptor_pb2.MethodDescriptorProto( 365 name='IdentifyMollusc', 366 input_type='google.animalia.mollusca.IdentifySquidRequest', 367 output_type='google.longrunning.Operation', 368 ), 369 ), 370 ), 371 ), 372 ), 373 ) 374 375 method_options = fd[1].service[0].method[0].options 376 # Notice that a signature field collides with the name of an imported module 377 method_options.Extensions[client_pb2.method_signature].append('mollusc') 378 method_options.Extensions[operations_pb2.operation_info].MergeFrom( 379 operations_pb2.OperationInfo( 380 response_type='google.animalia.mollusca.MolluscResponse', 381 metadata_type='google.animalia.mollusca.MolluscMetadata', 382 ) 383 ) 384 api_schema = api.API.build( 385 fd, 386 package='google.animalia.mollusca', 387 prior_protos={ 388 'google/longrunning/operations.proto': lro_proto, 389 } 390 ) 391 392 actual_imports = { 393 ref_type.ident.python_import 394 for service in api_schema.services.values() 395 for method in service.methods.values() 396 for ref_type in method.ref_types 397 } 398 399 expected_imports = { 400 imp.Import( 401 package=('google', 'animalia', 'mollusca', 'types'), 402 module='mollusc', 403 alias='gam_mollusc', 404 ), 405 imp.Import( 406 package=('google', 'animalia', 'mollusca', 'types'), 407 module='squid', 408 ), 409 imp.Import(package=('google', 'api_core'), module='operation',), 410 imp.Import(package=('google', 'api_core'), module='operation_async',), 411 } 412 413 assert expected_imports == actual_imports 414 415 method = ( 416 api_schema 417 .services['google.animalia.mollusca.SquidIdentificationService'] 418 .methods['IdentifyMollusc'] 419 ) 420 421 actual_response_import = method.lro.response_type.ident.python_import 422 expected_response_import = imp.Import( 423 package=('google', 'animalia', 'mollusca', 'types'), 424 module='mollusc', 425 alias='gam_mollusc', 426 ) 427 assert actual_response_import == expected_response_import 428 429 430def test_proto_builder_constructor(): 431 sentinel_message = descriptor_pb2.DescriptorProto() 432 sentinel_enum = descriptor_pb2.EnumDescriptorProto() 433 sentinel_service = descriptor_pb2.ServiceDescriptorProto() 434 435 # Create a file descriptor proto. It does not matter that none 436 # of the sentinels have actual data because this test just ensures 437 # they are sent off to the correct methods unmodified. 438 fdp = make_file_pb2( 439 messages=(sentinel_message,), 440 enums=(sentinel_enum,), 441 services=(sentinel_service,), 442 ) 443 444 # Test the load function. 445 with mock.patch.object(api._ProtoBuilder, '_load_children') as lc: 446 pb = api._ProtoBuilder(fdp, 447 file_to_generate=True, 448 naming=make_naming(), 449 ) 450 451 # There should be three total calls to load the different types 452 # of children. 453 assert lc.call_count == 3 454 455 # The enum type should come first. 456 _, args, _ = lc.mock_calls[0] 457 assert args[0][0] == sentinel_enum 458 assert args[1] == pb._load_enum 459 460 # The message type should come second. 461 _, args, _ = lc.mock_calls[1] 462 assert args[0][0] == sentinel_message 463 assert args[1] == pb._load_message 464 465 # The services should come third. 466 _, args, _ = lc.mock_calls[2] 467 assert args[0][0] == sentinel_service 468 assert args[1] == pb._load_service 469 470 471def test_not_target_file(): 472 """Establish that services are not ignored for untargeted protos.""" 473 message_pb = make_message_pb2( 474 name='Foo', fields=(make_field_pb2(name='bar', type=3, number=1),) 475 ) 476 service_pb = descriptor_pb2.ServiceDescriptorProto() 477 fdp = make_file_pb2(messages=(message_pb,), services=(service_pb,)) 478 479 # Actually make the proto object. 480 proto = api.Proto.build(fdp, file_to_generate=False, naming=make_naming()) 481 482 # The proto object should have the message, but no service. 483 assert len(proto.messages) == 1 484 assert len(proto.services) == 0 485 486 487def test_messages(): 488 L = descriptor_pb2.SourceCodeInfo.Location 489 490 message_pb = make_message_pb2( 491 name='Foo', fields=(make_field_pb2(name='bar', type=3, number=1),) 492 ) 493 locations = ( 494 L(path=(4, 0), leading_comments='This is the Foo message.'), 495 L(path=(4, 0, 2, 0), leading_comments='This is the bar field.'), 496 ) 497 fdp = make_file_pb2( 498 messages=(message_pb,), 499 locations=locations, 500 package='google.example.v2', 501 ) 502 503 # Make the proto object. 504 proto = api.Proto.build(fdp, file_to_generate=True, naming=make_naming()) 505 506 # Get the message. 507 assert len(proto.messages) == 1 508 message = proto.messages['google.example.v2.Foo'] 509 assert isinstance(message, wrappers.MessageType) 510 assert message.meta.doc == 'This is the Foo message.' 511 assert len(message.fields) == 1 512 assert message.fields['bar'].meta.doc == 'This is the bar field.' 513 514 515def test_messages_reverse_declaration_order(): 516 # Test that if a message is used as a field higher in the same file, 517 # that things still work. 518 message_pbs = ( 519 make_message_pb2(name='Foo', fields=( 520 make_field_pb2(name='bar', number=1, 521 type_name='.google.example.v3.Bar'), 522 ), 523 ), 524 make_message_pb2(name='Bar'), 525 ) 526 fdp = make_file_pb2( 527 messages=message_pbs, 528 package='google.example.v3', 529 ) 530 531 # Make the proto object. 532 proto = api.Proto.build(fdp, file_to_generate=True, naming=make_naming()) 533 534 # Get the message. 535 assert len(proto.messages) == 2 536 Foo = proto.messages['google.example.v3.Foo'] 537 assert Foo.fields['bar'].message == proto.messages['google.example.v3.Bar'] 538 539 540def test_messages_recursive(): 541 # Test that if a message is used inside itself, that things will still 542 # work. 543 message_pbs = ( 544 make_message_pb2(name='Foo', fields=( 545 make_field_pb2(name='foo', number=1, 546 type_name='.google.example.v3.Foo'), 547 ), 548 ), 549 ) 550 fdp = make_file_pb2( 551 messages=message_pbs, 552 package='google.example.v3', 553 ) 554 555 # Make the proto object. 556 proto = api.Proto.build(fdp, file_to_generate=True, naming=make_naming()) 557 558 # Get the message. 559 assert len(proto.messages) == 1 560 Foo = proto.messages['google.example.v3.Foo'] 561 assert Foo.fields['foo'].message == proto.messages['google.example.v3.Foo'] 562 563 564def test_messages_nested(): 565 # Test that a nested message works properly. 566 message_pbs = ( 567 make_message_pb2(name='Foo', nested_type=( 568 make_message_pb2(name='Bar'), 569 )), 570 ) 571 fdp = make_file_pb2( 572 messages=message_pbs, 573 package='google.example.v3', 574 ) 575 576 # Make the proto object. 577 proto = api.Proto.build(fdp, file_to_generate=True, naming=make_naming()) 578 579 # Set short variables for the names. 580 foo = 'google.example.v3.Foo' 581 bar = 'google.example.v3.Foo.Bar' 582 583 # Get the message. 584 assert len(proto.all_messages) == 2 585 assert proto.all_messages[foo].name == 'Foo' 586 assert proto.all_messages[bar].name == 'Bar' 587 588 # Assert that the `messages` property only shows top-level messages. 589 assert len(proto.messages) == 1 590 assert proto.messages[foo] is proto.messages[foo] 591 assert bar not in proto.messages 592 593 594def test_out_of_order_enums(): 595 # Enums can be referenced as field types before they 596 # are defined in the proto file. 597 # This happens when they're a nested type within a message. 598 messages = ( 599 make_message_pb2( 600 name='Squid', 601 fields=( 602 make_field_pb2( 603 name='base_color', 604 type_name='google.mollusca.Chromatophore.Color', 605 number=1, 606 ), 607 ), 608 ), 609 make_message_pb2( 610 name='Chromatophore', 611 enum_type=( 612 descriptor_pb2.EnumDescriptorProto(name='Color', value=()), 613 ), 614 ) 615 ) 616 fd = ( 617 make_file_pb2( 618 name='squid.proto', 619 package='google.mollusca', 620 messages=messages, 621 services=( 622 descriptor_pb2.ServiceDescriptorProto( 623 name='SquidService', 624 ), 625 ), 626 ), 627 ) 628 api_schema = api.API.build(fd, package='google.mollusca') 629 field_type = ( 630 api_schema 631 .messages['google.mollusca.Squid'] 632 .fields['base_color'] 633 .type 634 ) 635 enum_type = api_schema.enums['google.mollusca.Chromatophore.Color'] 636 assert field_type == enum_type 637 638 639def test_undefined_type(): 640 fd = ( 641 make_file_pb2( 642 name='mollusc.proto', 643 package='google.mollusca', 644 messages=( 645 make_message_pb2( 646 name='Mollusc', 647 fields=( 648 make_field_pb2( 649 name='class', 650 type_name='google.mollusca.Class', 651 number=1, 652 ), 653 ) 654 ), 655 ), 656 ), 657 ) 658 with pytest.raises(TypeError): 659 api.API.build(fd, package='google.mollusca') 660 661 662def test_python_modules_nested(): 663 fd = ( 664 make_file_pb2( 665 name='dep.proto', 666 package='google.dep', 667 messages=(make_message_pb2(name='ImportedMessage', fields=()),), 668 ), 669 make_file_pb2( 670 name='common.proto', 671 package='google.example.v1.common', 672 messages=(make_message_pb2(name='Bar'),), 673 ), 674 make_file_pb2( 675 name='foo.proto', 676 package='google.example.v1', 677 messages=( 678 make_message_pb2( 679 name='GetFooRequest', 680 fields=( 681 make_field_pb2(name='primitive', number=2, type=1), 682 make_field_pb2( 683 name='foo', 684 number=3, 685 type=1, 686 type_name='.google.example.v1.GetFooRequest.Foo', 687 ), 688 ), 689 nested_type=( 690 make_message_pb2( 691 name='Foo', 692 fields=( 693 make_field_pb2( 694 name='imported_message', 695 number=1, 696 type_name='.google.dep.ImportedMessage'), 697 ), 698 ), 699 ), 700 ), 701 make_message_pb2( 702 name='GetFooResponse', 703 fields=( 704 make_field_pb2( 705 name='foo', 706 number=1, 707 type_name='.google.example.v1.GetFooRequest.Foo', 708 ), 709 ), 710 ), 711 ), 712 services=(descriptor_pb2.ServiceDescriptorProto( 713 name='FooService', 714 method=( 715 descriptor_pb2.MethodDescriptorProto( 716 name='GetFoo', 717 input_type='google.example.v1.GetFooRequest', 718 output_type='google.example.v1.GetFooResponse', 719 ), 720 ), 721 ),), 722 ), 723 ) 724 725 api_schema = api.API.build(fd, package='google.example.v1') 726 727 assert api_schema.protos['foo.proto'].python_modules == ( 728 imp.Import(package=('google', 'dep'), module='dep_pb2'), 729 ) 730 731 732def test_services(): 733 L = descriptor_pb2.SourceCodeInfo.Location 734 735 # Make a silly helper method to not repeat some of the structure. 736 def _n(method_name: str): 737 return { 738 'service': 'google.example.v2.FooService', 739 'method': method_name, 740 } 741 742 # Set up retry information. 743 opts = Options(retry={'methodConfig': [ 744 {'name': [_n('TimeoutableGetFoo')], 'timeout': '30s'}, 745 {'name': [_n('RetryableGetFoo')], 'retryPolicy': { 746 'maxAttempts': 3, 747 'initialBackoff': '%dn' % 1e6, 748 'maxBackoff': '60s', 749 'backoffMultiplier': 1.5, 750 'retryableStatusCodes': ['UNAVAILABLE', 'ABORTED'], 751 }}, 752 ]}) 753 754 # Set up messages for our RPC. 755 request_message_pb = make_message_pb2( 756 name='GetFooRequest', fields=(make_field_pb2(name='name', type=9, number=1),) 757 ) 758 response_message_pb = make_message_pb2(name='GetFooResponse', fields=()) 759 760 # Set up the service with an RPC. 761 service_pb = descriptor_pb2.ServiceDescriptorProto( 762 name='FooService', 763 method=( 764 descriptor_pb2.MethodDescriptorProto( 765 name='GetFoo', 766 input_type='google.example.v2.GetFooRequest', 767 output_type='google.example.v2.GetFooResponse', 768 ), 769 descriptor_pb2.MethodDescriptorProto( 770 name='TimeoutableGetFoo', 771 input_type='google.example.v2.GetFooRequest', 772 output_type='google.example.v2.GetFooResponse', 773 ), 774 descriptor_pb2.MethodDescriptorProto( 775 name='RetryableGetFoo', 776 input_type='google.example.v2.GetFooRequest', 777 output_type='google.example.v2.GetFooResponse', 778 ), 779 ), 780 ) 781 782 # Fake-document our fake stuff. 783 locations = ( 784 L(path=(6, 0), leading_comments='This is the FooService service.'), 785 L(path=(6, 0, 2, 0), leading_comments='This is the GetFoo method.'), 786 L(path=(4, 0), leading_comments='This is the GetFooRequest message.'), 787 L(path=(4, 1), leading_comments='This is the GetFooResponse message.'), 788 ) 789 790 # Finally, set up the file that encompasses these. 791 fdp = make_file_pb2( 792 name='test.proto', 793 package='google.example.v2', 794 messages=(request_message_pb, response_message_pb), 795 services=(service_pb,), 796 locations=locations, 797 ) 798 799 # Make the proto object. 800 proto = api.API.build( 801 [fdp], 802 'google.example.v2', 803 opts=opts, 804 ).protos['test.proto'] 805 806 # Establish that our data looks correct. 807 assert len(proto.services) == 1 808 assert len(proto.messages) == 2 809 service = proto.services['google.example.v2.FooService'] 810 assert service.meta.doc == 'This is the FooService service.' 811 assert len(service.methods) == 3 812 method = service.methods['GetFoo'] 813 assert method.meta.doc == 'This is the GetFoo method.' 814 assert isinstance(method.input, wrappers.MessageType) 815 assert isinstance(method.output, wrappers.MessageType) 816 assert method.input.name == 'GetFooRequest' 817 assert method.input.meta.doc == 'This is the GetFooRequest message.' 818 assert method.output.name == 'GetFooResponse' 819 assert method.output.meta.doc == 'This is the GetFooResponse message.' 820 assert not method.timeout 821 assert not method.retry 822 823 # Establish that the retry information on a timeout-able method also 824 # looks correct. 825 timeout_method = service.methods['TimeoutableGetFoo'] 826 assert timeout_method.timeout == pytest.approx(30.0) 827 assert not timeout_method.retry 828 829 # Establish that the retry information on the retryable method also 830 # looks correct. 831 retry_method = service.methods['RetryableGetFoo'] 832 assert retry_method.timeout is None 833 assert retry_method.retry.max_attempts == 3 834 assert retry_method.retry.initial_backoff == pytest.approx(0.001) 835 assert retry_method.retry.backoff_multiplier == pytest.approx(1.5) 836 assert retry_method.retry.max_backoff == pytest.approx(60.0) 837 assert retry_method.retry.retryable_exceptions == { 838 exceptions.ServiceUnavailable, exceptions.Aborted, 839 } 840 841 842def test_prior_protos(): 843 L = descriptor_pb2.SourceCodeInfo.Location 844 845 # Set up a prior proto that mimics google/protobuf/empty.proto 846 empty_proto = api.Proto.build(make_file_pb2( 847 name='empty.proto', package='google.protobuf', 848 messages=(make_message_pb2(name='Empty'),), 849 ), file_to_generate=False, naming=make_naming()) 850 851 # Set up the service with an RPC. 852 service_pb = descriptor_pb2.ServiceDescriptorProto( 853 name='PingService', 854 method=(descriptor_pb2.MethodDescriptorProto( 855 name='Ping', 856 input_type='google.protobuf.Empty', 857 output_type='google.protobuf.Empty', 858 ),), 859 ) 860 861 # Fake-document our fake stuff. 862 locations = ( 863 L(path=(6, 0), leading_comments='This is the PingService service.'), 864 L(path=(6, 0, 2, 0), leading_comments='This is the Ping method.'), 865 ) 866 867 # Finally, set up the file that encompasses these. 868 fdp = make_file_pb2( 869 package='google.example.v1', 870 services=(service_pb,), 871 locations=locations, 872 ) 873 874 # Make the proto object. 875 proto = api.Proto.build(fdp, file_to_generate=True, prior_protos={ 876 'google/protobuf/empty.proto': empty_proto, 877 }, naming=make_naming()) 878 879 # Establish that our data looks correct. 880 assert len(proto.services) == 1 881 assert len(empty_proto.messages) == 1 882 assert len(proto.messages) == 0 883 service = proto.services['google.example.v1.PingService'] 884 assert service.meta.doc == 'This is the PingService service.' 885 assert len(service.methods) == 1 886 method = service.methods['Ping'] 887 assert isinstance(method.input, wrappers.MessageType) 888 assert isinstance(method.output, wrappers.MessageType) 889 assert method.input.name == 'Empty' 890 assert method.output.name == 'Empty' 891 assert method.meta.doc == 'This is the Ping method.' 892 893 894def test_lro(): 895 # Set up a prior proto that mimics google/protobuf/empty.proto 896 lro_proto = api.Proto.build(make_file_pb2( 897 name='operations.proto', package='google.longrunning', 898 messages=(make_message_pb2(name='Operation'),), 899 ), file_to_generate=False, naming=make_naming()) 900 901 # Set up a method with LRO annotations. 902 method_pb2 = descriptor_pb2.MethodDescriptorProto( 903 name='AsyncDoThing', 904 input_type='google.example.v3.AsyncDoThingRequest', 905 output_type='google.longrunning.Operation', 906 ) 907 method_pb2.options.Extensions[operations_pb2.operation_info].MergeFrom( 908 operations_pb2.OperationInfo( 909 response_type='google.example.v3.AsyncDoThingResponse', 910 metadata_type='google.example.v3.AsyncDoThingMetadata', 911 ), 912 ) 913 914 # Set up the service with an RPC. 915 service_pb = descriptor_pb2.ServiceDescriptorProto( 916 name='LongRunningService', 917 method=(method_pb2,), 918 ) 919 920 # Set up the messages, including the annotated ones. 921 messages = ( 922 make_message_pb2(name='AsyncDoThingRequest', fields=()), 923 make_message_pb2(name='AsyncDoThingResponse', fields=()), 924 make_message_pb2(name='AsyncDoThingMetadata', fields=()), 925 ) 926 927 # Finally, set up the file that encompasses these. 928 fdp = make_file_pb2( 929 package='google.example.v3', 930 messages=messages, 931 services=(service_pb,), 932 ) 933 934 # Make the proto object. 935 proto = api.Proto.build(fdp, file_to_generate=True, prior_protos={ 936 'google/longrunning/operations.proto': lro_proto, 937 }, naming=make_naming()) 938 939 # Establish that our data looks correct. 940 assert len(proto.services) == 1 941 assert len(proto.messages) == 3 942 assert len(lro_proto.messages) == 1 943 944 945def test_lro_missing_annotation(): 946 # Set up a prior proto that mimics google/protobuf/empty.proto 947 lro_proto = api.Proto.build(make_file_pb2( 948 name='operations.proto', package='google.longrunning', 949 messages=(make_message_pb2(name='Operation'),), 950 ), file_to_generate=False, naming=make_naming()) 951 952 # Set up a method with an LRO but no annotation. 953 method_pb2 = descriptor_pb2.MethodDescriptorProto( 954 name='AsyncDoThing', 955 input_type='google.example.v3.AsyncDoThingRequest', 956 output_type='google.longrunning.Operation', 957 ) 958 959 # Set up the service with an RPC. 960 service_pb = descriptor_pb2.ServiceDescriptorProto( 961 name='LongRunningService', 962 method=(method_pb2,), 963 ) 964 965 # Set up the messages, including the annotated ones. 966 messages = ( 967 make_message_pb2(name='AsyncDoThingRequest', fields=()), 968 ) 969 970 # Finally, set up the file that encompasses these. 971 fdp = make_file_pb2( 972 package='google.example.v3', 973 messages=messages, 974 services=(service_pb,), 975 ) 976 977 # Make the proto object. 978 with pytest.raises(TypeError): 979 api.Proto.build(fdp, file_to_generate=True, prior_protos={ 980 'google/longrunning/operations.proto': lro_proto, 981 }, naming=make_naming()) 982 983 984def test_cross_file_lro(): 985 # Protobuf annotations for longrunning operations use strings to name types. 986 # As far as the protobuf compiler is concerned they don't reference the 987 # _types_ at all, so the corresponding proto file that owns the types 988 # does not need to be imported. 989 # This creates a potential issue when building rich structures around 990 # LRO returning methods. This test is intended to verify that the issue 991 # is handled correctly. 992 993 # Set up a prior proto that mimics google/protobuf/empty.proto 994 lro_proto = api.Proto.build(make_file_pb2( 995 name='operations.proto', package='google.longrunning', 996 messages=(make_message_pb2(name='Operation'),), 997 ), file_to_generate=False, naming=make_naming()) 998 999 # Set up a method with LRO annotations. 1000 method_pb2 = descriptor_pb2.MethodDescriptorProto( 1001 name='AsyncDoThing', 1002 input_type='google.example.v3.AsyncDoThingRequest', 1003 output_type='google.longrunning.Operation', 1004 ) 1005 method_pb2.options.Extensions[operations_pb2.operation_info].MergeFrom( 1006 operations_pb2.OperationInfo( 1007 response_type='google.example.v3.AsyncDoThingResponse', 1008 metadata_type='google.example.v3.AsyncDoThingMetadata', 1009 ), 1010 ) 1011 1012 # Set up the service with an RPC. 1013 service_file = make_file_pb2( 1014 name='service_file.proto', 1015 package='google.example.v3', 1016 messages=( 1017 make_message_pb2(name='AsyncDoThingRequest', fields=()), 1018 ), 1019 services=( 1020 descriptor_pb2.ServiceDescriptorProto( 1021 name='LongRunningService', 1022 method=(method_pb2,), 1023 ), 1024 ) 1025 ) 1026 1027 # Set up the messages, including the annotated ones. 1028 # This file is distinct and is not explicitly imported 1029 # into the file that defines the service. 1030 messages_file = make_file_pb2( 1031 name='messages_file.proto', 1032 package='google.example.v3', 1033 messages=( 1034 make_message_pb2(name='AsyncDoThingResponse', fields=()), 1035 make_message_pb2(name='AsyncDoThingMetadata', fields=()), 1036 ), 1037 ) 1038 1039 api_schema = api.API.build( 1040 file_descriptors=( 1041 service_file, 1042 messages_file, 1043 ), 1044 package='google.example.v3', 1045 prior_protos={'google/longrunning/operations.proto': lro_proto, }, 1046 ) 1047 1048 method = ( 1049 api_schema. 1050 all_protos['service_file.proto']. 1051 services['google.example.v3.LongRunningService']. 1052 methods['AsyncDoThing'] 1053 ) 1054 1055 assert method.lro 1056 assert method.lro.response_type.name == 'AsyncDoThingResponse' 1057 assert method.lro.metadata_type.name == 'AsyncDoThingMetadata' 1058 1059 1060def test_enums(): 1061 L = descriptor_pb2.SourceCodeInfo.Location 1062 enum_pb = descriptor_pb2.EnumDescriptorProto(name='Silly', value=( 1063 descriptor_pb2.EnumValueDescriptorProto(name='ZERO', number=0), 1064 descriptor_pb2.EnumValueDescriptorProto(name='ONE', number=1), 1065 descriptor_pb2.EnumValueDescriptorProto(name='THREE', number=3), 1066 )) 1067 fdp = make_file_pb2(package='google.enum.v1', enums=(enum_pb,), locations=( 1068 L(path=(5, 0), leading_comments='This is the Silly enum.'), 1069 L(path=(5, 0, 2, 0), leading_comments='This is the zero value.'), 1070 L(path=(5, 0, 2, 1), leading_comments='This is the one value.'), 1071 )) 1072 proto = api.Proto.build(fdp, file_to_generate=True, naming=make_naming()) 1073 assert len(proto.enums) == 1 1074 enum = proto.enums['google.enum.v1.Silly'] 1075 assert enum.meta.doc == 'This is the Silly enum.' 1076 assert isinstance(enum, wrappers.EnumType) 1077 assert len(enum.values) == 3 1078 assert all([isinstance(i, wrappers.EnumValueType) for i in enum.values]) 1079 assert enum.values[0].name == 'ZERO' 1080 assert enum.values[0].meta.doc == 'This is the zero value.' 1081 assert enum.values[1].name == 'ONE' 1082 assert enum.values[1].meta.doc == 'This is the one value.' 1083 assert enum.values[2].name == 'THREE' 1084 assert enum.values[2].meta.doc == '' 1085 1086 1087def test_file_level_resources(): 1088 fdp = make_file_pb2( 1089 name="nomenclature.proto", 1090 package="nomenclature.linneaen.v1", 1091 messages=( 1092 make_message_pb2( 1093 name="CreateSpeciesRequest", 1094 fields=( 1095 make_field_pb2(name='species', number=1, type=9), 1096 ), 1097 ), 1098 make_message_pb2( 1099 name="CreateSpeciesResponse", 1100 ), 1101 ), 1102 services=( 1103 descriptor_pb2.ServiceDescriptorProto( 1104 name="SpeciesService", 1105 method=( 1106 descriptor_pb2.MethodDescriptorProto( 1107 name="CreateSpecies", 1108 input_type="nomenclature.linneaen.v1.CreateSpeciesRequest", 1109 output_type="nomenclature.linneaen.v1.CreateSpeciesResponse", 1110 ), 1111 ), 1112 ), 1113 ), 1114 ) 1115 res_pb2 = fdp.options.Extensions[resource_pb2.resource_definition] 1116 definitions = [ 1117 ("nomenclature.linnaen.com/Species", 1118 "families/{family}/genera/{genus}/species/{species}"), 1119 ("nomenclature.linnaen.com/Phylum", 1120 "kingdoms/{kingdom}/phyla/{phylum}"), 1121 ] 1122 for type_, pattern in definitions: 1123 resource_definition = res_pb2.add() 1124 resource_definition.type = type_ 1125 resource_definition.pattern.append(pattern) 1126 1127 species_field = fdp.message_type[0].field[0] 1128 resource_reference = species_field.options.Extensions[resource_pb2.resource_reference] 1129 resource_reference.type = "nomenclature.linnaen.com/Species" 1130 1131 api_schema = api.API.build([fdp], package='nomenclature.linneaen.v1') 1132 actual = api_schema.protos['nomenclature.proto'].resource_messages 1133 expected = collections.OrderedDict(( 1134 ("nomenclature.linnaen.com/Species", 1135 wrappers.CommonResource( 1136 type_name="nomenclature.linnaen.com/Species", 1137 pattern="families/{family}/genera/{genus}/species/{species}" 1138 ).message_type), 1139 ("nomenclature.linnaen.com/Phylum", 1140 wrappers.CommonResource( 1141 type_name="nomenclature.linnaen.com/Phylum", 1142 pattern="kingdoms/{kingdom}/phyla/{phylum}" 1143 ).message_type), 1144 )) 1145 1146 assert actual == expected 1147 1148 # The proto file _owns_ the file level resources, but the service needs to 1149 # see them too because the client class owns all the helper methods. 1150 service = api_schema.services["nomenclature.linneaen.v1.SpeciesService"] 1151 actual = service.visible_resources 1152 assert actual == expected 1153 1154 # The service doesn't own any method that owns a message that references 1155 # Phylum, so the service doesn't count it among its resource messages. 1156 expected.pop("nomenclature.linnaen.com/Phylum") 1157 expected = frozenset(expected.values()) 1158 actual = service.resource_messages 1159 1160 assert actual == expected 1161 1162 1163def test_resources_referenced_but_not_typed(reference_attr="type"): 1164 fdp = make_file_pb2( 1165 name="nomenclature.proto", 1166 package="nomenclature.linneaen.v1", 1167 messages=( 1168 make_message_pb2( 1169 name="Species", 1170 ), 1171 make_message_pb2( 1172 name="CreateSpeciesRequest", 1173 fields=( 1174 make_field_pb2(name='species', number=1, type=9), 1175 ), 1176 ), 1177 make_message_pb2( 1178 name="CreateSpeciesResponse", 1179 ), 1180 ), 1181 services=( 1182 descriptor_pb2.ServiceDescriptorProto( 1183 name="SpeciesService", 1184 method=( 1185 descriptor_pb2.MethodDescriptorProto( 1186 name="CreateSpecies", 1187 input_type="nomenclature.linneaen.v1.CreateSpeciesRequest", 1188 output_type="nomenclature.linneaen.v1.CreateSpeciesResponse", 1189 ), 1190 ), 1191 ), 1192 ), 1193 ) 1194 1195 # Set up the resource 1196 species_resource_opts = fdp.message_type[0].options.Extensions[resource_pb2.resource] 1197 species_resource_opts.type = "nomenclature.linnaen.com/Species" 1198 species_resource_opts.pattern.append( 1199 "families/{family}/genera/{genus}/species/{species}") 1200 1201 # Set up the reference 1202 name_resource_opts = fdp.message_type[1].field[0].options.Extensions[resource_pb2.resource_reference] 1203 if reference_attr == "type": 1204 name_resource_opts.type = species_resource_opts.type 1205 else: 1206 name_resource_opts.child_type = species_resource_opts.type 1207 1208 api_schema = api.API.build([fdp], package="nomenclature.linneaen.v1") 1209 expected = {api_schema.messages["nomenclature.linneaen.v1.Species"]} 1210 actual = api_schema.services["nomenclature.linneaen.v1.SpeciesService"].resource_messages 1211 1212 assert actual == expected 1213 1214 1215def test_resources_referenced_but_not_typed_child_type(): 1216 test_resources_referenced_but_not_typed("child_type") 1217 1218 1219def test_map_field_name_disambiguation(): 1220 squid_file_pb = descriptor_pb2.FileDescriptorProto( 1221 name="mollusc.proto", 1222 package="animalia.mollusca.v2", 1223 message_type=( 1224 descriptor_pb2.DescriptorProto( 1225 name="Mollusc", 1226 ), 1227 ), 1228 ) 1229 method_types_file_pb = descriptor_pb2.FileDescriptorProto( 1230 name="mollusc_service.proto", 1231 package="animalia.mollusca.v2", 1232 message_type=( 1233 descriptor_pb2.DescriptorProto( 1234 name="CreateMolluscRequest", 1235 field=( 1236 descriptor_pb2.FieldDescriptorProto( 1237 name="mollusc", 1238 type="TYPE_MESSAGE", 1239 type_name=".animalia.mollusca.v2.Mollusc", 1240 number=1, 1241 ), 1242 descriptor_pb2.FieldDescriptorProto( 1243 name="molluscs_map", 1244 type="TYPE_MESSAGE", 1245 number=2, 1246 type_name=".animalia.mollusca.v2.CreateMolluscRequest.MolluscsMapEntry", 1247 label="LABEL_REPEATED", 1248 ), 1249 ), 1250 nested_type=( 1251 descriptor_pb2.DescriptorProto( 1252 name="MolluscsMapEntry", 1253 field=( 1254 descriptor_pb2.FieldDescriptorProto( 1255 name="key", 1256 type="TYPE_STRING", 1257 number=1, 1258 ), 1259 descriptor_pb2.FieldDescriptorProto( 1260 name="value", 1261 type="TYPE_MESSAGE", 1262 number=2, 1263 # We use the same type for the map value as for 1264 # the singleton above to better highlight the 1265 # problem raised in 1266 # https://github.com/googleapis/gapic-generator-python/issues/618. 1267 # The module _is_ disambiguated for singleton 1268 # fields but NOT for map fields. 1269 type_name=".animalia.mollusca.v2.Mollusc" 1270 ), 1271 ), 1272 options=descriptor_pb2.MessageOptions(map_entry=True), 1273 ), 1274 ), 1275 ), 1276 ), 1277 ) 1278 my_api = api.API.build( 1279 file_descriptors=[squid_file_pb, method_types_file_pb], 1280 ) 1281 create = my_api.messages['animalia.mollusca.v2.CreateMolluscRequest'] 1282 mollusc = create.fields['mollusc'] 1283 molluscs_map = create.fields['molluscs_map'] 1284 mollusc_ident = str(mollusc.type.ident) 1285 mollusc_map_ident = str(molluscs_map.message.fields['value'].type.ident) 1286 1287 # The same module used in the same place should have the same import alias. 1288 # Because there's a "mollusc" name used, the import should be disambiguated. 1289 assert mollusc_ident == mollusc_map_ident == "am_mollusc.Mollusc" 1290