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