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 |