Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(888)

Side by Side Diff: boto/tests/test_resumable_downloads.py

Issue 8386013: Merging in latest boto. (Closed) Base URL: svn://svn.chromium.org/boto
Patch Set: Redoing vendor drop by deleting and then merging. Created 9 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « boto/tests/test_gsconnection.py ('k') | boto/tests/test_resumable_uploads.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 #
3 # Copyright 2010 Google Inc.
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining a
6 # copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish, dis-
9 # tribute, sublicense, and/or sell copies of the Software, and to permit
10 # persons to whom the Software is furnished to do so, subject to the fol-
11 # lowing conditions:
12 #
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
15 #
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
18 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22 # IN THE SOFTWARE.
23
24 """
25 Tests of resumable downloads.
26 """
27
28 import errno
29 import getopt
30 import os
31 import random
32 import re
33 import shutil
34 import socket
35 import StringIO
36 import sys
37 import tempfile
38 import time
39 import unittest
40
41 import boto
42 from boto import storage_uri
43 from boto.s3.resumable_download_handler import get_cur_file_size
44 from boto.s3.resumable_download_handler import ResumableDownloadHandler
45 from boto.exception import ResumableTransferDisposition
46 from boto.exception import ResumableDownloadException
47 from boto.exception import StorageResponseError
48 from boto.tests.cb_test_harnass import CallbackTestHarnass
49
50
51 class ResumableDownloadTests(unittest.TestCase):
52 """
53 Resumable download test suite.
54 """
55
56 def get_suite_description(self):
57 return 'Resumable download test suite'
58
59 @staticmethod
60 def resilient_close(key):
61 try:
62 key.close()
63 except StorageResponseError, e:
64 pass
65
66 @classmethod
67 def setUp(cls):
68 """
69 Creates file-like object for detination of each download test.
70
71 This method's namingCase is required by the unittest framework.
72 """
73 cls.dst_fp = open(cls.dst_file_name, 'w')
74
75 @classmethod
76 def tearDown(cls):
77 """
78 Deletes any objects or files created by last test run, and closes
79 any keys in case they were read incompletely (which would leave
80 partial buffers of data for subsequent tests to trip over).
81
82 This method's namingCase is required by the unittest framework.
83 """
84 # Recursively delete dst dir and then re-create it, so in effect we
85 # remove all dirs and files under that directory.
86 shutil.rmtree(cls.tmp_dir)
87 os.mkdir(cls.tmp_dir)
88
89 # Close test objects.
90 cls.resilient_close(cls.empty_src_key)
91 cls.resilient_close(cls.small_src_key)
92 cls.resilient_close(cls.larger_src_key)
93
94 @classmethod
95 def build_test_input_object(cls, obj_name, size, debug):
96 buf = []
97 for i in range(size):
98 buf.append(str(random.randint(0, 9)))
99 string_data = ''.join(buf)
100 uri = cls.src_bucket_uri.clone_replace_name(obj_name)
101 key = uri.new_key(validate=False)
102 key.set_contents_from_file(StringIO.StringIO(string_data))
103 # Set debug on key's connection after creating data, so only the test
104 # runs will show HTTP output (if called passed debug>0).
105 key.bucket.connection.debug = debug
106 return (string_data, key)
107
108 @classmethod
109 def set_up_class(cls, debug):
110 """
111 Initializes test suite.
112 """
113
114 # Create the test bucket.
115 hostname = socket.gethostname().split('.')[0]
116 uri_base_str = 'gs://res_download_test_%s_%s_%s' % (
117 hostname, os.getpid(), int(time.time()))
118 cls.src_bucket_uri = storage_uri('%s_dst' % uri_base_str)
119 cls.src_bucket_uri.create_bucket()
120
121 # Create test source objects.
122 cls.empty_src_key_size = 0
123 (cls.empty_src_key_as_string, cls.empty_src_key) = (
124 cls.build_test_input_object('empty', cls.empty_src_key_size,
125 debug=debug))
126 cls.small_src_key_size = 2 * 1024 # 2 KB.
127 (cls.small_src_key_as_string, cls.small_src_key) = (
128 cls.build_test_input_object('small', cls.small_src_key_size,
129 debug=debug))
130 cls.larger_src_key_size = 500 * 1024 # 500 KB.
131 (cls.larger_src_key_as_string, cls.larger_src_key) = (
132 cls.build_test_input_object('larger', cls.larger_src_key_size,
133 debug=debug))
134
135 # Use a designated tmpdir prefix to make it easy to find the end of
136 # the tmp path.
137 cls.tmpdir_prefix = 'tmp_resumable_download_test'
138
139 # Create temp dir and name for download file.
140 cls.tmp_dir = tempfile.mkdtemp(prefix=cls.tmpdir_prefix)
141 cls.dst_file_name = '%s%sdst_file' % (cls.tmp_dir, os.sep)
142
143 cls.tracker_file_name = '%s%stracker' % (cls.tmp_dir, os.sep)
144
145 cls.created_test_data = True
146
147 @classmethod
148 def tear_down_class(cls):
149 """
150 Deletes test objects and bucket and tmp dir created by set_up_class.
151 """
152 if not hasattr(cls, 'created_test_data'):
153 return
154 # Call cls.tearDown() in case the tests got interrupted, to ensure
155 # dst objects get deleted.
156 cls.tearDown()
157
158 # Delete test objects.
159 cls.empty_src_key.delete()
160 cls.small_src_key.delete()
161 cls.larger_src_key.delete()
162
163 # Retry (for up to 2 minutes) the bucket gets deleted (it may not
164 # the first time round, due to eventual consistency of bucket delete
165 # operations).
166 for i in range(60):
167 try:
168 cls.src_bucket_uri.delete_bucket()
169 break
170 except StorageResponseError:
171 print 'Test bucket (%s) not yet deleted, still trying' % (
172 cls.src_bucket_uri.uri)
173 time.sleep(2)
174 shutil.rmtree(cls.tmp_dir)
175 cls.tmp_dir = tempfile.mkdtemp(prefix=cls.tmpdir_prefix)
176
177 def test_non_resumable_download(self):
178 """
179 Tests that non-resumable downloads work
180 """
181 self.small_src_key.get_contents_to_file(self.dst_fp)
182 self.assertEqual(self.small_src_key_size,
183 get_cur_file_size(self.dst_fp))
184 self.assertEqual(self.small_src_key_as_string,
185 self.small_src_key.get_contents_as_string())
186
187 def test_download_without_persistent_tracker(self):
188 """
189 Tests a single resumable download, with no tracker persistence
190 """
191 res_download_handler = ResumableDownloadHandler()
192 self.small_src_key.get_contents_to_file(
193 self.dst_fp, res_download_handler=res_download_handler)
194 self.assertEqual(self.small_src_key_size,
195 get_cur_file_size(self.dst_fp))
196 self.assertEqual(self.small_src_key_as_string,
197 self.small_src_key.get_contents_as_string())
198
199 def test_failed_download_with_persistent_tracker(self):
200 """
201 Tests that failed resumable download leaves a correct tracker file
202 """
203 harnass = CallbackTestHarnass()
204 res_download_handler = ResumableDownloadHandler(
205 tracker_file_name=self.tracker_file_name, num_retries=0)
206 try:
207 self.small_src_key.get_contents_to_file(
208 self.dst_fp, cb=harnass.call,
209 res_download_handler=res_download_handler)
210 self.fail('Did not get expected ResumableDownloadException')
211 except ResumableDownloadException, e:
212 # We'll get a ResumableDownloadException at this point because
213 # of CallbackTestHarnass (above). Check that the tracker file was
214 # created correctly.
215 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
216 self.assertTrue(os.path.exists(self.tracker_file_name))
217 f = open(self.tracker_file_name)
218 etag_line = f.readline()
219 m = re.search(ResumableDownloadHandler.ETAG_REGEX, etag_line)
220 f.close()
221 self.assertTrue(m)
222
223 def test_retryable_exception_recovery(self):
224 """
225 Tests handling of a retryable exception
226 """
227 # Test one of the RETRYABLE_EXCEPTIONS.
228 exception = ResumableDownloadHandler.RETRYABLE_EXCEPTIONS[0]
229 harnass = CallbackTestHarnass(exception=exception)
230 res_download_handler = ResumableDownloadHandler(num_retries=1)
231 self.small_src_key.get_contents_to_file(
232 self.dst_fp, cb=harnass.call,
233 res_download_handler=res_download_handler)
234 # Ensure downloaded object has correct content.
235 self.assertEqual(self.small_src_key_size,
236 get_cur_file_size(self.dst_fp))
237 self.assertEqual(self.small_src_key_as_string,
238 self.small_src_key.get_contents_as_string())
239
240 def test_non_retryable_exception_handling(self):
241 """
242 Tests resumable download that fails with a non-retryable exception
243 """
244 harnass = CallbackTestHarnass(
245 exception=OSError(errno.EACCES, 'Permission denied'))
246 res_download_handler = ResumableDownloadHandler(num_retries=1)
247 try:
248 self.small_src_key.get_contents_to_file(
249 self.dst_fp, cb=harnass.call,
250 res_download_handler=res_download_handler)
251 self.fail('Did not get expected OSError')
252 except OSError, e:
253 # Ensure the error was re-raised.
254 self.assertEqual(e.errno, 13)
255
256 def test_failed_and_restarted_download_with_persistent_tracker(self):
257 """
258 Tests resumable download that fails once and then completes,
259 with tracker file
260 """
261 harnass = CallbackTestHarnass()
262 res_download_handler = ResumableDownloadHandler(
263 tracker_file_name=self.tracker_file_name, num_retries=1)
264 self.small_src_key.get_contents_to_file(
265 self.dst_fp, cb=harnass.call,
266 res_download_handler=res_download_handler)
267 # Ensure downloaded object has correct content.
268 self.assertEqual(self.small_src_key_size,
269 get_cur_file_size(self.dst_fp))
270 self.assertEqual(self.small_src_key_as_string,
271 self.small_src_key.get_contents_as_string())
272 # Ensure tracker file deleted.
273 self.assertFalse(os.path.exists(self.tracker_file_name))
274
275 def test_multiple_in_process_failures_then_succeed(self):
276 """
277 Tests resumable download that fails twice in one process, then completes
278 """
279 res_download_handler = ResumableDownloadHandler(num_retries=3)
280 self.small_src_key.get_contents_to_file(
281 self.dst_fp, res_download_handler=res_download_handler)
282 # Ensure downloaded object has correct content.
283 self.assertEqual(self.small_src_key_size,
284 get_cur_file_size(self.dst_fp))
285 self.assertEqual(self.small_src_key_as_string,
286 self.small_src_key.get_contents_as_string())
287
288 def test_multiple_in_process_failures_then_succeed_with_tracker_file(self):
289 """
290 Tests resumable download that fails completely in one process,
291 then when restarted completes, using a tracker file
292 """
293 # Set up test harnass that causes more failures than a single
294 # ResumableDownloadHandler instance will handle, writing enough data
295 # before the first failure that some of it survives that process run.
296 harnass = CallbackTestHarnass(
297 fail_after_n_bytes=self.larger_src_key_size/2, num_times_to_fail=2)
298 res_download_handler = ResumableDownloadHandler(
299 tracker_file_name=self.tracker_file_name, num_retries=0)
300 try:
301 self.larger_src_key.get_contents_to_file(
302 self.dst_fp, cb=harnass.call,
303 res_download_handler=res_download_handler)
304 self.fail('Did not get expected ResumableDownloadException')
305 except ResumableDownloadException, e:
306 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
307 # Ensure a tracker file survived.
308 self.assertTrue(os.path.exists(self.tracker_file_name))
309 # Try it one more time; this time should succeed.
310 self.larger_src_key.get_contents_to_file(
311 self.dst_fp, cb=harnass.call,
312 res_download_handler=res_download_handler)
313 self.assertEqual(self.larger_src_key_size,
314 get_cur_file_size(self.dst_fp))
315 self.assertEqual(self.larger_src_key_as_string,
316 self.larger_src_key.get_contents_as_string())
317 self.assertFalse(os.path.exists(self.tracker_file_name))
318 # Ensure some of the file was downloaded both before and after failure.
319 self.assertTrue(
320 len(harnass.transferred_seq_before_first_failure) > 1 and
321 len(harnass.transferred_seq_after_first_failure) > 1)
322
323 def test_download_with_inital_partial_download_before_failure(self):
324 """
325 Tests resumable download that successfully downloads some content
326 before it fails, then restarts and completes
327 """
328 # Set up harnass to fail download after several hundred KB so download
329 # server will have saved something before we retry.
330 harnass = CallbackTestHarnass(
331 fail_after_n_bytes=self.larger_src_key_size/2)
332 res_download_handler = ResumableDownloadHandler(num_retries=1)
333 self.larger_src_key.get_contents_to_file(
334 self.dst_fp, cb=harnass.call,
335 res_download_handler=res_download_handler)
336 # Ensure downloaded object has correct content.
337 self.assertEqual(self.larger_src_key_size,
338 get_cur_file_size(self.dst_fp))
339 self.assertEqual(self.larger_src_key_as_string,
340 self.larger_src_key.get_contents_as_string())
341 # Ensure some of the file was downloaded both before and after failure.
342 self.assertTrue(
343 len(harnass.transferred_seq_before_first_failure) > 1 and
344 len(harnass.transferred_seq_after_first_failure) > 1)
345
346 def test_zero_length_object_download(self):
347 """
348 Tests downloading a zero-length object (exercises boundary conditions).
349 """
350 res_download_handler = ResumableDownloadHandler()
351 self.empty_src_key.get_contents_to_file(
352 self.dst_fp, res_download_handler=res_download_handler)
353 self.assertEqual(0, get_cur_file_size(self.dst_fp))
354
355 def test_download_with_object_size_change_between_starts(self):
356 """
357 Tests resumable download on an object that changes sizes between inital
358 download start and restart
359 """
360 harnass = CallbackTestHarnass(
361 fail_after_n_bytes=self.larger_src_key_size/2, num_times_to_fail=2)
362 # Set up first process' ResumableDownloadHandler not to do any
363 # retries (initial download request will establish expected size to
364 # download server).
365 res_download_handler = ResumableDownloadHandler(
366 tracker_file_name=self.tracker_file_name, num_retries=0)
367 try:
368 self.larger_src_key.get_contents_to_file(
369 self.dst_fp, cb=harnass.call,
370 res_download_handler=res_download_handler)
371 self.fail('Did not get expected ResumableDownloadException')
372 except ResumableDownloadException, e:
373 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
374 # Ensure a tracker file survived.
375 self.assertTrue(os.path.exists(self.tracker_file_name))
376 # Try it again, this time with different src key (simulating an
377 # object that changes sizes between downloads).
378 try:
379 self.small_src_key.get_contents_to_file(
380 self.dst_fp, res_download_handler=res_download_handler)
381 self.fail('Did not get expected ResumableDownloadException')
382 except ResumableDownloadException, e:
383 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
384 self.assertNotEqual(
385 e.message.find('md5 signature doesn\'t match etag'), -1)
386
387 def test_download_with_file_content_change_during_download(self):
388 """
389 Tests resumable download on an object where the file content changes
390 without changing length while download in progress
391 """
392 harnass = CallbackTestHarnass(
393 fail_after_n_bytes=self.larger_src_key_size/2, num_times_to_fail=2)
394 # Set up first process' ResumableDownloadHandler not to do any
395 # retries (initial download request will establish expected size to
396 # download server).
397 res_download_handler = ResumableDownloadHandler(
398 tracker_file_name=self.tracker_file_name, num_retries=0)
399 dst_filename = self.dst_fp.name
400 try:
401 self.larger_src_key.get_contents_to_file(
402 self.dst_fp, cb=harnass.call,
403 res_download_handler=res_download_handler)
404 self.fail('Did not get expected ResumableDownloadException')
405 except ResumableDownloadException, e:
406 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
407 # Ensure a tracker file survived.
408 self.assertTrue(os.path.exists(self.tracker_file_name))
409 # Before trying again change the first byte of the file fragment
410 # that was already downloaded.
411 orig_size = get_cur_file_size(self.dst_fp)
412 self.dst_fp.seek(0, os.SEEK_SET)
413 self.dst_fp.write('a')
414 # Ensure the file size didn't change.
415 self.assertEqual(orig_size, get_cur_file_size(self.dst_fp))
416 try:
417 self.larger_src_key.get_contents_to_file(
418 self.dst_fp, cb=harnass.call,
419 res_download_handler=res_download_handler)
420 self.fail('Did not get expected ResumableDownloadException')
421 except ResumableDownloadException, e:
422 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
423 self.assertNotEqual(
424 e.message.find('md5 signature doesn\'t match etag'), -1)
425 # Ensure the bad data wasn't left around.
426 self.assertFalse(os.path.exists(dst_filename))
427
428 def test_download_with_invalid_tracker_etag(self):
429 """
430 Tests resumable download with a tracker file containing an invalid etag
431 """
432 invalid_etag_tracker_file_name = (
433 '%s%sinvalid_etag_tracker' % (self.tmp_dir, os.sep))
434 f = open(invalid_etag_tracker_file_name, 'w')
435 f.write('3.14159\n')
436 f.close()
437 res_download_handler = ResumableDownloadHandler(
438 tracker_file_name=invalid_etag_tracker_file_name)
439 # An error should be printed about the invalid tracker, but then it
440 # should run the update successfully.
441 self.small_src_key.get_contents_to_file(
442 self.dst_fp, res_download_handler=res_download_handler)
443 self.assertEqual(self.small_src_key_size,
444 get_cur_file_size(self.dst_fp))
445 self.assertEqual(self.small_src_key_as_string,
446 self.small_src_key.get_contents_as_string())
447
448 def test_download_with_inconsistent_etag_in_tracker(self):
449 """
450 Tests resumable download with an inconsistent etag in tracker file
451 """
452 inconsistent_etag_tracker_file_name = (
453 '%s%sinconsistent_etag_tracker' % (self.tmp_dir, os.sep))
454 f = open(inconsistent_etag_tracker_file_name, 'w')
455 good_etag = self.small_src_key.etag.strip('"\'')
456 new_val_as_list = []
457 for c in reversed(good_etag):
458 new_val_as_list.append(c)
459 f.write('%s\n' % ''.join(new_val_as_list))
460 f.close()
461 res_download_handler = ResumableDownloadHandler(
462 tracker_file_name=inconsistent_etag_tracker_file_name)
463 # An error should be printed about the expired tracker, but then it
464 # should run the update successfully.
465 self.small_src_key.get_contents_to_file(
466 self.dst_fp, res_download_handler=res_download_handler)
467 self.assertEqual(self.small_src_key_size,
468 get_cur_file_size(self.dst_fp))
469 self.assertEqual(self.small_src_key_as_string,
470 self.small_src_key.get_contents_as_string())
471
472 def test_download_with_unwritable_tracker_file(self):
473 """
474 Tests resumable download with an unwritable tracker file
475 """
476 # Make dir where tracker_file lives temporarily unwritable.
477 save_mod = os.stat(self.tmp_dir).st_mode
478 try:
479 os.chmod(self.tmp_dir, 0)
480 res_download_handler = ResumableDownloadHandler(
481 tracker_file_name=self.tracker_file_name)
482 except ResumableDownloadException, e:
483 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
484 self.assertNotEqual(
485 e.message.find('Couldn\'t write URI tracker file'), -1)
486 finally:
487 # Restore original protection of dir where tracker_file lives.
488 os.chmod(self.tmp_dir, save_mod)
489
490 if __name__ == '__main__':
491 if sys.version_info[:3] < (2, 5, 1):
492 sys.exit('These tests must be run on at least Python 2.5.1\n')
493
494 # Use -d to see more HTTP protocol detail during tests. Note that
495 # unlike the upload test case, you won't see much for the downloads
496 # because there's no HTTP server state protocol for in the download case
497 # (and the actual Range GET HTTP protocol detail is suppressed by the
498 # normal boto.s3.Key.get_file() processing).
499 debug = 0
500 opts, args = getopt.getopt(sys.argv[1:], 'd', ['debug'])
501 for o, a in opts:
502 if o in ('-d', '--debug'):
503 debug = 2
504
505 test_loader = unittest.TestLoader()
506 test_loader.testMethodPrefix = 'test_'
507 suite = test_loader.loadTestsFromTestCase(ResumableDownloadTests)
508 # Seems like there should be a cleaner way to find the test_class.
509 test_class = suite.__getattribute__('_tests')[0]
510 # We call set_up_class() and tear_down_class() ourselves because we
511 # don't assume the user has Python 2.7 (which supports classmethods
512 # that do it, with camelCase versions of these names).
513 try:
514 print 'Setting up %s...' % test_class.get_suite_description()
515 test_class.set_up_class(debug)
516 print 'Running %s...' % test_class.get_suite_description()
517 unittest.TextTestRunner(verbosity=2).run(suite)
518 finally:
519 print 'Cleaning up after %s...' % test_class.get_suite_description()
520 test_class.tear_down_class()
521 print ''
OLDNEW
« no previous file with comments | « boto/tests/test_gsconnection.py ('k') | boto/tests/test_resumable_uploads.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698