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