From 816f965c8560bf65d8043bd67672c660a2b1300b Mon Sep 17 00:00:00 2001 From: Dov Shlachter Date: Fri, 16 Oct 2020 14:21:32 -0700 Subject: [PATCH] fix: minor typo in ads template (#664) CircleCI machinery now invokes the alternative (Ads) templates for the showcase_alternative_templates_* tests. Includes numerous fixes and additions to the Ads grpc transport, client class, and unit test templates. The showcase system tests now selectively enable async tests via an environment variable. The async client code has not yet been added to the Ads templates, and the corresponding system tests have been disabled for alternative templates. --- .../services/%service/transports/base.py.j2 | 10 +- .../services/%service/transports/grpc.py.j2 | 82 ++++-- gapic/ads-templates/noxfile.py.j2 | 2 +- gapic/ads-templates/setup.py.j2 | 2 +- .../%name_%version/%sub/test_%service.py.j2 | 102 ++++++- .../%sub/services/%service/async_client.py.j2 | 10 + .../services/%service/transports/grpc.py.j2 | 20 +- gapic/templates/setup.py.j2 | 2 +- .../%name_%version/%sub/test_%service.py.j2 | 88 +++++- noxfile.py | 48 +++- tests/system/conftest.py | 69 ++--- tests/system/test_grpc_lro.py | 30 ++- tests/system/test_grpc_streams.py | 250 +++++++++--------- tests/system/test_grpc_unary.py | 54 ++-- tests/system/test_pagination.py | 74 +++--- tests/system/test_resource_crud.py | 82 +++--- tests/system/test_retry.py | 22 +- 17 files changed, 590 insertions(+), 357 deletions(-) diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2 index 2053e9fe4..f25ba96a1 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/base.py.j2 @@ -55,10 +55,10 @@ class {{ service.name }}Transport(metaclass=abc.ABCMeta): credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): - The client info used to send a user-agent string along with - API requests. If ``None``, then default info will be used. - Generally, you only need to set this if you're developing + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing your own client library. """ # Save the hostname. Default to port 443 (HTTPS) if none is specified. @@ -89,7 +89,7 @@ class {{ service.name }}Transport(metaclass=abc.ABCMeta): {% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %} {% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %} predicate=retries.if_exception_type( - {%- for ex in method.retry.retryable_exceptions|sort(attribute='__name__) %} + {%- for ex in method.retry.retryable_exceptions|sort(attribute='__name__') %} exceptions.{{ ex.__name__ }}, {%- endfor %} ), diff --git a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2 b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2 index 7fbf53e78..699554937 100644 --- a/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2 +++ b/gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/transports/grpc.py.j2 @@ -1,7 +1,8 @@ {% extends '_base.py.j2' %} {% block content %} -from typing import Callable, Dict, Tuple +import warnings +from typing import Callable, Dict, Optional, Sequence, Tuple from google.api_core import grpc_helpers # type: ignore {%- if service.has_lro %} @@ -10,7 +11,7 @@ from google.api_core import operations_v1 # type: ignore from google.api_core import gapic_v1 # type: ignore from google import auth # type: ignore from google.auth import credentials # type: ignore - +from google.auth.transport.grpc import SslCredentials # type: ignore import grpc # type: ignore @@ -38,8 +39,13 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): def __init__(self, *, host: str{% if service.host %} = '{{ service.host }}'{% endif %}, credentials: credentials.Credentials = None, + credentials_file: str = None, + scopes: Sequence[str] = None, channel: grpc.Channel = None, + api_mtls_endpoint: str = None, + client_cert_source: Callable[[], Tuple[bytes, bytes]] = None, ssl_channel_credentials: grpc.ChannelCredentials = None, + quota_project_id: Optional[str] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, ) -> None: """Instantiate the transport. @@ -53,14 +59,29 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): are specified, the client will attempt to ascertain the credentials from the environment. This argument is ignored if ``channel`` is provided. + credentials_file (Optional[str]): A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. + scopes (Optional(Sequence[str])): A list of scopes. This argument is + ignored if ``channel`` is provided. channel (Optional[grpc.Channel]): A ``Channel`` instance through which to make calls. + api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint. + If provided, it overrides the ``host`` argument and tries to create + a mutual TLS channel with client SSL credentials from + ``client_cert_source`` or applicatin default SSL credentials. + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + Deprecated. A callback to provide client SSL certificate bytes and + private key bytes, both in PEM format. It is ignored if + ``api_mtls_endpoint`` is None. ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials for grpc channel. It is ignored if ``channel`` is provided. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): - The client info used to send a user-agent string along with - API requests. If ``None``, then default info will be used. - Generally, you only need to set this if you're developing + quota_project_id (Optional[str]): An optional project to use for billing + and quota. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing your own client library. Raises: @@ -74,6 +95,33 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): # If a channel was explicitly provided, set it. self._grpc_channel = channel + elif api_mtls_endpoint: + warnings.warn("api_mtls_endpoint and client_cert_source are deprecated", DeprecationWarning) + + host = api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443" + + if credentials is None: + credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id) + + # Create SSL credentials with client_cert_source or application + # default SSL credentials. + if client_cert_source: + cert, key = client_cert_source() + ssl_credentials = grpc.ssl_channel_credentials( + certificate_chain=cert, private_key=key + ) + else: + ssl_credentials = SslCredentials().ssl_credentials + + # create a new channel. The provided one is ignored. + self._grpc_channel = type(self).create_channel( + host, + credentials=credentials, + credentials_file=credentials_file, + ssl_credentials=ssl_credentials, + scopes=scopes or self.AUTH_SCOPES, + quota_project_id=quota_project_id, + ) else: host = host if ":" in host else host + ":443" @@ -81,7 +129,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): credentials, _ = auth.default(scopes=self.AUTH_SCOPES) # create a new channel. The provided one is ignored. - self._grpc_channel = grpc_helpers.create_channel( + self._grpc_channel = type(self).create_channel( host, credentials=credentials, ssl_credentials=ssl_channel_credentials, @@ -102,6 +150,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): def create_channel(cls, host: str{% if service.host %} = '{{ service.host }}'{% endif %}, credentials: credentials.Credentials = None, + scopes: Optional[Sequence[str]] = None, **kwargs) -> grpc.Channel: """Create and return a gRPC channel object. Args: @@ -111,6 +160,9 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): credentials identify this application to the service. If none are specified, the client will attempt to ascertain the credentials from the environment. + scopes (Optional[Sequence[str]]): A optional list of scopes needed for this + service. These are only used when credentials are not specified and + are passed to :func:`google.auth.default`. kwargs (Optional[dict]): Keyword arguments, which are passed to the channel creation. Returns: @@ -119,26 +171,14 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): return grpc_helpers.create_channel( host, credentials=credentials, - scopes=cls.AUTH_SCOPES, + scopes=scopes or cls.AUTH_SCOPES, **kwargs ) @property def grpc_channel(self) -> grpc.Channel: - """Create the channel designed to connect to this service. - - This property caches on the instance; repeated calls return - the same channel. + """Return the channel designed to connect to this service. """ - # Sanity check: Only create a new channel if we do not already - # have one. - if not hasattr(self, '_grpc_channel'): - self._grpc_channel = self.create_channel( - self._host, - credentials=self._credentials, - ) - - # Return the channel from cache. return self._grpc_channel {%- if service.has_lro %} diff --git a/gapic/ads-templates/noxfile.py.j2 b/gapic/ads-templates/noxfile.py.j2 index 71f99a414..4760bc548 100644 --- a/gapic/ads-templates/noxfile.py.j2 +++ b/gapic/ads-templates/noxfile.py.j2 @@ -20,7 +20,7 @@ def unit(session): '--cov-config=.coveragerc', '--cov-report=term', '--cov-report=html', - os.path.join('tests', 'unit', '{{ api.naming.versioned_module_name }}'), + os.path.join('tests', 'unit', 'gapic', '{{ api.naming.versioned_module_name }}'), ) diff --git a/gapic/ads-templates/setup.py.j2 b/gapic/ads-templates/setup.py.j2 index 6587ebd21..92ae4ea7a 100644 --- a/gapic/ads-templates/setup.py.j2 +++ b/gapic/ads-templates/setup.py.j2 @@ -24,7 +24,7 @@ setuptools.setup( 'grpc-google-iam-v1', {%- endif %} ), - python_requires='>={% if opts.lazy_import %}3.7{% else %}3.6{% endif %}',{# Lazy import requires module-level getattr #} + python_requires='>=3.7',{# Lazy import requires module-level getattr #} setup_requires=[ 'libcst >= 0.2.5', ], diff --git a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 55768e7cd..810a6f2e9 100644 --- a/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/ads-templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -144,7 +144,7 @@ def test_{{ service.client_name|snake_case }}_client_options(): # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"}): with pytest.raises(ValueError): - client = client_class() + client = {{ service.client_name }}() @mock.patch.object({{ service.client_name }}, "DEFAULT_ENDPOINT", modify_default_endpoint({{ service.client_name }})) @@ -222,7 +222,7 @@ def test_{{ service.client_name|snake_case }}_mtls_env_auto(use_client_cert_env) def test_{{ service.client_name|snake_case }}_client_options_from_dict(): - with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport.__init__') as grpc_transport: + with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}GrpcTransport.__init__') as grpc_transport: grpc_transport.return_value = None client = {{ service.client_name }}( client_options={'api_endpoint': 'squid.clam.whelk'} @@ -583,6 +583,15 @@ def test_transport_instance(): assert client.transport is transport +def test_transport_get_channel(): + # A client may be instantiated with a custom transport instance. + transport = transports.{{ service.name }}GrpcTransport( + credentials=credentials.AnonymousCredentials(), + ) + channel = transport.grpc_channel + assert channel + + def test_transport_grpc_default(): # A client should use the gRPC transport by default. client = {{ service.client_name }}( @@ -593,18 +602,20 @@ def test_transport_grpc_default(): transports.{{ service.name }}GrpcTransport, ) - -def test_transport_adc(): +@pytest.mark.parametrize("transport_class", [ + transports.{{ service.grpc_transport_name }}, +]) +def test_transport_adc(transport_class): # Test default credentials are used if not provided. with mock.patch.object(auth, 'default') as adc: adc.return_value = (credentials.AnonymousCredentials(), None) - transports.{{ service.name }}Transport() + transport_class() adc.assert_called_once() def test_{{ service.name|snake_case }}_base_transport(): # Instantiate the base transport. - with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}GrpcTransport.__init__') as Transport: + with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport.__init__') as Transport: Transport.return_value = None transport = transports.{{ service.name }}Transport( credentials=credentials.AnonymousCredentials(), @@ -695,6 +706,85 @@ def test_{{ service.name|snake_case }}_grpc_transport_channel(): assert transport._host == "squid.clam.whelk:443" +@pytest.mark.parametrize("transport_class", [transports.{{ service.grpc_transport_name }}]) +def test_{{ service.name|snake_case }}_transport_channel_mtls_with_client_cert_source( + transport_class +): + with mock.patch("grpc.ssl_channel_credentials", autospec=True) as grpc_ssl_channel_cred: + with mock.patch.object(transport_class, "create_channel", autospec=True) as grpc_create_channel: + mock_ssl_cred = mock.Mock() + grpc_ssl_channel_cred.return_value = mock_ssl_cred + + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + + cred = credentials.AnonymousCredentials() + with pytest.warns(DeprecationWarning): + with mock.patch.object(auth, 'default') as adc: + adc.return_value = (cred, None) + transport = transport_class( + host="squid.clam.whelk", + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=client_cert_source_callback, + ) + adc.assert_called_once() + + grpc_ssl_channel_cred.assert_called_once_with( + certificate_chain=b"cert bytes", private_key=b"key bytes" + ) + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=cred, + credentials_file=None, + scopes=( + {%- for scope in service.oauth_scopes %} + '{{ scope }}', + {%- endfor %} + ), + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + ) + assert transport.grpc_channel == mock_grpc_channel + + +@pytest.mark.parametrize("transport_class", [transports.{{ service.grpc_transport_name }},]) +def test_{{ service.name|snake_case }}_transport_channel_mtls_with_adc( + transport_class +): + mock_ssl_cred = mock.Mock() + with mock.patch.multiple( + "google.auth.transport.grpc.SslCredentials", + __init__=mock.Mock(return_value=None), + ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred), + ): + with mock.patch.object(transport_class, "create_channel", autospec=True) as grpc_create_channel: + mock_grpc_channel = mock.Mock() + grpc_create_channel.return_value = mock_grpc_channel + mock_cred = mock.Mock() + + with pytest.warns(DeprecationWarning): + transport = transport_class( + host="squid.clam.whelk", + credentials=mock_cred, + api_mtls_endpoint="mtls.squid.clam.whelk", + client_cert_source=None, + ) + + grpc_create_channel.assert_called_once_with( + "mtls.squid.clam.whelk:443", + credentials=mock_cred, + credentials_file=None, + scopes=( + {%- for scope in service.oauth_scopes %} + '{{ scope }}', + {%- endfor %} + ), + ssl_credentials=mock_ssl_cred, + quota_project_id=None, + ) + assert transport.grpc_channel == mock_grpc_channel + + {% if service.has_lro -%} def test_{{ service.name|snake_case }}_grpc_lro_client(): client = {{ service.client_name }}( diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 index 275c7c9e8..0f2e88700 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 @@ -50,6 +50,16 @@ class {{ service.async_client_name }}: from_service_account_file = {{ service.client_name }}.from_service_account_file from_service_account_json = from_service_account_file + @property + def transport(self) -> {{ service.name }}Transport: + """Return the transport used by the client instance. + + Returns: + {{ service.name }}Transport: The transport used by the client instance. + """ + return self._client.transport + + get_transport_class = functools.partial(type({{ service.client_name }}).get_transport_class, type({{ service.client_name }})) def __init__(self, *, diff --git a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index 4283b12ad..47eaffeb1 100644 --- a/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -76,7 +76,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): If provided, it overrides the ``host`` argument and tries to create a mutual TLS channel with client SSL credentials from ``client_cert_source`` or applicatin default SSL credentials. - client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): Deprecated. A callback to provide client SSL certificate bytes and private key bytes, both in PEM format. It is ignored if ``api_mtls_endpoint`` is None. @@ -84,10 +84,10 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): for grpc channel. It is ignored if ``channel`` is provided. quota_project_id (Optional[str]): An optional project to use for billing and quota. - client_info (google.api_core.gapic_v1.client_info.ClientInfo): - The client info used to send a user-agent string along with - API requests. If ``None``, then default info will be used. - Generally, you only need to set this if you're developing + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing your own client library. Raises: @@ -105,7 +105,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): self._grpc_channel = channel elif api_mtls_endpoint: warnings.warn("api_mtls_endpoint and client_cert_source are deprecated", DeprecationWarning) - + host = api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443" if credentials is None: @@ -203,12 +203,8 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): @property def grpc_channel(self) -> grpc.Channel: - """Create the channel designed to connect to this service. - - This property caches on the instance; repeated calls return - the same channel. + """Return the channel designed to connect to this service. """ - # Return the channel from cache. return self._grpc_channel {%- if service.has_lro %} @@ -339,7 +335,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): response_deserializer=iam_policy.TestIamPermissionsResponse.FromString, ) return self._stubs["test_iam_permissions"] - {% endif %} + {% endif %} __all__ = ( '{{ service.name }}GrpcTransport', diff --git a/gapic/templates/setup.py.j2 b/gapic/templates/setup.py.j2 index e2d1ad659..fdfbf7a63 100644 --- a/gapic/templates/setup.py.j2 +++ b/gapic/templates/setup.py.j2 @@ -23,7 +23,7 @@ setuptools.setup( 'grpc-google-iam-v1', {%- endif %} ), - python_requires='>={% if opts.lazy_import %}3.7{% else %}3.6{% endif %}',{# Lazy import requires module-level getattr #} + python_requires='>=3.6', scripts=[ 'scripts/fixup_{{ api.naming.versioned_module_name }}_keywords.py', ], diff --git a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index f9365635d..359b548a0 100644 --- a/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -432,7 +432,7 @@ async def test_{{ method.name|snake_case }}_async(transport: str = 'grpc_asyncio # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.{{ method.name|snake_case }}), + type(client.transport.{{ method.name|snake_case }}), '__call__') as call: # Designate an appropriate return value for the call. {% if method.void -%} @@ -561,7 +561,7 @@ async def test_{{ method.name|snake_case }}_field_headers_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.{{ method.name|snake_case }}), + type(client.transport.{{ method.name|snake_case }}), '__call__') as call: {% if method.void -%} call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(None) @@ -693,7 +693,7 @@ async def test_{{ method.name|snake_case }}_flattened_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.{{ method.name|snake_case }}), + type(client.transport.{{ method.name|snake_case }}), '__call__') as call: # Designate an appropriate return value for the call. {% if method.void -%} @@ -880,7 +880,7 @@ async def test_{{ method.name|snake_case }}_async_pager(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.{{ method.name|snake_case }}), + type(client.transport.{{ method.name|snake_case }}), '__call__', new_callable=mock.AsyncMock) as call: # Set the response to a series of pages. call.side_effect = ( @@ -928,7 +928,7 @@ async def test_{{ method.name|snake_case }}_async_pages(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.{{ method.name|snake_case }}), + type(client.transport.{{ method.name|snake_case }}), '__call__', new_callable=mock.AsyncMock) as call: # Set the response to a series of pages. call.side_effect = ( @@ -1291,7 +1291,7 @@ def test_{{ service.name|snake_case }}_grpc_lro_async_client(): credentials=credentials.AnonymousCredentials(), transport='grpc_asyncio', ) - transport = client._client.transport + transport = client.transport # Ensure that we have a api-core operations client. assert isinstance( @@ -1416,7 +1416,7 @@ async def test_set_iam_policy_async(transport: str = "grpc_asyncio"): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.set_iam_policy), "__call__" + type(client.transport.set_iam_policy), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( @@ -1478,7 +1478,7 @@ async def test_set_iam_policy_field_headers_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.set_iam_policy), "__call__" + type(client.transport.set_iam_policy), "__call__" ) as call: call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(policy.Policy()) @@ -1512,6 +1512,27 @@ def test_set_iam_policy_from_dict(): call.assert_called() +@pytest.mark.asyncio +async def test_set_iam_policy_from_dict_async(): + client = {{ service.async_client_name }}( + credentials=credentials.AnonymousCredentials(), + ) + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.set_iam_policy), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + policy.Policy() + ) + + response = await client.set_iam_policy( + request={ + "resource": "resource_value", + "policy": policy.Policy(version=774), + } + ) + call.assert_called() + + def test_get_iam_policy(transport: str = "grpc"): client = {{ service.client_name }}( credentials=credentials.AnonymousCredentials(), transport=transport, @@ -1554,7 +1575,7 @@ async def test_get_iam_policy_async(transport: str = "grpc_asyncio"): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.get_iam_policy), "__call__" + type(client.transport.get_iam_policy), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( @@ -1616,7 +1637,7 @@ async def test_get_iam_policy_field_headers_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.get_iam_policy), "__call__" + type(client.transport.get_iam_policy), "__call__" ) as call: call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(policy.Policy()) @@ -1649,6 +1670,26 @@ def test_get_iam_policy_from_dict(): ) call.assert_called() +@pytest.mark.asyncio +async def test_get_iam_policy_from_dict_async(): + client = {{ service.async_client_name }}( + credentials=credentials.AnonymousCredentials(), + ) + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object(type(client.transport.get_iam_policy), "__call__") as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + policy.Policy() + ) + + response = await client.get_iam_policy( + request={ + "resource": "resource_value", + "options": options.GetPolicyOptions(requested_policy_version=2598), + } + ) + call.assert_called() + def test_test_iam_permissions(transport: str = "grpc"): client = {{ service.client_name }}( @@ -1694,7 +1735,7 @@ async def test_test_iam_permissions_async(transport: str = "grpc_asyncio"): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.test_iam_permissions), "__call__" + type(client.transport.test_iam_permissions), "__call__" ) as call: # Designate an appropriate return value for the call. call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( @@ -1756,7 +1797,7 @@ async def test_test_iam_permissions_field_headers_async(): # Mock the actual call within the gRPC stub, and fake the request. with mock.patch.object( - type(client._client.transport.test_iam_permissions), "__call__" + type(client.transport.test_iam_permissions), "__call__" ) as call: call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( iam_policy.TestIamPermissionsResponse() @@ -1792,6 +1833,29 @@ def test_test_iam_permissions_from_dict(): } ) call.assert_called() + +@pytest.mark.asyncio +async def test_test_iam_permissions_from_dict_async(): + client = {{ service.async_client_name }}( + credentials=credentials.AnonymousCredentials(), + ) + # Mock the actual call within the gRPC stub, and fake the request. + with mock.patch.object( + type(client.transport.test_iam_permissions), "__call__" + ) as call: + # Designate an appropriate return value for the call. + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + iam_policy.TestIamPermissionsResponse() + ) + + response = await client.test_iam_permissions( + request={ + "resource": "resource_value", + "permissions": ["permissions_value"], + } + ) + call.assert_called() + {% endif %} {% endblock %} diff --git a/noxfile.py b/noxfile.py index bcab1b7e8..ba283480e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -87,16 +87,20 @@ def showcase_library( # Write out a client library for Showcase. template_opt = f"python-gapic-templates={templates}" - opts = f"--python_gapic_opt={template_opt}" - opts += ",".join(other_opts + ("lazy-import",)) - session.run( - "protoc", - "--experimental_allow_proto3_optional", + opts = "--python_gapic_opt=" + opts += ",".join(other_opts + (f"{template_opt}",)) + cmd_tup = ( + f"protoc", + f"--experimental_allow_proto3_optional", f"--descriptor_set_in={tmp_dir}{path.sep}showcase.desc", + opts, f"--python_gapic_out={tmp_dir}", - "google/showcase/v1beta1/echo.proto", - "google/showcase/v1beta1/identity.proto", - "google/showcase/v1beta1/messaging.proto", + f"google/showcase/v1beta1/echo.proto", + f"google/showcase/v1beta1/identity.proto", + f"google/showcase/v1beta1/messaging.proto", + ) + session.run( + *cmd_tup, external=True, ) @@ -108,20 +112,27 @@ def showcase_library( @nox.session(python="3.8") def showcase( - session, templates="DEFAULT", other_opts: typing.Iterable[str] = (), + session, + templates="DEFAULT", + other_opts: typing.Iterable[str] = (), + env: typing.Optional[typing.Dict[str, str]] = None, ): """Run the Showcase test suite.""" with showcase_library(session, templates=templates, other_opts=other_opts): session.install("mock", "pytest", "pytest-asyncio") session.run( - "py.test", "--quiet", *(session.posargs or [path.join("tests", "system")]) + "py.test", "--quiet", *(session.posargs or [path.join("tests", "system")]), + env=env, ) @nox.session(python="3.8") def showcase_mtls( - session, templates="DEFAULT", other_opts: typing.Iterable[str] = (), + session, + templates="DEFAULT", + other_opts: typing.Iterable[str] = (), + env: typing.Optional[typing.Dict[str, str]] = None, ): """Run the Showcase mtls test suite.""" @@ -132,19 +143,30 @@ def showcase_mtls( "--quiet", "--mtls", *(session.posargs or [path.join("tests", "system")]), + env=env, ) @nox.session(python="3.8") def showcase_alternative_templates(session): templates = path.join(path.dirname(__file__), "gapic", "ads-templates") - showcase(session, templates=templates, other_opts=("old-naming",)) + showcase( + session, + templates=templates, + other_opts=("old-naming",), + env={"GAPIC_PYTHON_ASYNC": "False"}, + ) @nox.session(python="3.8") def showcase_mtls_alternative_templates(session): templates = path.join(path.dirname(__file__), "gapic", "ads-templates") - showcase_mtls(session, templates=templates, other_opts=("old-naming",)) + showcase_mtls( + session, + templates=templates, + other_opts=("old-naming",), + env={"GAPIC_PYTHON_ASYNC": "False"}, + ) @nox.session(python=["3.6", "3.7", "3.8"]) diff --git a/tests/system/conftest.py b/tests/system/conftest.py index db3742aa0..e01f596b2 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -13,32 +13,53 @@ # limitations under the License. import collections +import distutils +import grpc import mock import os import pytest -import asyncio import google.api_core.client_options as ClientOptions from google.auth import credentials -from google.showcase import EchoClient, EchoAsyncClient -from google.showcase import IdentityClient, IdentityAsyncClient +from google.showcase import EchoClient +from google.showcase import IdentityClient from google.showcase import MessagingClient -import grpc -from grpc.experimental import aio +if distutils.util.strtobool(os.environ.get("GAPIC_PYTHON_ASYNC", "true")): + from grpc.experimental import aio + import asyncio + from google.showcase import EchoAsyncClient + from google.showcase import IdentityAsyncClient + + @pytest.fixture + def async_echo(use_mtls, event_loop): + return construct_client( + EchoAsyncClient, + use_mtls, + transport="grpc_asyncio", + channel_creator=aio.insecure_channel + ) -_test_event_loop = asyncio.new_event_loop() + @pytest.fixture + def async_identity(use_mtls, event_loop): + return construct_client( + IdentityAsyncClient, + use_mtls, + transport="grpc_asyncio", + channel_creator=aio.insecure_channel + ) -# NOTE(lidiz) We must override the default event_loop fixture from -# pytest-asyncio. pytest fixture frees resources once there isn't any reference -# to it. So, the event loop might close before tests finishes. In the -# customized version, we don't close the event loop. + _test_event_loop = asyncio.new_event_loop() + # NOTE(lidiz) We must override the default event_loop fixture from + # pytest-asyncio. pytest fixture frees resources once there isn't any reference + # to it. So, the event loop might close before tests finishes. In the + # customized version, we don't close the event loop. -@pytest.fixture -def event_loop(): - asyncio.set_event_loop(_test_event_loop) - return asyncio.get_event_loop() + @pytest.fixture + def event_loop(): + asyncio.set_event_loop(_test_event_loop) + return asyncio.get_event_loop() dir = os.path.dirname(__file__) @@ -99,16 +120,6 @@ def echo(use_mtls): return construct_client(EchoClient, use_mtls) -@pytest.fixture -def async_echo(use_mtls, event_loop): - return construct_client( - EchoAsyncClient, - use_mtls, - transport="grpc_asyncio", - channel_creator=aio.insecure_channel - ) - - @pytest.fixture def identity(): transport = IdentityClient.get_transport_class('grpc')( @@ -117,16 +128,6 @@ def identity(): return IdentityClient(transport=transport) -@pytest.fixture -def async_identity(use_mtls, event_loop): - return construct_client( - IdentityAsyncClient, - use_mtls, - transport="grpc_asyncio", - channel_creator=aio.insecure_channel - ) - - @pytest.fixture def identity(use_mtls): return construct_client(IdentityClient, use_mtls) diff --git a/tests/system/test_grpc_lro.py b/tests/system/test_grpc_lro.py index a4578a168..6dbd3af39 100644 --- a/tests/system/test_grpc_lro.py +++ b/tests/system/test_grpc_lro.py @@ -12,10 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import distutils +import os import pytest from datetime import datetime, timedelta, timezone -from google import showcase_v1beta1 +from google import showcase def test_lro(echo): @@ -26,18 +28,20 @@ def test_lro(echo): }} ) response = future.result() - assert isinstance(response, showcase_v1beta1.WaitResponse) + assert isinstance(response, showcase.WaitResponse) assert response.content.endswith('the snails...eventually.') -@pytest.mark.asyncio -async def test_lro_async(async_echo): - future = await async_echo.wait({ - 'end_time': datetime.now(tz=timezone.utc) + timedelta(seconds=1), - 'success': { - 'content': 'The hail in Wales falls mainly on the snails...eventually.' - }} - ) - response = await future.result() - assert isinstance(response, showcase_v1beta1.WaitResponse) - assert response.content.endswith('the snails...eventually.') +if distutils.util.strtobool(os.environ.get("GAPIC_PYTHON_ASYNC", "true")): + + @pytest.mark.asyncio + async def test_lro_async(async_echo): + future = await async_echo.wait({ + 'end_time': datetime.now(tz=timezone.utc) + timedelta(seconds=1), + 'success': { + 'content': 'The hail in Wales falls mainly on the snails...eventually.' + }} + ) + response = await future.result() + assert isinstance(response, showcase.WaitResponse) + assert response.content.endswith('the snails...eventually.') diff --git a/tests/system/test_grpc_streams.py b/tests/system/test_grpc_streams.py index f77e81998..f9da07948 100644 --- a/tests/system/test_grpc_streams.py +++ b/tests/system/test_grpc_streams.py @@ -12,9 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import distutils import logging +import os import pytest -import asyncio import threading from google import showcase @@ -77,129 +78,124 @@ def test_stream_stream_passing_dict(echo): assert responses.trailing_metadata() == metadata -@pytest.mark.asyncio -async def test_async_unary_stream_reader(async_echo): - content = 'The hail in Wales falls mainly on the snails.' - call = await async_echo.expand({ - 'content': content, - }, metadata=metadata) - - # Consume the response and ensure it matches what we expect. - # with pytest.raises(exceptions.NotFound) as exc: - for ground_truth in content.split(' '): - response = await call.read() - assert response.content == ground_truth - assert ground_truth == 'snails.' - - trailing_metadata = await call.trailing_metadata() - assert trailing_metadata == metadata - - -@pytest.mark.asyncio -async def test_async_unary_stream_async_generator(async_echo): - content = 'The hail in Wales falls mainly on the snails.' - call = await async_echo.expand({ - 'content': content, - }, metadata=metadata) - - # Consume the response and ensure it matches what we expect. - # with pytest.raises(exceptions.NotFound) as exc: - tokens = iter(content.split(' ')) - async for response in call: - ground_truth = next(tokens) - assert response.content == ground_truth - assert ground_truth == 'snails.' - - trailing_metadata = await call.trailing_metadata() - assert trailing_metadata == metadata - - -@pytest.mark.asyncio -async def test_async_stream_unary_iterable(async_echo): - requests = [] - requests.append(showcase.EchoRequest(content="hello")) - requests.append(showcase.EchoRequest(content="world!")) - - call = await async_echo.collect(requests) - response = await call - assert response.content == 'hello world!' - - -@pytest.mark.asyncio -async def test_async_stream_unary_async_generator(async_echo): - - async def async_generator(): - yield showcase.EchoRequest(content="hello") - yield showcase.EchoRequest(content="world!") - - call = await async_echo.collect(async_generator()) - response = await call - assert response.content == 'hello world!' - - -@pytest.mark.asyncio -async def test_async_stream_unary_writer(async_echo): - call = await async_echo.collect() - await call.write(showcase.EchoRequest(content="hello")) - await call.write(showcase.EchoRequest(content="world!")) - await call.done_writing() - - response = await call - assert response.content == 'hello world!' - - -@pytest.mark.asyncio -async def test_async_stream_unary_passing_dict(async_echo): - requests = [{'content': 'hello'}, {'content': 'world!'}] - call = await async_echo.collect(iter(requests)) - response = await call - assert response.content == 'hello world!' - - -@pytest.mark.asyncio -async def test_async_stream_stream_reader_writier(async_echo): - call = await async_echo.chat(metadata=metadata) - await call.write(showcase.EchoRequest(content="hello")) - await call.write(showcase.EchoRequest(content="world!")) - await call.done_writing() - - contents = [ - (await call.read()).content, - (await call.read()).content - ] - assert contents == ['hello', 'world!'] - - trailing_metadata = await call.trailing_metadata() - assert trailing_metadata == metadata - - -@pytest.mark.asyncio -async def test_async_stream_stream_async_generator(async_echo): - - async def async_generator(): - yield showcase.EchoRequest(content="hello") - yield showcase.EchoRequest(content="world!") - - call = await async_echo.chat(async_generator(), metadata=metadata) - - contents = [] - async for response in call: - contents.append(response.content) - assert contents == ['hello', 'world!'] - - trailing_metadata = await call.trailing_metadata() - assert trailing_metadata == metadata - - -@pytest.mark.asyncio -async def test_async_stream_stream_passing_dict(async_echo): - requests = [{'content': 'hello'}, {'content': 'world!'}] - call = await async_echo.chat(iter(requests), metadata=metadata) - - contents = [] - async for response in call: - contents.append(response.content) - assert contents == ['hello', 'world!'] - - trailing_metadata = await call.trailing_metadata() - assert trailing_metadata == metadata +if distutils.util.strtobool(os.environ.get("GAPIC_PYTHON_ASYNC", "true")): + import asyncio + + @pytest.mark.asyncio + async def test_async_unary_stream_reader(async_echo): + content = 'The hail in Wales falls mainly on the snails.' + call = await async_echo.expand({ + 'content': content, + }, metadata=metadata) + + # Consume the response and ensure it matches what we expect. + # with pytest.raises(exceptions.NotFound) as exc: + for ground_truth in content.split(' '): + response = await call.read() + assert response.content == ground_truth + assert ground_truth == 'snails.' + + trailing_metadata = await call.trailing_metadata() + assert trailing_metadata == metadata + + @pytest.mark.asyncio + async def test_async_unary_stream_async_generator(async_echo): + content = 'The hail in Wales falls mainly on the snails.' + call = await async_echo.expand({ + 'content': content, + }, metadata=metadata) + + # Consume the response and ensure it matches what we expect. + # with pytest.raises(exceptions.NotFound) as exc: + tokens = iter(content.split(' ')) + async for response in call: + ground_truth = next(tokens) + assert response.content == ground_truth + assert ground_truth == 'snails.' + + trailing_metadata = await call.trailing_metadata() + assert trailing_metadata == metadata + + @pytest.mark.asyncio + async def test_async_stream_unary_iterable(async_echo): + requests = [] + requests.append(showcase.EchoRequest(content="hello")) + requests.append(showcase.EchoRequest(content="world!")) + + call = await async_echo.collect(requests) + response = await call + assert response.content == 'hello world!' + + @pytest.mark.asyncio + async def test_async_stream_unary_async_generator(async_echo): + + async def async_generator(): + yield showcase.EchoRequest(content="hello") + yield showcase.EchoRequest(content="world!") + + call = await async_echo.collect(async_generator()) + response = await call + assert response.content == 'hello world!' + + @pytest.mark.asyncio + async def test_async_stream_unary_writer(async_echo): + call = await async_echo.collect() + await call.write(showcase.EchoRequest(content="hello")) + await call.write(showcase.EchoRequest(content="world!")) + await call.done_writing() + + response = await call + assert response.content == 'hello world!' + + @pytest.mark.asyncio + async def test_async_stream_unary_passing_dict(async_echo): + requests = [{'content': 'hello'}, {'content': 'world!'}] + call = await async_echo.collect(iter(requests)) + response = await call + assert response.content == 'hello world!' + + @pytest.mark.asyncio + async def test_async_stream_stream_reader_writier(async_echo): + call = await async_echo.chat(metadata=metadata) + await call.write(showcase.EchoRequest(content="hello")) + await call.write(showcase.EchoRequest(content="world!")) + await call.done_writing() + + contents = [ + (await call.read()).content, + (await call.read()).content + ] + assert contents == ['hello', 'world!'] + + trailing_metadata = await call.trailing_metadata() + assert trailing_metadata == metadata + + @pytest.mark.asyncio + async def test_async_stream_stream_async_generator(async_echo): + + async def async_generator(): + yield showcase.EchoRequest(content="hello") + yield showcase.EchoRequest(content="world!") + + call = await async_echo.chat(async_generator(), metadata=metadata) + + contents = [] + async for response in call: + contents.append(response.content) + assert contents == ['hello', 'world!'] + + trailing_metadata = await call.trailing_metadata() + assert trailing_metadata == metadata + + @pytest.mark.asyncio + async def test_async_stream_stream_passing_dict(async_echo): + requests = [{'content': 'hello'}, {'content': 'world!'}] + call = await async_echo.chat(iter(requests), metadata=metadata) + + contents = [] + async for response in call: + contents.append(response.content) + assert contents == ['hello', 'world!'] + + trailing_metadata = await call.trailing_metadata() + assert trailing_metadata == metadata diff --git a/tests/system/test_grpc_unary.py b/tests/system/test_grpc_unary.py index c1694d975..f8ca2f652 100644 --- a/tests/system/test_grpc_unary.py +++ b/tests/system/test_grpc_unary.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import distutils +import os import pytest -import asyncio from google.api_core import exceptions from google.rpc import code_pb2 @@ -48,31 +49,32 @@ def test_unary_error(echo): assert exc.value.message == message -@pytest.mark.asyncio -async def test_async_unary_with_request_object(async_echo): - response = await async_echo.echo(showcase.EchoRequest( - content='The hail in Wales falls mainly on the snails.', - ), timeout=1) - assert response.content == 'The hail in Wales falls mainly on the snails.' - - -@pytest.mark.asyncio -async def test_async_unary_with_dict(async_echo): - response = await async_echo.echo({ - 'content': 'The hail in Wales falls mainly on the snails.', - }) - assert response.content == 'The hail in Wales falls mainly on the snails.' +if distutils.util.strtobool(os.environ.get("GAPIC_PYTHON_ASYNC", "true")): + import asyncio + @pytest.mark.asyncio + async def test_async_unary_with_request_object(async_echo): + response = await async_echo.echo(showcase.EchoRequest( + content='The hail in Wales falls mainly on the snails.', + ), timeout=1) + assert response.content == 'The hail in Wales falls mainly on the snails.' -@pytest.mark.asyncio -async def test_async_unary_error(async_echo): - message = 'Bad things! Bad things!' - with pytest.raises(exceptions.InvalidArgument) as exc: - await async_echo.echo({ - 'error': { - 'code': code_pb2.Code.Value('INVALID_ARGUMENT'), - 'message': message, - }, + @pytest.mark.asyncio + async def test_async_unary_with_dict(async_echo): + response = await async_echo.echo({ + 'content': 'The hail in Wales falls mainly on the snails.', }) - assert exc.value.code == 400 - assert exc.value.message == message + assert response.content == 'The hail in Wales falls mainly on the snails.' + + @pytest.mark.asyncio + async def test_async_unary_error(async_echo): + message = 'Bad things! Bad things!' + with pytest.raises(exceptions.InvalidArgument) as exc: + await async_echo.echo({ + 'error': { + 'code': code_pb2.Code.Value('INVALID_ARGUMENT'), + 'message': message, + }, + }) + assert exc.value.code == 400 + assert exc.value.message == message diff --git a/tests/system/test_pagination.py b/tests/system/test_pagination.py index 8f53a6c01..eb195ea12 100644 --- a/tests/system/test_pagination.py +++ b/tests/system/test_pagination.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import distutils +import os import pytest from google import showcase @@ -47,39 +49,39 @@ def test_pagination_pages(echo): for i in text.split(' ')] -@pytest.mark.asyncio -async def test_pagination_async(async_echo): - text = 'The hail in Wales falls mainly on the snails.' - results = [] - async for i in await async_echo.paged_expand({ - 'content': text, - 'page_size': 3, - }): - results.append(i) - - assert len(results) == 9 - assert results == [showcase.EchoResponse(content=i) - for i in text.split(' ')] - - -@pytest.mark.asyncio -async def test_pagination_pages_async(async_echo): - text = "The hail in Wales falls mainly on the snails." - page_results = [] - async for page in (await async_echo.paged_expand({ - 'content': text, - 'page_size': 3, - })).pages: - page_results.append(page) - - assert len(page_results) == 3 - assert not page_results[-1].next_page_token - - # The monolithic surface uses a wrapper type that needs an explicit property - # for a 'raw_page': we need to duplicate that interface, even though the - # architecture is different. - assert page_results[0].raw_page is page_results[0] - - results = [r for p in page_results for r in p.responses] - assert results == [showcase.EchoResponse(content=i) - for i in text.split(' ')] +if distutils.util.strtobool(os.environ.get("GAPIC_PYTHON_ASYNC", "true")): + @pytest.mark.asyncio + async def test_pagination_async(async_echo): + text = 'The hail in Wales falls mainly on the snails.' + results = [] + async for i in await async_echo.paged_expand({ + 'content': text, + 'page_size': 3, + }): + results.append(i) + + assert len(results) == 9 + assert results == [showcase.EchoResponse(content=i) + for i in text.split(' ')] + + @pytest.mark.asyncio + async def test_pagination_pages_async(async_echo): + text = "The hail in Wales falls mainly on the snails." + page_results = [] + async for page in (await async_echo.paged_expand({ + 'content': text, + 'page_size': 3, + })).pages: + page_results.append(page) + + assert len(page_results) == 3 + assert not page_results[-1].next_page_token + + # The monolithic surface uses a wrapper type that needs an explicit property + # for a 'raw_page': we need to duplicate that interface, even though the + # architecture is different. + assert page_results[0].raw_page is page_results[0] + + results = [r for p in page_results for r in p.responses] + assert results == [showcase.EchoResponse(content=i) + for i in text.split(' ')] diff --git a/tests/system/test_resource_crud.py b/tests/system/test_resource_crud.py index 5372da4b6..85bafe561 100644 --- a/tests/system/test_resource_crud.py +++ b/tests/system/test_resource_crud.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import distutils +import os import pytest @@ -76,43 +78,43 @@ def test_path_parsing(messaging): assert expected == actual -@pytest.mark.asyncio -async def test_crud_with_request_async(async_identity): - pager = await async_identity.list_users() - count = len(pager.users) - user = await async_identity.create_user(request={'user': { - 'display_name': 'Guido van Rossum', - 'email': 'guido@guido.fake', - }}) - try: - assert user.display_name == 'Guido van Rossum' - assert user.email == 'guido@guido.fake' - pager = (await async_identity.list_users()) - assert len(pager.users) == count + 1 - assert (await async_identity.get_user({ - 'name': user.name - })).display_name == 'Guido van Rossum' - finally: - await async_identity.delete_user({'name': user.name}) - - -@pytest.mark.asyncio -async def test_crud_flattened_async(async_identity): - count = len((await async_identity.list_users()).users) - user = await async_identity.create_user( - display_name='Monty Python', - email='monty@python.org', - ) - try: - assert user.display_name == 'Monty Python' - assert user.email == 'monty@python.org' - assert len((await async_identity.list_users()).users) == count + 1 - assert (await async_identity.get_user(name=user.name)).display_name == 'Monty Python' - finally: - await async_identity.delete_user(name=user.name) - - -def test_path_methods_async(async_identity): - expected = "users/bdfl" - actual = async_identity.user_path("bdfl") - assert expected == actual +if distutils.util.strtobool(os.environ.get("GAPIC_PYTHON_ASYNC", "true")): + + @pytest.mark.asyncio + async def test_crud_with_request_async(async_identity): + pager = await async_identity.list_users() + count = len(pager.users) + user = await async_identity.create_user(request={'user': { + 'display_name': 'Guido van Rossum', + 'email': 'guido@guido.fake', + }}) + try: + assert user.display_name == 'Guido van Rossum' + assert user.email == 'guido@guido.fake' + pager = (await async_identity.list_users()) + assert len(pager.users) == count + 1 + assert (await async_identity.get_user({ + 'name': user.name + })).display_name == 'Guido van Rossum' + finally: + await async_identity.delete_user({'name': user.name}) + + @pytest.mark.asyncio + async def test_crud_flattened_async(async_identity): + count = len((await async_identity.list_users()).users) + user = await async_identity.create_user( + display_name='Monty Python', + email='monty@python.org', + ) + try: + assert user.display_name == 'Monty Python' + assert user.email == 'monty@python.org' + assert len((await async_identity.list_users()).users) == count + 1 + assert (await async_identity.get_user(name=user.name)).display_name == 'Monty Python' + finally: + await async_identity.delete_user(name=user.name) + + def test_path_methods_async(async_identity): + expected = "users/bdfl" + actual = async_identity.user_path("bdfl") + assert expected == actual diff --git a/tests/system/test_retry.py b/tests/system/test_retry.py index 0bc70f9f8..97e0c60be 100644 --- a/tests/system/test_retry.py +++ b/tests/system/test_retry.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import distutils +import os import pytest from google.api_core import exceptions @@ -28,12 +30,14 @@ def test_retry_bubble(echo): }) -@pytest.mark.asyncio -async def test_retry_bubble_async(async_echo): - with pytest.raises(exceptions.DeadlineExceeded): - await async_echo.echo({ - 'error': { - 'code': code_pb2.Code.Value('DEADLINE_EXCEEDED'), - 'message': 'This took longer than you said it should.', - }, - }) +if distutils.util.strtobool(os.environ.get("GAPIC_PYTHON_ASYNC", "true")): + + @pytest.mark.asyncio + async def test_retry_bubble_async(async_echo): + with pytest.raises(exceptions.DeadlineExceeded): + await async_echo.echo({ + 'error': { + 'code': code_pb2.Code.Value('DEADLINE_EXCEEDED'), + 'message': 'This took longer than you said it should.', + }, + })