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