Index: third_party/gsutil/gslib/tests/util.py |
diff --git a/third_party/gsutil/gslib/tests/util.py b/third_party/gsutil/gslib/tests/util.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..06da870bf8170ff8ee4b39b6ea028da57d0e795b |
--- /dev/null |
+++ b/third_party/gsutil/gslib/tests/util.py |
@@ -0,0 +1,369 @@ |
+# -*- coding: utf-8 -*- |
+# Copyright 2013 Google Inc. All Rights Reserved. |
+# |
+# Licensed under the Apache License, Version 2.0 (the "License"); |
+# you may not use this file except in compliance with the License. |
+# You may obtain a copy of the License at |
+# |
+# http://www.apache.org/licenses/LICENSE-2.0 |
+# |
+# Unless required by applicable law or agreed to in writing, software |
+# distributed under the License is distributed on an "AS IS" BASIS, |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+# See the License for the specific language governing permissions and |
+# limitations under the License. |
+ |
+from __future__ import absolute_import |
+ |
+from contextlib import contextmanager |
+import functools |
+import os |
+import pkgutil |
+import posixpath |
+import re |
+import tempfile |
+import unittest |
+import urlparse |
+ |
+import boto |
+import gslib.tests as gslib_tests |
+ |
+if not hasattr(unittest.TestCase, 'assertIsNone'): |
+ # external dependency unittest2 required for Python <= 2.6 |
+ import unittest2 as unittest # pylint: disable=g-import-not-at-top |
+ |
+# Flags for running different types of tests. |
+RUN_INTEGRATION_TESTS = True |
+RUN_UNIT_TESTS = True |
+RUN_S3_TESTS = False |
+ |
+PARALLEL_COMPOSITE_UPLOAD_TEST_CONFIG = '/tmp/.boto.parallel_upload_test_config' |
+ |
+ |
+def _HasS3Credentials(): |
+ return (boto.config.get('Credentials', 'aws_access_key_id', None) and |
+ boto.config.get('Credentials', 'aws_secret_access_key', None)) |
+ |
+HAS_S3_CREDS = _HasS3Credentials() |
+ |
+ |
+def _HasGSHost(): |
+ return boto.config.get('Credentials', 'gs_host', None) is not None |
+ |
+HAS_GS_HOST = _HasGSHost() |
+ |
+ |
+def _UsingJSONApi(): |
+ return boto.config.get('GSUtil', 'prefer_api', 'json').upper() != 'XML' |
+ |
+USING_JSON_API = _UsingJSONApi() |
+ |
+ |
+def _ArgcompleteAvailable(): |
+ argcomplete = None |
+ try: |
+ # pylint: disable=g-import-not-at-top |
+ import argcomplete |
+ except ImportError: |
+ pass |
+ return argcomplete is not None |
+ |
+ARGCOMPLETE_AVAILABLE = _ArgcompleteAvailable() |
+ |
+ |
+def _NormalizeURI(uri): |
+ """Normalizes the path component of a URI. |
+ |
+ Args: |
+ uri: URI to normalize. |
+ |
+ Returns: |
+ Normalized URI. |
+ |
+ Examples: |
+ gs://foo//bar -> gs://foo/bar |
+ gs://foo/./bar -> gs://foo/bar |
+ """ |
+ # Note: we have to do this dance of changing gs:// to file:// because on |
+ # Windows, the urlparse function won't work with URL schemes that are not |
+ # known. urlparse('gs://foo/bar') on Windows turns into: |
+ # scheme='gs', netloc='', path='//foo/bar' |
+ # while on non-Windows platforms, it turns into: |
+ # scheme='gs', netloc='foo', path='/bar' |
+ uri = uri.replace('gs://', 'file://') |
+ parsed = list(urlparse.urlparse(uri)) |
+ parsed[2] = posixpath.normpath(parsed[2]) |
+ if parsed[2].startswith('//'): |
+ # The normpath function doesn't change '//foo' -> '/foo' by design. |
+ parsed[2] = parsed[2][1:] |
+ unparsed = urlparse.urlunparse(parsed) |
+ unparsed = unparsed.replace('file://', 'gs://') |
+ return unparsed |
+ |
+ |
+def GenerationFromURI(uri): |
+ """Returns a the generation for a StorageUri. |
+ |
+ Args: |
+ uri: boto.storage_uri.StorageURI object to get the URI from. |
+ |
+ Returns: |
+ Generation string for the URI. |
+ """ |
+ if not (uri.generation or uri.version_id): |
+ if uri.scheme == 's3': return 'null' |
+ return uri.generation or uri.version_id |
+ |
+ |
+def ObjectToURI(obj, *suffixes): |
+ """Returns the storage URI string for a given StorageUri or file object. |
+ |
+ Args: |
+ obj: The object to get the URI from. Can be a file object, a subclass of |
+ boto.storage_uri.StorageURI, or a string. If a string, it is assumed to |
+ be a local on-disk path. |
+ *suffixes: Suffixes to append. For example, ObjectToUri(bucketuri, 'foo') |
+ would return the URI for a key name 'foo' inside the given |
+ bucket. |
+ |
+ Returns: |
+ Storage URI string. |
+ """ |
+ if isinstance(obj, file): |
+ return 'file://%s' % os.path.abspath(os.path.join(obj.name, *suffixes)) |
+ if isinstance(obj, basestring): |
+ return 'file://%s' % os.path.join(obj, *suffixes) |
+ uri = obj.uri |
+ if suffixes: |
+ uri = _NormalizeURI('/'.join([uri] + list(suffixes))) |
+ |
+ # Storage URIs shouldn't contain a trailing slash. |
+ if uri.endswith('/'): |
+ uri = uri[:-1] |
+ return uri |
+ |
+# The mock storage service comes from the Boto library, but it is not |
+# distributed with Boto when installed as a package. To get around this, we |
+# copy the file to gslib/tests/mock_storage_service.py when building the gsutil |
+# package. Try and import from both places here. |
+# pylint: disable=g-import-not-at-top |
+try: |
+ from gslib.tests import mock_storage_service |
+except ImportError: |
+ try: |
+ from boto.tests.integration.s3 import mock_storage_service |
+ except ImportError: |
+ try: |
+ from tests.integration.s3 import mock_storage_service |
+ except ImportError: |
+ import mock_storage_service |
+ |
+ |
+class GSMockConnection(mock_storage_service.MockConnection): |
+ |
+ def __init__(self, *args, **kwargs): |
+ kwargs['provider'] = 'gs' |
+ self.debug = 0 |
+ super(GSMockConnection, self).__init__(*args, **kwargs) |
+ |
+mock_connection = GSMockConnection() |
+ |
+ |
+class GSMockBucketStorageUri(mock_storage_service.MockBucketStorageUri): |
+ |
+ def connect(self, access_key_id=None, secret_access_key=None): |
+ return mock_connection |
+ |
+ def compose(self, components, headers=None): |
+ """Dummy implementation to allow parallel uploads with tests.""" |
+ return self.new_key() |
+ |
+ |
+TEST_BOTO_REMOVE_SECTION = 'TestRemoveSection' |
+ |
+ |
+def _SetBotoConfig(section, name, value, revert_list): |
+ """Sets boto configuration temporarily for testing. |
+ |
+ SetBotoConfigForTest and SetBotoConfigFileForTest should be called by tests |
+ instead of this function. Those functions will ensure that the configuration |
+ is reverted to its original setting using _RevertBotoConfig. |
+ |
+ Args: |
+ section: Boto config section to set |
+ name: Boto config name to set |
+ value: Value to set |
+ revert_list: List for tracking configs to revert. |
+ """ |
+ prev_value = boto.config.get(section, name, None) |
+ if not boto.config.has_section(section): |
+ revert_list.append((section, TEST_BOTO_REMOVE_SECTION, None)) |
+ boto.config.add_section(section) |
+ revert_list.append((section, name, prev_value)) |
+ if value is None: |
+ boto.config.remove_option(section, name) |
+ else: |
+ boto.config.set(section, name, value) |
+ |
+ |
+def _RevertBotoConfig(revert_list): |
+ """Reverts boto config modifications made by _SetBotoConfig. |
+ |
+ Args: |
+ revert_list: List of boto config modifications created by calls to |
+ _SetBotoConfig. |
+ """ |
+ sections_to_remove = [] |
+ for section, name, value in revert_list: |
+ if value is None: |
+ if name == TEST_BOTO_REMOVE_SECTION: |
+ sections_to_remove.append(section) |
+ else: |
+ boto.config.remove_option(section, name) |
+ else: |
+ boto.config.set(section, name, value) |
+ for section in sections_to_remove: |
+ boto.config.remove_section(section) |
+ |
+ |
+def PerformsFileToObjectUpload(func): |
+ """Decorator indicating that a test uploads from a local file to an object. |
+ |
+ This forces the test to run once normally, and again with special boto |
+ config settings that will ensure that the test follows the parallel composite |
+ upload code path. |
+ |
+ Args: |
+ func: Function to wrap. |
+ |
+ Returns: |
+ Wrapped function. |
+ """ |
+ @functools.wraps(func) |
+ def Wrapper(*args, **kwargs): |
+ # Run the test normally once. |
+ func(*args, **kwargs) |
+ |
+ # Try again, forcing parallel composite uploads. |
+ with SetBotoConfigForTest([ |
+ ('GSUtil', 'parallel_composite_upload_threshold', '1'), |
+ ('GSUtil', 'check_hashes', 'always')]): |
+ func(*args, **kwargs) |
+ |
+ return Wrapper |
+ |
+ |
+@contextmanager |
+def SetBotoConfigForTest(boto_config_list): |
+ """Sets the input list of boto configs for the duration of a 'with' clause. |
+ |
+ Args: |
+ boto_config_list: list of tuples of: |
+ (boto config section to set, boto config name to set, value to set) |
+ |
+ Yields: |
+ Once after config is set. |
+ """ |
+ revert_configs = [] |
+ tmp_filename = None |
+ try: |
+ tmp_fd, tmp_filename = tempfile.mkstemp(prefix='gsutil-temp-cfg') |
+ os.close(tmp_fd) |
+ for boto_config in boto_config_list: |
+ _SetBotoConfig(boto_config[0], boto_config[1], boto_config[2], |
+ revert_configs) |
+ with open(tmp_filename, 'w') as tmp_file: |
+ boto.config.write(tmp_file) |
+ |
+ with SetBotoConfigFileForTest(tmp_filename): |
+ yield |
+ finally: |
+ _RevertBotoConfig(revert_configs) |
+ if tmp_filename: |
+ try: |
+ os.remove(tmp_filename) |
+ except OSError: |
+ pass |
+ |
+ |
+@contextmanager |
+def SetEnvironmentForTest(env_variable_dict): |
+ """Sets OS environment variables for a single test.""" |
+ |
+ def _ApplyDictToEnvironment(dict_to_apply): |
+ for k, v in dict_to_apply.iteritems(): |
+ old_values[k] = os.environ.get(k) |
+ if v is not None: |
+ os.environ[k] = v |
+ elif k in os.environ: |
+ del os.environ[k] |
+ |
+ old_values = {} |
+ for k in env_variable_dict: |
+ old_values[k] = os.environ.get(k) |
+ |
+ try: |
+ _ApplyDictToEnvironment(env_variable_dict) |
+ yield |
+ finally: |
+ _ApplyDictToEnvironment(old_values) |
+ |
+ |
+@contextmanager |
+def SetBotoConfigFileForTest(boto_config_path): |
+ """Sets a given file as the boto config file for a single test.""" |
+ # Setup for entering "with" block. |
+ try: |
+ old_boto_config_env_variable = os.environ['BOTO_CONFIG'] |
+ boto_config_was_set = True |
+ except KeyError: |
+ boto_config_was_set = False |
+ os.environ['BOTO_CONFIG'] = boto_config_path |
+ |
+ try: |
+ yield |
+ finally: |
+ # Teardown for exiting "with" block. |
+ if boto_config_was_set: |
+ os.environ['BOTO_CONFIG'] = old_boto_config_env_variable |
+ else: |
+ os.environ.pop('BOTO_CONFIG', None) |
+ |
+ |
+def GetTestNames(): |
+ """Returns a list of the names of the test modules in gslib.tests.""" |
+ matcher = re.compile(r'^test_(?P<name>.*)$') |
+ names = [] |
+ for _, modname, _ in pkgutil.iter_modules(gslib_tests.__path__): |
+ m = matcher.match(modname) |
+ if m: |
+ names.append(m.group('name')) |
+ return names |
+ |
+ |
+@contextmanager |
+def WorkingDirectory(new_working_directory): |
+ """Changes the working directory for the duration of a 'with' call. |
+ |
+ Args: |
+ new_working_directory: The directory to switch to before executing wrapped |
+ code. A None value indicates that no switching is necessary. |
+ |
+ Yields: |
+ Once after working directory has been changed. |
+ """ |
+ prev_working_directory = None |
+ try: |
+ prev_working_directory = os.getcwd() |
+ except OSError: |
+ # This can happen if the current working directory no longer exists. |
+ pass |
+ |
+ if new_working_directory: |
+ os.chdir(new_working_directory) |
+ |
+ try: |
+ yield |
+ finally: |
+ if new_working_directory and prev_working_directory: |
+ os.chdir(prev_working_directory) |