summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorTim Burke <tim.burke@gmail.com>2015-03-03 12:35:03 -0800
committerTim Burke <tim.burke@gmail.com>2015-03-23 18:35:45 -0700
commita4fb70ece189aff85f234ab6b3f275b69e936c03 (patch)
tree5fe51c0846b7fbb45137bb5689bde08f61033dd8 /tests
parent925c01ebfbdfb6478a3786f24a9572deae40f8f8 (diff)
downloadpython-swiftclient-a4fb70ece189aff85f234ab6b3f275b69e936c03.tar.gz
Compare each chunk of large objects when uploading
Previously, we compared the ETag from Swift against the MD5 of the entire large object. However, the ETag for large objects is generally the MD5 of the concatenation of the ETags for each segment, unless the object is a DLO whose segments span more than one page of a container listing. Rather than worry about ETags, just compare each chunk of the segmented file. This allows the use of --skip-identical when uploading SLOs and DLOs. Additionally, there are several test-related improvements: * The default arguments for OutputManager are now evaluated on construction, rather than on definition, so that TestOutputManager.test_instantiation will succeed when using nosetest as a test runner. (See also: bug 1251507) * An account_username option is now available in the functional tests config file for auth systems that do not follow the account:username format. * CaptureOutput no longer writes to the captured stream, and MockHttpTest now captures output. These were polluting test output unnecessarily. (See also: bug 1201376) Change-Id: Ic484e9a0c186c9283c4012c6a2fa77b96b8edf8a Closes-Bug: #1201376 Closes-Bug: #1379252 Related-Bug: #1251507
Diffstat (limited to 'tests')
-rw-r--r--tests/functional/test_swiftclient.py10
-rw-r--r--tests/unit/test_service.py128
-rw-r--r--tests/unit/utils.py9
3 files changed, 143 insertions, 4 deletions
diff --git a/tests/functional/test_swiftclient.py b/tests/functional/test_swiftclient.py
index f5d14aa..4b57f1d 100644
--- a/tests/functional/test_swiftclient.py
+++ b/tests/functional/test_swiftclient.py
@@ -51,8 +51,13 @@ class TestFunctional(testtools.TestCase):
auth_ssl = config.getboolean('func_test', 'auth_ssl')
auth_prefix = config.get('func_test', 'auth_prefix')
self.auth_version = config.get('func_test', 'auth_version')
- self.account = config.get('func_test', 'account')
- self.username = config.get('func_test', 'username')
+ try:
+ self.account_username = config.get('func_test',
+ 'account_username')
+ except configparser.NoOptionError:
+ account = config.get('func_test', 'account')
+ username = config.get('func_test', 'username')
+ self.account_username = "%s:%s" % (account, username)
self.password = config.get('func_test', 'password')
self.auth_url = ""
if auth_ssl:
@@ -62,7 +67,6 @@ class TestFunctional(testtools.TestCase):
self.auth_url += "%s:%s%s" % (auth_host, auth_port, auth_prefix)
if self.auth_version == "1":
self.auth_url += 'v1.0'
- self.account_username = "%s:%s" % (self.account, self.username)
else:
self.skip_tests = True
diff --git a/tests/unit/test_service.py b/tests/unit/test_service.py
index 3309813..073f06e 100644
--- a/tests/unit/test_service.py
+++ b/tests/unit/test_service.py
@@ -817,3 +817,131 @@ class TestServiceUpload(testtools.TestCase):
contents = mock_conn.put_object.call_args[0][2]
self.assertEqual(contents.get_md5sum(), md5(b'a' * 30).hexdigest())
+
+ def test_upload_object_job_identical_etag(self):
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(b'a' * 30)
+ f.flush()
+
+ mock_conn = mock.Mock()
+ mock_conn.head_object.return_value = {
+ 'content-length': 30,
+ 'etag': md5(b'a' * 30).hexdigest()}
+ type(mock_conn).attempts = mock.PropertyMock(return_value=2)
+
+ s = SwiftService()
+ r = s._upload_object_job(conn=mock_conn,
+ container='test_c',
+ source=f.name,
+ obj='test_o',
+ options={'changed': False,
+ 'skip_identical': True,
+ 'leave_segments': True,
+ 'header': '',
+ 'segment_size': 0})
+
+ self.assertTrue(r['success'])
+ self.assertIn('status', r)
+ self.assertEqual(r['status'], 'skipped-identical')
+ self.assertEqual(mock_conn.put_object.call_count, 0)
+ self.assertEqual(mock_conn.head_object.call_count, 1)
+ mock_conn.head_object.assert_called_with('test_c', 'test_o')
+
+ def test_upload_object_job_identical_slo_with_nesting(self):
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(b'a' * 30)
+ f.flush()
+ seg_etag = md5(b'a' * 10).hexdigest()
+ submanifest = "[%s]" % ",".join(
+ ['{"bytes":10,"hash":"%s"}' % seg_etag] * 2)
+ submanifest_etag = md5(seg_etag.encode('ascii') * 2).hexdigest()
+ manifest = "[%s]" % ",".join([
+ '{"sub_slo":true,"name":"/test_c_segments/test_sub_slo",'
+ '"bytes":20,"hash":"%s"}' % submanifest_etag,
+ '{"bytes":10,"hash":"%s"}' % seg_etag])
+
+ mock_conn = mock.Mock()
+ mock_conn.head_object.return_value = {
+ 'x-static-large-object': True,
+ 'content-length': 30,
+ 'etag': md5(submanifest_etag.encode('ascii') +
+ seg_etag.encode('ascii')).hexdigest()}
+ mock_conn.get_object.side_effect = [
+ (None, manifest),
+ (None, submanifest)]
+ type(mock_conn).attempts = mock.PropertyMock(return_value=2)
+
+ s = SwiftService()
+ r = s._upload_object_job(conn=mock_conn,
+ container='test_c',
+ source=f.name,
+ obj='test_o',
+ options={'changed': False,
+ 'skip_identical': True,
+ 'leave_segments': True,
+ 'header': '',
+ 'segment_size': 10})
+
+ self.assertIsNone(r.get('error'))
+ self.assertTrue(r['success'])
+ self.assertEqual('skipped-identical', r.get('status'))
+ self.assertEqual(0, mock_conn.put_object.call_count)
+ self.assertEqual([mock.call('test_c', 'test_o')],
+ mock_conn.head_object.mock_calls)
+ self.assertEqual([
+ mock.call('test_c', 'test_o',
+ query_string='multipart-manifest=get'),
+ mock.call('test_c_segments', 'test_sub_slo',
+ query_string='multipart-manifest=get'),
+ ], mock_conn.get_object.mock_calls)
+
+ def test_upload_object_job_identical_dlo(self):
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(b'a' * 30)
+ f.flush()
+ segment_etag = md5(b'a' * 10).hexdigest()
+
+ mock_conn = mock.Mock()
+ mock_conn.head_object.return_value = {
+ 'x-object-manifest': 'test_c_segments/test_o/prefix',
+ 'content-length': 30,
+ 'etag': md5(segment_etag.encode('ascii') * 3).hexdigest()}
+ mock_conn.get_container.side_effect = [
+ (None, [{"bytes": 10, "hash": segment_etag,
+ "name": "test_o/prefix/00"},
+ {"bytes": 10, "hash": segment_etag,
+ "name": "test_o/prefix/01"}]),
+ (None, [{"bytes": 10, "hash": segment_etag,
+ "name": "test_o/prefix/02"}]),
+ (None, {})]
+ type(mock_conn).attempts = mock.PropertyMock(return_value=2)
+
+ s = SwiftService()
+ with mock.patch('swiftclient.service.get_conn',
+ return_value=mock_conn):
+ r = s._upload_object_job(conn=mock_conn,
+ container='test_c',
+ source=f.name,
+ obj='test_o',
+ options={'changed': False,
+ 'skip_identical': True,
+ 'leave_segments': True,
+ 'header': '',
+ 'segment_size': 10})
+
+ self.assertIsNone(r.get('error'))
+ self.assertTrue(r['success'])
+ self.assertEqual('skipped-identical', r.get('status'))
+ self.assertEqual(0, mock_conn.put_object.call_count)
+ self.assertEqual(1, mock_conn.head_object.call_count)
+ self.assertEqual(3, mock_conn.get_container.call_count)
+ mock_conn.head_object.assert_called_with('test_c', 'test_o')
+ expected = [
+ mock.call('test_c_segments', prefix='test_o/prefix',
+ marker='', delimiter=None),
+ mock.call('test_c_segments', prefix='test_o/prefix',
+ marker="test_o/prefix/01", delimiter=None),
+ mock.call('test_c_segments', prefix='test_o/prefix',
+ marker="test_o/prefix/02", delimiter=None),
+ ]
+ mock_conn.get_container.assert_has_calls(expected)
diff --git a/tests/unit/utils.py b/tests/unit/utils.py
index 88d6d12..bb68f4f 100644
--- a/tests/unit/utils.py
+++ b/tests/unit/utils.py
@@ -207,6 +207,12 @@ class MockHttpTest(testtools.TestCase):
self.fake_connect = None
self.request_log = []
+ # Capture output, since the test-runner stdout/stderr moneky-patching
+ # won't cover the references to sys.stdout/sys.stderr in
+ # swiftclient.multithreading
+ self.capture_output = CaptureOutput()
+ self.capture_output.__enter__()
+
def fake_http_connection(*args, **kwargs):
self.validateMockedRequestsConsumed()
self.request_log = []
@@ -367,6 +373,7 @@ class MockHttpTest(testtools.TestCase):
# un-hygienic mocking on the swiftclient.client module; which may lead
# to some unfortunate test order dependency bugs by way of the broken
# window theory if any other modules are similarly patched
+ self.capture_output.__exit__()
reload_module(c)
@@ -392,7 +399,7 @@ class CaptureStream(object):
self.stream = stream
self._capture = six.StringIO()
self._buffer = CaptureStreamBuffer(self)
- self.streams = [self.stream, self._capture]
+ self.streams = [self._capture]
@property
def buffer(self):