1 # -*- coding: utf-8 -*-
2 # Copyright 2013 Google Inc. All Rights Reserved.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 """Contains gsutil base integration test case class."""
17 from __future__
import absolute_import
19 from contextlib
import contextmanager
29 from boto
.exception
import StorageResponseError
30 from boto
.s3
.deletemarker
import DeleteMarker
31 from boto
.storage_uri
import BucketStorageUri
34 from gslib
.gcs_json_api
import GcsJsonApi
35 from gslib
.hashing_helper
import Base64ToHexHash
36 from gslib
.project_id
import GOOG_PROJ_ID_HDR
37 from gslib
.project_id
import PopulateProjectId
38 from gslib
.tests
.testcase
import base
39 import gslib
.tests
.util
as util
40 from gslib
.tests
.util
import ObjectToURI
as suri
41 from gslib
.tests
.util
import RUN_S3_TESTS
42 from gslib
.tests
.util
import SetBotoConfigFileForTest
43 from gslib
.tests
.util
import SetBotoConfigForTest
44 from gslib
.tests
.util
import SetEnvironmentForTest
45 from gslib
.tests
.util
import unittest
46 import gslib
.third_party
.storage_apitools
.storage_v1_messages
as apitools_messages
47 from gslib
.util
import IS_WINDOWS
48 from gslib
.util
import Retry
49 from gslib
.util
import UTF8
52 LOGGER
= logging
.getLogger('integration-test')
54 # Contents of boto config file that will tell gsutil not to override the real
55 # error message with a warning about anonymous access if no credentials are
56 # provided in the config file. Also, because we retry 401s, reduce the number
57 # of retries so we don't go through a long exponential backoff in tests.
58 BOTO_CONFIG_CONTENTS_IGNORE_ANON_WARNING
= """
62 bypass_anonymous_access_warning = True
66 def SkipForGS(reason
):
68 return unittest
.skip(reason
)
70 return lambda func
: func
73 def SkipForS3(reason
):
75 return unittest
.skip(reason
)
77 return lambda func
: func
80 # TODO: Right now, most tests use the XML API. Instead, they should respect
81 # prefer_api in the same way that commands do.
82 @unittest.skipUnless(util
.RUN_INTEGRATION_TESTS
,
83 'Not running integration tests.')
84 class GsUtilIntegrationTestCase(base
.GsUtilTestCase
):
85 """Base class for gsutil integration tests."""
86 GROUP_TEST_ADDRESS
= 'gs-discussion@googlegroups.com'
88 '00b4903a97d097895ab58ef505d535916a712215b79c3e54932c2eb502ad97f5')
89 USER_TEST_ADDRESS
= 'gs-team@google.com'
91 '00b4903a9703325c6bfc98992d72e75600387a64b3b6bee9ef74613ef8842080')
92 DOMAIN_TEST
= 'google.com'
93 # No one can create this bucket without owning the gmail.com domain, and we
94 # won't create this bucket, so it shouldn't exist.
95 # It would be nice to use google.com here but JSON API disallows
96 # 'google' in resource IDs.
97 nonexistent_bucket_name
= 'nonexistent-bucket-foobar.gmail.com'
100 """Creates base configuration for integration tests."""
101 super(GsUtilIntegrationTestCase
, self
).setUp()
102 self
.bucket_uris
= []
104 # Set up API version and project ID handler.
105 self
.api_version
= boto
.config
.get_value(
106 'GSUtil', 'default_api_version', '1')
108 # Instantiate a JSON API for use by the current integration test.
109 self
.json_api
= GcsJsonApi(BucketStorageUri
, logging
.getLogger(),
112 if util
.RUN_S3_TESTS
:
113 self
.nonexistent_bucket_name
= (
114 'nonexistentbucket-asf801rj3r9as90mfnnkjxpo02')
116 # Retry with an exponential backoff if a server error is received. This
117 # ensures that we try *really* hard to clean up after ourselves.
118 # TODO: As long as we're still using boto to do the teardown,
119 # we decorate with boto exceptions. Eventually this should be migrated
120 # to CloudApi exceptions.
121 @Retry(StorageResponseError
, tries
=7, timeout_secs
=1)
123 super(GsUtilIntegrationTestCase
, self
).tearDown()
125 while self
.bucket_uris
:
126 bucket_uri
= self
.bucket_uris
[-1]
128 bucket_list
= self
._ListBucket
(bucket_uri
)
129 except StorageResponseError
, e
:
130 # This can happen for tests of rm -r command, which for bucket-only
131 # URIs delete the bucket at the end.
133 self
.bucket_uris
.pop()
139 for k
in bucket_list
:
141 if isinstance(k
, DeleteMarker
):
142 bucket_uri
.get_bucket().delete_key(k
.name
,
143 version_id
=k
.version_id
)
146 except StorageResponseError
, e
:
147 # This could happen if objects that have already been deleted are
148 # still showing up in the listing due to eventual consistency. In
149 # that case, we continue on until we've tried to deleted every
150 # object in the listing before raising the error on which to retry.
156 raise error
# pylint: disable=raising-bad-type
157 bucket_list
= self
._ListBucket
(bucket_uri
)
158 bucket_uri
.delete_bucket()
159 self
.bucket_uris
.pop()
161 def _ListBucket(self
, bucket_uri
):
162 if bucket_uri
.scheme
== 's3':
163 # storage_uri will omit delete markers from bucket listings, but
164 # these must be deleted before we can remove an S3 bucket.
165 return list(v
for v
in bucket_uri
.get_bucket().list_versions())
166 return list(bucket_uri
.list_bucket(all_versions
=True))
168 def AssertNObjectsInBucket(self
, bucket_uri
, num_objects
, versioned
=False):
169 """Checks (with retries) that 'ls bucket_uri/**' returns num_objects.
171 This is a common test pattern to deal with eventual listing consistency for
172 tests that rely on a set of objects to be listed.
175 bucket_uri: storage_uri for the bucket.
176 num_objects: number of objects expected in the bucket.
177 versioned: If True, perform a versioned listing.
180 AssertionError if number of objects does not match expected value.
183 Listing split across lines.
185 # Use @Retry as hedge against bucket listing eventual consistency.
186 @Retry(AssertionError, tries
=5, timeout_secs
=1)
188 command
= ['ls', '-a'] if versioned
else ['ls']
189 b_uri
= [suri(bucket_uri
) + '/**'] if num_objects
else [suri(bucket_uri
)]
190 listing
= self
.RunGsUtil(command
+ b_uri
, return_stdout
=True).split('\n')
191 # num_objects + one trailing newline.
192 self
.assertEquals(len(listing
), num_objects
+ 1)
196 def CreateBucket(self
, bucket_name
=None, test_objects
=0, storage_class
=None,
197 provider
=None, prefer_json_api
=False):
198 """Creates a test bucket.
200 The bucket and all of its contents will be deleted after the test.
203 bucket_name: Create the bucket with this name. If not provided, a
204 temporary test bucket name is constructed.
205 test_objects: The number of objects that should be placed in the bucket.
207 storage_class: storage class to use. If not provided we us standard.
208 provider: Provider to use - either "gs" (the default) or "s3".
209 prefer_json_api: If true, use the JSON creation functions where possible.
212 StorageUri for the created bucket.
215 provider
= self
.default_provider
217 if prefer_json_api
and provider
== 'gs':
218 json_bucket
= self
.CreateBucketJson(bucket_name
=bucket_name
,
219 test_objects
=test_objects
,
220 storage_class
=storage_class
)
221 bucket_uri
= boto
.storage_uri(
222 'gs://%s' % json_bucket
.name
.encode(UTF8
).lower(),
223 suppress_consec_slashes
=False)
224 self
.bucket_uris
.append(bucket_uri
)
227 bucket_name
= bucket_name
or self
.MakeTempName('bucket')
229 bucket_uri
= boto
.storage_uri('%s://%s' % (provider
, bucket_name
.lower()),
230 suppress_consec_slashes
=False)
233 # Apply API version and project ID headers if necessary.
234 headers
= {'x-goog-api-version': self
.api_version
}
235 headers
[GOOG_PROJ_ID_HDR
] = PopulateProjectId()
239 # Parallel tests can easily run into bucket creation quotas.
240 # Retry with exponential backoff so that we create them as fast as we
242 @Retry(StorageResponseError
, tries
=7, timeout_secs
=1)
243 def _CreateBucketWithExponentialBackoff():
244 bucket_uri
.create_bucket(storage_class
=storage_class
, headers
=headers
)
246 _CreateBucketWithExponentialBackoff()
247 self
.bucket_uris
.append(bucket_uri
)
248 for i
in range(test_objects
):
249 self
.CreateObject(bucket_uri
=bucket_uri
,
250 object_name
=self
.MakeTempName('obj'),
251 contents
='test %d' % i
)
254 def CreateVersionedBucket(self
, bucket_name
=None, test_objects
=0):
255 """Creates a versioned test bucket.
257 The bucket and all of its contents will be deleted after the test.
260 bucket_name: Create the bucket with this name. If not provided, a
261 temporary test bucket name is constructed.
262 test_objects: The number of objects that should be placed in the bucket.
266 StorageUri for the created bucket with versioning enabled.
268 bucket_uri
= self
.CreateBucket(bucket_name
=bucket_name
,
269 test_objects
=test_objects
)
270 bucket_uri
.configure_versioning(True)
273 def CreateObject(self
, bucket_uri
=None, object_name
=None, contents
=None,
274 prefer_json_api
=False):
275 """Creates a test object.
278 bucket_uri: The URI of the bucket to place the object in. If not
279 specified, a new temporary bucket is created.
280 object_name: The name to use for the object. If not specified, a temporary
281 test object name is constructed.
282 contents: The contents to write to the object. If not specified, the key
283 is not written to, which means that it isn't actually created
285 prefer_json_api: If true, use the JSON creation functions where possible.
288 A StorageUri for the created object.
290 bucket_uri
= bucket_uri
or self
.CreateBucket()
292 if prefer_json_api
and bucket_uri
.scheme
== 'gs' and contents
:
293 object_name
= object_name
or self
.MakeTempName('obj')
294 json_object
= self
.CreateObjectJson(contents
=contents
,
295 bucket_name
=bucket_uri
.bucket_name
,
296 object_name
=object_name
)
297 object_uri
= bucket_uri
.clone_replace_name(object_name
)
298 # pylint: disable=protected-access
299 # Need to update the StorageUri with the correct values while
300 # avoiding creating a versioned string.
301 object_uri
._update
_from
_values
(None,
302 json_object
.generation
,
304 md5
=(Base64ToHexHash(json_object
.md5Hash
),
305 json_object
.md5Hash
.strip('\n"\'')))
306 # pylint: enable=protected-access
309 bucket_uri
= bucket_uri
or self
.CreateBucket()
310 object_name
= object_name
or self
.MakeTempName('obj')
311 key_uri
= bucket_uri
.clone_replace_name(object_name
)
312 if contents
is not None:
313 key_uri
.set_contents_from_string(contents
)
316 def CreateBucketJson(self
, bucket_name
=None, test_objects
=0,
318 """Creates a test bucket using the JSON API.
320 The bucket and all of its contents will be deleted after the test.
323 bucket_name: Create the bucket with this name. If not provided, a
324 temporary test bucket name is constructed.
325 test_objects: The number of objects that should be placed in the bucket.
327 storage_class: storage class to use. If not provided we us standard.
330 Apitools Bucket for the created bucket.
332 bucket_name
= bucket_name
or self
.MakeTempName('bucket')
333 bucket_metadata
= None
335 bucket_metadata
= apitools_messages
.Bucket(
336 name
=bucket_name
.lower(),
337 storageClass
=storage_class
)
339 # TODO: Add retry and exponential backoff.
340 bucket
= self
.json_api
.CreateBucket(bucket_name
.lower(),
341 metadata
=bucket_metadata
)
342 # Add bucket to list of buckets to be cleaned up.
343 # TODO: Clean up JSON buckets using JSON API.
344 self
.bucket_uris
.append(
345 boto
.storage_uri('gs://%s' % (bucket_name
.lower()),
346 suppress_consec_slashes
=False))
347 for i
in range(test_objects
):
348 self
.CreateObjectJson(bucket_name
=bucket_name
,
349 object_name
=self
.MakeTempName('obj'),
350 contents
='test %d' % i
)
353 def CreateObjectJson(self
, contents
, bucket_name
=None, object_name
=None):
354 """Creates a test object (GCS provider only) using the JSON API.
357 contents: The contents to write to the object.
358 bucket_name: Name of bucket to place the object in. If not
359 specified, a new temporary bucket is created.
360 object_name: The name to use for the object. If not specified, a temporary
361 test object name is constructed.
364 An apitools Object for the created object.
366 bucket_name
= bucket_name
or self
.CreateBucketJson().name
367 object_name
= object_name
or self
.MakeTempName('obj')
368 object_metadata
= apitools_messages
.Object(
371 contentType
='application/octet-stream')
372 return self
.json_api
.UploadObject(cStringIO
.StringIO(contents
),
373 object_metadata
, provider
='gs')
375 def RunGsUtil(self
, cmd
, return_status
=False, return_stdout
=False,
376 return_stderr
=False, expected_status
=0, stdin
=None):
377 """Runs the gsutil command.
380 cmd: The command to run, as a list, e.g. ['cp', 'foo', 'bar']
381 return_status: If True, the exit status code is returned.
382 return_stdout: If True, the standard output of the command is returned.
383 return_stderr: If True, the standard error of the command is returned.
384 expected_status: The expected return code. If not specified, defaults to
385 0. If the return code is a different value, an exception
387 stdin: A string of data to pipe to the process as standard input.
390 A tuple containing the desired return values specified by the return_*
393 cmd
= ([gslib
.GSUTIL_PATH
] + ['--testexceptiontraces'] +
394 ['-o', 'GSUtil:default_project_id=' + PopulateProjectId()] +
397 cmd
= [sys
.executable
] + cmd
398 p
= subprocess
.Popen(cmd
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
399 stdin
=subprocess
.PIPE
)
400 (stdout
, stderr
) = p
.communicate(stdin
)
401 status
= p
.returncode
403 if expected_status
is not None:
405 status
, expected_status
,
406 msg
='Expected status %d, got %d.\nCommand:\n%s\n\nstderr:\n%s' % (
407 expected_status
, status
, ' '.join(cmd
), stderr
))
411 toreturn
.append(status
)
414 stdout
= stdout
.replace('\r\n', '\n')
415 toreturn
.append(stdout
)
418 stderr
= stderr
.replace('\r\n', '\n')
419 toreturn
.append(stderr
)
421 if len(toreturn
) == 1:
424 return tuple(toreturn
)
426 def RunGsUtilTabCompletion(self
, cmd
, expected_results
=None):
427 """Runs the gsutil command in tab completion mode.
430 cmd: The command to run, as a list, e.g. ['cp', 'foo', 'bar']
431 expected_results: The expected tab completion results for the given input.
433 cmd
= [gslib
.GSUTIL_PATH
] + ['--testexceptiontraces'] + cmd
434 cmd_str
= ' '.join(cmd
)
436 @Retry(AssertionError, tries
=5, timeout_secs
=1)
437 def _RunTabCompletion():
438 """Runs the tab completion operation with retries."""
439 results_string
= None
440 with tempfile
.NamedTemporaryFile(
441 delete
=False) as tab_complete_result_file
:
442 # argcomplete returns results via the '8' file descriptor so we
443 # redirect to a file so we can capture them.
444 cmd_str_with_result_redirect
= '%s 8>%s' % (
445 cmd_str
, tab_complete_result_file
.name
)
446 env
= os
.environ
.copy()
447 env
['_ARGCOMPLETE'] = '1'
448 env
['COMP_LINE'] = cmd_str
449 env
['COMP_POINT'] = str(len(cmd_str
))
450 subprocess
.call(cmd_str_with_result_redirect
, env
=env
, shell
=True)
451 results_string
= tab_complete_result_file
.read().decode(
452 locale
.getpreferredencoding())
454 results
= results_string
.split('\013')
457 self
.assertEqual(results
, expected_results
)
459 # When tests are run in parallel, tab completion could take a long time,
460 # so choose a long timeout value.
461 with
SetBotoConfigForTest([('GSUtil', 'tab_completion_timeout', '120')]):
465 def SetAnonymousBotoCreds(self
):
466 boto_config_path
= self
.CreateTempFile(
467 contents
=BOTO_CONFIG_CONTENTS_IGNORE_ANON_WARNING
)
468 with
SetBotoConfigFileForTest(boto_config_path
):
469 # Make sure to reset Developer Shell credential port so that the child
470 # gsutil process is really anonymous.
471 with
SetEnvironmentForTest({'DEVSHELL_CLIENT_PORT': None}):