Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / tools / telemetry / third_party / gsutilz / gslib / tests / testcase / integration_testcase.py
blob0979c453a4e17565b1b4f6e36cefe6f29ed9e95b
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
20 import cStringIO
21 import locale
22 import logging
23 import os
24 import subprocess
25 import sys
26 import tempfile
28 import boto
29 from boto.exception import StorageResponseError
30 from boto.s3.deletemarker import DeleteMarker
31 from boto.storage_uri import BucketStorageUri
33 import gslib
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 = """
59 [Boto]
60 num_retries = 2
61 [Tests]
62 bypass_anonymous_access_warning = True
63 """
66 def SkipForGS(reason):
67 if not RUN_S3_TESTS:
68 return unittest.skip(reason)
69 else:
70 return lambda func: func
73 def SkipForS3(reason):
74 if RUN_S3_TESTS:
75 return unittest.skip(reason)
76 else:
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'
87 GROUP_TEST_ID = (
88 '00b4903a97d097895ab58ef505d535916a712215b79c3e54932c2eb502ad97f5')
89 USER_TEST_ADDRESS = 'gs-team@google.com'
90 USER_TEST_ID = (
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'
99 def setUp(self):
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(),
110 'gs')
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)
122 def tearDown(self):
123 super(GsUtilIntegrationTestCase, self).tearDown()
125 while self.bucket_uris:
126 bucket_uri = self.bucket_uris[-1]
127 try:
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.
132 if e.status == 404:
133 self.bucket_uris.pop()
134 continue
135 else:
136 raise
137 while bucket_list:
138 error = None
139 for k in bucket_list:
140 try:
141 if isinstance(k, DeleteMarker):
142 bucket_uri.get_bucket().delete_key(k.name,
143 version_id=k.version_id)
144 else:
145 k.delete()
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.
151 if e.status == 404:
152 error = e
153 else:
154 raise
155 if error:
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.
174 Args:
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.
179 Raises:
180 AssertionError if number of objects does not match expected value.
182 Returns:
183 Listing split across lines.
185 # Use @Retry as hedge against bucket listing eventual consistency.
186 @Retry(AssertionError, tries=5, timeout_secs=1)
187 def _Check1():
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)
193 return listing
194 return _Check1()
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.
202 Args:
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.
206 Defaults to 0.
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.
211 Returns:
212 StorageUri for the created bucket.
214 if not provider:
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)
225 return 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)
232 if provider == 'gs':
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()
236 else:
237 headers = {}
239 # Parallel tests can easily run into bucket creation quotas.
240 # Retry with exponential backoff so that we create them as fast as we
241 # reasonably can.
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)
252 return bucket_uri
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.
259 Args:
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.
263 Defaults to 0.
265 Returns:
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)
271 return bucket_uri
273 def CreateObject(self, bucket_uri=None, object_name=None, contents=None,
274 prefer_json_api=False):
275 """Creates a test object.
277 Args:
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
284 yet on the server.
285 prefer_json_api: If true, use the JSON creation functions where possible.
287 Returns:
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,
303 True,
304 md5=(Base64ToHexHash(json_object.md5Hash),
305 json_object.md5Hash.strip('\n"\'')))
306 # pylint: enable=protected-access
307 return object_uri
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)
314 return key_uri
316 def CreateBucketJson(self, bucket_name=None, test_objects=0,
317 storage_class=None):
318 """Creates a test bucket using the JSON API.
320 The bucket and all of its contents will be deleted after the test.
322 Args:
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.
326 Defaults to 0.
327 storage_class: storage class to use. If not provided we us standard.
329 Returns:
330 Apitools Bucket for the created bucket.
332 bucket_name = bucket_name or self.MakeTempName('bucket')
333 bucket_metadata = None
334 if storage_class:
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)
351 return bucket
353 def CreateObjectJson(self, contents, bucket_name=None, object_name=None):
354 """Creates a test object (GCS provider only) using the JSON API.
356 Args:
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.
363 Returns:
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(
369 name=object_name,
370 bucket=bucket_name,
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.
379 Args:
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
386 is raised.
387 stdin: A string of data to pipe to the process as standard input.
389 Returns:
390 A tuple containing the desired return values specified by the return_*
391 arguments.
393 cmd = ([gslib.GSUTIL_PATH] + ['--testexceptiontraces'] +
394 ['-o', 'GSUtil:default_project_id=' + PopulateProjectId()] +
395 cmd)
396 if IS_WINDOWS:
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:
404 self.assertEqual(
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))
409 toreturn = []
410 if return_status:
411 toreturn.append(status)
412 if return_stdout:
413 if IS_WINDOWS:
414 stdout = stdout.replace('\r\n', '\n')
415 toreturn.append(stdout)
416 if return_stderr:
417 if IS_WINDOWS:
418 stderr = stderr.replace('\r\n', '\n')
419 toreturn.append(stderr)
421 if len(toreturn) == 1:
422 return toreturn[0]
423 elif toreturn:
424 return tuple(toreturn)
426 def RunGsUtilTabCompletion(self, cmd, expected_results=None):
427 """Runs the gsutil command in tab completion mode.
429 Args:
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())
453 if results_string:
454 results = results_string.split('\013')
455 else:
456 results = []
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')]):
462 _RunTabCompletion()
464 @contextmanager
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}):
472 yield