From 652276fea60089c8f62cf9495dc873b932652487 Mon Sep 17 00:00:00 2001 From: Kota Tsuyuzaki Date: Mon, 15 Dec 2014 20:45:41 -0800 Subject: [PATCH] Support last modified on listing containers For now, last modified timestamp is supported only on object listing. (i.e. GET container) For example: GET container with json format results in like as: [{"hash": "d41d8cd98f00b204e9800998ecf8427e", "last_modified": "2015-06-10T04:58:23.460230", "bytes": 0, "name": "object", "content_type": "application/octet-stream"}] However, container listing (i.e. GET account) shows just a dict consists of ("name", "bytes", "name") for each container. For example: GET accounts with json format result in like as: [{"count": 0, "bytes": 0, "name": "container"}] This patch is for supporting last_modified key in the container listing results as well as object listing like as: [{"count": 0, "bytes": 0, "name": "container", "last_modified": "2015-06-10T04:58:23.460230"}] This patch is changing just output for listing. The original timestamp to show the last modified is already in container table of account.db as a "put_timestamp" column. Note that this patch *DOESN'T* change the put_timestamp semantics. i.e. the last_modified timestamp will be changed only at both PUT container and POST container. (PUT object doesn't affect the timestamp) Note that the tuple format of returning value from swift.account.backend.AccountBroker.list_containers is now (name, object_count, bytes_used, put_timestamp, 0) * put_timestamp is added * Original discussion was in working session at Vancouver Summit. Etherpads are around here: https://etherpad.openstack.org/p/liberty-swift-contributors-meetup https://etherpad.openstack.org/p/liberty-container-listing-update DocImpact Change-Id: Iba0503916f1481a20c59ae9136436f40183e4c5b --- .../account-containers-list-response.json | 6 +- .../account-containers-list-response.xml | 2 + swift/account/backend.py | 7 +- swift/account/reaper.py | 2 +- swift/account/utils.py | 18 ++- test/functional/swift_test_client.py | 2 +- test/functional/tests.py | 28 +++++ test/unit/account/test_backend.py | 10 +- test/unit/account/test_reaper.py | 4 +- test/unit/account/test_server.py | 108 ++++++++++++------ 10 files changed, 136 insertions(+), 51 deletions(-) diff --git a/api-ref/source/samples/account-containers-list-response.json b/api-ref/source/samples/account-containers-list-response.json index 4ae34aa4ca..d9864aa246 100644 --- a/api-ref/source/samples/account-containers-list-response.json +++ b/api-ref/source/samples/account-containers-list-response.json @@ -2,11 +2,13 @@ { "count": 0, "bytes": 0, - "name": "janeausten" + "name": "janeausten", + "last_modified": "2013-11-19T20:08:13.283452" }, { "count": 1, "bytes": 14, - "name": "marktwain" + "name": "marktwain", + "last_modified": "2016-04-29T16:23:50.460230" } ] diff --git a/api-ref/source/samples/account-containers-list-response.xml b/api-ref/source/samples/account-containers-list-response.xml index d8f51cfa0d..6e194aebce 100644 --- a/api-ref/source/samples/account-containers-list-response.xml +++ b/api-ref/source/samples/account-containers-list-response.xml @@ -4,10 +4,12 @@ janeausten 0 0 + 2013-11-19T20:08:13.283452 marktwain 1 14 + 2016-04-29T16:23:50.460230 diff --git a/swift/account/backend.py b/swift/account/backend.py index 31f6470b99..1fd7e5272b 100644 --- a/swift/account/backend.py +++ b/swift/account/backend.py @@ -379,7 +379,8 @@ class AccountBroker(DatabaseBroker): :param delimiter: delimiter for query :param reverse: reverse the result order. - :returns: list of tuples of (name, object_count, bytes_used, 0) + :returns: list of tuples of (name, object_count, bytes_used, + put_timestamp, 0) """ delim_force_gte = False (marker, end_marker, prefix, delimiter) = utf8encode( @@ -397,7 +398,7 @@ class AccountBroker(DatabaseBroker): results = [] while len(results) < limit: query = """ - SELECT name, object_count, bytes_used, 0 + SELECT name, object_count, bytes_used, put_timestamp, 0 FROM container WHERE """ query_args = [] @@ -459,7 +460,7 @@ class AccountBroker(DatabaseBroker): delim_force_gte = True dir_name = name[:end + 1] if dir_name != orig_marker: - results.append([dir_name, 0, 0, 1]) + results.append([dir_name, 0, 0, '0', 1]) curs.close() break results.append(row) diff --git a/swift/account/reaper.py b/swift/account/reaper.py index 676fac55c6..3fd111ff2a 100644 --- a/swift/account/reaper.py +++ b/swift/account/reaper.py @@ -268,7 +268,7 @@ class AccountReaper(Daemon): if not containers: break try: - for (container, _junk, _junk, _junk) in containers: + for (container, _junk, _junk, _junk, _junk) in containers: this_shard = int(md5(container).hexdigest(), 16) % \ len(nodes) if container_shard not in (this_shard, None): diff --git a/swift/account/utils.py b/swift/account/utils.py index 49092b7f97..4446eb7d32 100644 --- a/swift/account/utils.py +++ b/swift/account/utils.py @@ -81,24 +81,30 @@ def account_listing_response(account, req, response_content_type, broker=None, prefix, delimiter, reverse) if response_content_type == 'application/json': data = [] - for (name, object_count, bytes_used, is_subdir) in account_list: + for (name, object_count, bytes_used, put_timestamp, is_subdir) \ + in account_list: if is_subdir: data.append({'subdir': name}) else: - data.append({'name': name, 'count': object_count, - 'bytes': bytes_used}) + data.append( + {'name': name, 'count': object_count, + 'bytes': bytes_used, + 'last_modified': Timestamp(put_timestamp).isoformat}) account_list = json.dumps(data) elif response_content_type.endswith('/xml'): output_list = ['', '' % saxutils.quoteattr(account)] - for (name, object_count, bytes_used, is_subdir) in account_list: + for (name, object_count, bytes_used, put_timestamp, is_subdir) \ + in account_list: if is_subdir: output_list.append( '' % saxutils.quoteattr(name)) else: item = '%s%s' \ - '%s' % \ - (saxutils.escape(name), object_count, bytes_used) + '%s%s' \ + '' % \ + (saxutils.escape(name), object_count, + bytes_used, Timestamp(put_timestamp).isoformat) output_list.append(item) output_list.append('') account_list = '\n'.join(output_list) diff --git a/test/functional/swift_test_client.py b/test/functional/swift_test_client.py index a1c105b180..3b68d3a441 100644 --- a/test/functional/swift_test_client.py +++ b/test/functional/swift_test_client.py @@ -439,7 +439,7 @@ class Account(Base): tree = minidom.parseString(self.conn.response.read()) for x in tree.getElementsByTagName('container'): cont = {} - for key in ['name', 'count', 'bytes']: + for key in ['name', 'count', 'bytes', 'last_modified']: cont[key] = x.getElementsByTagName(key)[0].\ childNodes[0].nodeValue conts.append(cont) diff --git a/test/functional/tests.py b/test/functional/tests.py index a27fec9818..15d09378d4 100644 --- a/test/functional/tests.py +++ b/test/functional/tests.py @@ -28,6 +28,7 @@ from copy import deepcopy import eventlet from unittest2 import SkipTest from swift.common.http import is_success, is_client_error +from email.utils import parsedate from test.functional import normalized_urls, load_constraint, cluster_info from test.functional import check_response, retry @@ -299,6 +300,33 @@ class TestAccount(Base): results = [r for r in results if r in expected] self.assertEqual(expected, results) + def testContainerListingLastModified(self): + expected = {} + for container in self.env.containers: + res = container.info() + expected[container.name] = time.mktime( + parsedate(res['last_modified'])) + + for format_type in ['json', 'xml']: + actual = {} + containers = self.env.account.containers( + parms={'format': format_type}) + if isinstance(containers[0], dict): + for container in containers: + self.assertIn('name', container) # sanity + self.assertIn('last_modified', container) # sanity + # ceil by hand (wants easier way!) + datetime_str, micro_sec_str = \ + container['last_modified'].split('.') + timestamp = time.mktime( + time.strptime(datetime_str, + "%Y-%m-%dT%H:%M:%S")) + if int(micro_sec_str): + timestamp += 1 + actual[container['name']] = timestamp + + self.assertEqual(expected, actual) + def testInvalidAuthToken(self): hdrs = {'X-Auth-Token': 'bogus_auth_token'} self.assertRaises(ResponseError, self.env.account.info, hdrs=hdrs) diff --git a/test/unit/account/test_backend.py b/test/unit/account/test_backend.py index e9a5c51752..6810d77546 100644 --- a/test/unit/account/test_backend.py +++ b/test/unit/account/test_backend.py @@ -1225,18 +1225,19 @@ class TestAccountBrokerBeforeSPI(TestAccountBroker): 'from container table!') # manually insert an existing row to avoid migration + timestamp = Timestamp(time()).internal with broker.get() as conn: conn.execute(''' INSERT INTO container (name, put_timestamp, delete_timestamp, object_count, bytes_used, deleted) VALUES (?, ?, ?, ?, ?, ?) - ''', ('test_name', Timestamp(time()).internal, 0, 1, 2, 0)) + ''', ('test_name', timestamp, 0, 1, 2, 0)) conn.commit() # make sure we can iter containers without the migration for c in broker.list_containers_iter(1, None, None, None, None): - self.assertEqual(c, ('test_name', 1, 2, 0)) + self.assertEqual(c, ('test_name', 1, 2, timestamp, 0)) # stats table is mysteriously empty... stats = broker.get_policy_stats() @@ -1363,6 +1364,7 @@ class TestAccountBrokerBeforeSPI(TestAccountBroker): new_broker = AccountBroker(os.path.join(tempdir, 'new_account.db'), account='a') new_broker.initialize(next(ts).internal) + timestamp = next(ts).internal # manually insert an existing row to avoid migration for old database with old_broker.get() as conn: @@ -1371,7 +1373,7 @@ class TestAccountBrokerBeforeSPI(TestAccountBroker): delete_timestamp, object_count, bytes_used, deleted) VALUES (?, ?, ?, ?, ?, ?) - ''', ('test_name', next(ts).internal, 0, 1, 2, 0)) + ''', ('test_name', timestamp, 0, 1, 2, 0)) conn.commit() # get replication info and rows form old database @@ -1384,7 +1386,7 @@ class TestAccountBrokerBeforeSPI(TestAccountBroker): # make sure "test_name" container in new database self.assertEqual(new_broker.get_info()['container_count'], 1) for c in new_broker.list_containers_iter(1, None, None, None, None): - self.assertEqual(c, ('test_name', 1, 2, 0)) + self.assertEqual(c, ('test_name', 1, 2, timestamp, 0)) # full migration successful with new_broker.get() as conn: diff --git a/test/unit/account/test_reaper.py b/test/unit/account/test_reaper.py index 4961f19acb..ddf38b3003 100644 --- a/test/unit/account/test_reaper.py +++ b/test/unit/account/test_reaper.py @@ -87,7 +87,7 @@ class FakeAccountBroker(object): def list_containers_iter(self, *args): for cont in self.containers: - yield cont, None, None, None + yield cont, None, None, None, None def is_status_deleted(self): return True @@ -749,7 +749,7 @@ class TestReaper(unittest.TestCase): if container in self.containers_yielded: continue - yield container, None, None, None + yield container, None, None, None, None self.containers_yielded.append(container) def fake_reap_container(self, account, account_partition, diff --git a/test/unit/account/test_server.py b/test/unit/account/test_server.py index 3882c08aa2..3f61f2470f 100644 --- a/test/unit/account/test_server.py +++ b/test/unit/account/test_server.py @@ -34,7 +34,7 @@ from swift.common.swob import (Request, WsgiBytesIO, HTTPNoContent) from swift.common import constraints from swift.account.server import AccountController from swift.common.utils import (normalize_timestamp, replication, public, - mkdirs, storage_directory) + mkdirs, storage_directory, Timestamp) from swift.common.request_helpers import get_sys_meta_prefix from test.unit import patch_policies, debug_logger from swift.common.storage_policy import StoragePolicy, POLICIES @@ -847,18 +847,21 @@ class TestAccountController(unittest.TestCase): self.assertEqual(resp.charset, 'utf-8') def test_GET_with_containers_json(self): + put_timestamps = {} req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT', 'HTTP_X_TIMESTAMP': '0'}) req.get_response(self.controller) + put_timestamps['c1'] = normalize_timestamp(1) req = Request.blank('/sda1/p/a/c1', environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Put-Timestamp': '1', + headers={'X-Put-Timestamp': put_timestamps['c1'], 'X-Delete-Timestamp': '0', 'X-Object-Count': '0', 'X-Bytes-Used': '0', 'X-Timestamp': normalize_timestamp(0)}) req.get_response(self.controller) + put_timestamps['c2'] = normalize_timestamp(2) req = Request.blank('/sda1/p/a/c2', environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Put-Timestamp': '2', + headers={'X-Put-Timestamp': put_timestamps['c2'], 'X-Delete-Timestamp': '0', 'X-Object-Count': '0', 'X-Bytes-Used': '0', @@ -868,18 +871,23 @@ class TestAccountController(unittest.TestCase): environ={'REQUEST_METHOD': 'GET'}) resp = req.get_response(self.controller) self.assertEqual(resp.status_int, 200) - self.assertEqual(json.loads(resp.body), - [{'count': 0, 'bytes': 0, 'name': 'c1'}, - {'count': 0, 'bytes': 0, 'name': 'c2'}]) + self.assertEqual( + json.loads(resp.body), + [{'count': 0, 'bytes': 0, 'name': 'c1', + 'last_modified': Timestamp(put_timestamps['c1']).isoformat}, + {'count': 0, 'bytes': 0, 'name': 'c2', + 'last_modified': Timestamp(put_timestamps['c2']).isoformat}]) + put_timestamps['c1'] = normalize_timestamp(3) req = Request.blank('/sda1/p/a/c1', environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Put-Timestamp': '1', + headers={'X-Put-Timestamp': put_timestamps['c1'], 'X-Delete-Timestamp': '0', 'X-Object-Count': '1', 'X-Bytes-Used': '2', 'X-Timestamp': normalize_timestamp(0)}) req.get_response(self.controller) + put_timestamps['c2'] = normalize_timestamp(4) req = Request.blank('/sda1/p/a/c2', environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Put-Timestamp': '2', + headers={'X-Put-Timestamp': put_timestamps['c2'], 'X-Delete-Timestamp': '0', 'X-Object-Count': '3', 'X-Bytes-Used': '4', @@ -889,25 +897,31 @@ class TestAccountController(unittest.TestCase): environ={'REQUEST_METHOD': 'GET'}) resp = req.get_response(self.controller) self.assertEqual(resp.status_int, 200) - self.assertEqual(json.loads(resp.body), - [{'count': 1, 'bytes': 2, 'name': 'c1'}, - {'count': 3, 'bytes': 4, 'name': 'c2'}]) + self.assertEqual( + json.loads(resp.body), + [{'count': 1, 'bytes': 2, 'name': 'c1', + 'last_modified': Timestamp(put_timestamps['c1']).isoformat}, + {'count': 3, 'bytes': 4, 'name': 'c2', + 'last_modified': Timestamp(put_timestamps['c2']).isoformat}]) self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.charset, 'utf-8') def test_GET_with_containers_xml(self): + put_timestamps = {} req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT', 'HTTP_X_TIMESTAMP': '0'}) req.get_response(self.controller) + put_timestamps['c1'] = normalize_timestamp(1) req = Request.blank('/sda1/p/a/c1', environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Put-Timestamp': '1', + headers={'X-Put-Timestamp': put_timestamps['c1'], 'X-Delete-Timestamp': '0', 'X-Object-Count': '0', 'X-Bytes-Used': '0', 'X-Timestamp': normalize_timestamp(0)}) req.get_response(self.controller) + put_timestamps['c2'] = normalize_timestamp(2) req = Request.blank('/sda1/p/a/c2', environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Put-Timestamp': '2', + headers={'X-Put-Timestamp': put_timestamps['c2'], 'X-Delete-Timestamp': '0', 'X-Object-Count': '0', 'X-Bytes-Used': '0', @@ -926,24 +940,30 @@ class TestAccountController(unittest.TestCase): self.assertEqual(listing[0].nodeName, 'container') container = [n for n in listing[0].childNodes if n.nodeName != '#text'] self.assertEqual(sorted([n.nodeName for n in container]), - ['bytes', 'count', 'name']) + ['bytes', 'count', 'last_modified', 'name']) node = [n for n in container if n.nodeName == 'name'][0] self.assertEqual(node.firstChild.nodeValue, 'c1') node = [n for n in container if n.nodeName == 'count'][0] self.assertEqual(node.firstChild.nodeValue, '0') node = [n for n in container if n.nodeName == 'bytes'][0] self.assertEqual(node.firstChild.nodeValue, '0') + node = [n for n in container if n.nodeName == 'last_modified'][0] + self.assertEqual(node.firstChild.nodeValue, + Timestamp(put_timestamps['c1']).isoformat) self.assertEqual(listing[-1].nodeName, 'container') container = \ [n for n in listing[-1].childNodes if n.nodeName != '#text'] self.assertEqual(sorted([n.nodeName for n in container]), - ['bytes', 'count', 'name']) + ['bytes', 'count', 'last_modified', 'name']) node = [n for n in container if n.nodeName == 'name'][0] self.assertEqual(node.firstChild.nodeValue, 'c2') node = [n for n in container if n.nodeName == 'count'][0] self.assertEqual(node.firstChild.nodeValue, '0') node = [n for n in container if n.nodeName == 'bytes'][0] self.assertEqual(node.firstChild.nodeValue, '0') + node = [n for n in container if n.nodeName == 'last_modified'][0] + self.assertEqual(node.firstChild.nodeValue, + Timestamp(put_timestamps['c2']).isoformat) req = Request.blank('/sda1/p/a/c1', environ={'REQUEST_METHOD': 'PUT'}, headers={'X-Put-Timestamp': '1', 'X-Delete-Timestamp': '0', @@ -970,24 +990,30 @@ class TestAccountController(unittest.TestCase): self.assertEqual(listing[0].nodeName, 'container') container = [n for n in listing[0].childNodes if n.nodeName != '#text'] self.assertEqual(sorted([n.nodeName for n in container]), - ['bytes', 'count', 'name']) + ['bytes', 'count', 'last_modified', 'name']) node = [n for n in container if n.nodeName == 'name'][0] self.assertEqual(node.firstChild.nodeValue, 'c1') node = [n for n in container if n.nodeName == 'count'][0] self.assertEqual(node.firstChild.nodeValue, '1') node = [n for n in container if n.nodeName == 'bytes'][0] self.assertEqual(node.firstChild.nodeValue, '2') + node = [n for n in container if n.nodeName == 'last_modified'][0] + self.assertEqual(node.firstChild.nodeValue, + Timestamp(put_timestamps['c1']).isoformat) self.assertEqual(listing[-1].nodeName, 'container') container = [ n for n in listing[-1].childNodes if n.nodeName != '#text'] self.assertEqual(sorted([n.nodeName for n in container]), - ['bytes', 'count', 'name']) + ['bytes', 'count', 'last_modified', 'name']) node = [n for n in container if n.nodeName == 'name'][0] self.assertEqual(node.firstChild.nodeValue, 'c2') node = [n for n in container if n.nodeName == 'count'][0] self.assertEqual(node.firstChild.nodeValue, '3') node = [n for n in container if n.nodeName == 'bytes'][0] self.assertEqual(node.firstChild.nodeValue, '4') + node = [n for n in container if n.nodeName == 'last_modified'][0] + self.assertEqual(node.firstChild.nodeValue, + Timestamp(put_timestamps['c2']).isoformat) self.assertEqual(resp.charset, 'utf-8') def test_GET_xml_escapes_account_name(self): @@ -1054,15 +1080,16 @@ class TestAccountController(unittest.TestCase): req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT', 'HTTP_X_TIMESTAMP': '0'}) req.get_response(self.controller) + put_timestamp = normalize_timestamp(0) for c in range(5): req = Request.blank( '/sda1/p/a/c%d' % c, environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Put-Timestamp': str(c + 1), + headers={'X-Put-Timestamp': put_timestamp, 'X-Delete-Timestamp': '0', 'X-Object-Count': '2', 'X-Bytes-Used': '3', - 'X-Timestamp': normalize_timestamp(0)}) + 'X-Timestamp': put_timestamp}) req.get_response(self.controller) req = Request.blank('/sda1/p/a?limit=3', environ={'REQUEST_METHOD': 'GET'}) @@ -1079,31 +1106,38 @@ class TestAccountController(unittest.TestCase): req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT', 'HTTP_X_TIMESTAMP': '0'}) req.get_response(self.controller) + put_timestamp = normalize_timestamp(0) for c in range(5): req = Request.blank( '/sda1/p/a/c%d' % c, environ={'REQUEST_METHOD': 'PUT'}, - headers={'X-Put-Timestamp': str(c + 1), + headers={'X-Put-Timestamp': put_timestamp, 'X-Delete-Timestamp': '0', 'X-Object-Count': '2', 'X-Bytes-Used': '3', - 'X-Timestamp': normalize_timestamp(0)}) + 'X-Timestamp': put_timestamp}) req.get_response(self.controller) req = Request.blank('/sda1/p/a?limit=3&format=json', environ={'REQUEST_METHOD': 'GET'}) resp = req.get_response(self.controller) self.assertEqual(resp.status_int, 200) - self.assertEqual(json.loads(resp.body), - [{'count': 2, 'bytes': 3, 'name': 'c0'}, - {'count': 2, 'bytes': 3, 'name': 'c1'}, - {'count': 2, 'bytes': 3, 'name': 'c2'}]) + timestamp_str = Timestamp(put_timestamp).isoformat + expected = [{'count': 2, 'bytes': 3, 'name': 'c0', + 'last_modified': timestamp_str}, + {'count': 2, 'bytes': 3, 'name': 'c1', + 'last_modified': timestamp_str}, + {'count': 2, 'bytes': 3, 'name': 'c2', + 'last_modified': timestamp_str}] + self.assertEqual(json.loads(resp.body), expected) req = Request.blank('/sda1/p/a?limit=3&marker=c2&format=json', environ={'REQUEST_METHOD': 'GET'}) resp = req.get_response(self.controller) self.assertEqual(resp.status_int, 200) - self.assertEqual(json.loads(resp.body), - [{'count': 2, 'bytes': 3, 'name': 'c3'}, - {'count': 2, 'bytes': 3, 'name': 'c4'}]) + expected = [{'count': 2, 'bytes': 3, 'name': 'c3', + 'last_modified': timestamp_str}, + {'count': 2, 'bytes': 3, 'name': 'c4', + 'last_modified': timestamp_str}] + self.assertEqual(json.loads(resp.body), expected) def test_GET_limit_marker_xml(self): req = Request.blank('/sda1/p/a', environ={'REQUEST_METHOD': 'PUT', @@ -1131,24 +1165,31 @@ class TestAccountController(unittest.TestCase): self.assertEqual(listing[0].nodeName, 'container') container = [n for n in listing[0].childNodes if n.nodeName != '#text'] self.assertEqual(sorted([n.nodeName for n in container]), - ['bytes', 'count', 'name']) + ['bytes', 'count', 'last_modified', 'name']) node = [n for n in container if n.nodeName == 'name'][0] self.assertEqual(node.firstChild.nodeValue, 'c0') node = [n for n in container if n.nodeName == 'count'][0] self.assertEqual(node.firstChild.nodeValue, '2') node = [n for n in container if n.nodeName == 'bytes'][0] self.assertEqual(node.firstChild.nodeValue, '3') + node = [n for n in container if n.nodeName == 'last_modified'][0] + self.assertEqual(node.firstChild.nodeValue, + Timestamp('1').isoformat) self.assertEqual(listing[-1].nodeName, 'container') container = [ n for n in listing[-1].childNodes if n.nodeName != '#text'] self.assertEqual(sorted([n.nodeName for n in container]), - ['bytes', 'count', 'name']) + ['bytes', 'count', 'last_modified', 'name']) + node = [n for n in container if n.nodeName == 'name'][0] node = [n for n in container if n.nodeName == 'name'][0] self.assertEqual(node.firstChild.nodeValue, 'c2') node = [n for n in container if n.nodeName == 'count'][0] self.assertEqual(node.firstChild.nodeValue, '2') node = [n for n in container if n.nodeName == 'bytes'][0] self.assertEqual(node.firstChild.nodeValue, '3') + node = [n for n in container if n.nodeName == 'last_modified'][0] + self.assertEqual(node.firstChild.nodeValue, + Timestamp('3').isoformat) req = Request.blank('/sda1/p/a?limit=3&marker=c2&format=xml', environ={'REQUEST_METHOD': 'GET'}) resp = req.get_response(self.controller) @@ -1161,18 +1202,21 @@ class TestAccountController(unittest.TestCase): self.assertEqual(listing[0].nodeName, 'container') container = [n for n in listing[0].childNodes if n.nodeName != '#text'] self.assertEqual(sorted([n.nodeName for n in container]), - ['bytes', 'count', 'name']) + ['bytes', 'count', 'last_modified', 'name']) node = [n for n in container if n.nodeName == 'name'][0] self.assertEqual(node.firstChild.nodeValue, 'c3') node = [n for n in container if n.nodeName == 'count'][0] self.assertEqual(node.firstChild.nodeValue, '2') node = [n for n in container if n.nodeName == 'bytes'][0] self.assertEqual(node.firstChild.nodeValue, '3') + node = [n for n in container if n.nodeName == 'last_modified'][0] + self.assertEqual(node.firstChild.nodeValue, + Timestamp('4').isoformat) self.assertEqual(listing[-1].nodeName, 'container') container = [ n for n in listing[-1].childNodes if n.nodeName != '#text'] self.assertEqual(sorted([n.nodeName for n in container]), - ['bytes', 'count', 'name']) + ['bytes', 'count', 'last_modified', 'name']) node = [n for n in container if n.nodeName == 'name'][0] self.assertEqual(node.firstChild.nodeValue, 'c4') node = [n for n in container if n.nodeName == 'count'][0]