diff options
Diffstat (limited to 'tests/unit')
-rw-r--r-- | tests/unit/__init__.py | 0 | ||||
-rw-r--r-- | tests/unit/future/__init__.py | 0 | ||||
-rw-r--r-- | tests/unit/future/test__helpers.py | 37 | ||||
-rw-r--r-- | tests/unit/future/test_polling.py | 157 | ||||
-rw-r--r-- | tests/unit/gapic/test_config.py | 89 | ||||
-rw-r--r-- | tests/unit/gapic/test_method.py | 226 | ||||
-rw-r--r-- | tests/unit/operations_v1/__init__.py | 0 | ||||
-rw-r--r-- | tests/unit/operations_v1/test_operations_client.py | 101 | ||||
-rw-r--r-- | tests/unit/test_datetime_helpers.py | 22 | ||||
-rw-r--r-- | tests/unit/test_exceptions.py | 201 | ||||
-rw-r--r-- | tests/unit/test_general_helpers.py | 43 | ||||
-rw-r--r-- | tests/unit/test_grpc_helpers.py | 171 | ||||
-rw-r--r-- | tests/unit/test_operation.py | 223 | ||||
-rw-r--r-- | tests/unit/test_page_iterator.py | 545 | ||||
-rw-r--r-- | tests/unit/test_path_template.py | 90 | ||||
-rw-r--r-- | tests/unit/test_protobuf_helpers.py | 37 | ||||
-rw-r--r-- | tests/unit/test_retry.py | 255 | ||||
-rw-r--r-- | tests/unit/test_timeout.py | 132 |
18 files changed, 2329 insertions, 0 deletions
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/unit/__init__.py diff --git a/tests/unit/future/__init__.py b/tests/unit/future/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/unit/future/__init__.py diff --git a/tests/unit/future/test__helpers.py b/tests/unit/future/test__helpers.py new file mode 100644 index 0000000..660d23a --- /dev/null +++ b/tests/unit/future/test__helpers.py @@ -0,0 +1,37 @@ +# Copyright 2017, Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +from google.api_core.future import _helpers + + +@mock.patch('threading.Thread', autospec=True) +def test_start_deamon_thread(unused_thread): + deamon_thread = _helpers.start_daemon_thread(target=mock.sentinel.target) + assert deamon_thread.daemon is True + + +def test_safe_invoke_callback(): + callback = mock.Mock(spec=['__call__'], return_value=42) + result = _helpers.safe_invoke_callback(callback, 'a', b='c') + assert result == 42 + callback.assert_called_once_with('a', b='c') + + +def test_safe_invoke_callback_exception(): + callback = mock.Mock(spec=['__call__'], side_effect=ValueError()) + result = _helpers.safe_invoke_callback(callback, 'a', b='c') + assert result is None + callback.assert_called_once_with('a', b='c') diff --git a/tests/unit/future/test_polling.py b/tests/unit/future/test_polling.py new file mode 100644 index 0000000..7ad9aee --- /dev/null +++ b/tests/unit/future/test_polling.py @@ -0,0 +1,157 @@ +# Copyright 2017, Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import concurrent.futures +import threading +import time + +import mock +import pytest + +from google.api_core.future import polling + + +class PollingFutureImpl(polling.PollingFuture): + def done(self): + return False + + def cancel(self): + return True + + def cancelled(self): + return False + + def running(self): + return True + + +def test_polling_future_constructor(): + future = PollingFutureImpl() + assert not future.done() + assert not future.cancelled() + assert future.running() + assert future.cancel() + + +def test_set_result(): + future = PollingFutureImpl() + callback = mock.Mock() + + future.set_result(1) + + assert future.result() == 1 + future.add_done_callback(callback) + callback.assert_called_once_with(future) + + +def test_set_exception(): + future = PollingFutureImpl() + exception = ValueError('meep') + + future.set_exception(exception) + + assert future.exception() == exception + with pytest.raises(ValueError): + future.result() + + callback = mock.Mock() + future.add_done_callback(callback) + callback.assert_called_once_with(future) + + +def test_invoke_callback_exception(): + future = PollingFutureImplWithPoll() + future.set_result(42) + + # This should not raise, despite the callback causing an exception. + callback = mock.Mock(side_effect=ValueError) + future.add_done_callback(callback) + callback.assert_called_once_with(future) + + +class PollingFutureImplWithPoll(PollingFutureImpl): + def __init__(self): + super(PollingFutureImplWithPoll, self).__init__() + self.poll_count = 0 + self.event = threading.Event() + + def done(self): + self.poll_count += 1 + self.event.wait() + self.set_result(42) + return True + + +def test_result_with_polling(): + future = PollingFutureImplWithPoll() + + future.event.set() + result = future.result() + + assert result == 42 + assert future.poll_count == 1 + # Repeated calls should not cause additional polling + assert future.result() == result + assert future.poll_count == 1 + + +class PollingFutureImplTimeout(PollingFutureImplWithPoll): + def done(self): + time.sleep(1) + return False + + +def test_result_timeout(): + future = PollingFutureImplTimeout() + with pytest.raises(concurrent.futures.TimeoutError): + future.result(timeout=1) + + +def test_callback_background_thread(): + future = PollingFutureImplWithPoll() + callback = mock.Mock() + + future.add_done_callback(callback) + + assert future._polling_thread is not None + + # Give the thread a second to poll + time.sleep(1) + assert future.poll_count == 1 + + future.event.set() + future._polling_thread.join() + + callback.assert_called_once_with(future) + + +def test_double_callback_background_thread(): + future = PollingFutureImplWithPoll() + callback = mock.Mock() + callback2 = mock.Mock() + + future.add_done_callback(callback) + current_thread = future._polling_thread + assert current_thread is not None + + # only one polling thread should be created. + future.add_done_callback(callback2) + assert future._polling_thread is current_thread + + future.event.set() + future._polling_thread.join() + + assert future.poll_count == 1 + callback.assert_called_once_with(future) + callback2.assert_called_once_with(future) diff --git a/tests/unit/gapic/test_config.py b/tests/unit/gapic/test_config.py new file mode 100644 index 0000000..75a6e1c --- /dev/null +++ b/tests/unit/gapic/test_config.py @@ -0,0 +1,89 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.api_core import exceptions +from google.api_core.gapic_v1 import config + + +INTERFACE_CONFIG = { + 'retry_codes': { + 'idempotent': ['DEADLINE_EXCEEDED', 'UNAVAILABLE'], + 'other': ['FAILED_PRECONDITION'], + 'non_idempotent': [] + }, + 'retry_params': { + 'default': { + 'initial_retry_delay_millis': 1000, + 'retry_delay_multiplier': 2.5, + 'max_retry_delay_millis': 120000, + 'initial_rpc_timeout_millis': 120000, + 'rpc_timeout_multiplier': 1.0, + 'max_rpc_timeout_millis': 120000, + 'total_timeout_millis': 600000 + }, + 'other': { + 'initial_retry_delay_millis': 1000, + 'retry_delay_multiplier': 1, + 'max_retry_delay_millis': 1000, + 'initial_rpc_timeout_millis': 1000, + 'rpc_timeout_multiplier': 1, + 'max_rpc_timeout_millis': 1000, + 'total_timeout_millis': 1000 + }, + }, + 'methods': { + 'AnnotateVideo': { + 'timeout_millis': 60000, + 'retry_codes_name': 'idempotent', + 'retry_params_name': 'default' + }, + 'Other': { + 'timeout_millis': 60000, + 'retry_codes_name': 'other', + 'retry_params_name': 'other' + }, + 'Plain': { + 'timeout_millis': 30000 + } + } +} + + +def test_create_method_configs(): + method_configs = config.parse_method_configs(INTERFACE_CONFIG) + + retry, timeout = method_configs['AnnotateVideo'] + assert retry._predicate(exceptions.DeadlineExceeded(None)) + assert retry._predicate(exceptions.ServiceUnavailable(None)) + assert retry._initial == 1.0 + assert retry._multiplier == 2.5 + assert retry._maximum == 120.0 + assert retry._deadline == 600.0 + assert timeout._initial == 120.0 + assert timeout._multiplier == 1.0 + assert timeout._maximum == 120.0 + + retry, timeout = method_configs['Other'] + assert retry._predicate(exceptions.FailedPrecondition(None)) + assert retry._initial == 1.0 + assert retry._multiplier == 1.0 + assert retry._maximum == 1.0 + assert retry._deadline == 1.0 + assert timeout._initial == 1.0 + assert timeout._multiplier == 1.0 + assert timeout._maximum == 1.0 + + retry, timeout = method_configs['Plain'] + assert retry is None + assert timeout._timeout == 30.0 diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py new file mode 100644 index 0000000..35ac144 --- /dev/null +++ b/tests/unit/gapic/test_method.py @@ -0,0 +1,226 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime + +import mock + +from google.api_core import exceptions +from google.api_core import retry +from google.api_core import timeout +import google.api_core.gapic_v1.method +import google.api_core.page_iterator + + +def _utcnow_monotonic(): + curr_value = datetime.datetime.min + delta = datetime.timedelta(seconds=0.5) + while True: + yield curr_value + curr_value += delta + + +def test_wrap_method_basic(): + method = mock.Mock(spec=['__call__'], return_value=42) + + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, metadata=None) + + result = wrapped_method(1, 2, meep='moop') + + assert result == 42 + method.assert_called_once_with(1, 2, meep='moop') + + +def test_wrap_method_with_default_metadata(): + method = mock.Mock(spec=['__call__']) + + wrapped_method = google.api_core.gapic_v1.method.wrap_method(method) + + wrapped_method(1, 2, meep='moop') + + method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY) + + metadata = method.call_args[1]['metadata'] + assert len(metadata) == 1 + assert metadata[0][0] == 'x-goog-api-client' + assert 'api-core' in metadata[0][1] + + +def test_wrap_method_with_custom_metadata(): + method = mock.Mock(spec=['__call__']) + + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, metadata={'foo': 'bar'}) + + wrapped_method(1, 2, meep='moop') + + method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY) + + metadata = method.call_args[1]['metadata'] + assert len(metadata) == 2 + assert ('foo', 'bar') in metadata + + +def test_wrap_method_with_merged_metadata(): + method = mock.Mock(spec=['__call__']) + + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, metadata={'x-goog-api-client': 'foo/1.2.3'}) + + wrapped_method(1, 2, meep='moop') + + method.assert_called_once_with(1, 2, meep='moop', metadata=mock.ANY) + + metadata = method.call_args[1]['metadata'] + assert len(metadata) == 1 + assert metadata[0][0] == 'x-goog-api-client' + assert metadata[0][1].endswith(' foo/1.2.3') + + +@mock.patch('time.sleep') +def test_wrap_method_with_default_retry_and_timeout(unusued_sleep): + method = mock.Mock( + spec=['__call__'], + side_effect=[exceptions.InternalServerError(None), 42] + ) + default_retry = retry.Retry() + default_timeout = timeout.ConstantTimeout(60) + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, default_retry, default_timeout) + + result = wrapped_method() + + assert result == 42 + assert method.call_count == 2 + method.assert_called_with(timeout=60, metadata=mock.ANY) + + +@mock.patch('time.sleep') +def test_wrap_method_with_default_retry_and_timeout_using_sentinel( + unusued_sleep): + method = mock.Mock( + spec=['__call__'], + side_effect=[exceptions.InternalServerError(None), 42] + ) + default_retry = retry.Retry() + default_timeout = timeout.ConstantTimeout(60) + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, default_retry, default_timeout) + + result = wrapped_method( + retry=google.api_core.gapic_v1.method.DEFAULT, + timeout=google.api_core.gapic_v1.method.DEFAULT) + + assert result == 42 + assert method.call_count == 2 + method.assert_called_with(timeout=60, metadata=mock.ANY) + + +@mock.patch('time.sleep') +def test_wrap_method_with_overriding_retry_and_timeout(unusued_sleep): + method = mock.Mock( + spec=['__call__'], + side_effect=[exceptions.NotFound(None), 42] + ) + default_retry = retry.Retry() + default_timeout = timeout.ConstantTimeout(60) + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, default_retry, default_timeout) + + result = wrapped_method( + retry=retry.Retry(retry.if_exception_type(exceptions.NotFound)), + timeout=timeout.ConstantTimeout(22)) + + assert result == 42 + assert method.call_count == 2 + method.assert_called_with(timeout=22, metadata=mock.ANY) + + +@mock.patch('time.sleep') +@mock.patch( + 'google.api_core.datetime_helpers.utcnow', + side_effect=_utcnow_monotonic(), + autospec=True) +def test_wrap_method_with_overriding_retry_deadline(utcnow, unused_sleep): + method = mock.Mock( + spec=['__call__'], + side_effect=([exceptions.InternalServerError(None)] * 4) + [42] + ) + default_retry = retry.Retry() + default_timeout = timeout.ExponentialTimeout(deadline=60) + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, default_retry, default_timeout) + + # Overriding only the retry's deadline should also override the timeout's + # deadline. + result = wrapped_method( + retry=default_retry.with_deadline(30)) + + assert result == 42 + timeout_args = [call[1]['timeout'] for call in method.call_args_list] + assert timeout_args == [5.0, 10.0, 20.0, 26.0, 25.0] + assert utcnow.call_count == ( + 1 + # First to set the deadline. + 5 + # One for each min(timeout, maximum, (DEADLINE - NOW).seconds) + 5 + ) + + +def test_wrap_method_with_overriding_timeout_as_a_number(): + method = mock.Mock(spec=['__call__'], return_value=42) + default_retry = retry.Retry() + default_timeout = timeout.ConstantTimeout(60) + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, default_retry, default_timeout) + + result = wrapped_method(timeout=22) + + assert result == 42 + method.assert_called_once_with(timeout=22, metadata=mock.ANY) + + +def test_wrap_with_paging(): + page_one = mock.Mock( + spec=['items', 'page_token', 'next_page_token'], + items=[1, 2], + next_page_token='icanhasnextpls') + page_two = mock.Mock( + spec=['items', 'page_token', 'next_page_token'], + items=[3, 4], + next_page_token=None) + method = mock.Mock( + spec=['__call__', '__name__'], side_effect=(page_one, page_two)) + method.__name__ = 'mockmethod' + + wrapped_method = google.api_core.gapic_v1.method.wrap_with_paging( + method, 'items', 'page_token', 'next_page_token') + + request = mock.Mock(spec=['page_token'], page_token=None) + result = wrapped_method(request, extra='param') + + # Should return an iterator and should not have actually called the + # method yet. + assert isinstance(result, google.api_core.page_iterator.Iterator) + method.assert_not_called() + assert request.page_token is None + + # Draining the iterator should call the method until no more pages are + # returned. + results = list(result) + + assert results == [1, 2, 3, 4] + assert method.call_count == 2 + method.assert_called_with(request, extra='param') + assert request.page_token == 'icanhasnextpls' diff --git a/tests/unit/operations_v1/__init__.py b/tests/unit/operations_v1/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/unit/operations_v1/__init__.py diff --git a/tests/unit/operations_v1/test_operations_client.py b/tests/unit/operations_v1/test_operations_client.py new file mode 100644 index 0000000..60b11b7 --- /dev/null +++ b/tests/unit/operations_v1/test_operations_client.py @@ -0,0 +1,101 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +from google.api_core import operations_v1 +from google.api_core import page_iterator +from google.longrunning import operations_pb2 + + +def make_operations_stub(channel): + return mock.Mock( + spec=[ + 'GetOperation', 'DeleteOperation', 'ListOperations', + 'CancelOperation']) + + +operations_stub_patch = mock.patch( + 'google.longrunning.operations_pb2.OperationsStub', + autospec=True, + side_effect=make_operations_stub) + + +@operations_stub_patch +def test_constructor(operations_stub): + stub = make_operations_stub(None) + operations_stub.side_effect = None + operations_stub.return_value = stub + + client = operations_v1.OperationsClient(mock.sentinel.channel) + + assert client.operations_stub == stub + operations_stub.assert_called_once_with(mock.sentinel.channel) + + +@operations_stub_patch +def test_get_operation(operations_stub): + client = operations_v1.OperationsClient(mock.sentinel.channel) + client.operations_stub.GetOperation.return_value = mock.sentinel.operation + + response = client.get_operation('name') + + request = client.operations_stub.GetOperation.call_args[0][0] + assert isinstance(request, operations_pb2.GetOperationRequest) + assert request.name == 'name' + + assert response == mock.sentinel.operation + + +@operations_stub_patch +def test_list_operations(operations_stub): + client = operations_v1.OperationsClient(mock.sentinel.channel) + operations = [ + operations_pb2.Operation(name='1'), + operations_pb2.Operation(name='2')] + list_response = operations_pb2.ListOperationsResponse( + operations=operations) + client.operations_stub.ListOperations.return_value = list_response + + response = client.list_operations('name', 'filter') + + assert isinstance(response, page_iterator.Iterator) + assert list(response) == operations + + request = client.operations_stub.ListOperations.call_args[0][0] + assert isinstance(request, operations_pb2.ListOperationsRequest) + assert request.name == 'name' + assert request.filter == 'filter' + + +@operations_stub_patch +def test_delete_operation(operations_stub): + client = operations_v1.OperationsClient(mock.sentinel.channel) + + client.delete_operation('name') + + request = client.operations_stub.DeleteOperation.call_args[0][0] + assert isinstance(request, operations_pb2.DeleteOperationRequest) + assert request.name == 'name' + + +@operations_stub_patch +def test_cancel_operation(operations_stub): + client = operations_v1.OperationsClient(mock.sentinel.channel) + + client.cancel_operation('name') + + request = client.operations_stub.CancelOperation.call_args[0][0] + assert isinstance(request, operations_pb2.CancelOperationRequest) + assert request.name == 'name' diff --git a/tests/unit/test_datetime_helpers.py b/tests/unit/test_datetime_helpers.py new file mode 100644 index 0000000..24f8dbd --- /dev/null +++ b/tests/unit/test_datetime_helpers.py @@ -0,0 +1,22 @@ +# Copyright 2017, Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime + +from google.api_core import datetime_helpers + + +def test_utcnow(): + result = datetime_helpers.utcnow() + assert isinstance(result, datetime.datetime) diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py new file mode 100644 index 0000000..df159be --- /dev/null +++ b/tests/unit/test_exceptions.py @@ -0,0 +1,201 @@ +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +import grpc +import mock +import requests +from six.moves import http_client + +from google.api_core import exceptions + + +def test_create_google_cloud_error(): + exception = exceptions.GoogleAPICallError('Testing') + exception.code = 600 + assert str(exception) == '600 Testing' + assert exception.message == 'Testing' + assert exception.errors == [] + assert exception.response is None + + +def test_create_google_cloud_error_with_args(): + error = { + 'domain': 'global', + 'location': 'test', + 'locationType': 'testing', + 'message': 'Testing', + 'reason': 'test', + } + response = mock.sentinel.response + exception = exceptions.GoogleAPICallError( + 'Testing', [error], response=response) + exception.code = 600 + assert str(exception) == '600 Testing' + assert exception.message == 'Testing' + assert exception.errors == [error] + assert exception.response == response + + +def test_from_http_status(): + message = 'message' + exception = exceptions.from_http_status(http_client.NOT_FOUND, message) + assert exception.code == http_client.NOT_FOUND + assert exception.message == message + assert exception.errors == [] + + +def test_from_http_status_with_errors_and_response(): + message = 'message' + errors = ['1', '2'] + response = mock.sentinel.response + exception = exceptions.from_http_status( + http_client.NOT_FOUND, message, errors=errors, response=response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == message + assert exception.errors == errors + assert exception.response == response + + +def test_from_http_status_unknown_code(): + message = 'message' + status_code = 156 + exception = exceptions.from_http_status(status_code, message) + assert exception.code == status_code + assert exception.message == message + + +def make_response(content): + response = requests.Response() + response._content = content + response.status_code = http_client.NOT_FOUND + response.request = requests.Request( + method='POST', url='https://example.com').prepare() + return response + + +def test_from_http_response_no_content(): + response = make_response(None) + + exception = exceptions.from_http_response(response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == 'POST https://example.com/: unknown error' + assert exception.response == response + + +def test_from_http_response_text_content(): + response = make_response(b'message') + + exception = exceptions.from_http_response(response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == 'POST https://example.com/: message' + + +def test_from_http_response_json_content(): + response = make_response(json.dumps({ + 'error': { + 'message': 'json message', + 'errors': ['1', '2'] + } + }).encode('utf-8')) + + exception = exceptions.from_http_response(response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == 'POST https://example.com/: json message' + assert exception.errors == ['1', '2'] + + +def test_from_http_response_bad_json_content(): + response = make_response(json.dumps({'meep': 'moop'}).encode('utf-8')) + + exception = exceptions.from_http_response(response) + + assert isinstance(exception, exceptions.NotFound) + assert exception.code == http_client.NOT_FOUND + assert exception.message == 'POST https://example.com/: unknown error' + + +def test_from_grpc_status(): + message = 'message' + exception = exceptions.from_grpc_status( + grpc.StatusCode.OUT_OF_RANGE, message) + assert isinstance(exception, exceptions.BadRequest) + assert isinstance(exception, exceptions.OutOfRange) + assert exception.code == http_client.BAD_REQUEST + assert exception.grpc_status_code == grpc.StatusCode.OUT_OF_RANGE + assert exception.message == message + assert exception.errors == [] + + +def test_from_grpc_status_with_errors_and_response(): + message = 'message' + response = mock.sentinel.response + errors = ['1', '2'] + exception = exceptions.from_grpc_status( + grpc.StatusCode.OUT_OF_RANGE, message, + errors=errors, response=response) + + assert isinstance(exception, exceptions.OutOfRange) + assert exception.message == message + assert exception.errors == errors + assert exception.response == response + + +def test_from_grpc_status_unknown_code(): + message = 'message' + exception = exceptions.from_grpc_status( + grpc.StatusCode.OK, message) + assert exception.grpc_status_code == grpc.StatusCode.OK + assert exception.message == message + + +def test_from_grpc_error(): + message = 'message' + error = mock.create_autospec(grpc.Call, instance=True) + error.code.return_value = grpc.StatusCode.INVALID_ARGUMENT + error.details.return_value = message + + exception = exceptions.from_grpc_error(error) + + assert isinstance(exception, exceptions.BadRequest) + assert isinstance(exception, exceptions.InvalidArgument) + assert exception.code == http_client.BAD_REQUEST + assert exception.grpc_status_code == grpc.StatusCode.INVALID_ARGUMENT + assert exception.message == message + assert exception.errors == [error] + assert exception.response == error + + +def test_from_grpc_error_non_call(): + message = 'message' + error = mock.create_autospec(grpc.RpcError, instance=True) + error.__str__.return_value = message + + exception = exceptions.from_grpc_error(error) + + assert isinstance(exception, exceptions.GoogleAPICallError) + assert exception.code is None + assert exception.grpc_status_code is None + assert exception.message == message + assert exception.errors == [error] + assert exception.response == error diff --git a/tests/unit/test_general_helpers.py b/tests/unit/test_general_helpers.py new file mode 100644 index 0000000..b878cc5 --- /dev/null +++ b/tests/unit/test_general_helpers.py @@ -0,0 +1,43 @@ +# Copyright 2017, Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools + +from google.api_core import general_helpers + + +def test_wraps_normal_func(): + + def func(): + return 42 + + @general_helpers.wraps(func) + def replacement(): + return func() + + assert replacement() == 42 + + +def test_wraps_partial(): + + def func(): + return 42 + + partial = functools.partial(func) + + @general_helpers.wraps(partial) + def replacement(): + return func() + + assert replacement() == 42 diff --git a/tests/unit/test_grpc_helpers.py b/tests/unit/test_grpc_helpers.py new file mode 100644 index 0000000..d5e0b3c --- /dev/null +++ b/tests/unit/test_grpc_helpers.py @@ -0,0 +1,171 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import grpc +import mock +import pytest + +from google.api_core import exceptions +from google.api_core import grpc_helpers + + +def test__patch_callable_name(): + callable = mock.Mock(spec=['__class__']) + callable.__class__ = mock.Mock(spec=['__name__']) + callable.__class__.__name__ = 'TestCallable' + + grpc_helpers._patch_callable_name(callable) + + assert callable.__name__ == 'TestCallable' + + +def test__patch_callable_name_no_op(): + callable = mock.Mock(spec=['__name__']) + callable.__name__ = 'test_callable' + + grpc_helpers._patch_callable_name(callable) + + assert callable.__name__ == 'test_callable' + + +class RpcErrorImpl(grpc.RpcError, grpc.Call): + def __init__(self, code): + super(RpcErrorImpl, self).__init__() + self._code = code + + def code(self): + return self._code + + def details(self): + return None + + +def test_wrap_unary_errors(): + grpc_error = RpcErrorImpl(grpc.StatusCode.INVALID_ARGUMENT) + callable_ = mock.Mock(spec=['__call__'], side_effect=grpc_error) + + wrapped_callable = grpc_helpers._wrap_unary_errors(callable_) + + with pytest.raises(exceptions.InvalidArgument) as exc_info: + wrapped_callable(1, 2, three='four') + + callable_.assert_called_once_with(1, 2, three='four') + assert exc_info.value.response == grpc_error + + +def test_wrap_stream_errors_invocation(): + grpc_error = RpcErrorImpl(grpc.StatusCode.INVALID_ARGUMENT) + callable_ = mock.Mock(spec=['__call__'], side_effect=grpc_error) + + wrapped_callable = grpc_helpers._wrap_stream_errors(callable_) + + with pytest.raises(exceptions.InvalidArgument) as exc_info: + wrapped_callable(1, 2, three='four') + + callable_.assert_called_once_with(1, 2, three='four') + assert exc_info.value.response == grpc_error + + +class RpcResponseIteratorImpl(object): + def __init__(self, exception): + self._exception = exception + + # Note: This matches grpc._channel._Rendezvous._next which is what is + # patched by _wrap_stream_errors. + def _next(self): + raise self._exception + + def __next__(self): # pragma: NO COVER + return self._next() + + def next(self): # pragma: NO COVER + return self._next() + + +def test_wrap_stream_errors_iterator(): + grpc_error = RpcErrorImpl(grpc.StatusCode.UNAVAILABLE) + response_iter = RpcResponseIteratorImpl(grpc_error) + callable_ = mock.Mock(spec=['__call__'], return_value=response_iter) + + wrapped_callable = grpc_helpers._wrap_stream_errors(callable_) + + got_iterator = wrapped_callable(1, 2, three='four') + + with pytest.raises(exceptions.ServiceUnavailable) as exc_info: + next(got_iterator) + + assert got_iterator == response_iter + callable_.assert_called_once_with(1, 2, three='four') + assert exc_info.value.response == grpc_error + + +@mock.patch('google.api_core.grpc_helpers._wrap_unary_errors') +def test_wrap_errors_non_streaming(wrap_unary_errors): + callable_ = mock.create_autospec(grpc.UnaryUnaryMultiCallable) + + result = grpc_helpers.wrap_errors(callable_) + + assert result == wrap_unary_errors.return_value + wrap_unary_errors.assert_called_once_with(callable_) + + +@mock.patch('google.api_core.grpc_helpers._wrap_stream_errors') +def test_wrap_errors_streaming(wrap_stream_errors): + callable_ = mock.create_autospec(grpc.UnaryStreamMultiCallable) + + result = grpc_helpers.wrap_errors(callable_) + + assert result == wrap_stream_errors.return_value + wrap_stream_errors.assert_called_once_with(callable_) + + +@mock.patch( + 'google.auth.default', + return_value=(mock.sentinel.credentials, mock.sentinel.projet)) +@mock.patch('google.auth.transport.grpc.secure_authorized_channel') +def test_create_channel_implicit(secure_authorized_channel, default): + target = 'example.com:443' + + channel = grpc_helpers.create_channel(target) + + assert channel is secure_authorized_channel.return_value + default.assert_called_once_with(scopes=None) + secure_authorized_channel.assert_called_once_with( + mock.sentinel.credentials, mock.ANY, target) + + +@mock.patch( + 'google.auth.default', + return_value=(mock.sentinel.credentials, mock.sentinel.projet)) +@mock.patch('google.auth.transport.grpc.secure_authorized_channel') +def test_create_channel_implicit_with_scopes( + secure_authorized_channel, default): + target = 'example.com:443' + + channel = grpc_helpers.create_channel(target, scopes=['one', 'two']) + + assert channel is secure_authorized_channel.return_value + default.assert_called_once_with(scopes=['one', 'two']) + + +@mock.patch('google.auth.transport.grpc.secure_authorized_channel') +def test_create_channel_explicit(secure_authorized_channel): + target = 'example.com:443' + + channel = grpc_helpers.create_channel( + target, credentials=mock.sentinel.credentials) + + assert channel is secure_authorized_channel.return_value + secure_authorized_channel.assert_called_once_with( + mock.sentinel.credentials, mock.ANY, target) diff --git a/tests/unit/test_operation.py b/tests/unit/test_operation.py new file mode 100644 index 0000000..1d765cc --- /dev/null +++ b/tests/unit/test_operation.py @@ -0,0 +1,223 @@ +# Copyright 2017, Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import mock + +from google.api_core import operation +from google.api_core import operations_v1 +from google.longrunning import operations_pb2 +from google.protobuf import struct_pb2 +from google.rpc import code_pb2 +from google.rpc import status_pb2 + +TEST_OPERATION_NAME = 'test/operation' + + +def make_operation_proto( + name=TEST_OPERATION_NAME, metadata=None, response=None, + error=None, **kwargs): + operation_proto = operations_pb2.Operation( + name=name, **kwargs) + + if metadata is not None: + operation_proto.metadata.Pack(metadata) + + if response is not None: + operation_proto.response.Pack(response) + + if error is not None: + operation_proto.error.CopyFrom(error) + + return operation_proto + + +def make_operation_future(client_operations_responses=None): + if client_operations_responses is None: + client_operations_responses = [make_operation_proto()] + + refresh = mock.Mock( + spec=['__call__'], side_effect=client_operations_responses) + refresh.responses = client_operations_responses + cancel = mock.Mock(spec=['__call__']) + operation_future = operation.Operation( + client_operations_responses[0], + refresh, + cancel, + result_type=struct_pb2.Struct, + metadata_type=struct_pb2.Struct) + + return operation_future, refresh, cancel + + +def test_constructor(): + future, refresh, _ = make_operation_future() + + assert future.operation == refresh.responses[0] + assert future.operation.done is False + assert future.operation.name == TEST_OPERATION_NAME + assert future.metadata is None + assert future.running() + + +def test_metadata(): + expected_metadata = struct_pb2.Struct() + future, _, _ = make_operation_future( + [make_operation_proto(metadata=expected_metadata)]) + + assert future.metadata == expected_metadata + + +def test_cancellation(): + responses = [ + make_operation_proto(), + # Second response indicates that the operation was cancelled. + make_operation_proto( + done=True, + error=status_pb2.Status(code=code_pb2.CANCELLED))] + future, _, cancel = make_operation_future(responses) + + assert future.cancel() + assert future.cancelled() + cancel.assert_called_once_with() + + # Cancelling twice should have no effect. + assert not future.cancel() + cancel.assert_called_once_with() + + +def test_result(): + expected_result = struct_pb2.Struct() + responses = [ + make_operation_proto(), + # Second operation response includes the result. + make_operation_proto(done=True, response=expected_result)] + future, _, _ = make_operation_future(responses) + + result = future.result() + + assert result == expected_result + assert future.done() + + +def test_exception(): + expected_exception = status_pb2.Status(message='meep') + responses = [ + make_operation_proto(), + # Second operation response includes the error. + make_operation_proto(done=True, error=expected_exception)] + future, _, _ = make_operation_future(responses) + + exception = future.exception() + + assert expected_exception.message in '{!r}'.format(exception) + + +def test_unexpected_result(): + responses = [ + make_operation_proto(), + # Second operation response is done, but has not error or response. + make_operation_proto(done=True)] + future, _, _ = make_operation_future(responses) + + exception = future.exception() + + assert 'Unexpected state' in '{!r}'.format(exception) + + +def test__refresh_http(): + api_request = mock.Mock( + return_value={'name': TEST_OPERATION_NAME, 'done': True}) + + result = operation._refresh_http(api_request, TEST_OPERATION_NAME) + + assert result.name == TEST_OPERATION_NAME + assert result.done is True + api_request.assert_called_once_with( + method='GET', path='operations/{}'.format(TEST_OPERATION_NAME)) + + +def test__cancel_http(): + api_request = mock.Mock() + + operation._cancel_http(api_request, TEST_OPERATION_NAME) + + api_request.assert_called_once_with( + method='POST', path='operations/{}:cancel'.format(TEST_OPERATION_NAME)) + + +def test_from_http_json(): + operation_json = {'name': TEST_OPERATION_NAME, 'done': True} + api_request = mock.sentinel.api_request + + future = operation.from_http_json( + operation_json, api_request, struct_pb2.Struct, + metadata_type=struct_pb2.Struct) + + assert future._result_type == struct_pb2.Struct + assert future._metadata_type == struct_pb2.Struct + assert future.operation.name == TEST_OPERATION_NAME + assert future.done + + +def test__refresh_grpc(): + operations_stub = mock.Mock(spec=['GetOperation']) + expected_result = make_operation_proto(done=True) + operations_stub.GetOperation.return_value = expected_result + + result = operation._refresh_grpc(operations_stub, TEST_OPERATION_NAME) + + assert result == expected_result + expected_request = operations_pb2.GetOperationRequest( + name=TEST_OPERATION_NAME) + operations_stub.GetOperation.assert_called_once_with(expected_request) + + +def test__cancel_grpc(): + operations_stub = mock.Mock(spec=['CancelOperation']) + + operation._cancel_grpc(operations_stub, TEST_OPERATION_NAME) + + expected_request = operations_pb2.CancelOperationRequest( + name=TEST_OPERATION_NAME) + operations_stub.CancelOperation.assert_called_once_with(expected_request) + + +def test_from_grpc(): + operation_proto = make_operation_proto(done=True) + operations_stub = mock.sentinel.operations_stub + + future = operation.from_grpc( + operation_proto, operations_stub, struct_pb2.Struct, + metadata_type=struct_pb2.Struct) + + assert future._result_type == struct_pb2.Struct + assert future._metadata_type == struct_pb2.Struct + assert future.operation.name == TEST_OPERATION_NAME + assert future.done + + +def test_from_gapic(): + operation_proto = make_operation_proto(done=True) + operations_client = mock.create_autospec( + operations_v1.OperationsClient, instance=True) + + future = operation.from_gapic( + operation_proto, operations_client, struct_pb2.Struct, + metadata_type=struct_pb2.Struct) + + assert future._result_type == struct_pb2.Struct + assert future._metadata_type == struct_pb2.Struct + assert future.operation.name == TEST_OPERATION_NAME + assert future.done diff --git a/tests/unit/test_page_iterator.py b/tests/unit/test_page_iterator.py new file mode 100644 index 0000000..5cecac8 --- /dev/null +++ b/tests/unit/test_page_iterator.py @@ -0,0 +1,545 @@ +# Copyright 2015 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import types + +import mock +import pytest +import six + +from google.api_core import page_iterator + + +def test__do_nothing_page_start(): + assert page_iterator._do_nothing_page_start(None, None, None) is None + + +class TestPage(object): + + def test_constructor(self): + parent = mock.sentinel.parent + item_to_value = mock.sentinel.item_to_value + + page = page_iterator.Page(parent, (1, 2, 3), item_to_value) + + assert page.num_items == 3 + assert page.remaining == 3 + assert page._parent is parent + assert page._item_to_value is item_to_value + + def test___iter__(self): + page = page_iterator.Page(None, (), None) + assert iter(page) is page + + def test_iterator_calls_parent_item_to_value(self): + parent = mock.sentinel.parent + + item_to_value = mock.Mock( + side_effect=lambda iterator, value: value, spec=['__call__']) + + page = page_iterator.Page(parent, (10, 11, 12), item_to_value) + page._remaining = 100 + + assert item_to_value.call_count == 0 + assert page.remaining == 100 + + assert six.next(page) == 10 + assert item_to_value.call_count == 1 + item_to_value.assert_called_with(parent, 10) + assert page.remaining == 99 + + assert six.next(page) == 11 + assert item_to_value.call_count == 2 + item_to_value.assert_called_with(parent, 11) + assert page.remaining == 98 + + assert six.next(page) == 12 + assert item_to_value.call_count == 3 + item_to_value.assert_called_with(parent, 12) + assert page.remaining == 97 + + +class PageIteratorImpl(page_iterator.Iterator): + def _next_page(self): + return mock.create_autospec(page_iterator.Page, instance=True) + + +class TestIterator(object): + + def test_constructor(self): + client = mock.sentinel.client + item_to_value = mock.sentinel.item_to_value + token = 'ab13nceor03' + max_results = 1337 + + iterator = PageIteratorImpl( + client, item_to_value, page_token=token, max_results=max_results) + + assert not iterator._started + assert iterator.client is client + assert iterator._item_to_value == item_to_value + assert iterator.max_results == max_results + # Changing attributes. + assert iterator.page_number == 0 + assert iterator.next_page_token == token + assert iterator.num_results == 0 + + def test_pages_property_starts(self): + iterator = PageIteratorImpl(None, None) + + assert not iterator._started + + assert isinstance(iterator.pages, types.GeneratorType) + + assert iterator._started + + def test_pages_property_restart(self): + iterator = PageIteratorImpl(None, None) + + assert iterator.pages + + # Make sure we cannot restart. + with pytest.raises(ValueError): + assert iterator.pages + + def test__page_iter_increment(self): + iterator = PageIteratorImpl(None, None) + page = page_iterator.Page( + iterator, ('item',), page_iterator._item_to_value_identity) + iterator._next_page = mock.Mock(side_effect=[page, None]) + + assert iterator.num_results == 0 + + page_iter = iterator._page_iter(increment=True) + next(page_iter) + + assert iterator.num_results == 1 + + def test__page_iter_no_increment(self): + iterator = PageIteratorImpl(None, None) + + assert iterator.num_results == 0 + + page_iter = iterator._page_iter(increment=False) + next(page_iter) + + # results should still be 0 after fetching a page. + assert iterator.num_results == 0 + + def test__items_iter(self): + # Items to be returned. + item1 = 17 + item2 = 100 + item3 = 211 + + # Make pages from mock responses + parent = mock.sentinel.parent + page1 = page_iterator.Page( + parent, (item1, item2), page_iterator._item_to_value_identity) + page2 = page_iterator.Page( + parent, (item3,), page_iterator._item_to_value_identity) + + iterator = PageIteratorImpl(None, None) + iterator._next_page = mock.Mock(side_effect=[page1, page2, None]) + + items_iter = iterator._items_iter() + + assert isinstance(items_iter, types.GeneratorType) + + # Consume items and check the state of the iterator. + assert iterator.num_results == 0 + + assert six.next(items_iter) == item1 + assert iterator.num_results == 1 + + assert six.next(items_iter) == item2 + assert iterator.num_results == 2 + + assert six.next(items_iter) == item3 + assert iterator.num_results == 3 + + with pytest.raises(StopIteration): + six.next(items_iter) + + def test___iter__(self): + iterator = PageIteratorImpl(None, None) + iterator._next_page = mock.Mock(side_effect=[(1, 2), (3,), None]) + + assert not iterator._started + + result = list(iterator) + + assert result == [1, 2, 3] + assert iterator._started + + def test___iter__restart(self): + iterator = PageIteratorImpl(None, None) + + iter(iterator) + + # Make sure we cannot restart. + with pytest.raises(ValueError): + iter(iterator) + + def test___iter___restart_after_page(self): + iterator = PageIteratorImpl(None, None) + + assert iterator.pages + + # Make sure we cannot restart after starting the page iterator + with pytest.raises(ValueError): + iter(iterator) + + +class TestHTTPIterator(object): + + def test_constructor(self): + client = mock.sentinel.client + path = '/foo' + iterator = page_iterator.HTTPIterator( + client, mock.sentinel.api_request, + path, mock.sentinel.item_to_value) + + assert not iterator._started + assert iterator.client is client + assert iterator.path == path + assert iterator._item_to_value is mock.sentinel.item_to_value + assert iterator._items_key == 'items' + assert iterator.max_results is None + assert iterator.extra_params == {} + assert iterator._page_start == page_iterator._do_nothing_page_start + # Changing attributes. + assert iterator.page_number == 0 + assert iterator.next_page_token is None + assert iterator.num_results == 0 + + def test_constructor_w_extra_param_collision(self): + extra_params = {'pageToken': 'val'} + + with pytest.raises(ValueError): + page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value, + extra_params=extra_params) + + def test_iterate(self): + path = '/foo' + item1 = {'name': '1'} + item2 = {'name': '2'} + api_request = mock.Mock(return_value={'items': [item1, item2]}) + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, api_request, path=path, + item_to_value=page_iterator._item_to_value_identity) + + assert iterator.num_results == 0 + + items_iter = iter(iterator) + + val1 = six.next(items_iter) + assert val1 == item1 + assert iterator.num_results == 1 + + val2 = six.next(items_iter) + assert val2 == item2 + assert iterator.num_results == 2 + + with pytest.raises(StopIteration): + six.next(items_iter) + + api_request.assert_called_once_with( + method='GET', path=path, query_params={}) + + def test__has_next_page_new(self): + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value) + + # The iterator should *always* indicate that it has a next page + # when created so that it can fetch the initial page. + assert iterator._has_next_page() + + def test__has_next_page_without_token(self): + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value) + + iterator.page_number = 1 + + # The iterator should not indicate that it has a new page if the + # initial page has been requested and there's no page token. + assert not iterator._has_next_page() + + def test__has_next_page_w_number_w_token(self): + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value) + + iterator.page_number = 1 + iterator.next_page_token = mock.sentinel.token + + # The iterator should indicate that it has a new page if the + # initial page has been requested and there's is a page token. + assert iterator._has_next_page() + + def test__has_next_page_w_max_results_not_done(self): + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value, + max_results=3, + page_token=mock.sentinel.token) + + iterator.page_number = 1 + + # The iterator should indicate that it has a new page if there + # is a page token and it has not consumed more than max_results. + assert iterator.num_results < iterator.max_results + assert iterator._has_next_page() + + def test__has_next_page_w_max_results_done(self): + + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value, + max_results=3, + page_token=mock.sentinel.token) + + iterator.page_number = 1 + iterator.num_results = 3 + + # The iterator should not indicate that it has a new page if there + # if it has consumed more than max_results. + assert iterator.num_results == iterator.max_results + assert not iterator._has_next_page() + + def test__get_query_params_no_token(self): + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value) + + assert iterator._get_query_params() == {} + + def test__get_query_params_w_token(self): + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value) + iterator.next_page_token = 'token' + + assert iterator._get_query_params() == { + 'pageToken': iterator.next_page_token} + + def test__get_query_params_w_max_results(self): + max_results = 3 + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value, + max_results=max_results) + + iterator.num_results = 1 + local_max = max_results - iterator.num_results + + assert iterator._get_query_params() == { + 'maxResults': local_max} + + def test__get_query_params_extra_params(self): + extra_params = {'key': 'val'} + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value, + extra_params=extra_params) + + assert iterator._get_query_params() == extra_params + + def test__get_next_page_response_with_post(self): + path = '/foo' + page_response = {'items': ['one', 'two']} + api_request = mock.Mock(return_value=page_response) + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, api_request, path=path, + item_to_value=page_iterator._item_to_value_identity) + iterator._HTTP_METHOD = 'POST' + + response = iterator._get_next_page_response() + + assert response == page_response + + api_request.assert_called_once_with( + method='POST', path=path, data={}) + + def test__get_next_page_bad_http_method(self): + iterator = page_iterator.HTTPIterator( + mock.sentinel.client, + mock.sentinel.api_request, + mock.sentinel.path, + mock.sentinel.item_to_value) + iterator._HTTP_METHOD = 'NOT-A-VERB' + + with pytest.raises(ValueError): + iterator._get_next_page_response() + + +class TestGRPCIterator(object): + + def test_constructor(self): + client = mock.sentinel.client + items_field = 'items' + iterator = page_iterator.GRPCIterator( + client, mock.sentinel.method, mock.sentinel.request, items_field) + + assert not iterator._started + assert iterator.client is client + assert iterator.max_results is None + assert iterator._method == mock.sentinel.method + assert iterator._request == mock.sentinel.request + assert iterator._items_field == items_field + assert iterator._item_to_value is page_iterator._item_to_value_identity + assert (iterator._request_token_field == + page_iterator.GRPCIterator._DEFAULT_REQUEST_TOKEN_FIELD) + assert (iterator._response_token_field == + page_iterator.GRPCIterator._DEFAULT_RESPONSE_TOKEN_FIELD) + # Changing attributes. + assert iterator.page_number == 0 + assert iterator.next_page_token is None + assert iterator.num_results == 0 + + def test_constructor_options(self): + client = mock.sentinel.client + items_field = 'items' + request_field = 'request' + response_field = 'response' + iterator = page_iterator.GRPCIterator( + client, mock.sentinel.method, mock.sentinel.request, items_field, + item_to_value=mock.sentinel.item_to_value, + request_token_field=request_field, + response_token_field=response_field, + max_results=42) + + assert iterator.client is client + assert iterator.max_results == 42 + assert iterator._method == mock.sentinel.method + assert iterator._request == mock.sentinel.request + assert iterator._items_field == items_field + assert iterator._item_to_value is mock.sentinel.item_to_value + assert iterator._request_token_field == request_field + assert iterator._response_token_field == response_field + + def test_iterate(self): + request = mock.Mock(spec=['page_token'], page_token=None) + response1 = mock.Mock(items=['a', 'b'], next_page_token='1') + response2 = mock.Mock(items=['c'], next_page_token='2') + response3 = mock.Mock(items=['d'], next_page_token='') + method = mock.Mock(side_effect=[response1, response2, response3]) + iterator = page_iterator.GRPCIterator( + mock.sentinel.client, method, request, 'items') + + assert iterator.num_results == 0 + + items = list(iterator) + assert items == ['a', 'b', 'c', 'd'] + + method.assert_called_with(request) + assert method.call_count == 3 + assert request.page_token == '2' + + def test_iterate_with_max_results(self): + request = mock.Mock(spec=['page_token'], page_token=None) + response1 = mock.Mock(items=['a', 'b'], next_page_token='1') + response2 = mock.Mock(items=['c'], next_page_token='2') + response3 = mock.Mock(items=['d'], next_page_token='') + method = mock.Mock(side_effect=[response1, response2, response3]) + iterator = page_iterator.GRPCIterator( + mock.sentinel.client, method, request, 'items', max_results=3) + + assert iterator.num_results == 0 + + items = list(iterator) + + assert items == ['a', 'b', 'c'] + assert iterator.num_results == 3 + + method.assert_called_with(request) + assert method.call_count == 2 + assert request.page_token is '1' + + +class GAXPageIterator(object): + """Fake object that matches gax.PageIterator""" + def __init__(self, pages, page_token=None): + self._pages = iter(pages) + self.page_token = page_token + + def next(self): + return six.next(self._pages) + + __next__ = next + + +class TestGAXIterator(object): + + def test_constructor(self): + client = mock.sentinel.client + token = 'zzzyy78kl' + page_iter = GAXPageIterator((), page_token=token) + item_to_value = page_iterator._item_to_value_identity + max_results = 1337 + iterator = page_iterator._GAXIterator( + client, page_iter, item_to_value, max_results=max_results) + + assert not iterator._started + assert iterator.client is client + assert iterator._item_to_value is item_to_value + assert iterator.max_results == max_results + assert iterator._gax_page_iter is page_iter + # Changing attributes. + assert iterator.page_number == 0 + assert iterator.next_page_token == token + assert iterator.num_results == 0 + + def test__next_page(self): + page_items = (29, 31) + page_token = '2sde98ds2s0hh' + page_iter = GAXPageIterator([page_items], page_token=page_token) + iterator = page_iterator._GAXIterator( + mock.sentinel.client, + page_iter, + page_iterator._item_to_value_identity) + + page = iterator._next_page() + + assert iterator.next_page_token == page_token + assert isinstance(page, page_iterator.Page) + assert list(page) == list(page_items) + + next_page = iterator._next_page() + + assert next_page is None diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py new file mode 100644 index 0000000..daeeeec --- /dev/null +++ b/tests/unit/test_path_template.py @@ -0,0 +1,90 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import unicode_literals + +import mock +import pytest + +from google.api_core import path_template + + +@pytest.mark.parametrize('tmpl, args, kwargs, expected_result', [ + # Basic positional params + ['/v1/*', ['a'], {}, '/v1/a'], + ['/v1/**', ['a/b'], {}, '/v1/a/b'], + ['/v1/*/*', ['a', 'b'], {}, '/v1/a/b'], + ['/v1/*/*/**', ['a', 'b', 'c/d'], {}, '/v1/a/b/c/d'], + # Basic named params + ['/v1/{name}', [], {'name': 'parent'}, '/v1/parent'], + ['/v1/{name=**}', [], {'name': 'parent/child'}, '/v1/parent/child'], + # Named params with a sub-template + ['/v1/{name=parent/*}', [], {'name': 'parent/child'}, '/v1/parent/child'], + ['/v1/{name=parent/**}', [], {'name': 'parent/child/object'}, + '/v1/parent/child/object'], + # Combining positional and named params + ['/v1/*/{name}', ['a'], {'name': 'parent'}, '/v1/a/parent'], + ['/v1/{name}/*', ['a'], {'name': 'parent'}, '/v1/parent/a'], + ['/v1/{parent}/*/{child}/*', ['a', 'b'], + {'parent': 'thor', 'child': 'thorson'}, '/v1/thor/a/thorson/b'], + ['/v1/{name}/**', ['a/b'], {'name': 'parent'}, '/v1/parent/a/b'], + # Combining positional and named params with sub-templates. + ['/v1/{name=parent/*}/*', ['a'], {'name': 'parent/child'}, + '/v1/parent/child/a'], + ['/v1/*/{name=parent/**}', ['a'], {'name': 'parent/child/object'}, + '/v1/a/parent/child/object'], +]) +def test_expand_success(tmpl, args, kwargs, expected_result): + result = path_template.expand(tmpl, *args, **kwargs) + assert result == expected_result + assert path_template.validate(tmpl, result) + + +@pytest.mark.parametrize('tmpl, args, kwargs, exc_match', [ + # Missing positional arg. + ['v1/*', [], {}, 'Positional'], + # Missing named arg. + ['v1/{name}', [], {}, 'Named'], +]) +def test_expanded_failure(tmpl, args, kwargs, exc_match): + with pytest.raises(ValueError, match=exc_match): + path_template.expand(tmpl, *args, **kwargs) + + +@pytest.mark.parametrize('tmpl, path', [ + # Single segment template, but multi segment value + ['v1/*', 'v1/a/b'], + ['v1/*/*', 'v1/a/b/c'], + # Single segement named template, but multi segment value + ['v1/{name}', 'v1/a/b'], + ['v1/{name}/{value}', 'v1/a/b/c'], + # Named value with a sub-template but invalid value + ['v1/{name=parent/*}', 'v1/grandparent/child'], +]) +def test_validate_failure(tmpl, path): + assert not path_template.validate(tmpl, path) + + +def test__expand_variable_match_unexpected(): + match = mock.Mock(spec=['group']) + match.group.return_value = None + with pytest.raises(ValueError, match='Unknown'): + path_template._expand_variable_match([], {}, match) + + +def test__replace_variable_with_pattern(): + match = mock.Mock(spec=['group']) + match.group.return_value = None + with pytest.raises(ValueError, match='Unknown'): + path_template._replace_variable_with_pattern(match) diff --git a/tests/unit/test_protobuf_helpers.py b/tests/unit/test_protobuf_helpers.py new file mode 100644 index 0000000..6233536 --- /dev/null +++ b/tests/unit/test_protobuf_helpers.py @@ -0,0 +1,37 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from google.api_core import protobuf_helpers +from google.protobuf import any_pb2 +from google.type import date_pb2 +from google.type import timeofday_pb2 + + +def test_from_any_pb_success(): + in_message = date_pb2.Date(year=1990) + in_message_any = any_pb2.Any() + in_message_any.Pack(in_message) + out_message = protobuf_helpers.from_any_pb(date_pb2.Date, in_message_any) + + assert in_message == out_message + + +def test_from_any_pb_failure(): + in_message = any_pb2.Any() + in_message.Pack(date_pb2.Date(year=1990)) + + with pytest.raises(TypeError): + protobuf_helpers.from_any_pb(timeofday_pb2.TimeOfDay, in_message) diff --git a/tests/unit/test_retry.py b/tests/unit/test_retry.py new file mode 100644 index 0000000..a671ad3 --- /dev/null +++ b/tests/unit/test_retry.py @@ -0,0 +1,255 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import itertools +import re + +import mock +import pytest + +from google.api_core import exceptions +from google.api_core import retry + + +def test_if_exception_type(): + predicate = retry.if_exception_type(ValueError) + + assert predicate(ValueError()) + assert not predicate(TypeError()) + + +def test_if_exception_type_multiple(): + predicate = retry.if_exception_type(ValueError, TypeError) + + assert predicate(ValueError()) + assert predicate(TypeError()) + assert not predicate(RuntimeError()) + + +def test_if_transient_error(): + assert retry.if_transient_error(exceptions.InternalServerError('')) + assert retry.if_transient_error(exceptions.TooManyRequests('')) + assert not retry.if_transient_error(exceptions.InvalidArgument('')) + + +# Make uniform return half of its maximum, which will be the calculated +# sleep time. +@mock.patch('random.uniform', autospec=True, side_effect=lambda m, n: n/2.0) +def test_exponential_sleep_generator_base_2(uniform): + gen = retry.exponential_sleep_generator( + 1, 60, multiplier=2) + + result = list(itertools.islice(gen, 8)) + assert result == [1, 2, 4, 8, 16, 32, 60, 60] + + +@mock.patch('time.sleep', autospec=True) +@mock.patch( + 'google.api_core.datetime_helpers.utcnow', + return_value=datetime.datetime.min, + autospec=True) +def test_retry_target_success(utcnow, sleep): + predicate = retry.if_exception_type(ValueError) + call_count = [0] + + def target(): + call_count[0] += 1 + if call_count[0] < 3: + raise ValueError() + return 42 + + result = retry.retry_target(target, predicate, range(10), None) + + assert result == 42 + assert call_count[0] == 3 + sleep.assert_has_calls([mock.call(0), mock.call(1)]) + + +@mock.patch('time.sleep', autospec=True) +@mock.patch( + 'google.api_core.datetime_helpers.utcnow', + return_value=datetime.datetime.min, + autospec=True) +def test_retry_target_w_on_error(utcnow, sleep): + predicate = retry.if_exception_type(ValueError) + call_count = {'target': 0} + to_raise = ValueError() + + def target(): + call_count['target'] += 1 + if call_count['target'] < 3: + raise to_raise + return 42 + + on_error = mock.Mock() + + result = retry.retry_target( + target, predicate, range(10), None, on_error=on_error) + + assert result == 42 + assert call_count['target'] == 3 + + on_error.assert_has_calls([mock.call(to_raise), mock.call(to_raise)]) + sleep.assert_has_calls([mock.call(0), mock.call(1)]) + + +@mock.patch('time.sleep', autospec=True) +@mock.patch( + 'google.api_core.datetime_helpers.utcnow', + return_value=datetime.datetime.min, + autospec=True) +def test_retry_target_non_retryable_error(utcnow, sleep): + predicate = retry.if_exception_type(ValueError) + exception = TypeError() + target = mock.Mock(side_effect=exception) + + with pytest.raises(TypeError) as exc_info: + retry.retry_target(target, predicate, range(10), None) + + assert exc_info.value == exception + sleep.assert_not_called() + + +@mock.patch('time.sleep', autospec=True) +@mock.patch( + 'google.api_core.datetime_helpers.utcnow', autospec=True) +def test_retry_target_deadline_exceeded(utcnow, sleep): + predicate = retry.if_exception_type(ValueError) + exception = ValueError('meep') + target = mock.Mock(side_effect=exception) + # Setup the timeline so that the first call takes 5 seconds but the second + # call takes 6, which puts the retry over the deadline. + utcnow.side_effect = [ + # The first call to utcnow establishes the start of the timeline. + datetime.datetime.min, + datetime.datetime.min + datetime.timedelta(seconds=5), + datetime.datetime.min + datetime.timedelta(seconds=11)] + + with pytest.raises(exceptions.RetryError) as exc_info: + retry.retry_target(target, predicate, range(10), deadline=10) + + assert exc_info.value.cause == exception + assert exc_info.match('Deadline of 10.0s exceeded') + assert exc_info.match('last exception: meep') + assert target.call_count == 2 + + +def test_retry_target_bad_sleep_generator(): + with pytest.raises(ValueError, match='Sleep generator'): + retry.retry_target( + mock.sentinel.target, mock.sentinel.predicate, [], None) + + +class TestRetry(object): + def test_constructor_defaults(self): + retry_ = retry.Retry() + assert retry_._predicate == retry.if_transient_error + assert retry_._initial == 1 + assert retry_._maximum == 60 + assert retry_._multiplier == 2 + assert retry_._deadline == 120 + + def test_constructor_options(self): + retry_ = retry.Retry( + predicate=mock.sentinel.predicate, + initial=1, + maximum=2, + multiplier=3, + deadline=4, + ) + assert retry_._predicate == mock.sentinel.predicate + assert retry_._initial == 1 + assert retry_._maximum == 2 + assert retry_._multiplier == 3 + assert retry_._deadline == 4 + + def test_with_deadline(self): + retry_ = retry.Retry() + new_retry = retry_.with_deadline(42) + assert retry_ is not new_retry + assert new_retry._deadline == 42 + + def test_with_predicate(self): + retry_ = retry.Retry() + new_retry = retry_.with_predicate(mock.sentinel.predicate) + assert retry_ is not new_retry + assert new_retry._predicate == mock.sentinel.predicate + + def test_with_delay_noop(self): + retry_ = retry.Retry() + new_retry = retry_.with_delay() + assert retry_ is not new_retry + assert new_retry._initial == retry_._initial + assert new_retry._maximum == retry_._maximum + assert new_retry._multiplier == retry_._multiplier + + def test_with_delay(self): + retry_ = retry.Retry() + new_retry = retry_.with_delay( + initial=1, maximum=2, multiplier=3) + assert retry_ is not new_retry + assert new_retry._initial == 1 + assert new_retry._maximum == 2 + assert new_retry._multiplier == 3 + + def test___str__(self): + retry_ = retry.Retry() + assert re.match(( + r'<Retry predicate=<function.*?if_exception_type.*?>, ' + r'initial=1.0, maximum=60.0, multiplier=2.0, deadline=120.0>'), + str(retry_)) + + @mock.patch('time.sleep', autospec=True) + def test___call___and_execute_success(self, sleep): + retry_ = retry.Retry() + target = mock.Mock(spec=['__call__'], return_value=42) + # __name__ is needed by functools.partial. + target.__name__ = 'target' + + decorated = retry_(target) + target.assert_not_called() + + result = decorated('meep') + + assert result == 42 + target.assert_called_once_with('meep') + sleep.assert_not_called() + + # Make uniform return half of its maximum, which will be the calculated + # sleep time. + @mock.patch( + 'random.uniform', autospec=True, side_effect=lambda m, n: n/2.0) + @mock.patch('time.sleep', autospec=True) + def test___call___and_execute_retry(self, sleep, uniform): + + on_error = mock.Mock(spec=['__call__'], side_effect=[None]) + retry_ = retry.Retry( + predicate=retry.if_exception_type(ValueError), + ) + + target = mock.Mock(spec=['__call__'], side_effect=[ValueError(), 42]) + # __name__ is needed by functools.partial. + target.__name__ = 'target' + + decorated = retry_(target, on_error=on_error) + target.assert_not_called() + + result = decorated('meep') + + assert result == 42 + assert target.call_count == 2 + target.assert_has_calls([mock.call('meep'), mock.call('meep')]) + sleep.assert_called_once_with(retry_._initial) + assert on_error.call_count == 1 diff --git a/tests/unit/test_timeout.py b/tests/unit/test_timeout.py new file mode 100644 index 0000000..40caef4 --- /dev/null +++ b/tests/unit/test_timeout.py @@ -0,0 +1,132 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import itertools + +import mock + +from google.api_core import timeout + + +def test__exponential_timeout_generator_base_2(): + gen = timeout._exponential_timeout_generator( + 1.0, 60.0, 2.0, deadline=None) + + result = list(itertools.islice(gen, 8)) + assert result == [1, 2, 4, 8, 16, 32, 60, 60] + + +@mock.patch('google.api_core.datetime_helpers.utcnow', autospec=True) +def test__exponential_timeout_generator_base_deadline(utcnow): + # Make each successive call to utcnow() advance one second. + utcnow.side_effect = [ + datetime.datetime.min + datetime.timedelta(seconds=n) + for n in range(15)] + + gen = timeout._exponential_timeout_generator( + 1.0, 60.0, 2.0, deadline=30.0) + + result = list(itertools.islice(gen, 14)) + # Should grow until the cumulative time is > 30s, then start decreasing as + # the cumulative time approaches 60s. + assert result == [1, 2, 4, 8, 16, 24, 23, 22, 21, 20, 19, 18, 17, 16] + + +class TestConstantTimeout(object): + + def test_constructor(self): + timeout_ = timeout.ConstantTimeout() + assert timeout_._timeout is None + + def test_constructor_args(self): + timeout_ = timeout.ConstantTimeout(42.0) + assert timeout_._timeout == 42.0 + + def test___str__(self): + timeout_ = timeout.ConstantTimeout(1) + assert str(timeout_) == '<ConstantTimeout timeout=1.0>' + + def test_apply(self): + target = mock.Mock(spec=['__call__', '__name__'], __name__='target') + timeout_ = timeout.ConstantTimeout(42.0) + wrapped = timeout_(target) + + wrapped() + + target.assert_called_once_with(timeout=42.0) + + def test_apply_passthrough(self): + target = mock.Mock(spec=['__call__', '__name__'], __name__='target') + timeout_ = timeout.ConstantTimeout(42.0) + wrapped = timeout_(target) + + wrapped(1, 2, meep='moop') + + target.assert_called_once_with(1, 2, meep='moop', timeout=42.0) + + +class TestExponentialTimeout(object): + + def test_constructor(self): + timeout_ = timeout.ExponentialTimeout() + assert timeout_._initial == timeout._DEFAULT_INITIAL_TIMEOUT + assert timeout_._maximum == timeout._DEFAULT_MAXIMUM_TIMEOUT + assert timeout_._multiplier == timeout._DEFAULT_TIMEOUT_MULTIPLIER + assert timeout_._deadline == timeout._DEFAULT_DEADLINE + + def test_constructor_args(self): + timeout_ = timeout.ExponentialTimeout(1, 2, 3, 4) + assert timeout_._initial == 1 + assert timeout_._maximum == 2 + assert timeout_._multiplier == 3 + assert timeout_._deadline == 4 + + def test_with_timeout(self): + original_timeout = timeout.ExponentialTimeout() + timeout_ = original_timeout.with_deadline(42) + assert original_timeout is not timeout_ + assert timeout_._initial == timeout._DEFAULT_INITIAL_TIMEOUT + assert timeout_._maximum == timeout._DEFAULT_MAXIMUM_TIMEOUT + assert timeout_._multiplier == timeout._DEFAULT_TIMEOUT_MULTIPLIER + assert timeout_._deadline == 42 + + def test___str__(self): + timeout_ = timeout.ExponentialTimeout(1, 2, 3, 4) + assert str(timeout_) == ( + '<ExponentialTimeout initial=1.0, maximum=2.0, multiplier=3.0, ' + 'deadline=4.0>') + + def test_apply(self): + target = mock.Mock(spec=['__call__', '__name__'], __name__='target') + timeout_ = timeout.ExponentialTimeout(1, 10, 2) + wrapped = timeout_(target) + + wrapped() + target.assert_called_with(timeout=1) + + wrapped() + target.assert_called_with(timeout=2) + + wrapped() + target.assert_called_with(timeout=4) + + def test_apply_passthrough(self): + target = mock.Mock(spec=['__call__', '__name__'], __name__='target') + timeout_ = timeout.ExponentialTimeout(42.0, 100, 2) + wrapped = timeout_(target) + + wrapped(1, 2, meep='moop') + + target.assert_called_once_with(1, 2, meep='moop', timeout=42.0) |