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

Side by Side Diff: third_party/gsutil/boto/tests/integration/gs/test_resumable_uploads.py

Issue 12317103: Added gsutil to depot tools (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: added readme Created 7 years, 9 months 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
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 Google Cloud Storage resumable uploads.
26 """
27
28 import StringIO
29 import errno
30 import random
31 import os
32 import time
33
34 import boto
35 from boto import storage_uri
36 from boto.gs.resumable_upload_handler import ResumableUploadHandler
37 from boto.exception import InvalidUriError
38 from boto.exception import ResumableTransferDisposition
39 from boto.exception import ResumableUploadException
40 from cb_test_harness import CallbackTestHarness
41 from tests.integration.gs.testcase import GSTestCase
42
43
44 SMALL_KEY_SIZE = 2 * 1024 # 2 KB.
45 LARGE_KEY_SIZE = 500 * 1024 # 500 KB.
46 LARGEST_KEY_SIZE = 1024 * 1024 # 1 MB.
47
48
49 class ResumableUploadTests(GSTestCase):
50 """Resumable upload test suite."""
51
52 def build_input_file(self, size):
53 buf = []
54 # I manually construct the random data here instead of calling
55 # os.urandom() because I want to constrain the range of data (in
56 # this case to 0'..'9') so the test
57 # code can easily overwrite part of the StringIO file with
58 # known-to-be-different values.
59 for i in range(size):
60 buf.append(str(random.randint(0, 9)))
61 file_as_string = ''.join(buf)
62 return (file_as_string, StringIO.StringIO(file_as_string))
63
64 def make_small_file(self):
65 return self.build_input_file(SMALL_KEY_SIZE)
66
67 def make_large_file(self):
68 return self.build_input_file(LARGE_KEY_SIZE)
69
70 def make_tracker_file(self, tmpdir=None):
71 if not tmpdir:
72 tmpdir = self._MakeTempDir()
73 tracker_file = os.path.join(tmpdir, 'tracker')
74 return tracker_file
75
76 def test_non_resumable_upload(self):
77 """
78 Tests that non-resumable uploads work
79 """
80 small_src_file_as_string, small_src_file = self.make_small_file()
81 # Seek to end incase its the first test.
82 small_src_file.seek(0, os.SEEK_END)
83 dst_key = self._MakeKey(set_contents=False)
84 try:
85 dst_key.set_contents_from_file(small_src_file)
86 self.fail("should fail as need to rewind the filepointer")
87 except AttributeError:
88 pass
89 # Now try calling with a proper rewind.
90 dst_key.set_contents_from_file(small_src_file, rewind=True)
91 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
92 self.assertEqual(small_src_file_as_string,
93 dst_key.get_contents_as_string())
94
95 def test_upload_without_persistent_tracker(self):
96 """
97 Tests a single resumable upload, with no tracker URI persistence
98 """
99 res_upload_handler = ResumableUploadHandler()
100 small_src_file_as_string, small_src_file = self.make_small_file()
101 small_src_file.seek(0)
102 dst_key = self._MakeKey(set_contents=False)
103 dst_key.set_contents_from_file(
104 small_src_file, res_upload_handler=res_upload_handler)
105 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
106 self.assertEqual(small_src_file_as_string,
107 dst_key.get_contents_as_string())
108
109 def test_failed_upload_with_persistent_tracker(self):
110 """
111 Tests that failed resumable upload leaves a correct tracker URI file
112 """
113 harness = CallbackTestHarness()
114 tracker_file_name = self.make_tracker_file()
115 res_upload_handler = ResumableUploadHandler(
116 tracker_file_name=tracker_file_name, num_retries=0)
117 small_src_file_as_string, small_src_file = self.make_small_file()
118 small_src_file.seek(0)
119 dst_key = self._MakeKey(set_contents=False)
120 try:
121 dst_key.set_contents_from_file(
122 small_src_file, cb=harness.call,
123 res_upload_handler=res_upload_handler)
124 self.fail('Did not get expected ResumableUploadException')
125 except ResumableUploadException, e:
126 # We'll get a ResumableUploadException at this point because
127 # of CallbackTestHarness (above). Check that the tracker file was
128 # created correctly.
129 self.assertEqual(e.disposition,
130 ResumableTransferDisposition.ABORT_CUR_PROCESS)
131 self.assertTrue(os.path.exists(tracker_file_name))
132 f = open(tracker_file_name)
133 uri_from_file = f.readline().strip()
134 f.close()
135 self.assertEqual(uri_from_file,
136 res_upload_handler.get_tracker_uri())
137
138 def test_retryable_exception_recovery(self):
139 """
140 Tests handling of a retryable exception
141 """
142 # Test one of the RETRYABLE_EXCEPTIONS.
143 exception = ResumableUploadHandler.RETRYABLE_EXCEPTIONS[0]
144 harness = CallbackTestHarness(exception=exception)
145 res_upload_handler = ResumableUploadHandler(num_retries=1)
146 small_src_file_as_string, small_src_file = self.make_small_file()
147 small_src_file.seek(0)
148 dst_key = self._MakeKey(set_contents=False)
149 dst_key.set_contents_from_file(
150 small_src_file, cb=harness.call,
151 res_upload_handler=res_upload_handler)
152 # Ensure uploaded object has correct content.
153 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
154 self.assertEqual(small_src_file_as_string,
155 dst_key.get_contents_as_string())
156
157 def test_broken_pipe_recovery(self):
158 """
159 Tests handling of a Broken Pipe (which interacts with an httplib bug)
160 """
161 exception = IOError(errno.EPIPE, "Broken pipe")
162 harness = CallbackTestHarness(exception=exception)
163 res_upload_handler = ResumableUploadHandler(num_retries=1)
164 small_src_file_as_string, small_src_file = self.make_small_file()
165 small_src_file.seek(0)
166 dst_key = self._MakeKey(set_contents=False)
167 dst_key.set_contents_from_file(
168 small_src_file, cb=harness.call,
169 res_upload_handler=res_upload_handler)
170 # Ensure uploaded object has correct content.
171 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
172 self.assertEqual(small_src_file_as_string,
173 dst_key.get_contents_as_string())
174
175 def test_non_retryable_exception_handling(self):
176 """
177 Tests a resumable upload that fails with a non-retryable exception
178 """
179 harness = CallbackTestHarness(
180 exception=OSError(errno.EACCES, 'Permission denied'))
181 res_upload_handler = ResumableUploadHandler(num_retries=1)
182 small_src_file_as_string, small_src_file = self.make_small_file()
183 small_src_file.seek(0)
184 dst_key = self._MakeKey(set_contents=False)
185 try:
186 dst_key.set_contents_from_file(
187 small_src_file, cb=harness.call,
188 res_upload_handler=res_upload_handler)
189 self.fail('Did not get expected OSError')
190 except OSError, e:
191 # Ensure the error was re-raised.
192 self.assertEqual(e.errno, 13)
193
194 def test_failed_and_restarted_upload_with_persistent_tracker(self):
195 """
196 Tests resumable upload that fails once and then completes, with tracker
197 file
198 """
199 harness = CallbackTestHarness()
200 tracker_file_name = self.make_tracker_file()
201 res_upload_handler = ResumableUploadHandler(
202 tracker_file_name=tracker_file_name, num_retries=1)
203 small_src_file_as_string, small_src_file = self.make_small_file()
204 small_src_file.seek(0)
205 dst_key = self._MakeKey(set_contents=False)
206 dst_key.set_contents_from_file(
207 small_src_file, cb=harness.call,
208 res_upload_handler=res_upload_handler)
209 # Ensure uploaded object has correct content.
210 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
211 self.assertEqual(small_src_file_as_string,
212 dst_key.get_contents_as_string())
213 # Ensure tracker file deleted.
214 self.assertFalse(os.path.exists(tracker_file_name))
215
216 def test_multiple_in_process_failures_then_succeed(self):
217 """
218 Tests resumable upload that fails twice in one process, then completes
219 """
220 res_upload_handler = ResumableUploadHandler(num_retries=3)
221 small_src_file_as_string, small_src_file = self.make_small_file()
222 small_src_file.seek(0)
223 dst_key = self._MakeKey(set_contents=False)
224 dst_key.set_contents_from_file(
225 small_src_file, res_upload_handler=res_upload_handler)
226 # Ensure uploaded object has correct content.
227 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
228 self.assertEqual(small_src_file_as_string,
229 dst_key.get_contents_as_string())
230
231 def test_multiple_in_process_failures_then_succeed_with_tracker_file(self):
232 """
233 Tests resumable upload that fails completely in one process,
234 then when restarted completes, using a tracker file
235 """
236 # Set up test harness that causes more failures than a single
237 # ResumableUploadHandler instance will handle, writing enough data
238 # before the first failure that some of it survives that process run.
239 harness = CallbackTestHarness(
240 fail_after_n_bytes=LARGE_KEY_SIZE/2, num_times_to_fail=2)
241 tracker_file_name = self.make_tracker_file()
242 res_upload_handler = ResumableUploadHandler(
243 tracker_file_name=tracker_file_name, num_retries=1)
244 larger_src_file_as_string, larger_src_file = self.make_large_file()
245 larger_src_file.seek(0)
246 dst_key = self._MakeKey(set_contents=False)
247 try:
248 dst_key.set_contents_from_file(
249 larger_src_file, cb=harness.call,
250 res_upload_handler=res_upload_handler)
251 self.fail('Did not get expected ResumableUploadException')
252 except ResumableUploadException, e:
253 self.assertEqual(e.disposition,
254 ResumableTransferDisposition.ABORT_CUR_PROCESS)
255 # Ensure a tracker file survived.
256 self.assertTrue(os.path.exists(tracker_file_name))
257 # Try it one more time; this time should succeed.
258 larger_src_file.seek(0)
259 dst_key.set_contents_from_file(
260 larger_src_file, cb=harness.call,
261 res_upload_handler=res_upload_handler)
262 self.assertEqual(LARGE_KEY_SIZE, dst_key.size)
263 self.assertEqual(larger_src_file_as_string,
264 dst_key.get_contents_as_string())
265 self.assertFalse(os.path.exists(tracker_file_name))
266 # Ensure some of the file was uploaded both before and after failure.
267 self.assertTrue(len(harness.transferred_seq_before_first_failure) > 1
268 and
269 len(harness.transferred_seq_after_first_failure) > 1)
270
271 def test_upload_with_inital_partial_upload_before_failure(self):
272 """
273 Tests resumable upload that successfully uploads some content
274 before it fails, then restarts and completes
275 """
276 # Set up harness to fail upload after several hundred KB so upload
277 # server will have saved something before we retry.
278 harness = CallbackTestHarness(
279 fail_after_n_bytes=LARGE_KEY_SIZE/2)
280 res_upload_handler = ResumableUploadHandler(num_retries=1)
281 larger_src_file_as_string, larger_src_file = self.make_large_file()
282 larger_src_file.seek(0)
283 dst_key = self._MakeKey(set_contents=False)
284 dst_key.set_contents_from_file(
285 larger_src_file, cb=harness.call,
286 res_upload_handler=res_upload_handler)
287 # Ensure uploaded object has correct content.
288 self.assertEqual(LARGE_KEY_SIZE, dst_key.size)
289 self.assertEqual(larger_src_file_as_string,
290 dst_key.get_contents_as_string())
291 # Ensure some of the file was uploaded both before and after failure.
292 self.assertTrue(len(harness.transferred_seq_before_first_failure) > 1
293 and
294 len(harness.transferred_seq_after_first_failure) > 1)
295
296 def test_empty_file_upload(self):
297 """
298 Tests uploading an empty file (exercises boundary conditions).
299 """
300 res_upload_handler = ResumableUploadHandler()
301 empty_src_file = StringIO.StringIO('')
302 empty_src_file.seek(0)
303 dst_key = self._MakeKey(set_contents=False)
304 dst_key.set_contents_from_file(
305 empty_src_file, res_upload_handler=res_upload_handler)
306 self.assertEqual(0, dst_key.size)
307
308 def test_upload_retains_metadata(self):
309 """
310 Tests that resumable upload correctly sets passed metadata
311 """
312 res_upload_handler = ResumableUploadHandler()
313 headers = {'Content-Type' : 'text/plain', 'Content-Encoding' : 'gzip',
314 'x-goog-meta-abc' : 'my meta', 'x-goog-acl' : 'public-read'}
315 small_src_file_as_string, small_src_file = self.make_small_file()
316 small_src_file.seek(0)
317 dst_key = self._MakeKey(set_contents=False)
318 dst_key.set_contents_from_file(
319 small_src_file, headers=headers,
320 res_upload_handler=res_upload_handler)
321 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
322 self.assertEqual(small_src_file_as_string,
323 dst_key.get_contents_as_string())
324 dst_key.open_read()
325 self.assertEqual('text/plain', dst_key.content_type)
326 self.assertEqual('gzip', dst_key.content_encoding)
327 self.assertTrue('abc' in dst_key.metadata)
328 self.assertEqual('my meta', str(dst_key.metadata['abc']))
329 acl = dst_key.get_acl()
330 for entry in acl.entries.entry_list:
331 if str(entry.scope) == '<AllUsers>':
332 self.assertEqual('READ', str(acl.entries.entry_list[1].permissio n))
333 return
334 self.fail('No <AllUsers> scope found')
335
336 def test_upload_with_file_size_change_between_starts(self):
337 """
338 Tests resumable upload on a file that changes sizes between initial
339 upload start and restart
340 """
341 harness = CallbackTestHarness(
342 fail_after_n_bytes=LARGE_KEY_SIZE/2)
343 tracker_file_name = self.make_tracker_file()
344 # Set up first process' ResumableUploadHandler not to do any
345 # retries (initial upload request will establish expected size to
346 # upload server).
347 res_upload_handler = ResumableUploadHandler(
348 tracker_file_name=tracker_file_name, num_retries=0)
349 larger_src_file_as_string, larger_src_file = self.make_large_file()
350 larger_src_file.seek(0)
351 dst_key = self._MakeKey(set_contents=False)
352 try:
353 dst_key.set_contents_from_file(
354 larger_src_file, cb=harness.call,
355 res_upload_handler=res_upload_handler)
356 self.fail('Did not get expected ResumableUploadException')
357 except ResumableUploadException, e:
358 # First abort (from harness-forced failure) should be
359 # ABORT_CUR_PROCESS.
360 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT_C UR_PROCESS)
361 # Ensure a tracker file survived.
362 self.assertTrue(os.path.exists(tracker_file_name))
363 # Try it again, this time with different size source file.
364 # Wait 1 second between retry attempts, to give upload server a
365 # chance to save state so it can respond to changed file size with
366 # 500 response in the next attempt.
367 time.sleep(1)
368 try:
369 largest_src_file = self.build_input_file(LARGEST_KEY_SIZE)[1]
370 largest_src_file.seek(0)
371 dst_key.set_contents_from_file(
372 largest_src_file, res_upload_handler=res_upload_handler)
373 self.fail('Did not get expected ResumableUploadException')
374 except ResumableUploadException, e:
375 # This abort should be a hard abort (file size changing during
376 # transfer).
377 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
378 self.assertNotEqual(e.message.find('file size changed'), -1, e.messa ge)
379
380 def test_upload_with_file_size_change_during_upload(self):
381 """
382 Tests resumable upload on a file that changes sizes while upload
383 in progress
384 """
385 # Create a file we can change during the upload.
386 test_file_size = 500 * 1024 # 500 KB.
387 test_file = self.build_input_file(test_file_size)[1]
388 harness = CallbackTestHarness(fp_to_change=test_file,
389 fp_change_pos=test_file_size)
390 res_upload_handler = ResumableUploadHandler(num_retries=1)
391 dst_key = self._MakeKey(set_contents=False)
392 try:
393 dst_key.set_contents_from_file(
394 test_file, cb=harness.call,
395 res_upload_handler=res_upload_handler)
396 self.fail('Did not get expected ResumableUploadException')
397 except ResumableUploadException, e:
398 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
399 self.assertNotEqual(
400 e.message.find('File changed during upload'), -1)
401
402 def test_upload_with_file_content_change_during_upload(self):
403 """
404 Tests resumable upload on a file that changes one byte of content
405 (so, size stays the same) while upload in progress
406 """
407 test_file_size = 500 * 1024 # 500 KB.
408 test_file = self.build_input_file(test_file_size)[1]
409 harness = CallbackTestHarness(fail_after_n_bytes=test_file_size/2,
410 fp_to_change=test_file,
411 # Write to byte 1, as the CallbackTestHarn ess writes
412 # 3 bytes. This will result in the data on the server
413 # being different than the local file.
414 fp_change_pos=1)
415 res_upload_handler = ResumableUploadHandler(num_retries=1)
416 dst_key = self._MakeKey(set_contents=False)
417 bucket_uri = storage_uri('gs://' + dst_key.bucket.name)
418 dst_key_uri = bucket_uri.clone_replace_name(dst_key.name)
419 try:
420 dst_key.set_contents_from_file(
421 test_file, cb=harness.call,
422 res_upload_handler=res_upload_handler)
423 self.fail('Did not get expected ResumableUploadException')
424 except ResumableUploadException, e:
425 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
426 # Ensure the file size didn't change.
427 test_file.seek(0, os.SEEK_END)
428 self.assertEqual(test_file_size, test_file.tell())
429 self.assertNotEqual(
430 e.message.find('md5 signature doesn\'t match etag'), -1)
431 # Ensure the bad data wasn't left around.
432 try:
433 dst_key_uri.get_key()
434 self.fail('Did not get expected InvalidUriError')
435 except InvalidUriError, e:
436 pass
437
438 def test_upload_with_content_length_header_set(self):
439 """
440 Tests resumable upload on a file when the user supplies a
441 Content-Length header. This is used by gsutil, for example,
442 to set the content length when gzipping a file.
443 """
444 res_upload_handler = ResumableUploadHandler()
445 small_src_file_as_string, small_src_file = self.make_small_file()
446 small_src_file.seek(0)
447 dst_key = self._MakeKey(set_contents=False)
448 try:
449 dst_key.set_contents_from_file(
450 small_src_file, res_upload_handler=res_upload_handler,
451 headers={'Content-Length' : SMALL_KEY_SIZE})
452 self.fail('Did not get expected ResumableUploadException')
453 except ResumableUploadException, e:
454 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
455 self.assertNotEqual(
456 e.message.find('Attempt to specify Content-Length header'), -1)
457
458 def test_upload_with_syntactically_invalid_tracker_uri(self):
459 """
460 Tests resumable upload with a syntactically invalid tracker URI
461 """
462 tmp_dir = self._MakeTempDir()
463 syntactically_invalid_tracker_file_name = os.path.join(tmp_dir,
464 'synt_invalid_uri_tracker')
465 with open(syntactically_invalid_tracker_file_name, 'w') as f:
466 f.write('ftp://example.com')
467 res_upload_handler = ResumableUploadHandler(
468 tracker_file_name=syntactically_invalid_tracker_file_name)
469 small_src_file_as_string, small_src_file = self.make_small_file()
470 # An error should be printed about the invalid URI, but then it
471 # should run the update successfully.
472 small_src_file.seek(0)
473 dst_key = self._MakeKey(set_contents=False)
474 dst_key.set_contents_from_file(
475 small_src_file, res_upload_handler=res_upload_handler)
476 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
477 self.assertEqual(small_src_file_as_string,
478 dst_key.get_contents_as_string())
479
480 def test_upload_with_invalid_upload_id_in_tracker_file(self):
481 """
482 Tests resumable upload with invalid upload ID
483 """
484 invalid_upload_id = ('http://pub.storage.googleapis.com/?upload_id='
485 'AyzB2Uo74W4EYxyi5dp_-r68jz8rtbvshsv4TX7srJVkJ57CxTY5Dw2')
486 tmpdir = self._MakeTempDir()
487 invalid_upload_id_tracker_file_name = os.path.join(tmpdir,
488 'invalid_upload_id_tracker')
489 with open(invalid_upload_id_tracker_file_name, 'w') as f:
490 f.write(invalid_upload_id)
491
492 res_upload_handler = ResumableUploadHandler(
493 tracker_file_name=invalid_upload_id_tracker_file_name)
494 small_src_file_as_string, small_src_file = self.make_small_file()
495 # An error should occur, but then the tracker URI should be
496 # regenerated and the the update should succeed.
497 small_src_file.seek(0)
498 dst_key = self._MakeKey(set_contents=False)
499 dst_key.set_contents_from_file(
500 small_src_file, res_upload_handler=res_upload_handler)
501 self.assertEqual(SMALL_KEY_SIZE, dst_key.size)
502 self.assertEqual(small_src_file_as_string,
503 dst_key.get_contents_as_string())
504 self.assertNotEqual(invalid_upload_id,
505 res_upload_handler.get_tracker_uri())
506
507 def test_upload_with_unwritable_tracker_file(self):
508 """
509 Tests resumable upload with an unwritable tracker file
510 """
511 # Make dir where tracker_file lives temporarily unwritable.
512 tmp_dir = self._MakeTempDir()
513 tracker_file_name = self.make_tracker_file(tmp_dir)
514 save_mod = os.stat(tmp_dir).st_mode
515 try:
516 os.chmod(tmp_dir, 0)
517 res_upload_handler = ResumableUploadHandler(
518 tracker_file_name=tracker_file_name)
519 except ResumableUploadException, e:
520 self.assertEqual(e.disposition, ResumableTransferDisposition.ABORT)
521 self.assertNotEqual(
522 e.message.find('Couldn\'t write URI tracker file'), -1)
523 finally:
524 # Restore original protection of dir where tracker_file lives.
525 os.chmod(tmp_dir, save_mod)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698