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.
16 from __future__
import absolute_import
18 from contextlib
import contextmanager
29 import gslib
.tests
as gslib_tests
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
35 # Flags for running different types of tests.
36 RUN_INTEGRATION_TESTS
= True
40 PARALLEL_COMPOSITE_UPLOAD_TEST_CONFIG
= '/tmp/.boto.parallel_upload_test_config'
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))
47 HAS_S3_CREDS
= _HasS3Credentials()
51 return boto
.config
.get('Credentials', 'gs_host', None) is not None
53 HAS_GS_HOST
= _HasGSHost()
57 return boto
.config
.get('GSUtil', 'prefer_api', 'json').upper() != 'XML'
59 USING_JSON_API
= _UsingJSONApi()
62 def _ArgcompleteAvailable():
65 # pylint: disable=g-import-not-at-top
69 return argcomplete
is not None
71 ARGCOMPLETE_AVAILABLE
= _ArgcompleteAvailable()
74 def _NormalizeURI(uri
):
75 """Normalizes the path component of a URI.
78 uri: URI to normalize.
84 gs://foo//bar -> gs://foo/bar
85 gs://foo/./bar -> gs://foo/bar
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://')
104 def GenerationFromURI(uri
):
105 """Returns a the generation for a StorageUri.
108 uri: boto.storage_uri.StorageURI object to get the URI from.
111 Generation string for the URI.
113 if not (uri
.generation
or uri
.version_id
):
114 if uri
.scheme
== 's3': return 'null'
115 return uri
.generation
or uri
.version_id
118 def ObjectToURI(obj
, *suffixes
):
119 """Returns the storage URI string for a given StorageUri or file object.
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
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
)
138 uri
= _NormalizeURI('/'.join([uri
] + list(suffixes
)))
140 # Storage URIs shouldn't contain a trailing slash.
141 if uri
.endswith('/'):
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
151 from gslib
.tests
import mock_storage_service
154 from boto
.tests
.integration
.s3
import mock_storage_service
157 from tests
.integration
.s3
import mock_storage_service
159 import mock_storage_service
162 class GSMockConnection(mock_storage_service
.MockConnection
):
164 def __init__(self
, *args
, **kwargs
):
165 kwargs
['provider'] = 'gs'
167 super(GSMockConnection
, self
).__init
__(*args
, **kwargs
)
169 mock_connection
= GSMockConnection()
172 class GSMockBucketStorageUri(mock_storage_service
.MockBucketStorageUri
):
174 def connect(self
, access_key_id
=None, secret_access_key
=None):
175 return mock_connection
177 def compose(self
, components
, headers
=None):
178 """Dummy implementation to allow parallel uploads with tests."""
179 return self
.new_key()
182 TEST_BOTO_REMOVE_SECTION
= 'TestRemoveSection'
185 def _SetBotoConfig(section
, name
, value
, revert_list
):
186 """Sets boto configuration temporarily for testing.
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.
193 section: Boto config section to set
194 name: Boto config name to set
196 revert_list: List for tracking configs to revert.
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
))
204 boto
.config
.remove_option(section
, name
)
206 boto
.config
.set(section
, name
, value
)
209 def _RevertBotoConfig(revert_list
):
210 """Reverts boto config modifications made by _SetBotoConfig.
213 revert_list: List of boto config modifications created by calls to
216 sections_to_remove
= []
217 for section
, name
, value
in revert_list
:
219 if name
== TEST_BOTO_REMOVE_SECTION
:
220 sections_to_remove
.append(section
)
222 boto
.config
.remove_option(section
, name
)
224 boto
.config
.set(section
, name
, value
)
225 for section
in sections_to_remove
:
226 boto
.config
.remove_section(section
)
229 def PerformsFileToObjectUpload(func
):
230 """Decorator indicating that a test uploads from a local file to an object.
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
237 func: Function to wrap.
242 @functools.wraps(func
)
243 def Wrapper(*args
, **kwargs
):
244 # Run the test normally once.
245 func(*args
, **kwargs
)
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
)
257 def SetBotoConfigForTest(boto_config_list
):
258 """Sets the input list of boto configs for the duration of a 'with' clause.
261 boto_config_list: list of tuples of:
262 (boto config section to set, boto config name to set, value to set)
265 Once after config is set.
270 tmp_fd
, tmp_filename
= tempfile
.mkstemp(prefix
='gsutil-temp-cfg')
272 for boto_config
in boto_config_list
:
273 _SetBotoConfig(boto_config
[0], boto_config
[1], boto_config
[2],
275 with
open(tmp_filename
, 'w') as tmp_file
:
276 boto
.config
.write(tmp_file
)
278 with
SetBotoConfigFileForTest(tmp_filename
):
281 _RevertBotoConfig(revert_configs
)
284 os
.remove(tmp_filename
)
290 def SetEnvironmentForTest(env_variable_dict
):
291 """Sets OS environment variables for a single test."""
293 def _ApplyDictToEnvironment(dict_to_apply
):
294 for k
, v
in dict_to_apply
.iteritems():
295 old_values
[k
] = os
.environ
.get(k
)
298 elif k
in os
.environ
:
302 for k
in env_variable_dict
:
303 old_values
[k
] = os
.environ
.get(k
)
306 _ApplyDictToEnvironment(env_variable_dict
)
309 _ApplyDictToEnvironment(old_values
)
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.
317 old_boto_config_env_variable
= os
.environ
['BOTO_CONFIG']
318 boto_config_was_set
= True
320 boto_config_was_set
= False
321 os
.environ
['BOTO_CONFIG'] = boto_config_path
326 # Teardown for exiting "with" block.
327 if boto_config_was_set
:
328 os
.environ
['BOTO_CONFIG'] = old_boto_config_env_variable
330 os
.environ
.pop('BOTO_CONFIG', None)
334 """Returns a list of the names of the test modules in gslib.tests."""
335 matcher
= re
.compile(r
'^test_(?P<name>.*)$')
337 for _
, modname
, _
in pkgutil
.iter_modules(gslib_tests
.__path
__):
338 m
= matcher
.match(modname
)
340 names
.append(m
.group('name'))
345 def WorkingDirectory(new_working_directory
):
346 """Changes the working directory for the duration of a 'with' call.
349 new_working_directory: The directory to switch to before executing wrapped
350 code. A None value indicates that no switching is necessary.
353 Once after working directory has been changed.
355 prev_working_directory
= None
357 prev_working_directory
= os
.getcwd()
359 # This can happen if the current working directory no longer exists.
362 if new_working_directory
:
363 os
.chdir(new_working_directory
)
368 if new_working_directory
and prev_working_directory
:
369 os
.chdir(prev_working_directory
)