OLD | NEW |
(Empty) | |
| 1 # -*- coding: utf-8 -*- |
| 2 # Copyright 2013 Google Inc. All Rights Reserved. |
| 3 # |
| 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 |
| 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # |
| 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 |
| 16 from __future__ import absolute_import |
| 17 |
| 18 from contextlib import contextmanager |
| 19 import functools |
| 20 import os |
| 21 import pkgutil |
| 22 import posixpath |
| 23 import re |
| 24 import tempfile |
| 25 import unittest |
| 26 import urlparse |
| 27 |
| 28 import boto |
| 29 import gslib.tests as gslib_tests |
| 30 |
| 31 if not hasattr(unittest.TestCase, 'assertIsNone'): |
| 32 # external dependency unittest2 required for Python <= 2.6 |
| 33 import unittest2 as unittest # pylint: disable=g-import-not-at-top |
| 34 |
| 35 # Flags for running different types of tests. |
| 36 RUN_INTEGRATION_TESTS = True |
| 37 RUN_UNIT_TESTS = True |
| 38 RUN_S3_TESTS = False |
| 39 |
| 40 PARALLEL_COMPOSITE_UPLOAD_TEST_CONFIG = '/tmp/.boto.parallel_upload_test_config' |
| 41 |
| 42 |
| 43 def _HasS3Credentials(): |
| 44 return (boto.config.get('Credentials', 'aws_access_key_id', None) and |
| 45 boto.config.get('Credentials', 'aws_secret_access_key', None)) |
| 46 |
| 47 HAS_S3_CREDS = _HasS3Credentials() |
| 48 |
| 49 |
| 50 def _HasGSHost(): |
| 51 return boto.config.get('Credentials', 'gs_host', None) is not None |
| 52 |
| 53 HAS_GS_HOST = _HasGSHost() |
| 54 |
| 55 |
| 56 def _UsingJSONApi(): |
| 57 return boto.config.get('GSUtil', 'prefer_api', 'json').upper() != 'XML' |
| 58 |
| 59 USING_JSON_API = _UsingJSONApi() |
| 60 |
| 61 |
| 62 def _ArgcompleteAvailable(): |
| 63 argcomplete = None |
| 64 try: |
| 65 # pylint: disable=g-import-not-at-top |
| 66 import argcomplete |
| 67 except ImportError: |
| 68 pass |
| 69 return argcomplete is not None |
| 70 |
| 71 ARGCOMPLETE_AVAILABLE = _ArgcompleteAvailable() |
| 72 |
| 73 |
| 74 def _NormalizeURI(uri): |
| 75 """Normalizes the path component of a URI. |
| 76 |
| 77 Args: |
| 78 uri: URI to normalize. |
| 79 |
| 80 Returns: |
| 81 Normalized URI. |
| 82 |
| 83 Examples: |
| 84 gs://foo//bar -> gs://foo/bar |
| 85 gs://foo/./bar -> gs://foo/bar |
| 86 """ |
| 87 # Note: we have to do this dance of changing gs:// to file:// because on |
| 88 # Windows, the urlparse function won't work with URL schemes that are not |
| 89 # known. urlparse('gs://foo/bar') on Windows turns into: |
| 90 # scheme='gs', netloc='', path='//foo/bar' |
| 91 # while on non-Windows platforms, it turns into: |
| 92 # scheme='gs', netloc='foo', path='/bar' |
| 93 uri = uri.replace('gs://', 'file://') |
| 94 parsed = list(urlparse.urlparse(uri)) |
| 95 parsed[2] = posixpath.normpath(parsed[2]) |
| 96 if parsed[2].startswith('//'): |
| 97 # The normpath function doesn't change '//foo' -> '/foo' by design. |
| 98 parsed[2] = parsed[2][1:] |
| 99 unparsed = urlparse.urlunparse(parsed) |
| 100 unparsed = unparsed.replace('file://', 'gs://') |
| 101 return unparsed |
| 102 |
| 103 |
| 104 def GenerationFromURI(uri): |
| 105 """Returns a the generation for a StorageUri. |
| 106 |
| 107 Args: |
| 108 uri: boto.storage_uri.StorageURI object to get the URI from. |
| 109 |
| 110 Returns: |
| 111 Generation string for the URI. |
| 112 """ |
| 113 if not (uri.generation or uri.version_id): |
| 114 if uri.scheme == 's3': return 'null' |
| 115 return uri.generation or uri.version_id |
| 116 |
| 117 |
| 118 def ObjectToURI(obj, *suffixes): |
| 119 """Returns the storage URI string for a given StorageUri or file object. |
| 120 |
| 121 Args: |
| 122 obj: The object to get the URI from. Can be a file object, a subclass of |
| 123 boto.storage_uri.StorageURI, or a string. If a string, it is assumed to |
| 124 be a local on-disk path. |
| 125 *suffixes: Suffixes to append. For example, ObjectToUri(bucketuri, 'foo') |
| 126 would return the URI for a key name 'foo' inside the given |
| 127 bucket. |
| 128 |
| 129 Returns: |
| 130 Storage URI string. |
| 131 """ |
| 132 if isinstance(obj, file): |
| 133 return 'file://%s' % os.path.abspath(os.path.join(obj.name, *suffixes)) |
| 134 if isinstance(obj, basestring): |
| 135 return 'file://%s' % os.path.join(obj, *suffixes) |
| 136 uri = obj.uri |
| 137 if suffixes: |
| 138 uri = _NormalizeURI('/'.join([uri] + list(suffixes))) |
| 139 |
| 140 # Storage URIs shouldn't contain a trailing slash. |
| 141 if uri.endswith('/'): |
| 142 uri = uri[:-1] |
| 143 return uri |
| 144 |
| 145 # The mock storage service comes from the Boto library, but it is not |
| 146 # distributed with Boto when installed as a package. To get around this, we |
| 147 # copy the file to gslib/tests/mock_storage_service.py when building the gsutil |
| 148 # package. Try and import from both places here. |
| 149 # pylint: disable=g-import-not-at-top |
| 150 try: |
| 151 from gslib.tests import mock_storage_service |
| 152 except ImportError: |
| 153 try: |
| 154 from boto.tests.integration.s3 import mock_storage_service |
| 155 except ImportError: |
| 156 try: |
| 157 from tests.integration.s3 import mock_storage_service |
| 158 except ImportError: |
| 159 import mock_storage_service |
| 160 |
| 161 |
| 162 class GSMockConnection(mock_storage_service.MockConnection): |
| 163 |
| 164 def __init__(self, *args, **kwargs): |
| 165 kwargs['provider'] = 'gs' |
| 166 self.debug = 0 |
| 167 super(GSMockConnection, self).__init__(*args, **kwargs) |
| 168 |
| 169 mock_connection = GSMockConnection() |
| 170 |
| 171 |
| 172 class GSMockBucketStorageUri(mock_storage_service.MockBucketStorageUri): |
| 173 |
| 174 def connect(self, access_key_id=None, secret_access_key=None): |
| 175 return mock_connection |
| 176 |
| 177 def compose(self, components, headers=None): |
| 178 """Dummy implementation to allow parallel uploads with tests.""" |
| 179 return self.new_key() |
| 180 |
| 181 |
| 182 TEST_BOTO_REMOVE_SECTION = 'TestRemoveSection' |
| 183 |
| 184 |
| 185 def _SetBotoConfig(section, name, value, revert_list): |
| 186 """Sets boto configuration temporarily for testing. |
| 187 |
| 188 SetBotoConfigForTest and SetBotoConfigFileForTest should be called by tests |
| 189 instead of this function. Those functions will ensure that the configuration |
| 190 is reverted to its original setting using _RevertBotoConfig. |
| 191 |
| 192 Args: |
| 193 section: Boto config section to set |
| 194 name: Boto config name to set |
| 195 value: Value to set |
| 196 revert_list: List for tracking configs to revert. |
| 197 """ |
| 198 prev_value = boto.config.get(section, name, None) |
| 199 if not boto.config.has_section(section): |
| 200 revert_list.append((section, TEST_BOTO_REMOVE_SECTION, None)) |
| 201 boto.config.add_section(section) |
| 202 revert_list.append((section, name, prev_value)) |
| 203 if value is None: |
| 204 boto.config.remove_option(section, name) |
| 205 else: |
| 206 boto.config.set(section, name, value) |
| 207 |
| 208 |
| 209 def _RevertBotoConfig(revert_list): |
| 210 """Reverts boto config modifications made by _SetBotoConfig. |
| 211 |
| 212 Args: |
| 213 revert_list: List of boto config modifications created by calls to |
| 214 _SetBotoConfig. |
| 215 """ |
| 216 sections_to_remove = [] |
| 217 for section, name, value in revert_list: |
| 218 if value is None: |
| 219 if name == TEST_BOTO_REMOVE_SECTION: |
| 220 sections_to_remove.append(section) |
| 221 else: |
| 222 boto.config.remove_option(section, name) |
| 223 else: |
| 224 boto.config.set(section, name, value) |
| 225 for section in sections_to_remove: |
| 226 boto.config.remove_section(section) |
| 227 |
| 228 |
| 229 def PerformsFileToObjectUpload(func): |
| 230 """Decorator indicating that a test uploads from a local file to an object. |
| 231 |
| 232 This forces the test to run once normally, and again with special boto |
| 233 config settings that will ensure that the test follows the parallel composite |
| 234 upload code path. |
| 235 |
| 236 Args: |
| 237 func: Function to wrap. |
| 238 |
| 239 Returns: |
| 240 Wrapped function. |
| 241 """ |
| 242 @functools.wraps(func) |
| 243 def Wrapper(*args, **kwargs): |
| 244 # Run the test normally once. |
| 245 func(*args, **kwargs) |
| 246 |
| 247 # Try again, forcing parallel composite uploads. |
| 248 with SetBotoConfigForTest([ |
| 249 ('GSUtil', 'parallel_composite_upload_threshold', '1'), |
| 250 ('GSUtil', 'check_hashes', 'always')]): |
| 251 func(*args, **kwargs) |
| 252 |
| 253 return Wrapper |
| 254 |
| 255 |
| 256 @contextmanager |
| 257 def SetBotoConfigForTest(boto_config_list): |
| 258 """Sets the input list of boto configs for the duration of a 'with' clause. |
| 259 |
| 260 Args: |
| 261 boto_config_list: list of tuples of: |
| 262 (boto config section to set, boto config name to set, value to set) |
| 263 |
| 264 Yields: |
| 265 Once after config is set. |
| 266 """ |
| 267 revert_configs = [] |
| 268 tmp_filename = None |
| 269 try: |
| 270 tmp_fd, tmp_filename = tempfile.mkstemp(prefix='gsutil-temp-cfg') |
| 271 os.close(tmp_fd) |
| 272 for boto_config in boto_config_list: |
| 273 _SetBotoConfig(boto_config[0], boto_config[1], boto_config[2], |
| 274 revert_configs) |
| 275 with open(tmp_filename, 'w') as tmp_file: |
| 276 boto.config.write(tmp_file) |
| 277 |
| 278 with SetBotoConfigFileForTest(tmp_filename): |
| 279 yield |
| 280 finally: |
| 281 _RevertBotoConfig(revert_configs) |
| 282 if tmp_filename: |
| 283 try: |
| 284 os.remove(tmp_filename) |
| 285 except OSError: |
| 286 pass |
| 287 |
| 288 |
| 289 @contextmanager |
| 290 def SetEnvironmentForTest(env_variable_dict): |
| 291 """Sets OS environment variables for a single test.""" |
| 292 |
| 293 def _ApplyDictToEnvironment(dict_to_apply): |
| 294 for k, v in dict_to_apply.iteritems(): |
| 295 old_values[k] = os.environ.get(k) |
| 296 if v is not None: |
| 297 os.environ[k] = v |
| 298 elif k in os.environ: |
| 299 del os.environ[k] |
| 300 |
| 301 old_values = {} |
| 302 for k in env_variable_dict: |
| 303 old_values[k] = os.environ.get(k) |
| 304 |
| 305 try: |
| 306 _ApplyDictToEnvironment(env_variable_dict) |
| 307 yield |
| 308 finally: |
| 309 _ApplyDictToEnvironment(old_values) |
| 310 |
| 311 |
| 312 @contextmanager |
| 313 def SetBotoConfigFileForTest(boto_config_path): |
| 314 """Sets a given file as the boto config file for a single test.""" |
| 315 # Setup for entering "with" block. |
| 316 try: |
| 317 old_boto_config_env_variable = os.environ['BOTO_CONFIG'] |
| 318 boto_config_was_set = True |
| 319 except KeyError: |
| 320 boto_config_was_set = False |
| 321 os.environ['BOTO_CONFIG'] = boto_config_path |
| 322 |
| 323 try: |
| 324 yield |
| 325 finally: |
| 326 # Teardown for exiting "with" block. |
| 327 if boto_config_was_set: |
| 328 os.environ['BOTO_CONFIG'] = old_boto_config_env_variable |
| 329 else: |
| 330 os.environ.pop('BOTO_CONFIG', None) |
| 331 |
| 332 |
| 333 def GetTestNames(): |
| 334 """Returns a list of the names of the test modules in gslib.tests.""" |
| 335 matcher = re.compile(r'^test_(?P<name>.*)$') |
| 336 names = [] |
| 337 for _, modname, _ in pkgutil.iter_modules(gslib_tests.__path__): |
| 338 m = matcher.match(modname) |
| 339 if m: |
| 340 names.append(m.group('name')) |
| 341 return names |
| 342 |
| 343 |
| 344 @contextmanager |
| 345 def WorkingDirectory(new_working_directory): |
| 346 """Changes the working directory for the duration of a 'with' call. |
| 347 |
| 348 Args: |
| 349 new_working_directory: The directory to switch to before executing wrapped |
| 350 code. A None value indicates that no switching is necessary. |
| 351 |
| 352 Yields: |
| 353 Once after working directory has been changed. |
| 354 """ |
| 355 prev_working_directory = None |
| 356 try: |
| 357 prev_working_directory = os.getcwd() |
| 358 except OSError: |
| 359 # This can happen if the current working directory no longer exists. |
| 360 pass |
| 361 |
| 362 if new_working_directory: |
| 363 os.chdir(new_working_directory) |
| 364 |
| 365 try: |
| 366 yield |
| 367 finally: |
| 368 if new_working_directory and prev_working_directory: |
| 369 os.chdir(prev_working_directory) |
OLD | NEW |