Merge lp://qastaging/~thisfred/desktopcouch/backport-update-fields into lp://qastaging/desktopcouch

Proposed by Eric Casteleijn
Status: Merged
Approved by: Eric Casteleijn
Approved revision: not available
Merged at revision: not available
Proposed branch: lp://qastaging/~thisfred/desktopcouch/backport-update-fields
Merge into: lp://qastaging/desktopcouch
Diff against target: 820 lines (+293/-97)
9 files modified
desktopcouch/contacts/tests/test_contactspicker.py (+2/-0)
desktopcouch/records/server_base.py (+126/-39)
desktopcouch/records/tests/test_couchgrid.py (+4/-2)
desktopcouch/records/tests/test_field_registry.py (+2/-0)
desktopcouch/records/tests/test_record.py (+11/-8)
desktopcouch/records/tests/test_server.py (+120/-38)
desktopcouch/tests/test_local_files.py (+7/-4)
desktopcouch/tests/test_replication.py (+2/-0)
desktopcouch/tests/test_start_local_couchdb.py (+19/-6)
To merge this branch: bzr merge lp://qastaging/~thisfred/desktopcouch/backport-update-fields
Reviewer Review Type Date Requested Status
Guillermo Gonzalez Approve
Chad Miller (community) Approve
Review via email: mp+18287@code.qastaging.launchpad.net

Commit message

backported safer update_fields method from server code in a backwards compatible way, fixed tests so they run in lucid (testtools changed so that setUp has to call super's setUp, and same for tearDown), did pedantic cleanup in files I touched

To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) wrote :

- backported safer update_fields method from server code in a backwards compatible way
- fixed tests so they run in lucid (testtools changed so that setUp has to call super's setUp, and same for tearDown)
- did pedantic cleanup in files I touched

Revision history for this message
Chad Miller (cmiller) wrote :

Looks good. Thanks!

review: Approve
Revision history for this message
Guillermo Gonzalez (verterok) wrote :

looks good, all tests pass

review: Approve
Revision history for this message
Eric Casteleijn (thisfred) wrote :

Can't seem to set the commit message, (the ajax seems to hit a 404?)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'desktopcouch/contacts/tests/test_contactspicker.py'
--- desktopcouch/contacts/tests/test_contactspicker.py 2009-11-11 16:24:22 +0000
+++ desktopcouch/contacts/tests/test_contactspicker.py 2010-01-29 22:17:14 +0000
@@ -32,12 +32,14 @@
32 def setUp(self):32 def setUp(self):
33 """setup each test"""33 """setup each test"""
34 # Connect to CouchDB server34 # Connect to CouchDB server
35 super(TestContactsPicker, self).setUp()
35 self.dbname = 'contacts'36 self.dbname = 'contacts'
36 self.database = CouchDatabase(self.dbname, create=True,37 self.database = CouchDatabase(self.dbname, create=True,
37 ctx=test_environment.test_context)38 ctx=test_environment.test_context)
3839
39 def tearDown(self):40 def tearDown(self):
40 """tear down each test"""41 """tear down each test"""
42 super(TestContactsPicker, self).tearDown()
41 del self.database._server[self.dbname]43 del self.database._server[self.dbname]
4244
43 def test_can_contruct_contactspicker(self):45 def test_can_contruct_contactspicker(self):
4446
=== modified file 'desktopcouch/records/server_base.py'
--- desktopcouch/records/server_base.py 2009-12-15 20:59:18 +0000
+++ desktopcouch/records/server_base.py 2010-01-29 22:17:14 +0000
@@ -21,20 +21,42 @@
2121
22"""The Desktop Couch Records API."""22"""The Desktop Couch Records API."""
2323
24import httplib2, urlparse, cgi, copy
25from time import time
26
27# please keep desktopcouch python 2.5 compatible for now
28
29# pylint can't deal with failing imports even when they're handled
30# pylint: disable-msg=F0401
31try:
32 # Python 2.5
33 import simplejson as json
34except ImportError:
35 # Python 2.6+
36 import json
37# pylint: enable-msg=F0401
38
39from oauth import oauth
40
24from couchdb import Server41from couchdb import Server
25from couchdb.client import ResourceNotFound, ResourceConflict, uri as couchdburi42from couchdb.client import ResourceNotFound, ResourceConflict, uri as couchdburi
26from couchdb.design import ViewDefinition43from couchdb.design import ViewDefinition
27from record import Record44from record import Record
28import httplib2
29from oauth import oauth
30import urlparse
31import cgi
32from time import time
33import json
3445
35#DEFAULT_DESIGN_DOCUMENT = "design"46#DEFAULT_DESIGN_DOCUMENT = "design"
36DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.47DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
3748
49class FieldsConflict(Exception):
50 """Raised in case of an unrecoverable couchdb conflict."""
51
52 #pylint: disable-msg=W0231
53 def __init__(self, conflicts):
54 self.conflicts = conflicts
55 #pylint: enable-msg=W0231
56
57 def __str__(self):
58 return "<CouchDB Conflict Error: %s>" % self.conflicts
59
3860
39class NoSuchDatabase(Exception):61class NoSuchDatabase(Exception):
40 "Exception for trying to use a non-existent database"62 "Exception for trying to use a non-existent database"
@@ -47,21 +69,22 @@
47 return ("Database %s does not exist on this server. (Create it by "69 return ("Database %s does not exist on this server. (Create it by "
48 "passing create=True)") % self.database70 "passing create=True)") % self.database
4971
72
50class OAuthAuthentication(httplib2.Authentication):73class OAuthAuthentication(httplib2.Authentication):
51 """An httplib2.Authentication subclass for OAuth"""74 """An httplib2.Authentication subclass for OAuth"""
52 def __init__(self, oauth_data, host, request_uri, headers, response, 75 def __init__(self, oauth_data, host, request_uri, headers, response,
53 content, http, scheme):76 content, http, scheme):
54 self.oauth_data = oauth_data77 self.oauth_data = oauth_data
55 self.scheme = scheme78 self.scheme = scheme
56 httplib2.Authentication.__init__(self, None, host, request_uri, 79 httplib2.Authentication.__init__(self, None, host, request_uri,
57 headers, response, content, http)80 headers, response, content, http)
5881
59 def request(self, method, request_uri, headers, content):82 def request(self, method, request_uri, headers, content):
60 """Modify the request headers to add the appropriate83 """Modify the request headers to add the appropriate
61 Authorization header."""84 Authorization header."""
62 consumer = oauth.OAuthConsumer(self.oauth_data['consumer_key'], 85 consumer = oauth.OAuthConsumer(self.oauth_data['consumer_key'],
63 self.oauth_data['consumer_secret'])86 self.oauth_data['consumer_secret'])
64 access_token = oauth.OAuthToken(self.oauth_data['token'], 87 access_token = oauth.OAuthToken(self.oauth_data['token'],
65 self.oauth_data['token_secret'])88 self.oauth_data['token_secret'])
66 sig_method = oauth.OAuthSignatureMethod_HMAC_SHA189 sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1
67 full_http_url = "%s://%s%s" % (self.scheme, self.host, request_uri)90 full_http_url = "%s://%s%s" % (self.scheme, self.host, request_uri)
@@ -78,14 +101,15 @@
78 req.sign_request(sig_method(), consumer, access_token)101 req.sign_request(sig_method(), consumer, access_token)
79 headers.update(httplib2._normalize_headers(req.to_header()))102 headers.update(httplib2._normalize_headers(req.to_header()))
80103
104
81class OAuthCapableHttp(httplib2.Http):105class OAuthCapableHttp(httplib2.Http):
82 """Subclass of httplib2.Http which specifically uses our OAuth 106 """Subclass of httplib2.Http which specifically uses our OAuth
83 Authentication subclass (because httplib2 doesn't know about it)"""107 Authentication subclass (because httplib2 doesn't know about it)"""
84 def __init__(self, scheme="http", cache=None, timeout=None, proxy_info=None):108 def __init__(self, scheme="http", cache=None, timeout=None, proxy_info=None):
85 self.__scheme = scheme109 self.__scheme = scheme
86 super(OAuthCapableHttp, self).__init__(cache, timeout, proxy_info)110 super(OAuthCapableHttp, self).__init__(cache, timeout, proxy_info)
87111
88 def add_oauth_tokens(self, consumer_key, consumer_secret, 112 def add_oauth_tokens(self, consumer_key, consumer_secret,
89 token, token_secret):113 token, token_secret):
90 self.oauth_data = {114 self.oauth_data = {
91 "consumer_key": consumer_key,115 "consumer_key": consumer_key,
@@ -99,7 +123,7 @@
99 """Since we know we're talking to desktopcouch, and we know that it123 """Since we know we're talking to desktopcouch, and we know that it
100 requires OAuth, just return the OAuthAuthentication here rather124 requires OAuth, just return the OAuthAuthentication here rather
101 than checking to see which supported auth method is required."""125 than checking to see which supported auth method is required."""
102 yield OAuthAuthentication(self.oauth_data, host, request_uri, headers, 126 yield OAuthAuthentication(self.oauth_data, host, request_uri, headers,
103 response, content, self, self.__scheme)127 response, content, self, self.__scheme)
104128
105def row_is_deleted(row):129def row_is_deleted(row):
@@ -159,44 +183,107 @@
159 if "_attachments" in data:183 if "_attachments" in data:
160 for att_name, att_attributes in data["_attachments"].iteritems():184 for att_name, att_attributes in data["_attachments"].iteritems():
161 record.attach_by_reference(att_name,185 record.attach_by_reference(att_name,
162 make_getter(self.db, record_id, att_name, 186 make_getter(self.db, record_id, att_name,
163 att_attributes["content_type"]))187 att_attributes["content_type"]))
164 return record188 return record
165189
166 def put_record(self, record):190 def put_record(self, record):
167 """Put a record in back end storage."""191 """Put a record in back end storage."""
168 if not record.record_id:192 if not record.record_id:
169 from uuid import uuid4 # Do not rely on couchdb to create an ID for us.193 # Do not rely on couchdb to create an ID for us.
194 from uuid import uuid4
170 record.record_id = uuid4().hex195 record.record_id = uuid4().hex
171 self.db[record.record_id] = record._data196 self.db[record.record_id] = record._data
172197
173 # At this point, we've saved new document to the database, by we do not198 # At this point, we've saved new document to the database, by
174 # know the revision number of it. We need *a* specific revision now,199 # we do not know the revision number of it. We need *a*
175 # so that we can attach BLOBs to it.200 # specific revision now, so that we can attach BLOBs to it.
176 #201
177 # This is bad. We can get the most recent revision, but that doesn't202 # This is bad. We can get the most recent revision, but that
178 # assure us that what we're attaching records to is the revision we203 # doesn't assure us that what we're attaching records to is
179 # just sent.204 # the revision we just sent.
180205
181 retreived_document = self.db[record.record_id]206 retrieved_document = self.db[record.record_id]
182 for attachment_name in record.list_attachments():207 for attachment_name in record.list_attachments():
183 data, content_type = record.attachment_data(attachment_name)208 data, content_type = record.attachment_data(attachment_name)
184 self.db.put_attachment(retreived_document, data, attachment_name, content_type)209 self.db.put_attachment(
210 retrieved_document, data, attachment_name, content_type)
185211
186 return record.record_id212 return record.record_id
187213
188 def update_fields(self, record_id, fields):214 def update_fields(self, record_id, fields, cached_record=None):
189 """Safely update a number of fields. 'fields' being a215 """Safely update a number of fields. 'fields' being a
190 dictionary with fieldname: value for only the fields we want216 dictionary with path_tuple: new_value for only the fields we
191 to change the value of.217 want to change the value of, where path_tuple is a tuple of
218 fieldnames indicating the path to the possibly nested field
219 we're interested in. old_record a the copy of the record we
220 most recently read from the database.
221
222 In the case the underlying document was changed, we try to
223 merge, but only if none of the old values have changed. (i.e.,
224 do not overwrite changes originating elsewhere.)
225
226 This is slightly hairy, so that other code won't have to be.
192 """227 """
228 # Initially, the record in memory and in the db are the same
229 # as far as we know. (If they're not, we'll get a
230 # ResourceConflict later on, from which we can recover.)
231 if cached_record is None:
232 cached_record = self.db[record_id]
233 if isinstance(cached_record, Record):
234 cached_record = cached_record._data
235 record = copy.deepcopy(cached_record)
236 # Loop until either failure or success has been determined
193 while True:237 while True:
194 record = self.db[record_id]238 modified = False
195 record.update(fields)239 conflicts = {}
196 try:240 # loop through all the fields that need to be modified
197 self.db[record_id] = record241 for path, new_value in fields.items():
198 except ResourceConflict:242 if not isinstance(path, tuple):
199 continue243 path = (path,)
244 # Walk down in both copies of the record to the leaf
245 # node that represents the field, creating the path in
246 # the in memory record if necessary.
247 db_parent = record
248 cached_parent = cached_record
249 for field in path[:-1]:
250 db_parent = db_parent.setdefault(field, {})
251 cached_parent = cached_parent.get(field, {})
252 # Get the values of the fields in the two copies.
253 db_value = db_parent.get(path[-1])
254 cached_value = cached_parent.get(path[-1])
255 # If the value we intend to store is already in the
256 # database, we need do nothing, which is our favorite.
257 if db_value == new_value:
258 continue
259 # If the value in the db is different than the value
260 # our copy holds, we have a conflict. We could bail
261 # here, but we can give better feedback if we gather
262 # all the conflicts, so we continue the for loop
263 if db_value != cached_value:
264 conflicts[path] = (db_value, new_value)
265 continue
266 # Otherwise, it is safe to update the field with the
267 # new value.
268 modified = True
269 db_parent[path[-1]] = new_value
270 # If we had conflicts, we can bail.
271 if conflicts:
272 raise FieldsConflict(conflicts)
273 # If we made changes to the document, we'll need to save
274 # it.
275 if modified:
276 try:
277 self.db[record_id] = record
278 except ResourceConflict:
279 # We got a conflict, meaning the record has
280 # changed in the database since we last loaded it
281 # into memory. Let's get a fresh copy and try
282 # again.
283 record = self.db[record_id]
284 continue
285 # If we get here, nothing remains to be done, and we can
286 # take a well deserved break.
200 break287 break
201288
202 def delete_record(self, record_id):289 def delete_record(self, record_id):
@@ -212,7 +299,7 @@
212 if record_id not in self.db:299 if record_id not in self.db:
213 return False300 return False
214 record = self.db[record_id]301 record = self.db[record_id]
215 return not row_is_deleted(record) 302 return not row_is_deleted(record)
216303
217 def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):304 def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
218 """Remove a view, given its name. Raises a KeyError on a unknown305 """Remove a view, given its name. Raises a KeyError on a unknown
@@ -257,7 +344,7 @@
257344
258 return deleted_data345 return deleted_data
259346
260 def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT, 347 def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT,
261 params=None):348 params=None):
262 """Execute view and return results."""349 """Execute view and return results."""
263 if design_doc is None:350 if design_doc is None:
@@ -373,10 +460,10 @@
373 """Check to see if there are any changes on this database since last460 """Check to see if there are any changes on this database since last
374 call (or since this object was instantiated), call a function for each,461 call (or since this object was instantiated), call a function for each,
375 and return the number of changes reported.462 and return the number of changes reported.
376 463
377 The callback function is called for every single change, with the464 The callback function is called for every single change, with the
378 keyword parameters of the dictionary of values returned from couchdb.465 keyword parameters of the dictionary of values returned from couchdb.
379 466
380 >>> def f(seq=None, id=None, changes=None):467 >>> def f(seq=None, id=None, changes=None):
381 ... pass468 ... pass
382469
@@ -400,7 +487,7 @@
400487
401 # Can't use self._server.resource.get() directly because it encodes "/".488 # Can't use self._server.resource.get() directly because it encodes "/".
402 uri = couchdburi(self._server.resource.uri, self.db.name, "_changes",489 uri = couchdburi(self._server.resource.uri, self.db.name, "_changes",
403 since=self._changes_since) 490 since=self._changes_since)
404 resp, data = self._server.resource.http.request(uri, "GET", "", {})491 resp, data = self._server.resource.http.request(uri, "GET", "", {})
405 structure = json.loads(data)492 structure = json.loads(data)
406 for change in structure.get("results"):493 for change in structure.get("results"):
407494
=== modified file 'desktopcouch/records/tests/test_couchgrid.py'
--- desktopcouch/records/tests/test_couchgrid.py 2009-11-11 16:24:22 +0000
+++ desktopcouch/records/tests/test_couchgrid.py 2010-01-29 22:17:14 +0000
@@ -31,6 +31,7 @@
31 """Test the CouchGrid functionality"""31 """Test the CouchGrid functionality"""
3232
33 def setUp(self):33 def setUp(self):
34 super(TestCouchGrid, self).setUp()
34 self.dbname = self._testMethodName35 self.dbname = self._testMethodName
35 self.db = CouchDatabase(self.dbname, create=True,36 self.db = CouchDatabase(self.dbname, create=True,
36 ctx=test_environment.test_context)37 ctx=test_environment.test_context)
@@ -38,6 +39,7 @@
3839
39 def tearDown(self):40 def tearDown(self):
40 """tear down each test"""41 """tear down each test"""
42 super(TestCouchGrid, self).tearDown()
41 #delete the database43 #delete the database
42 del self.db._server[self.dbname]44 del self.db._server[self.dbname]
4345
@@ -46,7 +48,7 @@
46 database name.48 database name.
47 """49 """
48 try:50 try:
49 cw = CouchGrid(None, ctx=test_environment.test_context)51 CouchGrid(None, ctx=test_environment.test_context)
50 except TypeError, inst:52 except TypeError, inst:
51 self.assertEqual(53 self.assertEqual(
52 inst.args[0],"database_name is required and must be a string")54 inst.args[0],"database_name is required and must be a string")
@@ -104,7 +106,7 @@
104 cw.append_row([])106 cw.append_row([])
105107
106 #if this all worked, there should be three rows in the model108 #if this all worked, there should be three rows in the model
107 model = cw.get_model()109 cw.get_model()
108110
109 #should be catching the following exception111 #should be catching the following exception
110 except RuntimeError, inst:112 except RuntimeError, inst:
111113
=== modified file 'desktopcouch/records/tests/test_field_registry.py'
--- desktopcouch/records/tests/test_field_registry.py 2009-10-02 23:47:26 +0000
+++ desktopcouch/records/tests/test_field_registry.py 2010-01-29 22:17:14 +0000
@@ -57,6 +57,7 @@
57 """Test Case for FieldMapping objects."""57 """Test Case for FieldMapping objects."""
5858
59 def setUp(self):59 def setUp(self):
60 super(TestFieldMapping, self).setUp()
60 self.test_record = copy.deepcopy(TEST_RECORD)61 self.test_record = copy.deepcopy(TEST_RECORD)
6162
62 def test_simple_field_mapping(self):63 def test_simple_field_mapping(self):
@@ -90,6 +91,7 @@
9091
91 def setUp(self):92 def setUp(self):
92 """setup test fixtures"""93 """setup test fixtures"""
94 super(TestTransformer, self).setUp()
93 self.transformer = AppTransformer()95 self.transformer = AppTransformer()
9496
95 def test_from_app(self):97 def test_from_app(self):
9698
=== modified file 'desktopcouch/records/tests/test_record.py'
--- desktopcouch/records/tests/test_record.py 2010-01-22 20:52:35 +0000
+++ desktopcouch/records/tests/test_record.py 2010-01-29 22:17:14 +0000
@@ -34,6 +34,7 @@
3434
35 def setUp(self):35 def setUp(self):
36 """Test setup."""36 """Test setup."""
37 super(TestRecords, self).setUp()
37 self.dict = {38 self.dict = {
38 "a": "A",39 "a": "A",
39 "b": "B",40 "b": "B",
@@ -62,13 +63,13 @@
62 db = CouchDatabase('testing', create=True, ctx=ctx)63 db = CouchDatabase('testing', create=True, ctx=ctx)
63 record_id = db.put_record(self.record)64 record_id = db.put_record(self.record)
6465
65 retreived_record = db.get_record(record_id)66 db.get_record(record_id)
66 self.assertNotEquals(self.record.record_revision, None)67 self.assertNotEquals(self.record.record_revision, None)
6768
68 first = self.record.record_revision69 first = self.record.record_revision
6970
70 record_id = db.put_record(self.record)71 record_id = db.put_record(self.record)
71 retreived_record = db.get_record(record_id)72 db.get_record(record_id)
72 second = self.record.record_revision73 second = self.record.record_revision
7374
74 self.assertTrue(first < second)75 self.assertTrue(first < second)
@@ -81,16 +82,16 @@
81 self.assertRaises(KeyError, f, self.record)82 self.assertRaises(KeyError, f, self.record)
8283
83 del self.record["a"]84 del self.record["a"]
84 85
85 def test_iter(self):86 def test_iter(self):
86 self.assertEquals(sorted(list(iter(self.record))), 87 self.assertEquals(sorted(list(iter(self.record))),
87 ['a', 'b', 'record_type', 'subfield', 'subfield_uuid'])88 ['a', 'b', 'record_type', 'subfield', 'subfield_uuid'])
88 89
89 def test_setitem_internal(self):90 def test_setitem_internal(self):
90 def f(r):91 def f(r):
91 r["_id"] = "new!"92 r["_id"] = "new!"
92 self.assertRaises(IllegalKeyException, f, self.record)93 self.assertRaises(IllegalKeyException, f, self.record)
93 94
94 def test_no_record_type(self):95 def test_no_record_type(self):
95 self.assertRaises(NoRecordTypeSpecified, Record, {})96 self.assertRaises(NoRecordTypeSpecified, Record, {})
9697
@@ -232,7 +233,7 @@
232 globs = { "db": CouchDatabase('testing', create=True, ctx=ctx) }233 globs = { "db": CouchDatabase('testing', create=True, ctx=ctx) }
233 results = doctest.testfile('../doc/records.txt', globs=globs)234 results = doctest.testfile('../doc/records.txt', globs=globs)
234 self.assertEqual(0, results.failed)235 self.assertEqual(0, results.failed)
235 236
236 def test_record_id(self):237 def test_record_id(self):
237 data = {"_id":"recordid"}238 data = {"_id":"recordid"}
238 record = Record(data, record_type="url")239 record = Record(data, record_type="url")
@@ -242,14 +243,16 @@
242 record = Record(data, record_type="url", record_id=record_id)243 record = Record(data, record_type="url", record_id=record_id)
243 self.assertEqual(record_id, record.record_id)244 self.assertEqual(record_id, record.record_id)
244 data = {"_id":"differentid"}245 data = {"_id":"differentid"}
245 self.assertRaises(ValueError, 246 self.assertRaises(ValueError,
246 Record, data, record_id=record_id, record_type="url")247 Record, data, record_id=record_id, record_type="url")
247248
249
248class TestRecordFactory(TestCase):250class TestRecordFactory(TestCase):
249 """Test Record/Mergeable List factories."""251 """Test Record/Mergeable List factories."""
250252
251 def setUp(self):253 def setUp(self):
252 """Test setup."""254 """Test setup."""
255 super(TestRecordFactory, self).setUp()
253 self.dict = {256 self.dict = {
254 "a": "A",257 "a": "A",
255 "b": "B",258 "b": "B",
256259
=== modified file 'desktopcouch/records/tests/test_server.py'
--- desktopcouch/records/tests/test_server.py 2009-12-15 20:59:18 +0000
+++ desktopcouch/records/tests/test_server.py 2010-01-29 22:17:14 +0000
@@ -22,13 +22,17 @@
2222
23import desktopcouch.tests as test_environment23import desktopcouch.tests as test_environment
24from desktopcouch.records.server import CouchDatabase24from desktopcouch.records.server import CouchDatabase
25from desktopcouch.records.server_base import row_is_deleted, NoSuchDatabase25from desktopcouch.records.server_base import (
26 row_is_deleted, NoSuchDatabase, FieldsConflict)
26from desktopcouch.records.record import Record27from desktopcouch.records.record import Record
2728
29# pylint can't deal with failing imports even when they're handled
30# pylint: disable-msg=F0401
28try:31try:
29 from io import StringIO32 from io import StringIO
30except ImportError:33except ImportError:
31 from cStringIO import StringIO as StringIO34 from cStringIO import StringIO as StringIO
35# pylint: enable-msg=F0401
3236
33FAKE_RECORD_TYPE = "http://example.org/test"37FAKE_RECORD_TYPE = "http://example.org/test"
3438
@@ -43,9 +47,9 @@
43class TestCouchDatabase(testtools.TestCase):47class TestCouchDatabase(testtools.TestCase):
44 """tests specific for CouchDatabase"""48 """tests specific for CouchDatabase"""
4549
46
47 def setUp(self):50 def setUp(self):
48 """setup each test"""51 """setup each test"""
52 super(TestCouchDatabase, self).setUp()
49 # Connect to CouchDB server53 # Connect to CouchDB server
50 self.dbname = self._testMethodName54 self.dbname = self._testMethodName
51 self.database = CouchDatabase(self.dbname, create=True,55 self.database = CouchDatabase(self.dbname, create=True,
@@ -63,10 +67,12 @@
6367
64 def tearDown(self):68 def tearDown(self):
65 """tear down each test"""69 """tear down each test"""
70 super(TestCouchDatabase, self).tearDown()
66 del self.database._server[self.dbname]71 del self.database._server[self.dbname]
6772
68 def test_database_not_exists(self):73 def test_database_not_exists(self):
69 self.assertRaises(NoSuchDatabase, CouchDatabase, "this-must-not-exist", create=False)74 self.assertRaises(
75 NoSuchDatabase, CouchDatabase, "this-must-not-exist", create=False)
7076
71 def test_get_records_by_record_type_save_view(self):77 def test_get_records_by_record_type_save_view(self):
72 """Test getting mutliple records by type"""78 """Test getting mutliple records by type"""
@@ -136,7 +142,6 @@
136 design_doc = "design"142 design_doc = "design"
137 view1_name = "unit_tests_are_wonderful"143 view1_name = "unit_tests_are_wonderful"
138 view2_name = "unit_tests_are_marvelous"144 view2_name = "unit_tests_are_marvelous"
139 view3_name = "unit_tests_are_fantastic"
140145
141 map_js = """function(doc) { emit(doc._id, null) }"""146 map_js = """function(doc) { emit(doc._id, null) }"""
142 reduce_js = """\147 reduce_js = """\
@@ -242,7 +247,8 @@
242247
243 self.database.report_changes(rep) # Make sure nothing horks.248 self.database.report_changes(rep) # Make sure nothing horks.
244249
245 count = self.database.report_changes(lambda **kw: self.fail()) # Too soon to try again.250 # Too soon to try again.
251 count = self.database.report_changes(lambda **kw: self.fail())
246 self.assertEqual(0, count)252 self.assertEqual(0, count)
247253
248 def test_report_changes_exceptions(self):254 def test_report_changes_exceptions(self):
@@ -250,48 +256,66 @@
250 self.failUnless("changes" in kwargs)256 self.failUnless("changes" in kwargs)
251 self.failUnless("id" in kwargs)257 self.failUnless("id" in kwargs)
252258
253 self.database.report_changes(rep) # Consume pending.259 # Consume pending.
260 self.database.report_changes(rep)
254 self.database._changes_last_used = 0261 self.database._changes_last_used = 0
255262
256 saved_time = self.database._changes_last_used # Store time.263 # Store time.
257 saved_position = self.database._changes_since # Store position.264 saved_time = self.database._changes_last_used
258 self.test_put_record() # Queue new changes. This is 1 event!265 # Store position.
266 saved_position = self.database._changes_since
267 # Queue new changes. This is 1 event!
268 self.test_put_record()
259269
260 # Exceptions in our callbacks do not consume an event.270 # Exceptions in our callbacks do not consume an event.
261 self.assertRaises(ZeroDivisionError, self.database.report_changes, lambda **kw: 1/0)271 self.assertRaises(
272 ZeroDivisionError, self.database.report_changes, lambda **kw: 1/0)
262273
263 self.assertEqual(saved_position, self.database._changes_since) # Ensure pos'n is same.274 # Ensure pos'n is same.
264 self.assertEqual(saved_time, self.database._changes_last_used) # Ensure time is same.275 self.assertEqual(saved_position, self.database._changes_since)
276 # Ensure time is same.
277 self.assertEqual(saved_time, self.database._changes_last_used)
265278
266 # Next time we run, we get the same event again.279 # Next time we run, we get the same event again.
267 count = self.database.report_changes(rep) # Consume queued changes.280 # Consume queued changes.
268 self.assertEquals(1, count) # Ensure position different.281 count = self.database.report_changes(rep)
269 self.assertEqual(saved_position + 1, self.database._changes_since) # Ensure position different.282 # Ensure position different.
283 self.assertEquals(1, count)
284 # Ensure position different.
285 self.assertEqual(saved_position + 1, self.database._changes_since)
270286
271 def test_report_changes_all_ops_give_known_keys(self):287 def test_report_changes_all_ops_give_known_keys(self):
272 def rep(**kwargs):288 def rep(**kwargs):
273 self.failUnless("changes" in kwargs)289 self.failUnless("changes" in kwargs)
274 self.failUnless("id" in kwargs)290 self.failUnless("id" in kwargs)
275291
276 self.database._changes_last_used = 0 # Permit immediate run.292 # Permit immediate run.
277 count = self.database.report_changes(rep) # Test expected kw args.293 self.database._changes_last_used = 0
294 # Test expected kw args.
295 self.database.report_changes(rep)
278296
279 def test_report_changes_nochanges(self):297 def test_report_changes_nochanges(self):
280 def rep(**kwargs):298 def rep(**kwargs):
281 self.failUnless("changes" in kwargs)299 self.failUnless("changes" in kwargs)
282 self.failUnless("id" in kwargs)300 self.failUnless("id" in kwargs)
283301
284 count = self.database.report_changes(rep) # Consume queue.302 # Consume queue.
285 self.database._changes_last_used = 0 # Permit immediate run.303 count = self.database.report_changes(rep)
286 saved_position = self.database._changes_since # Store position.304 # Permit immediate run.
287 count = self.database.report_changes(rep)305 self.database._changes_last_used = 0
288 self.assertEquals(0, count) # Ensure event count is zero.306 # Store position.
289 self.assertEqual(saved_position, self.database._changes_since) # Pos'n is same.307 saved_position = self.database._changes_since
308 count = self.database.report_changes(rep)
309 # Ensure event count is zero.
310 self.assertEquals(0, count)
311 # Pos'n is same.
312 self.assertEqual(saved_position, self.database._changes_since)
290313
291 def test_attachments(self):314 def test_attachments(self):
292 content = StringIO("0123456789\n==========\n\n" * 5)315 content = StringIO("0123456789\n==========\n\n" * 5)
293316
294 constructed_record = Record({'record_number': 0}, record_type="http://example.com/")317 constructed_record = Record(
318 {'record_number': 0}, record_type="http://example.com/")
295319
296 # Before anything is attached, there are no attachments.320 # Before anything is attached, there are no attachments.
297 self.assertEqual(constructed_record.list_attachments(), [])321 self.assertEqual(constructed_record.list_attachments(), [])
@@ -307,7 +331,7 @@
307 self.assertEqual(out_content_type, "text/plain")331 self.assertEqual(out_content_type, "text/plain")
308332
309 # One can not put another document of the same name.333 # One can not put another document of the same name.
310 self.assertRaises(KeyError, constructed_record.attach, content, 334 self.assertRaises(KeyError, constructed_record.attach, content,
311 "another document", "text/x-rst")335 "another document", "text/x-rst")
312336
313 record_id = self.database.put_record(constructed_record)337 record_id = self.database.put_record(constructed_record)
@@ -315,37 +339,44 @@
315339
316 # We can add attachments after a document is put in the DB.340 # We can add attachments after a document is put in the DB.
317 retrieved_record.attach(content, "Document", "text/x-rst")341 retrieved_record.attach(content, "Document", "text/x-rst")
318 record_id = self.database.put_record(retrieved_record) # push new version342 # push new version
319 retrieved_record = self.database.get_record(record_id) # get new343 record_id = self.database.put_record(retrieved_record)
344 # get new
345 retrieved_record = self.database.get_record(record_id)
320346
321 # To replace, one must remove first.347 # To replace, one must remove first.
322 retrieved_record.detach("another document")348 retrieved_record.detach("another document")
323 retrieved_record.attach(content, "another document", "text/plain")349 retrieved_record.attach(content, "another document", "text/plain")
324350
325 record_id = self.database.put_record(retrieved_record) # push new version351 # push new version
326 retrieved_record = self.database.get_record(record_id) # get new352 record_id = self.database.put_record(retrieved_record)
353 # get new
354 retrieved_record = self.database.get_record(record_id)
327355
328 # We can get a list of attachments.356 # We can get a list of attachments.
329 self.assertEqual(set(retrieved_record.list_attachments()), 357 self.assertEqual(set(retrieved_record.list_attachments()),
330 set(["nu/mbe/rs", "Document", "another document"]))358 set(["nu/mbe/rs", "Document", "another document"]))
331359
332 # We can read from a document that we retrieved.360 # We can read from a document that we retrieved.
333 out_data, out_content_type = retrieved_record.attachment_data("nu/mbe/rs")361 out_data, out_content_type = retrieved_record.attachment_data(
362 "nu/mbe/rs")
334 self.assertEqual(out_data, content.getvalue())363 self.assertEqual(out_data, content.getvalue())
335 self.assertEqual(out_content_type, "text/plain")364 self.assertEqual(out_content_type, "text/plain")
336365
337 # Asking for a named document that does not exist causes KeyError.366 # Asking for a named document that does not exist causes KeyError.
338 self.assertRaises(KeyError, retrieved_record.attachment_data, 367 self.assertRaises(KeyError, retrieved_record.attachment_data,
339 "NoExist")368 "NoExist")
340 self.assertRaises(KeyError, constructed_record.attachment_data, 369 self.assertRaises(KeyError, constructed_record.attachment_data,
341 "No Exist")370 "No Exist")
342 self.assertRaises(KeyError, retrieved_record.detach, 371 self.assertRaises(KeyError, retrieved_record.detach,
343 "NoExist")372 "NoExist")
344373
345 for i, name in enumerate(retrieved_record.list_attachments()):374 for i, name in enumerate(retrieved_record.list_attachments()):
346 if i != 0:375 if i != 0:
347 retrieved_record.detach(name) # delete all but one.376 # delete all but one.
348 record_id = self.database.put_record(retrieved_record) # push new version.377 retrieved_record.detach(name)
378 # push new version.
379 record_id = self.database.put_record(retrieved_record)
349380
350 # We can remove records with attachments.381 # We can remove records with attachments.
351 self.database.delete_record(record_id)382 self.database.delete_record(record_id)
@@ -361,12 +392,63 @@
361 list(self.database.execute_view(view1_name, design_doc))]392 list(self.database.execute_view(view1_name, design_doc))]
362 # ordinary requests are in key order393 # ordinary requests are in key order
363 self.assertEqual(data, sorted(data))394 self.assertEqual(data, sorted(data))
364 395
365 # now request descending order and confirm that it *is* descending396 # now request descending order and confirm that it *is* descending
366 descdata = [i.key for i in397 descdata = [i.key for i in
367 list(self.database.execute_view(view1_name, design_doc, 398 list(self.database.execute_view(view1_name, design_doc,
368 {"descending": True}))]399 {"descending": True}))]
369 self.assertEqual(descdata, list(reversed(sorted(data))))400 self.assertEqual(descdata, list(reversed(sorted(data))))
370401
371 self.database.delete_view(view1_name, design_doc)402 self.database.delete_view(view1_name, design_doc)
372403
404 def test_update_fields_success(self):
405 """Test update_fields method"""
406 dictionary = {
407 'record_number': 0,
408 'field1': 1,
409 'field2': 2,
410 'nested': {
411 'sub1': 's1',
412 'sub2': 's2'}}
413 record = Record(dictionary, record_type="http://example.com/")
414 record_id = self.database.put_record(record)
415 # manipulate the database 'out of view'
416 non_working_copy = self.database.get_record(record_id)
417 non_working_copy['field2'] = 22
418 non_working_copy['field3'] = 3
419 self.database.put_record(non_working_copy)
420 self.database.update_fields(
421 record_id, {'field1': 11,('nested', 'sub2'): 's2-changed'},
422 cached_record=record)
423 working_copy = self.database.get_record(record_id)
424 self.assertEqual(0, working_copy['record_number'])
425 self.assertEqual(11, working_copy['field1'])
426 self.assertEqual(22, working_copy['field2'])
427 self.assertEqual(3, working_copy['field3'])
428 self.assertEqual('s2-changed', working_copy['nested']['sub2'])
429 self.assertEqual('s1', working_copy['nested']['sub1'])
430
431 def test_update_fields_failure(self):
432 """Test update_fields method"""
433 dictionary = {
434 'record_number': 0,
435 'field1': 1,
436 'field2': 2,
437 'nested': {
438 'sub1': 's1',
439 'sub2': 's2'}}
440 record = Record(dictionary, record_type="http://example.com/")
441 record_id = self.database.put_record(record)
442 # manipulate the database 'out of view'
443 non_working_copy = self.database.get_record(record_id)
444 non_working_copy['field1'] = 22
445 non_working_copy['field3'] = 3
446 self.database.put_record(non_working_copy)
447 try:
448 self.database.update_fields(
449 record_id, {'field1': 11, ('nested', 'sub2'): 's2-changed'},
450 cached_record=record)
451 # we want the exception
452 self.fail()
453 except FieldsConflict, e:
454 self.assertEqual({('field1',): (22, 11)}, e.conflicts)
373455
=== modified file 'desktopcouch/tests/test_local_files.py'
--- desktopcouch/tests/test_local_files.py 2009-11-22 02:36:00 +0000
+++ desktopcouch/tests/test_local_files.py 2010-01-29 22:17:14 +0000
@@ -3,14 +3,16 @@
3import testtools3import testtools
4import desktopcouch.tests as test_environment4import desktopcouch.tests as test_environment
5import desktopcouch5import desktopcouch
6import os6
77
8class TestLocalFiles(testtools.TestCase):8class TestLocalFiles(testtools.TestCase):
9 """Testing that local files returns the right things"""9 """Testing that local files returns the right things"""
1010
11 def setUp(self):11 def setUp(self):
12 super(TestLocalFiles, self).setUp()
12 cf = test_environment.test_context.configuration13 cf = test_environment.test_context.configuration
13 cf._fill_from_file(test_environment.test_context.file_ini) # Test loading from file.14 # Test loading from file.
15 cf._fill_from_file(test_environment.test_context.file_ini)
1416
15 def test_all_files_returned(self):17 def test_all_files_returned(self):
16 "Does local_files list all the files that it needs to?"18 "Does local_files list all the files that it needs to?"
@@ -23,12 +25,13 @@
2325
24 def test_xdg_overwrite_works(self):26 def test_xdg_overwrite_works(self):
25 # this should really check that it's in os.environ["TMP"]27 # this should really check that it's in os.environ["TMP"]
26 self.assertTrue(test_environment.test_context.file_ini.startswith("/tmp"))28 self.assertTrue(
29 test_environment.test_context.file_ini.startswith("/tmp"))
2730
28 def test_couch_chain_ini_files(self):31 def test_couch_chain_ini_files(self):
29 "Is compulsory-auth.ini picked up by the ini file finder?"32 "Is compulsory-auth.ini picked up by the ini file finder?"
30 import desktopcouch.local_files33 import desktopcouch.local_files
31 ok = [x for x 34 ok = [x for x
32 in test_environment.test_context.couch_chain_ini_files().split()35 in test_environment.test_context.couch_chain_ini_files().split()
33 if x.endswith("compulsory-auth.ini")]36 if x.endswith("compulsory-auth.ini")]
34 self.assertTrue(len(ok) > 0)37 self.assertTrue(len(ok) > 0)
3538
=== modified file 'desktopcouch/tests/test_replication.py'
--- desktopcouch/tests/test_replication.py 2009-11-12 22:17:52 +0000
+++ desktopcouch/tests/test_replication.py 2010-01-29 22:17:14 +0000
@@ -11,6 +11,7 @@
11 """Testing that the database/designdoc filesystem loader works"""11 """Testing that the database/designdoc filesystem loader works"""
1212
13 def setUp(self):13 def setUp(self):
14 super(TestReplication, self).setUp()
14 self.db_apple = desktopcouch.records.server.CouchDatabase("apple",15 self.db_apple = desktopcouch.records.server.CouchDatabase("apple",
15 create=True, ctx=test_environment.test_context)16 create=True, ctx=test_environment.test_context)
16 banana = test_environment.create_new_test_environment()17 banana = test_environment.create_new_test_environment()
@@ -18,4 +19,5 @@
18 create=True, ctx=banana)19 create=True, ctx=banana)
1920
20 def test_creation(self):21 def test_creation(self):
22 # XXX uhm?
21 pass23 pass
2224
=== modified file 'desktopcouch/tests/test_start_local_couchdb.py'
--- desktopcouch/tests/test_start_local_couchdb.py 2009-11-12 22:05:09 +0000
+++ desktopcouch/tests/test_start_local_couchdb.py 2010-01-29 22:17:14 +0000
@@ -71,6 +71,7 @@
7171
72 def setUp(self):72 def setUp(self):
73 # create temp folder with databases and design documents in73 # create temp folder with databases and design documents in
74 super(TestUpdateDesignDocuments, self).setUp()
74 xdg_data = os.path.split(test_environment.test_context.db_dir)[0]75 xdg_data = os.path.split(test_environment.test_context.db_dir)[0]
75 try:76 try:
76 os.mkdir(os.path.join(xdg_data, "desktop-couch"))77 os.mkdir(os.path.join(xdg_data, "desktop-couch"))
@@ -92,21 +93,33 @@
9293
93 # databases that should be created94 # databases that should be created
94 couchdb("cfg", create=True, ctx=test_environment.test_context)95 couchdb("cfg", create=True, ctx=test_environment.test_context)
95 couchdb("cfg_and_empty_design", create=True, ctx=test_environment.test_context)96 couchdb(
96 couchdb("cfg_and_design_no_views", create=True, ctx=test_environment.test_context)97 "cfg_and_empty_design", create=True,
97 couchdb("cfg_and_design_one_view_no_map", create=True, ctx=test_environment.test_context)98 ctx=test_environment.test_context)
98 couchdb("cfg_and_design_one_view_map_no_reduce", create=True, ctx=test_environment.test_context)99 couchdb(
100 "cfg_and_design_no_views", create=True,
101 ctx=test_environment.test_context)
102 couchdb(
103 "cfg_and_design_one_view_no_map", create=True,
104 ctx=test_environment.test_context)
105 couchdb(
106 "cfg_and_design_one_view_map_no_reduce", create=True,
107 ctx=test_environment.test_context)
99108
100 dbmock1 = mocker.mock()109 dbmock1 = mocker.mock()
101 mocker.result(dbmock1)110 mocker.result(dbmock1)
102 dbmock1.add_view("view1", "cfg_and_design_one_view_map_no_reduce:map",111 dbmock1.add_view("view1", "cfg_and_design_one_view_map_no_reduce:map",
103 None, "doc1")112 None, "doc1")
104 couchdb("cfg_and_design_one_view_map_reduce", create=True, ctx=test_environment.test_context)113 couchdb(
114 "cfg_and_design_one_view_map_reduce", create=True,
115 ctx=test_environment.test_context)
105 dbmock2 = mocker.mock()116 dbmock2 = mocker.mock()
106 mocker.result(dbmock2)117 mocker.result(dbmock2)
107 dbmock2.add_view("view1", "cfg_and_design_one_view_map_reduce:map",118 dbmock2.add_view("view1", "cfg_and_design_one_view_map_reduce:map",
108 "cfg_and_design_one_view_map_reduce:reduce", "doc1")119 "cfg_and_design_one_view_map_reduce:reduce", "doc1")
109 couchdb("cfg_and_design_two_views_map_reduce", create=True, ctx=test_environment.test_context)120 couchdb(
121 "cfg_and_design_two_views_map_reduce", create=True,
122 ctx=test_environment.test_context)
110 dbmock3 = mocker.mock()123 dbmock3 = mocker.mock()
111 mocker.result(dbmock3)124 mocker.result(dbmock3)
112 dbmock3.add_view("view1", "cfg_and_design_two_views_map_reduce:map1",125 dbmock3.add_view("view1", "cfg_and_design_two_views_map_reduce:map1",

Subscribers

People subscribed via source and target branches