| OLD | NEW |
| 1 # -*- coding: utf-8 -*- |
| 1 # Copyright 2013 Google Inc. All Rights Reserved. | 2 # Copyright 2013 Google Inc. All Rights Reserved. |
| 2 # | 3 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. | 5 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at | 6 # You may obtain a copy of the License at |
| 6 # | 7 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # | 9 # |
| 9 # Unless required by applicable law or agreed to in writing, software | 10 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and | 13 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. | 14 # limitations under the License. |
| 14 | |
| 15 """Contains gsutil base integration test case class.""" | 15 """Contains gsutil base integration test case class.""" |
| 16 | 16 |
| 17 import base | 17 from __future__ import absolute_import |
| 18 import boto | 18 |
| 19 import gslib | 19 from contextlib import contextmanager |
| 20 import gslib.tests.util as util | |
| 21 import logging | 20 import logging |
| 22 import subprocess | 21 import subprocess |
| 23 import sys | 22 import sys |
| 24 | 23 |
| 25 from boto.exception import GSResponseError | 24 import boto |
| 26 from contextlib import contextmanager | 25 from boto.exception import StorageResponseError |
| 27 from gslib.project_id import ProjectIdHandler | 26 from boto.s3.deletemarker import DeleteMarker |
| 28 from gslib.tests.util import SetBotoConfigForTest | 27 import gslib |
| 28 from gslib.project_id import GOOG_PROJ_ID_HDR |
| 29 from gslib.project_id import PopulateProjectId |
| 30 from gslib.tests.testcase import base |
| 31 import gslib.tests.util as util |
| 32 from gslib.tests.util import ObjectToURI as suri |
| 33 from gslib.tests.util import RUN_S3_TESTS |
| 34 from gslib.tests.util import SetBotoConfigFileForTest |
| 29 from gslib.tests.util import unittest | 35 from gslib.tests.util import unittest |
| 30 from gslib.util import IS_WINDOWS | 36 from gslib.util import IS_WINDOWS |
| 31 from gslib.util import Retry | 37 from gslib.util import Retry |
| 32 | 38 |
| 33 | 39 |
| 34 LOGGER = logging.getLogger('integration-test') | 40 LOGGER = logging.getLogger('integration-test') |
| 35 | 41 |
| 36 # Contents of boto config file that will tell gsutil not to override the real | 42 # Contents of boto config file that will tell gsutil not to override the real |
| 37 # error message with a warning about anonymous access if no credentials are | 43 # error message with a warning about anonymous access if no credentials are |
| 38 # provided in the config file. | 44 # provided in the config file. |
| 39 BOTO_CONFIG_CONTENTS_IGNORE_ANON_WARNING = """ | 45 BOTO_CONFIG_CONTENTS_IGNORE_ANON_WARNING = """ |
| 40 [Tests] | 46 [Tests] |
| 41 bypass_anonymous_access_warning = True | 47 bypass_anonymous_access_warning = True |
| 42 """ | 48 """ |
| 43 | 49 |
| 50 |
| 51 def SkipForGS(reason): |
| 52 if not RUN_S3_TESTS: |
| 53 return unittest.skip(reason) |
| 54 else: |
| 55 return lambda func: func |
| 56 |
| 57 |
| 58 def SkipForS3(reason): |
| 59 if RUN_S3_TESTS: |
| 60 return unittest.skip(reason) |
| 61 else: |
| 62 return lambda func: func |
| 63 |
| 64 |
| 44 @unittest.skipUnless(util.RUN_INTEGRATION_TESTS, | 65 @unittest.skipUnless(util.RUN_INTEGRATION_TESTS, |
| 45 'Not running integration tests.') | 66 'Not running integration tests.') |
| 46 class GsUtilIntegrationTestCase(base.GsUtilTestCase): | 67 class GsUtilIntegrationTestCase(base.GsUtilTestCase): |
| 47 """Base class for gsutil integration tests.""" | 68 """Base class for gsutil integration tests.""" |
| 48 GROUP_TEST_ADDRESS = 'gs-discussion@googlegroups.com' | 69 GROUP_TEST_ADDRESS = 'gs-discussion@googlegroups.com' |
| 49 GROUP_TEST_ID = '00b4903a97d097895ab58ef505d535916a712215b79c3e54932c2eb502ad9
7f5' | 70 GROUP_TEST_ID = ( |
| 71 '00b4903a97d097895ab58ef505d535916a712215b79c3e54932c2eb502ad97f5') |
| 50 USER_TEST_ADDRESS = 'gs-team@google.com' | 72 USER_TEST_ADDRESS = 'gs-team@google.com' |
| 51 USER_TEST_ID = '00b4903a9703325c6bfc98992d72e75600387a64b3b6bee9ef74613ef88420
80' | 73 USER_TEST_ID = ( |
| 74 '00b4903a9703325c6bfc98992d72e75600387a64b3b6bee9ef74613ef8842080') |
| 52 DOMAIN_TEST = 'google.com' | 75 DOMAIN_TEST = 'google.com' |
| 53 # No one can create this bucket without owning the google.com domain, and we | 76 # No one can create this bucket without owning the gmail.com domain, and we |
| 54 # won't create this bucket, so it shouldn't exist. | 77 # won't create this bucket, so it shouldn't exist. |
| 55 NONEXISTENT_BUCKET_NAME = 'nonexistent-bucket-foobar.google.com' | 78 # It would be nice to use google.com here but JSON API disallows |
| 79 # 'google' in resource IDs. |
| 80 nonexistent_bucket_name = 'nonexistent-bucket-foobar.gmail.com' |
| 56 | 81 |
| 57 def setUp(self): | 82 def setUp(self): |
| 83 """Creates base configuration for integration tests.""" |
| 58 super(GsUtilIntegrationTestCase, self).setUp() | 84 super(GsUtilIntegrationTestCase, self).setUp() |
| 59 self.bucket_uris = [] | 85 self.bucket_uris = [] |
| 60 | 86 |
| 61 # Set up API version and project ID handler. | 87 # Set up API version and project ID handler. |
| 62 self.api_version = boto.config.get_value( | 88 self.api_version = boto.config.get_value( |
| 63 'GSUtil', 'default_api_version', '1') | 89 'GSUtil', 'default_api_version', '1') |
| 64 self.proj_id_handler = ProjectIdHandler() | 90 |
| 91 if util.RUN_S3_TESTS: |
| 92 self.nonexistent_bucket_name = ( |
| 93 'nonexistentbucket-asf801rj3r9as90mfnnkjxpo02') |
| 65 | 94 |
| 66 # Retry with an exponential backoff if a server error is received. This | 95 # Retry with an exponential backoff if a server error is received. This |
| 67 # ensures that we try *really* hard to clean up after ourselves. | 96 # ensures that we try *really* hard to clean up after ourselves. |
| 68 @Retry(GSResponseError, tries=6, timeout_secs=1) | 97 # TODO: As long as we're still using boto to do the teardown, |
| 98 # we decorate with boto exceptions. Eventually this should be migrated |
| 99 # to CloudApi exceptions. |
| 100 @Retry(StorageResponseError, tries=7, timeout_secs=1) |
| 69 def tearDown(self): | 101 def tearDown(self): |
| 70 super(GsUtilIntegrationTestCase, self).tearDown() | 102 super(GsUtilIntegrationTestCase, self).tearDown() |
| 71 | 103 |
| 72 while self.bucket_uris: | 104 while self.bucket_uris: |
| 73 bucket_uri = self.bucket_uris[-1] | 105 bucket_uri = self.bucket_uris[-1] |
| 74 try: | 106 try: |
| 75 bucket_list = list(bucket_uri.list_bucket(all_versions=True)) | 107 bucket_list = self._ListBucket(bucket_uri) |
| 76 except GSResponseError, e: | 108 except StorageResponseError, e: |
| 77 # This can happen for tests of rm -r command, which for bucket-only | 109 # This can happen for tests of rm -r command, which for bucket-only |
| 78 # URIs delete the bucket at the end. | 110 # URIs delete the bucket at the end. |
| 79 if e.status == 404: | 111 if e.status == 404: |
| 80 self.bucket_uris.pop() | 112 self.bucket_uris.pop() |
| 81 continue | 113 continue |
| 82 else: | 114 else: |
| 83 raise | 115 raise |
| 84 while bucket_list: | 116 while bucket_list: |
| 85 error = None | 117 error = None |
| 86 for k in bucket_list: | 118 for k in bucket_list: |
| 87 try: | 119 try: |
| 88 k.delete() | 120 if isinstance(k, DeleteMarker): |
| 89 except GSResponseError, e: | 121 bucket_uri.get_bucket().delete_key(k.name, |
| 122 version_id=k.version_id) |
| 123 else: |
| 124 k.delete() |
| 125 except StorageResponseError, e: |
| 90 # This could happen if objects that have already been deleted are | 126 # This could happen if objects that have already been deleted are |
| 91 # still showing up in the listing due to eventual consistency. In | 127 # still showing up in the listing due to eventual consistency. In |
| 92 # that case, we continue on until we've tried to deleted every | 128 # that case, we continue on until we've tried to deleted every |
| 93 # object in the listing before raising the error on which to retry. | 129 # object in the listing before raising the error on which to retry. |
| 94 if e.status == 404: | 130 if e.status == 404: |
| 95 error = e | 131 error = e |
| 96 else: | 132 else: |
| 97 raise | 133 raise |
| 98 if error: | 134 if error: |
| 99 raise error | 135 raise error # pylint: disable=raising-bad-type |
| 100 bucket_list = list(bucket_uri.list_bucket(all_versions=True)) | 136 bucket_list = self._ListBucket(bucket_uri) |
| 101 bucket_uri.delete_bucket() | 137 bucket_uri.delete_bucket() |
| 102 self.bucket_uris.pop() | 138 self.bucket_uris.pop() |
| 103 | 139 |
| 140 def _ListBucket(self, bucket_uri): |
| 141 if bucket_uri.scheme == 's3': |
| 142 # storage_uri will omit delete markers from bucket listings, but |
| 143 # these must be deleted before we can remove an S3 bucket. |
| 144 return list(v for v in bucket_uri.get_bucket().list_versions()) |
| 145 return list(bucket_uri.list_bucket(all_versions=True)) |
| 146 |
| 147 def AssertNObjectsInBucket(self, bucket_uri, num_objects, versioned=False): |
| 148 """Checks (with retries) that 'ls bucket_uri/**' returns num_objects. |
| 149 |
| 150 This is a common test pattern to deal with eventual listing consistency for |
| 151 tests that rely on a set of objects to be listed. |
| 152 |
| 153 Args: |
| 154 bucket_uri: storage_uri for the bucket. |
| 155 num_objects: number of objects expected in the bucket. |
| 156 versioned: If True, perform a versioned listing. |
| 157 |
| 158 Raises: |
| 159 AssertionError if number of objects does not match expected value. |
| 160 |
| 161 Returns: |
| 162 Listing split across lines. |
| 163 """ |
| 164 # Use @Retry as hedge against bucket listing eventual consistency. |
| 165 @Retry(AssertionError, tries=3, timeout_secs=1) |
| 166 def _Check1(): |
| 167 command = ['ls', '-a'] if versioned else ['ls'] |
| 168 b_uri = [suri(bucket_uri) + '/**'] if num_objects else [suri(bucket_uri)] |
| 169 listing = self.RunGsUtil(command + b_uri, return_stdout=True).split('\n') |
| 170 # num_objects + one trailing newline. |
| 171 self.assertEquals(len(listing), num_objects + 1) |
| 172 return listing |
| 173 return _Check1() |
| 174 |
| 104 def CreateBucket(self, bucket_name=None, test_objects=0, storage_class=None, | 175 def CreateBucket(self, bucket_name=None, test_objects=0, storage_class=None, |
| 105 provider=None): | 176 provider=None): |
| 106 """Creates a test bucket. | 177 """Creates a test bucket. |
| 107 | 178 |
| 108 The bucket and all of its contents will be deleted after the test. | 179 The bucket and all of its contents will be deleted after the test. |
| 109 | 180 |
| 110 Args: | 181 Args: |
| 111 bucket_name: Create the bucket with this name. If not provided, a | 182 bucket_name: Create the bucket with this name. If not provided, a |
| 112 temporary test bucket name is constructed. | 183 temporary test bucket name is constructed. |
| 113 test_objects: The number of objects that should be placed in the bucket. | 184 test_objects: The number of objects that should be placed in the bucket. |
| 114 Defaults to 0. | 185 Defaults to 0. |
| 115 storage_class: storage class to use. If not provided we us standard. | 186 storage_class: storage class to use. If not provided we us standard. |
| 116 provider: Provider to use - either "gs" (the default) or "s3". | 187 provider: Provider to use - either "gs" (the default) or "s3". |
| 117 | 188 |
| 118 Returns: | 189 Returns: |
| 119 StorageUri for the created bucket. | 190 StorageUri for the created bucket. |
| 120 """ | 191 """ |
| 121 if not provider: | 192 if not provider: |
| 122 provider = 'gs' | 193 provider = self.default_provider |
| 123 bucket_name = bucket_name or self.MakeTempName('bucket') | 194 bucket_name = bucket_name or self.MakeTempName('bucket') |
| 124 | 195 |
| 125 bucket_uri = boto.storage_uri('%s://%s' % (provider, bucket_name.lower()), | 196 bucket_uri = boto.storage_uri('%s://%s' % (provider, bucket_name.lower()), |
| 126 suppress_consec_slashes=False) | 197 suppress_consec_slashes=False) |
| 127 | 198 |
| 128 if provider == 'gs': | 199 if provider == 'gs': |
| 129 # Apply API version and project ID headers if necessary. | 200 # Apply API version and project ID headers if necessary. |
| 130 headers = {'x-goog-api-version': self.api_version} | 201 headers = {'x-goog-api-version': self.api_version} |
| 131 self.proj_id_handler.FillInProjectHeaderIfNeeded( | 202 headers[GOOG_PROJ_ID_HDR] = PopulateProjectId() |
| 132 'test', bucket_uri, headers) | |
| 133 else: | 203 else: |
| 134 headers = {} | 204 headers = {} |
| 135 | 205 |
| 136 bucket_uri.create_bucket(storage_class=storage_class, headers=headers) | 206 # Parallel tests can easily run into bucket creation quotas. |
| 207 # Retry with exponential backoff so that we create them as fast as we |
| 208 # reasonably can. |
| 209 @Retry(StorageResponseError, tries=7, timeout_secs=1) |
| 210 def _CreateBucketWithExponentialBackoff(): |
| 211 bucket_uri.create_bucket(storage_class=storage_class, headers=headers) |
| 212 |
| 213 _CreateBucketWithExponentialBackoff() |
| 137 self.bucket_uris.append(bucket_uri) | 214 self.bucket_uris.append(bucket_uri) |
| 138 for i in range(test_objects): | 215 for i in range(test_objects): |
| 139 self.CreateObject(bucket_uri=bucket_uri, | 216 self.CreateObject(bucket_uri=bucket_uri, |
| 140 object_name=self.MakeTempName('obj'), | 217 object_name=self.MakeTempName('obj'), |
| 141 contents='test %d' % i) | 218 contents='test %d' % i) |
| 142 return bucket_uri | 219 return bucket_uri |
| 143 | 220 |
| 144 def CreateVersionedBucket(self, bucket_name=None, test_objects=0): | 221 def CreateVersionedBucket(self, bucket_name=None, test_objects=0): |
| 145 """Creates a versioned test bucket. | 222 """Creates a versioned test bucket. |
| 146 | 223 |
| (...skipping 10 matching lines...) Expand all Loading... |
| 157 """ | 234 """ |
| 158 bucket_uri = self.CreateBucket(bucket_name=bucket_name, | 235 bucket_uri = self.CreateBucket(bucket_name=bucket_name, |
| 159 test_objects=test_objects) | 236 test_objects=test_objects) |
| 160 bucket_uri.configure_versioning(True) | 237 bucket_uri.configure_versioning(True) |
| 161 return bucket_uri | 238 return bucket_uri |
| 162 | 239 |
| 163 def CreateObject(self, bucket_uri=None, object_name=None, contents=None): | 240 def CreateObject(self, bucket_uri=None, object_name=None, contents=None): |
| 164 """Creates a test object. | 241 """Creates a test object. |
| 165 | 242 |
| 166 Args: | 243 Args: |
| 167 bucket: The URI of the bucket to place the object in. If not specified, a | 244 bucket_uri: The URI of the bucket to place the object in. If not |
| 168 new temporary bucket is created. | 245 specified, a new temporary bucket is created. |
| 169 object_name: The name to use for the object. If not specified, a temporary | 246 object_name: The name to use for the object. If not specified, a temporary |
| 170 test object name is constructed. | 247 test object name is constructed. |
| 171 contents: The contents to write to the object. If not specified, the key | 248 contents: The contents to write to the object. If not specified, the key |
| 172 is not written to, which means that it isn't actually created | 249 is not written to, which means that it isn't actually created |
| 173 yet on the server. | 250 yet on the server. |
| 174 | 251 |
| 175 Returns: | 252 Returns: |
| 176 A StorageUri for the created object. | 253 A StorageUri for the created object. |
| 177 """ | 254 """ |
| 178 bucket_uri = bucket_uri or self.CreateBucket() | 255 bucket_uri = bucket_uri or self.CreateBucket() |
| (...skipping 14 matching lines...) Expand all Loading... |
| 193 return_stderr: If True, the standard error of the command is returned. | 270 return_stderr: If True, the standard error of the command is returned. |
| 194 expected_status: The expected return code. If not specified, defaults to | 271 expected_status: The expected return code. If not specified, defaults to |
| 195 0. If the return code is a different value, an exception | 272 0. If the return code is a different value, an exception |
| 196 is raised. | 273 is raised. |
| 197 stdin: A string of data to pipe to the process as standard input. | 274 stdin: A string of data to pipe to the process as standard input. |
| 198 | 275 |
| 199 Returns: | 276 Returns: |
| 200 A tuple containing the desired return values specified by the return_* | 277 A tuple containing the desired return values specified by the return_* |
| 201 arguments. | 278 arguments. |
| 202 """ | 279 """ |
| 203 cmd = [gslib.GSUTIL_PATH] + cmd | 280 cmd = [gslib.GSUTIL_PATH] + ['--testexceptiontraces'] + cmd |
| 204 if IS_WINDOWS: | 281 if IS_WINDOWS: |
| 205 cmd = [sys.executable] + cmd | 282 cmd = [sys.executable] + cmd |
| 206 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, | 283 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| 207 stdin=subprocess.PIPE) | 284 stdin=subprocess.PIPE) |
| 208 (stdout, stderr) = p.communicate(stdin) | 285 (stdout, stderr) = p.communicate(stdin) |
| 209 status = p.returncode | 286 status = p.returncode |
| 210 | 287 |
| 211 if expected_status is not None: | 288 if expected_status is not None: |
| 212 self.assertEqual( | 289 self.assertEqual( |
| 213 status, expected_status, | 290 status, expected_status, |
| 214 msg='Expected status %d, got %d.\nCommand:\n%s\n\nstderr:\n%s' % ( | 291 msg='Expected status %d, got %d.\nCommand:\n%s\n\nstderr:\n%s' % ( |
| 215 expected_status, status, ' '.join(cmd), stderr)) | 292 expected_status, status, ' '.join(cmd), stderr)) |
| 216 | 293 |
| 217 toreturn = [] | 294 toreturn = [] |
| 218 if return_status: | 295 if return_status: |
| 219 toreturn.append(status) | 296 toreturn.append(status) |
| 220 if return_stdout: | 297 if return_stdout: |
| 221 if IS_WINDOWS: | 298 if IS_WINDOWS: |
| 222 stdout = stdout.replace('\r\n', '\n') | 299 stdout = stdout.replace('\r\n', '\n') |
| 223 toreturn.append(stdout) | 300 toreturn.append(stdout) |
| 224 if return_stderr: | 301 if return_stderr: |
| 225 if IS_WINDOWS: | 302 if IS_WINDOWS: |
| 226 stderr = stderr.replace('\r\n', '\n') | 303 stderr = stderr.replace('\r\n', '\n') |
| 227 toreturn.append(stderr) | 304 toreturn.append(stderr) |
| 228 | 305 |
| 229 if len(toreturn) == 1: | 306 if len(toreturn) == 1: |
| 230 return toreturn[0] | 307 return toreturn[0] |
| 231 elif toreturn: | 308 elif toreturn: |
| 232 return tuple(toreturn) | 309 return tuple(toreturn) |
| 233 | 310 |
| 234 @contextmanager | 311 @contextmanager |
| 235 def SetAnonymousBotoCreds(self): | 312 def SetAnonymousBotoCreds(self): |
| 236 boto_config_path = self.CreateTempFile( | 313 boto_config_path = self.CreateTempFile( |
| 237 contents=BOTO_CONFIG_CONTENTS_IGNORE_ANON_WARNING) | 314 contents=BOTO_CONFIG_CONTENTS_IGNORE_ANON_WARNING) |
| 238 with SetBotoConfigForTest(boto_config_path): | 315 with SetBotoConfigFileForTest(boto_config_path): |
| 239 yield | 316 yield |
| OLD | NEW |