blob: b47b9dc4fa35666a67e39d3a9d4ceb9a5a9ea45c [file] [log] [blame]
Craig Citro15744b12015-03-02 13:34:32 -08001#!/usr/bin/env python
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05002#
Craig Citro751b7fb2014-09-23 11:20:38 -07003# Copyright 2014 Google Inc. All Rights Reserved.
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05004#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Http tests
18
John Asmuth864311d2014-04-24 15:46:08 -040019Unit tests for the googleapiclient.http.
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050020"""
INADA Naokid898a372015-03-04 03:52:46 +090021from __future__ import absolute_import
22from six.moves import range
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050023
24__author__ = 'jcgregorio@google.com (Joe Gregorio)'
25
Pat Feratec6050872015-03-03 18:24:59 -080026from six import PY3
Pat Ferateed9affd2015-03-03 16:03:15 -080027from six import BytesIO, StringIO
28from io import FileIO
Pat Ferated5b61bd2015-03-03 16:04:11 -080029from six.moves.urllib.parse import urlencode
Pat Ferateed9affd2015-03-03 16:03:15 -080030
Joe Gregorio7cbceab2011-06-27 10:46:54 -040031# Do not remove the httplib2 import
32import httplib2
Joe Gregorio9086bd32013-06-14 16:32:05 -040033import logging
Joe Gregoriod0bd3882011-11-22 09:49:47 -050034import os
Pat Ferate497a90f2015-03-09 09:52:54 -070035import unittest2 as unittest
Joe Gregorio9086bd32013-06-14 16:32:05 -040036import random
Joe Gregorio9086bd32013-06-14 16:32:05 -040037import time
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050038
John Asmuth864311d2014-04-24 15:46:08 -040039from googleapiclient.discovery import build
40from googleapiclient.errors import BatchError
41from googleapiclient.errors import HttpError
42from googleapiclient.errors import InvalidChunkSizeError
43from googleapiclient.http import BatchHttpRequest
44from googleapiclient.http import HttpMock
45from googleapiclient.http import HttpMockSequence
46from googleapiclient.http import HttpRequest
47from googleapiclient.http import MAX_URI_LENGTH
48from googleapiclient.http import MediaFileUpload
49from googleapiclient.http import MediaInMemoryUpload
50from googleapiclient.http import MediaIoBaseDownload
51from googleapiclient.http import MediaIoBaseUpload
52from googleapiclient.http import MediaUpload
53from googleapiclient.http import _StreamSlice
54from googleapiclient.http import set_user_agent
55from googleapiclient.model import JsonModel
Joe Gregorio654f4a22012-02-09 14:15:44 -050056from oauth2client.client import Credentials
57
58
59class MockCredentials(Credentials):
60 """Mock class for all Credentials objects."""
61 def __init__(self, bearer_token):
62 super(MockCredentials, self).__init__()
63 self._authorized = 0
64 self._refreshed = 0
65 self._applied = 0
66 self._bearer_token = bearer_token
67
68 def authorize(self, http):
69 self._authorized += 1
70
71 request_orig = http.request
72
73 # The closure that will replace 'httplib2.Http.request'.
74 def new_request(uri, method='GET', body=None, headers=None,
75 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
76 connection_type=None):
77 # Modify the request headers to add the appropriate
78 # Authorization header.
79 if headers is None:
80 headers = {}
81 self.apply(headers)
82
83 resp, content = request_orig(uri, method, body, headers,
84 redirections, connection_type)
85
86 return resp, content
87
88 # Replace the request method with our own closure.
89 http.request = new_request
90
91 # Set credentials as a property of the request method.
92 setattr(http.request, 'credentials', self)
93
94 return http
95
96 def refresh(self, http):
97 self._refreshed += 1
98
99 def apply(self, headers):
100 self._applied += 1
101 headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500102
103
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500104DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
105
106
107def datafile(filename):
108 return os.path.join(DATA_DIR, filename)
109
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500110class TestUserAgent(unittest.TestCase):
111
112 def test_set_user_agent(self):
113 http = HttpMockSequence([
114 ({'status': '200'}, 'echo_request_headers'),
115 ])
116
117 http = set_user_agent(http, "my_app/5.5")
118 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500119 self.assertEqual('my_app/5.5', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500120
121 def test_set_user_agent_nested(self):
122 http = HttpMockSequence([
123 ({'status': '200'}, 'echo_request_headers'),
124 ])
125
126 http = set_user_agent(http, "my_app/5.5")
127 http = set_user_agent(http, "my_library/0.1")
128 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500129 self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500130
Joe Gregorio910b9b12012-06-12 09:36:30 -0400131
132class TestMediaUpload(unittest.TestCase):
133
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500134 def test_media_file_upload_to_from_json(self):
135 upload = MediaFileUpload(
136 datafile('small.png'), chunksize=500, resumable=True)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500137 self.assertEqual('image/png', upload.mimetype())
138 self.assertEqual(190, upload.size())
139 self.assertEqual(True, upload.resumable())
140 self.assertEqual(500, upload.chunksize())
Pat Ferate2b140222015-03-03 18:05:11 -0800141 self.assertEqual(b'PNG', upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500142
143 json = upload.to_json()
144 new_upload = MediaUpload.new_from_json(json)
145
Joe Gregorio654f4a22012-02-09 14:15:44 -0500146 self.assertEqual('image/png', new_upload.mimetype())
147 self.assertEqual(190, new_upload.size())
148 self.assertEqual(True, new_upload.resumable())
149 self.assertEqual(500, new_upload.chunksize())
Pat Ferate2b140222015-03-03 18:05:11 -0800150 self.assertEqual(b'PNG', new_upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500151
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400152 def test_media_file_upload_raises_on_invalid_chunksize(self):
153 self.assertRaises(InvalidChunkSizeError, MediaFileUpload,
154 datafile('small.png'), mimetype='image/png', chunksize=-2,
155 resumable=True)
156
Ali Afshar1cb6b672012-03-12 08:46:14 -0400157 def test_media_inmemory_upload(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800158 media = MediaInMemoryUpload(b'abcdef', mimetype='text/plain', chunksize=10,
Ali Afshar1cb6b672012-03-12 08:46:14 -0400159 resumable=True)
160 self.assertEqual('text/plain', media.mimetype())
161 self.assertEqual(10, media.chunksize())
162 self.assertTrue(media.resumable())
Pat Ferate2b140222015-03-03 18:05:11 -0800163 self.assertEqual(b'bc', media.getbytes(1, 2))
Ali Afshar1cb6b672012-03-12 08:46:14 -0400164 self.assertEqual(6, media.size())
165
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500166 def test_http_request_to_from_json(self):
167
168 def _postproc(*kwargs):
169 pass
170
171 http = httplib2.Http()
172 media_upload = MediaFileUpload(
173 datafile('small.png'), chunksize=500, resumable=True)
174 req = HttpRequest(
175 http,
176 _postproc,
177 'http://example.com',
178 method='POST',
179 body='{}',
180 headers={'content-type': 'multipart/related; boundary="---flubber"'},
181 methodId='foo',
182 resumable=media_upload)
183
184 json = req.to_json()
185 new_req = HttpRequest.from_json(json, http, _postproc)
186
Joe Gregorio654f4a22012-02-09 14:15:44 -0500187 self.assertEqual({'content-type':
188 'multipart/related; boundary="---flubber"'},
189 new_req.headers)
190 self.assertEqual('http://example.com', new_req.uri)
191 self.assertEqual('{}', new_req.body)
192 self.assertEqual(http, new_req.http)
193 self.assertEqual(media_upload.to_json(), new_req.resumable.to_json())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500194
Joe Gregorio9086bd32013-06-14 16:32:05 -0400195 self.assertEqual(random.random, new_req._rand)
196 self.assertEqual(time.sleep, new_req._sleep)
197
Joe Gregorio910b9b12012-06-12 09:36:30 -0400198
199class TestMediaIoBaseUpload(unittest.TestCase):
200
201 def test_media_io_base_upload_from_file_io(self):
Pat Ferateed9affd2015-03-03 16:03:15 -0800202 fd = FileIO(datafile('small.png'), 'r')
203 upload = MediaIoBaseUpload(
204 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
205 self.assertEqual('image/png', upload.mimetype())
206 self.assertEqual(190, upload.size())
207 self.assertEqual(True, upload.resumable())
208 self.assertEqual(500, upload.chunksize())
Pat Ferate2b140222015-03-03 18:05:11 -0800209 self.assertEqual(b'PNG', upload.getbytes(1, 3))
Joe Gregorio910b9b12012-06-12 09:36:30 -0400210
211 def test_media_io_base_upload_from_file_object(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800212 f = open(datafile('small.png'), 'rb')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400213 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400214 fd=f, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400215 self.assertEqual('image/png', upload.mimetype())
216 self.assertEqual(190, upload.size())
217 self.assertEqual(True, upload.resumable())
218 self.assertEqual(500, upload.chunksize())
Pat Ferate2b140222015-03-03 18:05:11 -0800219 self.assertEqual(b'PNG', upload.getbytes(1, 3))
Joe Gregorio910b9b12012-06-12 09:36:30 -0400220 f.close()
221
222 def test_media_io_base_upload_serializable(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800223 f = open(datafile('small.png'), 'rb')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400224 upload = MediaIoBaseUpload(fd=f, mimetype='image/png')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400225
226 try:
227 json = upload.to_json()
228 self.fail('MediaIoBaseUpload should not be serializable.')
229 except NotImplementedError:
230 pass
231
Pat Feratec6050872015-03-03 18:24:59 -0800232 @unittest.skipIf(PY3, 'Strings and Bytes are different types')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400233 def test_media_io_base_upload_from_string_io(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800234 f = open(datafile('small.png'), 'rb')
Pat Ferateed9affd2015-03-03 16:03:15 -0800235 fd = StringIO(f.read())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400236 f.close()
237
238 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400239 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400240 self.assertEqual('image/png', upload.mimetype())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400241 self.assertEqual(190, upload.size())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400242 self.assertEqual(True, upload.resumable())
243 self.assertEqual(500, upload.chunksize())
Pat Ferate2b140222015-03-03 18:05:11 -0800244 self.assertEqual(b'PNG', upload.getbytes(1, 3))
Joe Gregorio910b9b12012-06-12 09:36:30 -0400245 f.close()
246
247 def test_media_io_base_upload_from_bytes(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800248 f = open(datafile('small.png'), 'rb')
Pat Ferateed9affd2015-03-03 16:03:15 -0800249 fd = BytesIO(f.read())
250 upload = MediaIoBaseUpload(
251 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
252 self.assertEqual('image/png', upload.mimetype())
253 self.assertEqual(190, upload.size())
254 self.assertEqual(True, upload.resumable())
255 self.assertEqual(500, upload.chunksize())
Pat Ferate2b140222015-03-03 18:05:11 -0800256 self.assertEqual(b'PNG', upload.getbytes(1, 3))
Joe Gregorio910b9b12012-06-12 09:36:30 -0400257
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400258 def test_media_io_base_upload_raises_on_invalid_chunksize(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800259 f = open(datafile('small.png'), 'rb')
Pat Ferateed9affd2015-03-03 16:03:15 -0800260 fd = BytesIO(f.read())
261 self.assertRaises(InvalidChunkSizeError, MediaIoBaseUpload,
262 fd, 'image/png', chunksize=-2, resumable=True)
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400263
264 def test_media_io_base_upload_streamable(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800265 fd = BytesIO(b'stuff')
Pat Ferateed9affd2015-03-03 16:03:15 -0800266 upload = MediaIoBaseUpload(
267 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
268 self.assertEqual(True, upload.has_stream())
269 self.assertEqual(fd, upload.stream())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400270
Joe Gregorio9086bd32013-06-14 16:32:05 -0400271 def test_media_io_base_next_chunk_retries(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800272 f = open(datafile('small.png'), 'rb')
Pat Ferateed9affd2015-03-03 16:03:15 -0800273 fd = BytesIO(f.read())
Joe Gregorio9086bd32013-06-14 16:32:05 -0400274 upload = MediaIoBaseUpload(
275 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
276
277 # Simulate 5XXs for both the request that creates the resumable upload and
278 # the upload itself.
279 http = HttpMockSequence([
280 ({'status': '500'}, ''),
281 ({'status': '500'}, ''),
282 ({'status': '503'}, ''),
283 ({'status': '200', 'location': 'location'}, ''),
284 ({'status': '500'}, ''),
285 ({'status': '500'}, ''),
286 ({'status': '503'}, ''),
287 ({'status': '200'}, '{}'),
288 ])
289
290 model = JsonModel()
291 uri = u'https://www.googleapis.com/someapi/v1/upload/?foo=bar'
292 method = u'POST'
293 request = HttpRequest(
294 http,
295 model.response,
296 uri,
297 method=method,
298 headers={},
299 resumable=upload)
300
301 sleeptimes = []
302 request._sleep = lambda x: sleeptimes.append(x)
303 request._rand = lambda: 10
304
305 request.execute(num_retries=3)
306 self.assertEqual([20, 40, 80, 20, 40, 80], sleeptimes)
307
Joe Gregorio910b9b12012-06-12 09:36:30 -0400308
Joe Gregorio708388c2012-06-15 13:43:04 -0400309class TestMediaIoBaseDownload(unittest.TestCase):
310
311 def setUp(self):
312 http = HttpMock(datafile('zoo.json'), {'status': '200'})
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400313 zoo = build('zoo', 'v1', http=http)
Joe Gregorio708388c2012-06-15 13:43:04 -0400314 self.request = zoo.animals().get_media(name='Lion')
Pat Ferateed9affd2015-03-03 16:03:15 -0800315 self.fd = BytesIO()
Joe Gregorio708388c2012-06-15 13:43:04 -0400316
317 def test_media_io_base_download(self):
318 self.request.http = HttpMockSequence([
319 ({'status': '200',
Pat Ferate2b140222015-03-03 18:05:11 -0800320 'content-range': '0-2/5'}, b'123'),
Joe Gregorio708388c2012-06-15 13:43:04 -0400321 ({'status': '200',
Pat Ferate2b140222015-03-03 18:05:11 -0800322 'content-range': '3-4/5'}, b'45'),
Joe Gregorio708388c2012-06-15 13:43:04 -0400323 ])
Joe Gregorio97ef1cc2013-06-13 14:47:10 -0400324 self.assertEqual(True, self.request.http.follow_redirects)
Joe Gregorio708388c2012-06-15 13:43:04 -0400325
326 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400327 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400328
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400329 self.assertEqual(self.fd, download._fd)
330 self.assertEqual(3, download._chunksize)
331 self.assertEqual(0, download._progress)
332 self.assertEqual(None, download._total_size)
333 self.assertEqual(False, download._done)
334 self.assertEqual(self.request.uri, download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400335
336 status, done = download.next_chunk()
337
Pat Ferate2b140222015-03-03 18:05:11 -0800338 self.assertEqual(self.fd.getvalue(), b'123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400339 self.assertEqual(False, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400340 self.assertEqual(3, download._progress)
341 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400342 self.assertEqual(3, status.resumable_progress)
343
344 status, done = download.next_chunk()
345
Pat Ferate2b140222015-03-03 18:05:11 -0800346 self.assertEqual(self.fd.getvalue(), b'12345')
Joe Gregorio708388c2012-06-15 13:43:04 -0400347 self.assertEqual(True, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400348 self.assertEqual(5, download._progress)
349 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400350
351 def test_media_io_base_download_handle_redirects(self):
352 self.request.http = HttpMockSequence([
Joe Gregorio238feb72013-06-19 13:15:31 -0400353 ({'status': '200',
Pat Ferate2b140222015-03-03 18:05:11 -0800354 'content-location': 'https://secure.example.net/lion'}, b''),
Joe Gregorio708388c2012-06-15 13:43:04 -0400355 ({'status': '200',
Pat Ferate2b140222015-03-03 18:05:11 -0800356 'content-range': '0-2/5'}, b'abc'),
Joe Gregorio708388c2012-06-15 13:43:04 -0400357 ])
358
359 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400360 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400361
362 status, done = download.next_chunk()
363
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400364 self.assertEqual('https://secure.example.net/lion', download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400365
366 def test_media_io_base_download_handle_4xx(self):
367 self.request.http = HttpMockSequence([
368 ({'status': '400'}, ''),
369 ])
370
371 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400372 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400373
374 try:
375 status, done = download.next_chunk()
376 self.fail('Should raise an exception')
377 except HttpError:
378 pass
379
380 # Even after raising an exception we can pick up where we left off.
381 self.request.http = HttpMockSequence([
382 ({'status': '200',
Pat Ferate2b140222015-03-03 18:05:11 -0800383 'content-range': '0-2/5'}, b'123'),
Joe Gregorio708388c2012-06-15 13:43:04 -0400384 ])
385
386 status, done = download.next_chunk()
387
Pat Ferate2b140222015-03-03 18:05:11 -0800388 self.assertEqual(self.fd.getvalue(), b'123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400389
Joe Gregorio9086bd32013-06-14 16:32:05 -0400390 def test_media_io_base_download_retries_5xx(self):
391 self.request.http = HttpMockSequence([
392 ({'status': '500'}, ''),
393 ({'status': '500'}, ''),
394 ({'status': '500'}, ''),
395 ({'status': '200',
Pat Ferate2b140222015-03-03 18:05:11 -0800396 'content-range': '0-2/5'}, b'123'),
Joe Gregorio9086bd32013-06-14 16:32:05 -0400397 ({'status': '503'}, ''),
398 ({'status': '503'}, ''),
399 ({'status': '503'}, ''),
400 ({'status': '200',
Pat Ferate2b140222015-03-03 18:05:11 -0800401 'content-range': '3-4/5'}, b'45'),
Joe Gregorio9086bd32013-06-14 16:32:05 -0400402 ])
403
404 download = MediaIoBaseDownload(
405 fd=self.fd, request=self.request, chunksize=3)
406
407 self.assertEqual(self.fd, download._fd)
408 self.assertEqual(3, download._chunksize)
409 self.assertEqual(0, download._progress)
410 self.assertEqual(None, download._total_size)
411 self.assertEqual(False, download._done)
412 self.assertEqual(self.request.uri, download._uri)
413
414 # Set time.sleep and random.random stubs.
415 sleeptimes = []
416 download._sleep = lambda x: sleeptimes.append(x)
417 download._rand = lambda: 10
418
419 status, done = download.next_chunk(num_retries=3)
420
421 # Check for exponential backoff using the rand function above.
422 self.assertEqual([20, 40, 80], sleeptimes)
423
Pat Ferate2b140222015-03-03 18:05:11 -0800424 self.assertEqual(self.fd.getvalue(), b'123')
Joe Gregorio9086bd32013-06-14 16:32:05 -0400425 self.assertEqual(False, done)
426 self.assertEqual(3, download._progress)
427 self.assertEqual(5, download._total_size)
428 self.assertEqual(3, status.resumable_progress)
429
430 # Reset time.sleep stub.
431 del sleeptimes[0:len(sleeptimes)]
432
433 status, done = download.next_chunk(num_retries=3)
434
435 # Check for exponential backoff using the rand function above.
436 self.assertEqual([20, 40, 80], sleeptimes)
437
Pat Ferate2b140222015-03-03 18:05:11 -0800438 self.assertEqual(self.fd.getvalue(), b'12345')
Joe Gregorio9086bd32013-06-14 16:32:05 -0400439 self.assertEqual(True, done)
440 self.assertEqual(5, download._progress)
441 self.assertEqual(5, download._total_size)
442
Joe Gregorio66f57522011-11-30 11:00:00 -0500443EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
444Content-Type: application/json
445MIME-Version: 1.0
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500446Host: www.googleapis.com
447content-length: 2\r\n\r\n{}"""
448
449
450NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
451Content-Type: application/json
452MIME-Version: 1.0
453Host: www.googleapis.com
454content-length: 0\r\n\r\n"""
Joe Gregorio66f57522011-11-30 11:00:00 -0500455
456
457RESPONSE = """HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400458Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500459Content-Length: 14
460ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
461
462
463BATCH_RESPONSE = """--batch_foobarbaz
464Content-Type: application/http
465Content-Transfer-Encoding: binary
466Content-ID: <randomness+1>
467
468HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400469Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500470Content-Length: 14
471ETag: "etag/pony"\r\n\r\n{"foo": 42}
472
473--batch_foobarbaz
474Content-Type: application/http
475Content-Transfer-Encoding: binary
476Content-ID: <randomness+2>
477
478HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400479Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500480Content-Length: 14
481ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
482--batch_foobarbaz--"""
483
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500484
Joe Gregorio3fb93672012-07-25 11:31:11 -0400485BATCH_ERROR_RESPONSE = """--batch_foobarbaz
486Content-Type: application/http
487Content-Transfer-Encoding: binary
488Content-ID: <randomness+1>
489
490HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400491Content-Type: application/json
Joe Gregorio3fb93672012-07-25 11:31:11 -0400492Content-Length: 14
493ETag: "etag/pony"\r\n\r\n{"foo": 42}
494
495--batch_foobarbaz
496Content-Type: application/http
497Content-Transfer-Encoding: binary
498Content-ID: <randomness+2>
499
500HTTP/1.1 403 Access Not Configured
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400501Content-Type: application/json
502Content-Length: 245
503ETag: "etag/sheep"\r\n\r\n{
Joe Gregorio3fb93672012-07-25 11:31:11 -0400504 "error": {
505 "errors": [
506 {
507 "domain": "usageLimits",
508 "reason": "accessNotConfigured",
509 "message": "Access Not Configured",
510 "debugInfo": "QuotaState: BLOCKED"
511 }
512 ],
513 "code": 403,
514 "message": "Access Not Configured"
515 }
516}
517
518--batch_foobarbaz--"""
519
520
Joe Gregorio654f4a22012-02-09 14:15:44 -0500521BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
522Content-Type: application/http
523Content-Transfer-Encoding: binary
524Content-ID: <randomness+1>
525
Joe Gregorioc752e332012-07-11 14:43:52 -0400526HTTP/1.1 401 Authorization Required
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400527Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500528Content-Length: 14
529ETag: "etag/pony"\r\n\r\n{"error": {"message":
530 "Authorizaton failed."}}
531
532--batch_foobarbaz
533Content-Type: application/http
534Content-Transfer-Encoding: binary
535Content-ID: <randomness+2>
536
537HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400538Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500539Content-Length: 14
540ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
541--batch_foobarbaz--"""
542
543
544BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
545Content-Type: application/http
546Content-Transfer-Encoding: binary
547Content-ID: <randomness+1>
548
549HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400550Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500551Content-Length: 14
552ETag: "etag/pony"\r\n\r\n{"foo": 42}
553--batch_foobarbaz--"""
554
555class Callbacks(object):
556 def __init__(self):
557 self.responses = {}
558 self.exceptions = {}
559
560 def f(self, request_id, response, exception):
561 self.responses[request_id] = response
562 self.exceptions[request_id] = exception
563
564
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500565class TestHttpRequest(unittest.TestCase):
566 def test_unicode(self):
567 http = HttpMock(datafile('zoo.json'), headers={'status': '200'})
568 model = JsonModel()
569 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
570 method = u'POST'
571 request = HttpRequest(
572 http,
573 model.response,
574 uri,
575 method=method,
576 body=u'{}',
577 headers={'content-type': 'application/json'})
578 request.execute()
579 self.assertEqual(uri, http.uri)
580 self.assertEqual(str, type(http.uri))
581 self.assertEqual(method, http.method)
582 self.assertEqual(str, type(http.method))
583
Joe Gregorio9086bd32013-06-14 16:32:05 -0400584 def test_retry(self):
585 num_retries = 5
586 resp_seq = [({'status': '500'}, '')] * num_retries
587 resp_seq.append(({'status': '200'}, '{}'))
588
589 http = HttpMockSequence(resp_seq)
590 model = JsonModel()
591 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
592 method = u'POST'
593 request = HttpRequest(
594 http,
595 model.response,
596 uri,
597 method=method,
598 body=u'{}',
599 headers={'content-type': 'application/json'})
600
601 sleeptimes = []
602 request._sleep = lambda x: sleeptimes.append(x)
603 request._rand = lambda: 10
604
605 request.execute(num_retries=num_retries)
606
607 self.assertEqual(num_retries, len(sleeptimes))
INADA Naokid898a372015-03-04 03:52:46 +0900608 for retry_num in range(num_retries):
Joe Gregorio9086bd32013-06-14 16:32:05 -0400609 self.assertEqual(10 * 2**(retry_num + 1), sleeptimes[retry_num])
610
611 def test_no_retry_fails_fast(self):
612 http = HttpMockSequence([
613 ({'status': '500'}, ''),
614 ({'status': '200'}, '{}')
615 ])
616 model = JsonModel()
617 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
618 method = u'POST'
619 request = HttpRequest(
620 http,
621 model.response,
622 uri,
623 method=method,
624 body=u'{}',
625 headers={'content-type': 'application/json'})
626
627 request._rand = lambda: 1.0
628 request._sleep = lambda _: self.fail('sleep should not have been called.')
629
630 try:
631 request.execute()
632 self.fail('Should have raised an exception.')
633 except HttpError:
634 pass
635
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500636
Joe Gregorio66f57522011-11-30 11:00:00 -0500637class TestBatch(unittest.TestCase):
638
639 def setUp(self):
640 model = JsonModel()
641 self.request1 = HttpRequest(
642 None,
643 model.response,
644 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
645 method='POST',
646 body='{}',
647 headers={'content-type': 'application/json'})
648
649 self.request2 = HttpRequest(
650 None,
651 model.response,
652 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500653 method='GET',
654 body='',
Joe Gregorio66f57522011-11-30 11:00:00 -0500655 headers={'content-type': 'application/json'})
656
657
658 def test_id_to_from_content_id_header(self):
659 batch = BatchHttpRequest()
660 self.assertEquals('12', batch._header_to_id(batch._id_to_header('12')))
661
662 def test_invalid_content_id_header(self):
663 batch = BatchHttpRequest()
664 self.assertRaises(BatchError, batch._header_to_id, '[foo+x]')
665 self.assertRaises(BatchError, batch._header_to_id, 'foo+1')
666 self.assertRaises(BatchError, batch._header_to_id, '<foo>')
667
668 def test_serialize_request(self):
669 batch = BatchHttpRequest()
670 request = HttpRequest(
671 None,
672 None,
673 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
674 method='POST',
Pat Ferate2b140222015-03-03 18:05:11 -0800675 body=u'{}',
Joe Gregorio66f57522011-11-30 11:00:00 -0500676 headers={'content-type': 'application/json'},
677 methodId=None,
678 resumable=None)
679 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500680 self.assertEqual(EXPECTED.splitlines(), s)
Joe Gregorio66f57522011-11-30 11:00:00 -0500681
Joe Gregoriodd813822012-01-25 10:32:47 -0500682 def test_serialize_request_media_body(self):
683 batch = BatchHttpRequest()
Pat Ferate2b140222015-03-03 18:05:11 -0800684 f = open(datafile('small.png'), 'rb')
Joe Gregoriodd813822012-01-25 10:32:47 -0500685 body = f.read()
686 f.close()
687
688 request = HttpRequest(
689 None,
690 None,
691 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
692 method='POST',
693 body=body,
694 headers={'content-type': 'application/json'},
695 methodId=None,
696 resumable=None)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500697 # Just testing it shouldn't raise an exception.
Joe Gregoriodd813822012-01-25 10:32:47 -0500698 s = batch._serialize_request(request).splitlines()
699
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500700 def test_serialize_request_no_body(self):
701 batch = BatchHttpRequest()
702 request = HttpRequest(
703 None,
704 None,
705 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
706 method='POST',
Pat Ferate2b140222015-03-03 18:05:11 -0800707 body=b'',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500708 headers={'content-type': 'application/json'},
709 methodId=None,
710 resumable=None)
711 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500712 self.assertEqual(NO_BODY_EXPECTED.splitlines(), s)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500713
Joe Gregorio66f57522011-11-30 11:00:00 -0500714 def test_deserialize_response(self):
715 batch = BatchHttpRequest()
716 resp, content = batch._deserialize_response(RESPONSE)
717
Joe Gregorio654f4a22012-02-09 14:15:44 -0500718 self.assertEqual(200, resp.status)
719 self.assertEqual('OK', resp.reason)
720 self.assertEqual(11, resp.version)
721 self.assertEqual('{"answer": 42}', content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500722
723 def test_new_id(self):
724 batch = BatchHttpRequest()
725
726 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500727 self.assertEqual('1', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500728
729 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500730 self.assertEqual('2', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500731
732 batch.add(self.request1, request_id='3')
733
734 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500735 self.assertEqual('4', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500736
737 def test_add(self):
738 batch = BatchHttpRequest()
739 batch.add(self.request1, request_id='1')
740 self.assertRaises(KeyError, batch.add, self.request1, request_id='1')
741
742 def test_add_fail_for_resumable(self):
743 batch = BatchHttpRequest()
744
745 upload = MediaFileUpload(
746 datafile('small.png'), chunksize=500, resumable=True)
747 self.request1.resumable = upload
748 self.assertRaises(BatchError, batch.add, self.request1, request_id='1')
749
750 def test_execute(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500751 batch = BatchHttpRequest()
752 callbacks = Callbacks()
753
754 batch.add(self.request1, callback=callbacks.f)
755 batch.add(self.request2, callback=callbacks.f)
756 http = HttpMockSequence([
757 ({'status': '200',
758 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
759 BATCH_RESPONSE),
760 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400761 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500762 self.assertEqual({'foo': 42}, callbacks.responses['1'])
763 self.assertEqual(None, callbacks.exceptions['1'])
764 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
765 self.assertEqual(None, callbacks.exceptions['2'])
Joe Gregorio66f57522011-11-30 11:00:00 -0500766
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500767 def test_execute_request_body(self):
768 batch = BatchHttpRequest()
769
770 batch.add(self.request1)
771 batch.add(self.request2)
772 http = HttpMockSequence([
773 ({'status': '200',
774 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
775 'echo_request_body'),
776 ])
777 try:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400778 batch.execute(http=http)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500779 self.fail('Should raise exception')
INADA Naokic1505df2014-08-20 15:19:53 +0900780 except BatchError as e:
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500781 boundary, _ = e.content.split(None, 1)
782 self.assertEqual('--', boundary[:2])
783 parts = e.content.split(boundary)
784 self.assertEqual(4, len(parts))
785 self.assertEqual('', parts[0])
Craig Citro4282aa32014-06-21 00:33:39 -0700786 self.assertEqual('--', parts[3].rstrip())
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500787 header = parts[1].splitlines()[1]
788 self.assertEqual('Content-Type: application/http', header)
789
Joe Gregorio654f4a22012-02-09 14:15:44 -0500790 def test_execute_refresh_and_retry_on_401(self):
791 batch = BatchHttpRequest()
792 callbacks = Callbacks()
793 cred_1 = MockCredentials('Foo')
794 cred_2 = MockCredentials('Bar')
795
796 http = HttpMockSequence([
797 ({'status': '200',
798 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
799 BATCH_RESPONSE_WITH_401),
800 ({'status': '200',
801 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
802 BATCH_SINGLE_RESPONSE),
803 ])
804
805 creds_http_1 = HttpMockSequence([])
806 cred_1.authorize(creds_http_1)
807
808 creds_http_2 = HttpMockSequence([])
809 cred_2.authorize(creds_http_2)
810
811 self.request1.http = creds_http_1
812 self.request2.http = creds_http_2
813
814 batch.add(self.request1, callback=callbacks.f)
815 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400816 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500817
818 self.assertEqual({'foo': 42}, callbacks.responses['1'])
819 self.assertEqual(None, callbacks.exceptions['1'])
820 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
821 self.assertEqual(None, callbacks.exceptions['2'])
822
823 self.assertEqual(1, cred_1._refreshed)
824 self.assertEqual(0, cred_2._refreshed)
825
826 self.assertEqual(1, cred_1._authorized)
827 self.assertEqual(1, cred_2._authorized)
828
829 self.assertEqual(1, cred_2._applied)
830 self.assertEqual(2, cred_1._applied)
831
832 def test_http_errors_passed_to_callback(self):
833 batch = BatchHttpRequest()
834 callbacks = Callbacks()
835 cred_1 = MockCredentials('Foo')
836 cred_2 = MockCredentials('Bar')
837
838 http = HttpMockSequence([
839 ({'status': '200',
840 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
841 BATCH_RESPONSE_WITH_401),
842 ({'status': '200',
843 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
844 BATCH_RESPONSE_WITH_401),
845 ])
846
847 creds_http_1 = HttpMockSequence([])
848 cred_1.authorize(creds_http_1)
849
850 creds_http_2 = HttpMockSequence([])
851 cred_2.authorize(creds_http_2)
852
853 self.request1.http = creds_http_1
854 self.request2.http = creds_http_2
855
856 batch.add(self.request1, callback=callbacks.f)
857 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400858 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500859
860 self.assertEqual(None, callbacks.responses['1'])
861 self.assertEqual(401, callbacks.exceptions['1'].resp.status)
Joe Gregorioc752e332012-07-11 14:43:52 -0400862 self.assertEqual(
863 'Authorization Required', callbacks.exceptions['1'].resp.reason)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500864 self.assertEqual({u'baz': u'qux'}, callbacks.responses['2'])
865 self.assertEqual(None, callbacks.exceptions['2'])
866
Joe Gregorio66f57522011-11-30 11:00:00 -0500867 def test_execute_global_callback(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500868 callbacks = Callbacks()
869 batch = BatchHttpRequest(callback=callbacks.f)
870
871 batch.add(self.request1)
872 batch.add(self.request2)
873 http = HttpMockSequence([
874 ({'status': '200',
875 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
876 BATCH_RESPONSE),
877 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400878 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500879 self.assertEqual({'foo': 42}, callbacks.responses['1'])
880 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500881
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400882 def test_execute_batch_http_error(self):
Joe Gregorio3fb93672012-07-25 11:31:11 -0400883 callbacks = Callbacks()
884 batch = BatchHttpRequest(callback=callbacks.f)
885
886 batch.add(self.request1)
887 batch.add(self.request2)
888 http = HttpMockSequence([
889 ({'status': '200',
890 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
891 BATCH_ERROR_RESPONSE),
892 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400893 batch.execute(http=http)
Joe Gregorio3fb93672012-07-25 11:31:11 -0400894 self.assertEqual({'foo': 42}, callbacks.responses['1'])
895 expected = ('<HttpError 403 when requesting '
896 'https://www.googleapis.com/someapi/v1/collection/?foo=bar returned '
897 '"Access Not Configured">')
898 self.assertEqual(expected, str(callbacks.exceptions['2']))
Ali Afshar6f11ea12012-02-07 10:32:14 -0500899
Joe Gregorio5c120db2012-08-23 09:13:55 -0400900
Joe Gregorioba5c7902012-08-03 12:48:16 -0400901class TestRequestUriTooLong(unittest.TestCase):
902
903 def test_turn_get_into_post(self):
904
905 def _postproc(resp, content):
906 return content
907
908 http = HttpMockSequence([
909 ({'status': '200'},
910 'echo_request_body'),
911 ({'status': '200'},
912 'echo_request_headers'),
913 ])
914
915 # Send a long query parameter.
916 query = {
917 'q': 'a' * MAX_URI_LENGTH + '?&'
918 }
919 req = HttpRequest(
920 http,
921 _postproc,
Pat Ferated5b61bd2015-03-03 16:04:11 -0800922 'http://example.com?' + urlencode(query),
Joe Gregorioba5c7902012-08-03 12:48:16 -0400923 method='GET',
924 body=None,
925 headers={},
926 methodId='foo',
927 resumable=None)
928
929 # Query parameters should be sent in the body.
930 response = req.execute()
931 self.assertEqual('q=' + 'a' * MAX_URI_LENGTH + '%3F%26', response)
932
933 # Extra headers should be set.
934 response = req.execute()
935 self.assertEqual('GET', response['x-http-method-override'])
936 self.assertEqual(str(MAX_URI_LENGTH + 8), response['content-length'])
937 self.assertEqual(
938 'application/x-www-form-urlencoded', response['content-type'])
939
Joe Gregorio5c120db2012-08-23 09:13:55 -0400940
941class TestStreamSlice(unittest.TestCase):
942 """Test _StreamSlice."""
943
944 def setUp(self):
Pat Ferate2b140222015-03-03 18:05:11 -0800945 self.stream = BytesIO(b'0123456789')
Joe Gregorio5c120db2012-08-23 09:13:55 -0400946
947 def test_read(self):
948 s = _StreamSlice(self.stream, 0, 4)
Pat Ferate2b140222015-03-03 18:05:11 -0800949 self.assertEqual(b'', s.read(0))
950 self.assertEqual(b'0', s.read(1))
951 self.assertEqual(b'123', s.read())
Joe Gregorio5c120db2012-08-23 09:13:55 -0400952
953 def test_read_too_much(self):
954 s = _StreamSlice(self.stream, 1, 4)
Pat Ferate2b140222015-03-03 18:05:11 -0800955 self.assertEqual(b'1234', s.read(6))
Joe Gregorio5c120db2012-08-23 09:13:55 -0400956
957 def test_read_all(self):
958 s = _StreamSlice(self.stream, 2, 1)
Pat Ferate2b140222015-03-03 18:05:11 -0800959 self.assertEqual(b'2', s.read(-1))
Joe Gregorio5c120db2012-08-23 09:13:55 -0400960
Ali Afshar164f37e2013-01-07 14:05:45 -0800961
962class TestResponseCallback(unittest.TestCase):
963 """Test adding callbacks to responses."""
964
965 def test_ensure_response_callback(self):
966 m = JsonModel()
967 request = HttpRequest(
968 None,
969 m.response,
970 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
971 method='POST',
972 body='{}',
973 headers={'content-type': 'application/json'})
974 h = HttpMockSequence([ ({'status': 200}, '{}')])
975 responses = []
976 def _on_response(resp, responses=responses):
977 responses.append(resp)
978 request.add_response_callback(_on_response)
979 request.execute(http=h)
980 self.assertEqual(1, len(responses))
981
982
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500983if __name__ == '__main__':
Joe Gregorio9086bd32013-06-14 16:32:05 -0400984 logging.getLogger().setLevel(logging.ERROR)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500985 unittest.main()