| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # | 2 # |
| 3 # Copyright 2010 Google Inc. | 3 # Copyright 2010 Google Inc. |
| 4 # | 4 # |
| 5 # Permission is hereby granted, free of charge, to any person obtaining a | 5 # Permission is hereby granted, free of charge, to any person obtaining a |
| 6 # copy of this software and associated documentation files (the | 6 # copy of this software and associated documentation files (the |
| 7 # "Software"), to deal in the Software without restriction, including | 7 # "Software"), to deal in the Software without restriction, including |
| 8 # without limitation the rights to use, copy, modify, merge, publish, dis- | 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 | 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- | 10 # persons to whom the Software is furnished to do so, subject to the fol- |
| (...skipping 27 matching lines...) Expand all Loading... |
| 38 import time | 38 import time |
| 39 import unittest | 39 import unittest |
| 40 | 40 |
| 41 import boto | 41 import boto |
| 42 from boto import storage_uri | 42 from boto import storage_uri |
| 43 from boto.s3.resumable_download_handler import get_cur_file_size | 43 from boto.s3.resumable_download_handler import get_cur_file_size |
| 44 from boto.s3.resumable_download_handler import ResumableDownloadHandler | 44 from boto.s3.resumable_download_handler import ResumableDownloadHandler |
| 45 from boto.exception import ResumableTransferDisposition | 45 from boto.exception import ResumableTransferDisposition |
| 46 from boto.exception import ResumableDownloadException | 46 from boto.exception import ResumableDownloadException |
| 47 from boto.exception import StorageResponseError | 47 from boto.exception import StorageResponseError |
| 48 from boto.tests.cb_test_harnass import CallbackTestHarnass | 48 from cb_test_harnass import CallbackTestHarnass |
| 49 |
| 50 # We don't use the OAuth2 authentication plugin directly; importing it here |
| 51 # ensures that it's loaded and available by default. |
| 52 try: |
| 53 from oauth2_plugin import oauth2_plugin |
| 54 except ImportError: |
| 55 # Do nothing - if user doesn't have OAuth2 configured it doesn't matter; |
| 56 # and if they do, the tests will fail (as they should in that case). |
| 57 pass |
| 49 | 58 |
| 50 | 59 |
| 51 class ResumableDownloadTests(unittest.TestCase): | 60 class ResumableDownloadTests(unittest.TestCase): |
| 52 """ | 61 """ |
| 53 Resumable download test suite. | 62 Resumable download test suite. |
| 54 """ | 63 """ |
| 55 | 64 |
| 56 def get_suite_description(self): | 65 def get_suite_description(self): |
| 57 return 'Resumable download test suite' | 66 return 'Resumable download test suite' |
| 58 | 67 |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 106 return (string_data, key) | 115 return (string_data, key) |
| 107 | 116 |
| 108 @classmethod | 117 @classmethod |
| 109 def set_up_class(cls, debug): | 118 def set_up_class(cls, debug): |
| 110 """ | 119 """ |
| 111 Initializes test suite. | 120 Initializes test suite. |
| 112 """ | 121 """ |
| 113 | 122 |
| 114 # Create the test bucket. | 123 # Create the test bucket. |
| 115 hostname = socket.gethostname().split('.')[0] | 124 hostname = socket.gethostname().split('.')[0] |
| 116 uri_base_str = 'gs://res_download_test_%s_%s_%s' % ( | 125 uri_base_str = 'gs://res-download-test-%s-%s-%s' % ( |
| 117 hostname, os.getpid(), int(time.time())) | 126 hostname, os.getpid(), int(time.time())) |
| 118 cls.src_bucket_uri = storage_uri('%s_dst' % uri_base_str) | 127 cls.src_bucket_uri = storage_uri('%s-dst' % uri_base_str) |
| 119 cls.src_bucket_uri.create_bucket() | 128 cls.src_bucket_uri.create_bucket() |
| 120 | 129 |
| 121 # Create test source objects. | 130 # Create test source objects. |
| 122 cls.empty_src_key_size = 0 | 131 cls.empty_src_key_size = 0 |
| 123 (cls.empty_src_key_as_string, cls.empty_src_key) = ( | 132 (cls.empty_src_key_as_string, cls.empty_src_key) = ( |
| 124 cls.build_test_input_object('empty', cls.empty_src_key_size, | 133 cls.build_test_input_object('empty', cls.empty_src_key_size, |
| 125 debug=debug)) | 134 debug=debug)) |
| 126 cls.small_src_key_size = 2 * 1024 # 2 KB. | 135 cls.small_src_key_size = 2 * 1024 # 2 KB. |
| 127 (cls.small_src_key_as_string, cls.small_src_key) = ( | 136 (cls.small_src_key_as_string, cls.small_src_key) = ( |
| 128 cls.build_test_input_object('small', cls.small_src_key_size, | 137 cls.build_test_input_object('small', cls.small_src_key_size, |
| (...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 205 tracker_file_name=self.tracker_file_name, num_retries=0) | 214 tracker_file_name=self.tracker_file_name, num_retries=0) |
| 206 try: | 215 try: |
| 207 self.small_src_key.get_contents_to_file( | 216 self.small_src_key.get_contents_to_file( |
| 208 self.dst_fp, cb=harnass.call, | 217 self.dst_fp, cb=harnass.call, |
| 209 res_download_handler=res_download_handler) | 218 res_download_handler=res_download_handler) |
| 210 self.fail('Did not get expected ResumableDownloadException') | 219 self.fail('Did not get expected ResumableDownloadException') |
| 211 except ResumableDownloadException, e: | 220 except ResumableDownloadException, e: |
| 212 # We'll get a ResumableDownloadException at this point because | 221 # We'll get a ResumableDownloadException at this point because |
| 213 # of CallbackTestHarnass (above). Check that the tracker file was | 222 # of CallbackTestHarnass (above). Check that the tracker file was |
| 214 # created correctly. | 223 # created correctly. |
| 215 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT) | 224 self.assertEqual(e.disposition, |
| 225 ResumableTransferDisposition.ABORT_CUR_PROCESS) |
| 216 self.assertTrue(os.path.exists(self.tracker_file_name)) | 226 self.assertTrue(os.path.exists(self.tracker_file_name)) |
| 217 f = open(self.tracker_file_name) | 227 f = open(self.tracker_file_name) |
| 218 etag_line = f.readline() | 228 etag_line = f.readline() |
| 219 m = re.search(ResumableDownloadHandler.ETAG_REGEX, etag_line) | 229 m = re.search(ResumableDownloadHandler.ETAG_REGEX, etag_line) |
| 220 f.close() | 230 f.close() |
| 221 self.assertTrue(m) | 231 self.assertTrue(m) |
| 222 | 232 |
| 223 def test_retryable_exception_recovery(self): | 233 def test_retryable_exception_recovery(self): |
| 224 """ | 234 """ |
| 225 Tests handling of a retryable exception | 235 Tests handling of a retryable exception |
| 226 """ | 236 """ |
| 227 # Test one of the RETRYABLE_EXCEPTIONS. | 237 # Test one of the RETRYABLE_EXCEPTIONS. |
| 228 exception = ResumableDownloadHandler.RETRYABLE_EXCEPTIONS[0] | 238 exception = ResumableDownloadHandler.RETRYABLE_EXCEPTIONS[0] |
| 229 harnass = CallbackTestHarnass(exception=exception) | 239 harnass = CallbackTestHarnass(exception=exception) |
| 230 res_download_handler = ResumableDownloadHandler(num_retries=1) | 240 res_download_handler = ResumableDownloadHandler(num_retries=1) |
| 231 self.small_src_key.get_contents_to_file( | 241 self.small_src_key.get_contents_to_file( |
| 232 self.dst_fp, cb=harnass.call, | 242 self.dst_fp, cb=harnass.call, |
| 233 res_download_handler=res_download_handler) | 243 res_download_handler=res_download_handler) |
| 234 # Ensure downloaded object has correct content. | 244 # Ensure downloaded object has correct content. |
| 235 self.assertEqual(self.small_src_key_size, | 245 self.assertEqual(self.small_src_key_size, |
| 236 get_cur_file_size(self.dst_fp)) | 246 get_cur_file_size(self.dst_fp)) |
| 237 self.assertEqual(self.small_src_key_as_string, | 247 self.assertEqual(self.small_src_key_as_string, |
| 238 self.small_src_key.get_contents_as_string()) | 248 self.small_src_key.get_contents_as_string()) |
| 239 | 249 |
| 250 def test_broken_pipe_recovery(self): |
| 251 """ |
| 252 Tests handling of a Broken Pipe (which interacts with an httplib bug) |
| 253 """ |
| 254 exception = IOError(errno.EPIPE, "Broken pipe") |
| 255 harnass = CallbackTestHarnass(exception=exception) |
| 256 res_download_handler = ResumableDownloadHandler(num_retries=1) |
| 257 self.small_src_key.get_contents_to_file( |
| 258 self.dst_fp, cb=harnass.call, |
| 259 res_download_handler=res_download_handler) |
| 260 # Ensure downloaded object has correct content. |
| 261 self.assertEqual(self.small_src_key_size, |
| 262 get_cur_file_size(self.dst_fp)) |
| 263 self.assertEqual(self.small_src_key_as_string, |
| 264 self.small_src_key.get_contents_as_string()) |
| 265 |
| 240 def test_non_retryable_exception_handling(self): | 266 def test_non_retryable_exception_handling(self): |
| 241 """ | 267 """ |
| 242 Tests resumable download that fails with a non-retryable exception | 268 Tests resumable download that fails with a non-retryable exception |
| 243 """ | 269 """ |
| 244 harnass = CallbackTestHarnass( | 270 harnass = CallbackTestHarnass( |
| 245 exception=OSError(errno.EACCES, 'Permission denied')) | 271 exception=OSError(errno.EACCES, 'Permission denied')) |
| 246 res_download_handler = ResumableDownloadHandler(num_retries=1) | 272 res_download_handler = ResumableDownloadHandler(num_retries=1) |
| 247 try: | 273 try: |
| 248 self.small_src_key.get_contents_to_file( | 274 self.small_src_key.get_contents_to_file( |
| 249 self.dst_fp, cb=harnass.call, | 275 self.dst_fp, cb=harnass.call, |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 296 harnass = CallbackTestHarnass( | 322 harnass = CallbackTestHarnass( |
| 297 fail_after_n_bytes=self.larger_src_key_size/2, num_times_to_fail=2) | 323 fail_after_n_bytes=self.larger_src_key_size/2, num_times_to_fail=2) |
| 298 res_download_handler = ResumableDownloadHandler( | 324 res_download_handler = ResumableDownloadHandler( |
| 299 tracker_file_name=self.tracker_file_name, num_retries=0) | 325 tracker_file_name=self.tracker_file_name, num_retries=0) |
| 300 try: | 326 try: |
| 301 self.larger_src_key.get_contents_to_file( | 327 self.larger_src_key.get_contents_to_file( |
| 302 self.dst_fp, cb=harnass.call, | 328 self.dst_fp, cb=harnass.call, |
| 303 res_download_handler=res_download_handler) | 329 res_download_handler=res_download_handler) |
| 304 self.fail('Did not get expected ResumableDownloadException') | 330 self.fail('Did not get expected ResumableDownloadException') |
| 305 except ResumableDownloadException, e: | 331 except ResumableDownloadException, e: |
| 306 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT) | 332 self.assertEqual(e.disposition, |
| 333 ResumableTransferDisposition.ABORT_CUR_PROCESS) |
| 307 # Ensure a tracker file survived. | 334 # Ensure a tracker file survived. |
| 308 self.assertTrue(os.path.exists(self.tracker_file_name)) | 335 self.assertTrue(os.path.exists(self.tracker_file_name)) |
| 309 # Try it one more time; this time should succeed. | 336 # Try it one more time; this time should succeed. |
| 310 self.larger_src_key.get_contents_to_file( | 337 self.larger_src_key.get_contents_to_file( |
| 311 self.dst_fp, cb=harnass.call, | 338 self.dst_fp, cb=harnass.call, |
| 312 res_download_handler=res_download_handler) | 339 res_download_handler=res_download_handler) |
| 313 self.assertEqual(self.larger_src_key_size, | 340 self.assertEqual(self.larger_src_key_size, |
| 314 get_cur_file_size(self.dst_fp)) | 341 get_cur_file_size(self.dst_fp)) |
| 315 self.assertEqual(self.larger_src_key_as_string, | 342 self.assertEqual(self.larger_src_key_as_string, |
| 316 self.larger_src_key.get_contents_as_string()) | 343 self.larger_src_key.get_contents_as_string()) |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 363 # retries (initial download request will establish expected size to | 390 # retries (initial download request will establish expected size to |
| 364 # download server). | 391 # download server). |
| 365 res_download_handler = ResumableDownloadHandler( | 392 res_download_handler = ResumableDownloadHandler( |
| 366 tracker_file_name=self.tracker_file_name, num_retries=0) | 393 tracker_file_name=self.tracker_file_name, num_retries=0) |
| 367 try: | 394 try: |
| 368 self.larger_src_key.get_contents_to_file( | 395 self.larger_src_key.get_contents_to_file( |
| 369 self.dst_fp, cb=harnass.call, | 396 self.dst_fp, cb=harnass.call, |
| 370 res_download_handler=res_download_handler) | 397 res_download_handler=res_download_handler) |
| 371 self.fail('Did not get expected ResumableDownloadException') | 398 self.fail('Did not get expected ResumableDownloadException') |
| 372 except ResumableDownloadException, e: | 399 except ResumableDownloadException, e: |
| 373 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT) | 400 # First abort (from harnass-forced failure) should be |
| 401 # ABORT_CUR_PROCESS. |
| 402 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT_C
UR_PROCESS) |
| 374 # Ensure a tracker file survived. | 403 # Ensure a tracker file survived. |
| 375 self.assertTrue(os.path.exists(self.tracker_file_name)) | 404 self.assertTrue(os.path.exists(self.tracker_file_name)) |
| 376 # Try it again, this time with different src key (simulating an | 405 # Try it again, this time with different src key (simulating an |
| 377 # object that changes sizes between downloads). | 406 # object that changes sizes between downloads). |
| 378 try: | 407 try: |
| 379 self.small_src_key.get_contents_to_file( | 408 self.small_src_key.get_contents_to_file( |
| 380 self.dst_fp, res_download_handler=res_download_handler) | 409 self.dst_fp, res_download_handler=res_download_handler) |
| 381 self.fail('Did not get expected ResumableDownloadException') | 410 self.fail('Did not get expected ResumableDownloadException') |
| 382 except ResumableDownloadException, e: | 411 except ResumableDownloadException, e: |
| 412 # This abort should be a hard abort (object size changing during |
| 413 # transfer). |
| 383 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT) | 414 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT) |
| 384 self.assertNotEqual( | 415 self.assertNotEqual( |
| 385 e.message.find('md5 signature doesn\'t match etag'), -1) | 416 e.message.find('md5 signature doesn\'t match etag'), -1) |
| 386 | 417 |
| 387 def test_download_with_file_content_change_during_download(self): | 418 def test_download_with_file_content_change_during_download(self): |
| 388 """ | 419 """ |
| 389 Tests resumable download on an object where the file content changes | 420 Tests resumable download on an object where the file content changes |
| 390 without changing length while download in progress | 421 without changing length while download in progress |
| 391 """ | 422 """ |
| 392 harnass = CallbackTestHarnass( | 423 harnass = CallbackTestHarnass( |
| 393 fail_after_n_bytes=self.larger_src_key_size/2, num_times_to_fail=2) | 424 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 | 425 # Set up first process' ResumableDownloadHandler not to do any |
| 395 # retries (initial download request will establish expected size to | 426 # retries (initial download request will establish expected size to |
| 396 # download server). | 427 # download server). |
| 397 res_download_handler = ResumableDownloadHandler( | 428 res_download_handler = ResumableDownloadHandler( |
| 398 tracker_file_name=self.tracker_file_name, num_retries=0) | 429 tracker_file_name=self.tracker_file_name, num_retries=0) |
| 399 dst_filename = self.dst_fp.name | 430 dst_filename = self.dst_fp.name |
| 400 try: | 431 try: |
| 401 self.larger_src_key.get_contents_to_file( | 432 self.larger_src_key.get_contents_to_file( |
| 402 self.dst_fp, cb=harnass.call, | 433 self.dst_fp, cb=harnass.call, |
| 403 res_download_handler=res_download_handler) | 434 res_download_handler=res_download_handler) |
| 404 self.fail('Did not get expected ResumableDownloadException') | 435 self.fail('Did not get expected ResumableDownloadException') |
| 405 except ResumableDownloadException, e: | 436 except ResumableDownloadException, e: |
| 406 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT) | 437 # First abort (from harnass-forced failure) should be |
| 438 # ABORT_CUR_PROCESS. |
| 439 self.assertEqual(e.disposition, |
| 440 ResumableTransferDisposition.ABORT_CUR_PROCESS) |
| 407 # Ensure a tracker file survived. | 441 # Ensure a tracker file survived. |
| 408 self.assertTrue(os.path.exists(self.tracker_file_name)) | 442 self.assertTrue(os.path.exists(self.tracker_file_name)) |
| 409 # Before trying again change the first byte of the file fragment | 443 # Before trying again change the first byte of the file fragment |
| 410 # that was already downloaded. | 444 # that was already downloaded. |
| 411 orig_size = get_cur_file_size(self.dst_fp) | 445 orig_size = get_cur_file_size(self.dst_fp) |
| 412 self.dst_fp.seek(0, os.SEEK_SET) | 446 self.dst_fp.seek(0, os.SEEK_SET) |
| 413 self.dst_fp.write('a') | 447 self.dst_fp.write('a') |
| 414 # Ensure the file size didn't change. | 448 # Ensure the file size didn't change. |
| 415 self.assertEqual(orig_size, get_cur_file_size(self.dst_fp)) | 449 self.assertEqual(orig_size, get_cur_file_size(self.dst_fp)) |
| 416 try: | 450 try: |
| 417 self.larger_src_key.get_contents_to_file( | 451 self.larger_src_key.get_contents_to_file( |
| 418 self.dst_fp, cb=harnass.call, | 452 self.dst_fp, cb=harnass.call, |
| 419 res_download_handler=res_download_handler) | 453 res_download_handler=res_download_handler) |
| 420 self.fail('Did not get expected ResumableDownloadException') | 454 self.fail('Did not get expected ResumableDownloadException') |
| 421 except ResumableDownloadException, e: | 455 except ResumableDownloadException, e: |
| 456 # This abort should be a hard abort (file content changing during |
| 457 # transfer). |
| 422 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT) | 458 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT) |
| 423 self.assertNotEqual( | 459 self.assertNotEqual( |
| 424 e.message.find('md5 signature doesn\'t match etag'), -1) | 460 e.message.find('md5 signature doesn\'t match etag'), -1) |
| 425 # Ensure the bad data wasn't left around. | 461 # Ensure the bad data wasn't left around. |
| 426 self.assertFalse(os.path.exists(dst_filename)) | 462 self.assertFalse(os.path.exists(dst_filename)) |
| 427 | 463 |
| 428 def test_download_with_invalid_tracker_etag(self): | 464 def test_download_with_invalid_tracker_etag(self): |
| 429 """ | 465 """ |
| 430 Tests resumable download with a tracker file containing an invalid etag | 466 Tests resumable download with a tracker file containing an invalid etag |
| 431 """ | 467 """ |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 512 # that do it, with camelCase versions of these names). | 548 # that do it, with camelCase versions of these names). |
| 513 try: | 549 try: |
| 514 print 'Setting up %s...' % test_class.get_suite_description() | 550 print 'Setting up %s...' % test_class.get_suite_description() |
| 515 test_class.set_up_class(debug) | 551 test_class.set_up_class(debug) |
| 516 print 'Running %s...' % test_class.get_suite_description() | 552 print 'Running %s...' % test_class.get_suite_description() |
| 517 unittest.TextTestRunner(verbosity=2).run(suite) | 553 unittest.TextTestRunner(verbosity=2).run(suite) |
| 518 finally: | 554 finally: |
| 519 print 'Cleaning up after %s...' % test_class.get_suite_description() | 555 print 'Cleaning up after %s...' % test_class.get_suite_description() |
| 520 test_class.tear_down_class() | 556 test_class.tear_down_class() |
| 521 print '' | 557 print '' |
| OLD | NEW |