aboutsummaryrefslogtreecommitdiff
path: root/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit')
-rw-r--r--tests/unit/__init__.py0
-rw-r--r--tests/unit/future/__init__.py0
-rw-r--r--tests/unit/future/test__helpers.py37
-rw-r--r--tests/unit/future/test_polling.py157
-rw-r--r--tests/unit/gapic/test_config.py89
-rw-r--r--tests/unit/gapic/test_method.py226
-rw-r--r--tests/unit/operations_v1/__init__.py0
-rw-r--r--tests/unit/operations_v1/test_operations_client.py101
-rw-r--r--tests/unit/test_datetime_helpers.py22
-rw-r--r--tests/unit/test_exceptions.py201
-rw-r--r--tests/unit/test_general_helpers.py43
-rw-r--r--tests/unit/test_grpc_helpers.py171
-rw-r--r--tests/unit/test_operation.py223
-rw-r--r--tests/unit/test_page_iterator.py545
-rw-r--r--tests/unit/test_path_template.py90
-rw-r--r--tests/unit/test_protobuf_helpers.py37
-rw-r--r--tests/unit/test_retry.py255
-rw-r--r--tests/unit/test_timeout.py132
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)