launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #05163
[Merge] lp:~lifeless/python-oops-datedir-repo/hashing into lp:python-oops-datedir-repo
Robert Collins has proposed merging lp:~lifeless/python-oops-datedir-repo/hashing into lp:python-oops-datedir-repo.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~lifeless/python-oops-datedir-repo/hashing/+merge/78039
Adds:
- bson serializer and deserializer
- generic deserializer
- hash based id allocation and filename allocation
--
https://code.launchpad.net/~lifeless/python-oops-datedir-repo/hashing/+merge/78039
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/python-oops-datedir-repo/hashing into lp:python-oops-datedir-repo.
=== added file 'MANIFEST.in'
=== modified file 'NEWS'
--- NEWS 2011-09-18 22:44:43 +0000
+++ NEWS 2011-10-04 05:12:28 +0000
@@ -6,6 +6,25 @@
NEXT
----
+0.0.7
+-----
+
+This release adds a bson serializer and changes the default for DateDirRepo to
+use that new serializer. Be sure that any code wanted to read your OOPSes is
+updated to use the new serializer or serializer_bson modules when reading.
+
+* Added a serializer_bson module containing a bson serializer.
+ (Robert Collins)
+
+* Added a serializer module which will autodetect bson or rfc822 OOPSes (needed
+ for legacy situations - recommend using the specific serializer if known.
+ (Robert Collins)
+
+* The DateDirRepo can now generate bson serialized OOPSes. (Robert Collins)
+
+* The DateDirRepo can now assign ids using a hash function rather than the (non
+ concurrent-safe) monotonic-id approach. (Robert Collins)
+
0.0.6
-----
=== modified file 'oops_datedir_repo/__init__.py'
--- oops_datedir_repo/__init__.py 2011-09-18 22:44:43 +0000
+++ oops_datedir_repo/__init__.py 2011-10-04 05:12:28 +0000
@@ -25,7 +25,7 @@
# established at this point, and setup.py will use a version of next-$(revno).
# If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno).
-__version__ = (0, 0, 6, 'beta', 0)
+__version__ = (0, 0, 7, 'beta', 0)
__all__ = [
'DateDirRepo',
=== modified file 'oops_datedir_repo/repository.py'
--- oops_datedir_repo/repository.py 2011-08-16 02:21:20 +0000
+++ oops_datedir_repo/repository.py 2011-10-04 05:12:28 +0000
@@ -23,11 +23,13 @@
]
import datetime
-import os
+from hashlib import md5
+import os.path
import stat
from pytz import utc
+import serializer_bson
import serializer_rfc822
from uniquefileallocator import UniqueFileAllocator
@@ -35,12 +37,34 @@
class DateDirRepo:
"""Publish oopses to a date-dir repository."""
- def __init__(self, error_dir, instance_id):
- self.log_namer = UniqueFileAllocator(
- output_root=error_dir,
- log_type="OOPS",
- log_subtype=instance_id,
- )
+ def __init__(self, error_dir, instance_id=None, serializer=None):
+ """Create a DateDirRepo.
+
+ :param error_dir: The base directory to write OOPSes into. OOPSes are
+ written into a subdirectory this named after the date (e.g.
+ 2011-12-30).
+ :param instance_id: If None, OOPS file names are named after the OOPS
+ id which is generated by hashing the serialized OOPS (without the
+ id field). Otherwise OOPS file names and ids are created by
+ allocating file names through a UniqueFileAllocator.
+ UniqueFileAllocator has significant performance and concurrency
+ limits and hash based naming is recommended.
+ :param serializer: If supplied should be the module (e.g.
+ oops_datedir_repo.serializer_rfc822) to use to serialize OOPSes.
+ Defaults to using serializer_bson.
+ """
+ if instance_id is not None:
+ self.log_namer = UniqueFileAllocator(
+ output_root=error_dir,
+ log_type="OOPS",
+ log_subtype=instance_id,
+ )
+ else:
+ self.log_namer = None
+ self.root = error_dir
+ if serializer is None:
+ serializer = serializer_bson
+ self.serializer = serializer
def publish(self, report, now=None):
"""Write the report to disk.
@@ -52,9 +76,19 @@
now = now.astimezone(utc)
else:
now = datetime.datetime.now(utc)
- oopsid, filename = self.log_namer.newId(now)
+ # Don't mess with the original report:
+ report = dict(report)
+ if self.log_namer is not None:
+ oopsid, filename = self.log_namer.newId(now)
+ else:
+ report.pop('id', None)
+ md5hash = md5(serializer_bson.dumps(report)).hexdigest()
+ oopsid = 'OOPS-%s' % md5hash
+ prefix = os.path.join(self.root, now.strftime('%Y-%m-%d'))
+ os.makedirs(prefix)
+ filename = os.path.join(prefix, oopsid)
report['id'] = oopsid
- serializer_rfc822.write(report, open(filename, 'wb'))
+ self.serializer.write(report, open(filename, 'wb'))
# Set file permission to: rw-r--r-- (so that reports from
# umask-restricted services can be gathered by a tool running as
# another user).
=== added file 'oops_datedir_repo/serializer.py'
--- oops_datedir_repo/serializer.py 1970-01-01 00:00:00 +0000
+++ oops_datedir_repo/serializer.py 2011-10-04 05:12:28 +0000
@@ -0,0 +1,55 @@
+# Copyright (c) 2011, Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Read from any known serializer.
+
+Where possible using the specific known serializer is better as it is more
+efficient and won't suffer false positives if two serializations happen to pun
+with each other (unlikely though that is).
+
+Typical usage:
+ >>> fp = file('an-oops', 'rb')
+ >>> report = serializer.read(fp)
+
+See the serializer_rfc822 and serializer_bson modules for information about
+serializing OOPS reports by hand. Generally just using the DateDirRepo.publish
+method is all that is needed.
+"""
+
+
+__all__ = [
+ 'read',
+ ]
+
+from StringIO import StringIO
+
+from oops_datedir_repo import (
+ serializer_bson,
+ serializer_rfc822,
+ )
+
+
+def read(fp):
+ """Deserialize an OOPS from a bson or rfc822 message.
+
+ The whole file is read regardless of the OOPS format.
+ """
+ # Deal with no-rewindable file pointers.
+ content = fp.read()
+ try:
+ return serializer_bson.read(StringIO(content))
+ except KeyError:
+ return serializer_rfc822.read(StringIO(content))
=== added file 'oops_datedir_repo/serializer_bson.py'
--- oops_datedir_repo/serializer_bson.py 1970-01-01 00:00:00 +0000
+++ oops_datedir_repo/serializer_bson.py 2011-10-04 05:12:28 +0000
@@ -0,0 +1,84 @@
+# Copyright (c) 2011, Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Read / Write an OOPS dict as a bson dict.
+
+This style of OOPS format is very extensible and maintains compatability with
+older rfc822 oops code: the previously mandatory keys are populated on read.
+
+Use of bson serializing is recommended.
+
+The reports this serializer handles always have the following variables (See
+the python-oops api docs for more information about these variables):
+
+* id: The name of this error report.
+* type: The type of the exception that occurred.
+* value: The value of the exception that occurred.
+* time: The time at which the exception occurred.
+* reporter: The reporting program.
+* topic: The identifier for the template/script that oopsed.
+* branch_nick: The branch nickname.
+* revno: The revision number of the branch.
+* tb_text: A text version of the traceback.
+* username: The user associated with the request.
+* url: The URL for the failed request.
+* req_vars: The request variables.
+* branch_nick: A name for the branch of code that was running when the report
+ was triggered.
+* revno: The revision that the branch was at.
+"""
+
+
+__all__ = [
+ 'dumps',
+ 'read',
+ 'write',
+ ]
+
+__metaclass__ = type
+
+import datetime
+import logging
+import rfc822
+import re
+import urllib
+
+import bson
+import iso8601
+
+
+def read(fp):
+ """Deserialize an OOPS from a bson message."""
+ report = bson.loads(fp.read())
+ for key in (
+ 'branch_nick', 'revno', 'type', 'value', 'time', 'topic',
+ 'username', 'url'):
+ report.setdefault(key, None)
+ report.setdefault('duration', -1)
+ report.setdefault('req_vars', [])
+ report.setdefault('tb_text', '')
+ report.setdefault('timeline', [])
+ return report
+
+
+def dumps(report):
+ """Return a binary string representing report."""
+ return bson.dumps(report)
+
+
+def write(report, fp):
+ """Write report to fp."""
+ return fp.write(dumps(report))
=== modified file 'oops_datedir_repo/serializer_rfc822.py'
--- oops_datedir_repo/serializer_rfc822.py 2011-09-18 22:44:43 +0000
+++ oops_datedir_repo/serializer_rfc822.py 2011-10-04 05:12:28 +0000
@@ -117,10 +117,10 @@
if db_id is not None:
db_id = intern(db_id) # This string is repeated lots.
statements.append(
- (int(start), int(end), db_id, statement))
+ [int(start), int(end), db_id, statement])
elif is_req_var(line):
key, value = line.split('=', 1)
- req_vars.append((urllib.unquote(key), urllib.unquote(value)))
+ req_vars.append([urllib.unquote(key), urllib.unquote(value)])
elif is_traceback(line):
break
=== modified file 'oops_datedir_repo/tests/__init__.py'
--- oops_datedir_repo/tests/__init__.py 2011-08-16 02:21:20 +0000
+++ oops_datedir_repo/tests/__init__.py 2011-10-04 05:12:28 +0000
@@ -23,6 +23,8 @@
test_mod_names = [
'repository',
'uniquefileallocator',
+ 'serializer',
+ 'serializer_bson',
'serializer_rfc822',
]
return TestLoader().loadTestsFromNames(
=== modified file 'oops_datedir_repo/tests/test_repository.py'
--- oops_datedir_repo/tests/test_repository.py 2011-08-16 02:21:20 +0000
+++ oops_datedir_repo/tests/test_repository.py 2011-10-04 05:12:28 +0000
@@ -19,14 +19,19 @@
__metaclass__ = type
import datetime
+from hashlib import md5
import os.path
import stat
from fixtures import TempDir
+import bson
from pytz import utc
import testtools
-from oops_datedir_repo import DateDirRepo
+from oops_datedir_repo import (
+ DateDirRepo,
+ serializer_bson,
+ )
from oops_datedir_repo.uniquefileallocator import UniqueFileAllocator
@@ -59,3 +64,35 @@
def test_sets_log_namer_to_a_UniqueFileAllocator(self):
repo = DateDirRepo(self.useFixture(TempDir()).path, 'T')
self.assertIsInstance(repo.log_namer, UniqueFileAllocator)
+
+ def test_default_serializer_bson(self):
+ repo = DateDirRepo(self.useFixture(TempDir()).path, 'T')
+ self.assertEqual(serializer_bson, repo.serializer)
+
+ def test_settable_serializer(self):
+ an_object = object()
+ repo = DateDirRepo(self.useFixture(TempDir()).path, 'T', an_object)
+ self.assertEqual(an_object, repo.serializer)
+
+ def test_no_instance_id_no_log_namer(self):
+ repo = DateDirRepo(self.useFixture(TempDir()).path)
+ self.assertEqual(None, repo.log_namer)
+
+ def test_publish_via_hash(self):
+ repo = DateDirRepo(self.useFixture(TempDir()).path)
+ now = datetime.datetime(2006, 04, 01, 00, 30, 00, tzinfo=utc)
+ report = {'time': now}
+ # NB: bson output depends on dict order, so the resulting hash can be
+ # machine specific. This is fine because its merely a strategy to get
+ # unique ids, and after creating the id it is preserved in what is
+ # written to disk: we don't need it to be deterministic across
+ # machines / instances.
+ expected_md5 = md5(serializer_bson.dumps(report)).hexdigest()
+ expected_id = "OOPS-%s" % expected_md5
+ self.assertEqual(expected_id, repo.publish(report, now))
+ # The file on disk should match the given id.
+ with open(repo.root + '/2006-04-01/' + expected_id, 'rb') as fp:
+ # And the content serialized should include the id.
+ self.assertEqual(
+ {'id': expected_id, 'time': now},
+ bson.loads(fp.read()))
=== added file 'oops_datedir_repo/tests/test_serializer.py'
--- oops_datedir_repo/tests/test_serializer.py 1970-01-01 00:00:00 +0000
+++ oops_datedir_repo/tests/test_serializer.py 2011-10-04 05:12:28 +0000
@@ -0,0 +1,70 @@
+# Copyright (c) 2011, Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the generic serialization support."""
+
+__metaclass__ = type
+
+import datetime
+import StringIO
+
+import bson
+from pytz import utc
+import testtools
+
+from oops_datedir_repo.serializer import read
+from oops_datedir_repo.serializer_bson import dumps
+from oops_datedir_repo.serializer_rfc822 import write
+
+
+class TestParsing(testtools.TestCase):
+
+ source_dict = {
+ 'id': 'OOPS-A0001',
+ 'type': 'NotFound',
+ 'value': 'error message',
+ 'time': datetime.datetime(2005, 4, 1, tzinfo=utc),
+ 'topic': 'IFoo:+foo-template',
+ 'tb_text': 'traceback\ntext\n',
+ 'username': 'Sample User',
+ 'url': 'http://localhost:9000/foo',
+ 'duration': 42.0,
+ 'req_vars': [
+ ['HTTP_USER_AGENT', 'Mozilla/5.0'],
+ ['HTTP_REFERER', 'http://localhost:9000/'],
+ ['name=foo', 'hello\nworld'],
+ ],
+ 'timeline': [
+ [1, 5, 'store_a', 'SELECT 1'],
+ [5, 10, 'store_b', 'SELECT 2'],
+ ],
+ }
+ expected_dict = dict(source_dict)
+ # Unsupplied but filled on read
+ expected_dict['branch_nick'] = None
+ expected_dict['revno'] = None
+
+ def test_read_detect_rfc822(self):
+ source_file = StringIO.StringIO()
+ write(dict(self.source_dict), source_file)
+ source_file.seek(0)
+ self.assertEqual(self.expected_dict, read(source_file))
+
+ def test_read_detect_bson(self):
+ source_file = StringIO.StringIO()
+ source_file.write(dumps(dict(self.source_dict)))
+ source_file.seek(0)
+ self.assertEqual(self.expected_dict, read(source_file))
=== added file 'oops_datedir_repo/tests/test_serializer_bson.py'
--- oops_datedir_repo/tests/test_serializer_bson.py 1970-01-01 00:00:00 +0000
+++ oops_datedir_repo/tests/test_serializer_bson.py 2011-10-04 05:12:28 +0000
@@ -0,0 +1,121 @@
+# Copyright (c) 2011, Canonical Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for bson based serialization."""
+
+__metaclass__ = type
+
+import datetime
+import StringIO
+
+import bson
+from pytz import utc
+import testtools
+
+from oops_datedir_repo.serializer_bson import (
+ dumps,
+ read,
+ )
+
+
+class TestParsing(testtools.TestCase):
+
+ def test_read(self):
+ source_dict = {
+ 'id': 'OOPS-A0001',
+ 'type': 'NotFound',
+ 'value': 'error message',
+ 'time': datetime.datetime(2005, 4, 1, tzinfo=utc),
+ 'topic': 'IFoo:+foo-template',
+ 'tb_text': 'traceback\ntext\n',
+ 'username': 'Sample User',
+ 'url': 'http://localhost:9000/foo',
+ 'duration': 42,
+ 'req_vars': [
+ ['HTTP_USER_AGENT', 'Mozilla/5.0'],
+ ['HTTP_REFERER', 'http://localhost:9000/'],
+ ['name=foo', 'hello\nworld'],
+ ],
+ 'timeline': [
+ [1, 5, 'store_a', 'SELECT 1'],
+ [5, 10, 'store_b', 'SELECT 2'],
+ ]
+ }
+ source_file = StringIO.StringIO(bson.dumps(source_dict))
+ expected_dict = dict(source_dict)
+ # Unsupplied but filled on read
+ expected_dict['branch_nick'] = None
+ expected_dict['revno'] = None
+ report = read(source_file)
+ self.assertEqual(expected_dict, report)
+
+ def test_minimal_oops(self):
+ # If we get a crazy-small oops, we can read it sensibly. Because there
+ # is existing legacy code, all keys are filled in with None, [] rather
+ # than being empty.
+ source_dict = {
+ 'id': 'OOPS-A0001',
+ }
+ source_file = StringIO.StringIO(bson.dumps(source_dict))
+ report = read(source_file)
+ self.assertEqual(report['id'], 'OOPS-A0001')
+ self.assertEqual(report['type'], None)
+ self.assertEqual(report['value'], None)
+ self.assertEqual(report['time'], None)
+ self.assertEqual(report['topic'], None)
+ self.assertEqual(report['tb_text'], '')
+ self.assertEqual(report['username'], None)
+ self.assertEqual(report['url'], None)
+ self.assertEqual(report['duration'], -1)
+ self.assertEqual(len(report['req_vars']), 0)
+ self.assertEqual(len(report['timeline']), 0)
+ self.assertEqual(report['branch_nick'], None)
+ self.assertEqual(report['revno'], None)
+
+
+class TestSerializing(testtools.TestCase):
+
+ def test_dumps(self):
+ report = {
+ 'id': 'OOPS-A0001',
+ 'type': 'NotFound',
+ 'value': 'error message',
+ 'time': datetime.datetime(2005, 04, 01, 00, 00, 00, tzinfo=utc),
+ 'topic': 'IFoo:+foo-template',
+ 'tb_text': 'traceback-text',
+ 'username': 'Sample User',
+ 'url': 'http://localhost:9000/foo',
+ 'duration': 42,
+ 'req_vars': [('HTTP_USER_AGENT', 'Mozilla/5.0'),
+ ('HTTP_REFERER', 'http://localhost:9000/'),
+ ('name=foo', 'hello\nworld')],
+ 'timeline': [(1, 5, 'store_a', 'SELECT 1'),
+ (5, 10, 'store_b', 'SELECT\n2')],
+ 'informational': False,
+ 'branch_nick': 'mybranch',
+ 'revno': '45',
+ }
+ self.assertEqual(dumps(report), bson.dumps(report))
+
+ def test_minimal_oops(self):
+ # An oops with just an id, though arguably crazy, is written
+ # sensibly.
+ report = {'id': 'OOPS-1234'}
+ self.assertEqual(dumps(report), bson.dumps(report))
+
+ def test_bad_strings(self):
+ report = {'id': u'\xeafoo'}
+ self.assertEqual(dumps(report), bson.dumps(report))
=== modified file 'oops_datedir_repo/tests/test_serializer_rfc822.py'
--- oops_datedir_repo/tests/test_serializer_rfc822.py 2011-09-18 22:44:43 +0000
+++ oops_datedir_repo/tests/test_serializer_rfc822.py 2011-10-04 05:12:28 +0000
@@ -66,18 +66,14 @@
self.assertEqual(report['url'], 'http://localhost:9000/foo')
self.assertEqual(report['duration'], 42)
self.assertEqual(len(report['req_vars']), 3)
- self.assertEqual(report['req_vars'][0], ('HTTP_USER_AGENT',
- 'Mozilla/5.0'))
- self.assertEqual(report['req_vars'][1], ('HTTP_REFERER',
- 'http://localhost:9000/'))
- self.assertEqual(report['req_vars'][2], ('name=foo', 'hello\nworld'))
+ self.assertEqual(report['req_vars'][0],
+ ['HTTP_USER_AGENT', 'Mozilla/5.0'])
+ self.assertEqual(report['req_vars'][1],
+ ['HTTP_REFERER', 'http://localhost:9000/'])
+ self.assertEqual(report['req_vars'][2], ['name=foo', 'hello\nworld'])
self.assertEqual(len(report['timeline']), 2)
- self.assertEqual(
- report['timeline'][0],
- (1, 5, 'store_a', 'SELECT 1'))
- self.assertEqual(
- report['timeline'][1],
- (5, 10, 'store_b', 'SELECT 2'))
+ self.assertEqual(report['timeline'][0], [1, 5, 'store_a', 'SELECT 1'])
+ self.assertEqual(report['timeline'][1], [5, 10, 'store_b', 'SELECT 2'])
def test_read_blankline_req_vars(self):
"""Test ErrorReport.read() for old logs with a blankline between
@@ -104,12 +100,17 @@
foo/bar"""))
report = read(fp)
self.assertEqual(report['id'], 'OOPS-A0001')
- self.assertEqual(report['req_vars'][0], ('HTTP_USER_AGENT',
- 'Mozilla/5.0'))
- self.assertEqual(report['req_vars'][1], ('HTTP_REFERER',
- 'http://localhost:9000/'))
- self.assertEqual(report['req_vars'][2], ('name=foo', 'hello\nworld'))
+ self.assertEqual(len(report['req_vars']), 3)
+ self.assertEqual(report['req_vars'][0],
+ ['HTTP_USER_AGENT', 'Mozilla/5.0'])
+ self.assertEqual(report['req_vars'][1],
+ ['HTTP_REFERER', 'http://localhost:9000/'])
+ self.assertEqual(report['req_vars'][2], ['name=foo', 'hello\nworld'])
self.assertEqual(len(report['timeline']), 2)
+ self.assertEqual(
+ report['timeline'][0],
+ [1, 5, 'store_a', 'SELECT 1 = 2'])
+ self.assertEqual(report['timeline'][1], [5, 10, 'store_b', 'SELECT 2'])
self.assertEqual(report['tb_text'], 'traceback-text\n foo/bar')
def test_read_no_store_id(self):
@@ -144,14 +145,14 @@
self.assertEqual(report['url'], 'http://localhost:9000/foo')
self.assertEqual(report['duration'], 42)
self.assertEqual(len(report['req_vars']), 3)
- self.assertEqual(report['req_vars'][0], ('HTTP_USER_AGENT',
- 'Mozilla/5.0'))
- self.assertEqual(report['req_vars'][1], ('HTTP_REFERER',
- 'http://localhost:9000/'))
- self.assertEqual(report['req_vars'][2], ('name=foo', 'hello\nworld'))
+ self.assertEqual(report['req_vars'][0],
+ ['HTTP_USER_AGENT', 'Mozilla/5.0'])
+ self.assertEqual(report['req_vars'][1],
+ ['HTTP_REFERER', 'http://localhost:9000/'])
+ self.assertEqual(report['req_vars'][2], ['name=foo', 'hello\nworld'])
self.assertEqual(len(report['timeline']), 2)
- self.assertEqual(report['timeline'][0], (1, 5, None, 'SELECT 1'))
- self.assertEqual(report['timeline'][1], (5, 10, None, 'SELECT 2'))
+ self.assertEqual(report['timeline'][0], [1, 5, None, 'SELECT 1'])
+ self.assertEqual(report['timeline'][1], [5, 10, None, 'SELECT 2'])
def test_read_branch_nick_revno(self):
"""Test ErrorReport.read()."""
=== modified file 'setup.py'
--- setup.py 2011-09-18 22:44:43 +0000
+++ setup.py 2011-10-04 05:12:28 +0000
@@ -22,7 +22,7 @@
description = file(os.path.join(os.path.dirname(__file__), 'README'), 'rb').read()
setup(name="oops_datedir_repo",
- version="0.0.6",
+ version="0.0.7",
description="OOPS disk serialisation and repository management.",
long_description=description,
maintainer="Launchpad Developers",
@@ -38,6 +38,7 @@
'Programming Language :: Python',
],
install_requires = [
+ 'bson',
'iso8601',
'oops',
'pytz',
=== modified file 'versions.cfg'
--- versions.cfg 2011-08-15 05:30:39 +0000
+++ versions.cfg 2011-10-04 05:12:28 +0000
@@ -2,6 +2,7 @@
versions = versions
[versions]
+bson = 0.3.2
fixtures = 0.3.6
iso8601 = 0.1.4
pytz = 2010o