# Copyright (C) The Arvados Authors. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 from __future__ import absolute_import from builtins import str from builtins import range import arvados import collections import httplib2 import itertools import json import mimetypes import os import socket import string import unittest import mock from . import run_test_server from apiclient import errors as apiclient_errors from apiclient import http as apiclient_http from arvados.api import OrderedJsonModel, RETRY_DELAY_INITIAL, RETRY_DELAY_BACKOFF, RETRY_COUNT from .arvados_testutil import fake_httplib2_response, queue_with if not mimetypes.inited: mimetypes.init() class ArvadosApiTest(run_test_server.TestCaseWithServers): MAIN_SERVER = {} ERROR_HEADERS = {'Content-Type': mimetypes.types_map['.json']} def api_error_response(self, code, *errors): return (fake_httplib2_response(code, **self.ERROR_HEADERS), json.dumps({'errors': errors, 'error_token': '1234567890+12345678'}).encode()) def test_new_api_objects_with_cache(self): clients = [arvados.api('v1', cache=True) for index in [0, 1]] self.assertIsNot(*clients) def test_empty_list(self): answer = arvados.api('v1').humans().list( filters=[['uuid', '=', None]]).execute() self.assertEqual(answer['items_available'], len(answer['items'])) def test_nonempty_list(self): answer = arvados.api('v1').collections().list().execute() self.assertNotEqual(0, answer['items_available']) self.assertNotEqual(0, len(answer['items'])) def test_timestamp_inequality_filter(self): api = arvados.api('v1') new_item = api.specimens().create(body={}).execute() for operator, should_include in [ ['<', False], ['>', False], ['<=', True], ['>=', True], ['=', True]]: response = api.specimens().list(filters=[ ['created_at', operator, new_item['created_at']], # Also filter by uuid to ensure (if it matches) it's on page 0 ['uuid', '=', new_item['uuid']]]).execute() uuids = [item['uuid'] for item in response['items']] did_include = new_item['uuid'] in uuids self.assertEqual( did_include, should_include, "'%s %s' filter should%s have matched '%s'" % ( operator, new_item['created_at'], ('' if should_include else ' not'), new_item['created_at'])) def test_exceptions_include_errors(self): mock_responses = { 'arvados.humans.get': self.api_error_response( 422, "Bad UUID format", "Bad output format"), } req_builder = apiclient_http.RequestMockBuilder(mock_responses) api = arvados.api('v1', requestBuilder=req_builder) with self.assertRaises(apiclient_errors.HttpError) as err_ctx: api.humans().get(uuid='xyz-xyz-abcdef').execute() err_s = str(err_ctx.exception) for msg in ["Bad UUID format", "Bad output format"]: self.assertIn(msg, err_s) @mock.patch('time.sleep') def test_exceptions_include_request_id(self, sleep): api = arvados.api('v1') api.request_id='fake-request-id' api._http.orig_http_request = mock.MagicMock() api._http.orig_http_request.side_effect = socket.error('mock error') caught = None try: api.users().current().execute() except Exception as e: caught = e self.assertRegex(str(caught), r'fake-request-id') def test_exceptions_without_errors_have_basic_info(self): mock_responses = { 'arvados.humans.delete': ( fake_httplib2_response(500, **self.ERROR_HEADERS), b"") } req_builder = apiclient_http.RequestMockBuilder(mock_responses) api = arvados.api('v1', requestBuilder=req_builder) with self.assertRaises(apiclient_errors.HttpError) as err_ctx: api.humans().delete(uuid='xyz-xyz-abcdef').execute() self.assertIn("500", str(err_ctx.exception)) def test_request_too_large(self): api = arvados.api('v1') maxsize = api._rootDesc.get('maxRequestSize', 0) with self.assertRaises(apiclient_errors.MediaUploadSizeError): text = "X" * maxsize arvados.api('v1').collections().create(body={"manifest_text": text}).execute() def test_default_request_timeout(self): api = arvados.api('v1') self.assertEqual(api._http.timeout, 300, "Default timeout value should be 300") def test_custom_request_timeout(self): api = arvados.api('v1', timeout=1234) self.assertEqual(api._http.timeout, 1234, "Requested timeout value was 1234") def test_ordered_json_model(self): mock_responses = { 'arvados.humans.get': ( None, json.dumps(collections.OrderedDict( (c, int(c, 16)) for c in string.hexdigits )).encode(), ), } req_builder = apiclient_http.RequestMockBuilder(mock_responses) api = arvados.api('v1', requestBuilder=req_builder, model=OrderedJsonModel()) result = api.humans().get(uuid='test').execute() self.assertEqual(string.hexdigits, ''.join(list(result.keys()))) class RetryREST(unittest.TestCase): def setUp(self): self.api = arvados.api('v1') self.assertTrue(hasattr(self.api._http, 'orig_http_request'), "test doesn't know how to intercept HTTP requests") self.mock_response = {'user': 'person'} self.request_success = (fake_httplib2_response(200), json.dumps(self.mock_response)) self.api._http.orig_http_request = mock.MagicMock() # All requests succeed by default. Tests override as needed. self.api._http.orig_http_request.return_value = self.request_success @mock.patch('time.sleep') def test_socket_error_retry_get(self, sleep): self.api._http.orig_http_request.side_effect = ( socket.error('mock error'), self.request_success, ) self.assertEqual(self.api.users().current().execute(), self.mock_response) self.assertGreater(self.api._http.orig_http_request.call_count, 1, "client got the right response without retrying") self.assertEqual(sleep.call_args_list, [mock.call(RETRY_DELAY_INITIAL)]) @mock.patch('time.sleep') def test_same_automatic_request_id_on_retry(self, sleep): self.api._http.orig_http_request.side_effect = ( socket.error('mock error'), self.request_success, ) self.api.users().current().execute() calls = self.api._http.orig_http_request.call_args_list self.assertEqual(len(calls), 2) self.assertEqual( calls[0][1]['headers']['X-Request-Id'], calls[1][1]['headers']['X-Request-Id']) self.assertRegex(calls[0][1]['headers']['X-Request-Id'], r'^req-[a-z0-9]{20}$') @mock.patch('time.sleep') def test_provided_request_id_on_retry(self, sleep): self.api.request_id='fake-request-id' self.api._http.orig_http_request.side_effect = ( socket.error('mock error'), self.request_success, ) self.api.users().current().execute() calls = self.api._http.orig_http_request.call_args_list self.assertEqual(len(calls), 2) for call in calls: self.assertEqual(call[1]['headers']['X-Request-Id'], 'fake-request-id') @mock.patch('time.sleep') def test_socket_error_retry_delay(self, sleep): self.api._http.orig_http_request.side_effect = socket.error('mock') self.api._http._retry_count = 3 with self.assertRaises(socket.error): self.api.users().current().execute() self.assertEqual(self.api._http.orig_http_request.call_count, 4) self.assertEqual(sleep.call_args_list, [ mock.call(RETRY_DELAY_INITIAL), mock.call(RETRY_DELAY_INITIAL * RETRY_DELAY_BACKOFF), mock.call(RETRY_DELAY_INITIAL * RETRY_DELAY_BACKOFF**2), ]) @mock.patch('time.time', side_effect=[i*2**20 for i in range(99)]) def test_close_old_connections_non_retryable(self, sleep): self._test_connection_close(expect=1) @mock.patch('time.time', side_effect=itertools.count()) def test_no_close_fresh_connections_non_retryable(self, sleep): self._test_connection_close(expect=0) @mock.patch('time.time', side_effect=itertools.count()) def test_override_max_idle_time(self, sleep): self.api._http._max_keepalive_idle = 0 self._test_connection_close(expect=1) def _test_connection_close(self, expect=0): # Do two POST requests. The second one must close all # connections +expect+ times. self.api.users().create(body={}).execute() mock_conns = {str(i): mock.MagicMock() for i in range(2)} self.api._http.connections = mock_conns.copy() self.api.users().create(body={}).execute() for c in mock_conns.values(): self.assertEqual(c.close.call_count, expect) @mock.patch('time.sleep') def test_socket_error_no_retry_post(self, sleep): self.api._http.orig_http_request.side_effect = ( socket.error('mock error'), self.request_success, ) with self.assertRaises(socket.error): self.api.users().create(body={}).execute() self.assertEqual(self.api._http.orig_http_request.call_count, 1, "client should try non-retryable method exactly once") self.assertEqual(sleep.call_args_list, []) if __name__ == '__main__': unittest.main()