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
1=== modified file 'desktopcouch/contacts/tests/test_contactspicker.py'
2--- desktopcouch/contacts/tests/test_contactspicker.py 2009-11-11 16:24:22 +0000
3+++ desktopcouch/contacts/tests/test_contactspicker.py 2010-01-29 22:17:14 +0000
4@@ -32,12 +32,14 @@
5 def setUp(self):
6 """setup each test"""
7 # Connect to CouchDB server
8+ super(TestContactsPicker, self).setUp()
9 self.dbname = 'contacts'
10 self.database = CouchDatabase(self.dbname, create=True,
11 ctx=test_environment.test_context)
12
13 def tearDown(self):
14 """tear down each test"""
15+ super(TestContactsPicker, self).tearDown()
16 del self.database._server[self.dbname]
17
18 def test_can_contruct_contactspicker(self):
19
20=== modified file 'desktopcouch/records/server_base.py'
21--- desktopcouch/records/server_base.py 2009-12-15 20:59:18 +0000
22+++ desktopcouch/records/server_base.py 2010-01-29 22:17:14 +0000
23@@ -21,20 +21,42 @@
24
25 """The Desktop Couch Records API."""
26
27+import httplib2, urlparse, cgi, copy
28+from time import time
29+
30+# please keep desktopcouch python 2.5 compatible for now
31+
32+# pylint can't deal with failing imports even when they're handled
33+# pylint: disable-msg=F0401
34+try:
35+ # Python 2.5
36+ import simplejson as json
37+except ImportError:
38+ # Python 2.6+
39+ import json
40+# pylint: enable-msg=F0401
41+
42+from oauth import oauth
43+
44 from couchdb import Server
45 from couchdb.client import ResourceNotFound, ResourceConflict, uri as couchdburi
46 from couchdb.design import ViewDefinition
47 from record import Record
48-import httplib2
49-from oauth import oauth
50-import urlparse
51-import cgi
52-from time import time
53-import json
54
55 #DEFAULT_DESIGN_DOCUMENT = "design"
56 DEFAULT_DESIGN_DOCUMENT = None # each view in its own eponymous design doc.
57
58+class FieldsConflict(Exception):
59+ """Raised in case of an unrecoverable couchdb conflict."""
60+
61+ #pylint: disable-msg=W0231
62+ def __init__(self, conflicts):
63+ self.conflicts = conflicts
64+ #pylint: enable-msg=W0231
65+
66+ def __str__(self):
67+ return "<CouchDB Conflict Error: %s>" % self.conflicts
68+
69
70 class NoSuchDatabase(Exception):
71 "Exception for trying to use a non-existent database"
72@@ -47,21 +69,22 @@
73 return ("Database %s does not exist on this server. (Create it by "
74 "passing create=True)") % self.database
75
76+
77 class OAuthAuthentication(httplib2.Authentication):
78 """An httplib2.Authentication subclass for OAuth"""
79- def __init__(self, oauth_data, host, request_uri, headers, response,
80+ def __init__(self, oauth_data, host, request_uri, headers, response,
81 content, http, scheme):
82 self.oauth_data = oauth_data
83 self.scheme = scheme
84- httplib2.Authentication.__init__(self, None, host, request_uri,
85+ httplib2.Authentication.__init__(self, None, host, request_uri,
86 headers, response, content, http)
87
88 def request(self, method, request_uri, headers, content):
89 """Modify the request headers to add the appropriate
90 Authorization header."""
91- consumer = oauth.OAuthConsumer(self.oauth_data['consumer_key'],
92+ consumer = oauth.OAuthConsumer(self.oauth_data['consumer_key'],
93 self.oauth_data['consumer_secret'])
94- access_token = oauth.OAuthToken(self.oauth_data['token'],
95+ access_token = oauth.OAuthToken(self.oauth_data['token'],
96 self.oauth_data['token_secret'])
97 sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1
98 full_http_url = "%s://%s%s" % (self.scheme, self.host, request_uri)
99@@ -78,14 +101,15 @@
100 req.sign_request(sig_method(), consumer, access_token)
101 headers.update(httplib2._normalize_headers(req.to_header()))
102
103+
104 class OAuthCapableHttp(httplib2.Http):
105- """Subclass of httplib2.Http which specifically uses our OAuth
106+ """Subclass of httplib2.Http which specifically uses our OAuth
107 Authentication subclass (because httplib2 doesn't know about it)"""
108 def __init__(self, scheme="http", cache=None, timeout=None, proxy_info=None):
109 self.__scheme = scheme
110 super(OAuthCapableHttp, self).__init__(cache, timeout, proxy_info)
111
112- def add_oauth_tokens(self, consumer_key, consumer_secret,
113+ def add_oauth_tokens(self, consumer_key, consumer_secret,
114 token, token_secret):
115 self.oauth_data = {
116 "consumer_key": consumer_key,
117@@ -99,7 +123,7 @@
118 """Since we know we're talking to desktopcouch, and we know that it
119 requires OAuth, just return the OAuthAuthentication here rather
120 than checking to see which supported auth method is required."""
121- yield OAuthAuthentication(self.oauth_data, host, request_uri, headers,
122+ yield OAuthAuthentication(self.oauth_data, host, request_uri, headers,
123 response, content, self, self.__scheme)
124
125 def row_is_deleted(row):
126@@ -159,44 +183,107 @@
127 if "_attachments" in data:
128 for att_name, att_attributes in data["_attachments"].iteritems():
129 record.attach_by_reference(att_name,
130- make_getter(self.db, record_id, att_name,
131+ make_getter(self.db, record_id, att_name,
132 att_attributes["content_type"]))
133 return record
134
135 def put_record(self, record):
136 """Put a record in back end storage."""
137 if not record.record_id:
138- from uuid import uuid4 # Do not rely on couchdb to create an ID for us.
139+ # Do not rely on couchdb to create an ID for us.
140+ from uuid import uuid4
141 record.record_id = uuid4().hex
142 self.db[record.record_id] = record._data
143
144- # At this point, we've saved new document to the database, by we do not
145- # know the revision number of it. We need *a* specific revision now,
146- # so that we can attach BLOBs to it.
147- #
148- # This is bad. We can get the most recent revision, but that doesn't
149- # assure us that what we're attaching records to is the revision we
150- # just sent.
151-
152- retreived_document = self.db[record.record_id]
153+ # At this point, we've saved new document to the database, by
154+ # we do not know the revision number of it. We need *a*
155+ # specific revision now, so that we can attach BLOBs to it.
156+
157+ # This is bad. We can get the most recent revision, but that
158+ # doesn't assure us that what we're attaching records to is
159+ # the revision we just sent.
160+
161+ retrieved_document = self.db[record.record_id]
162 for attachment_name in record.list_attachments():
163 data, content_type = record.attachment_data(attachment_name)
164- self.db.put_attachment(retreived_document, data, attachment_name, content_type)
165+ self.db.put_attachment(
166+ retrieved_document, data, attachment_name, content_type)
167
168 return record.record_id
169
170- def update_fields(self, record_id, fields):
171+ def update_fields(self, record_id, fields, cached_record=None):
172 """Safely update a number of fields. 'fields' being a
173- dictionary with fieldname: value for only the fields we want
174- to change the value of.
175+ dictionary with path_tuple: new_value for only the fields we
176+ want to change the value of, where path_tuple is a tuple of
177+ fieldnames indicating the path to the possibly nested field
178+ we're interested in. old_record a the copy of the record we
179+ most recently read from the database.
180+
181+ In the case the underlying document was changed, we try to
182+ merge, but only if none of the old values have changed. (i.e.,
183+ do not overwrite changes originating elsewhere.)
184+
185+ This is slightly hairy, so that other code won't have to be.
186 """
187+ # Initially, the record in memory and in the db are the same
188+ # as far as we know. (If they're not, we'll get a
189+ # ResourceConflict later on, from which we can recover.)
190+ if cached_record is None:
191+ cached_record = self.db[record_id]
192+ if isinstance(cached_record, Record):
193+ cached_record = cached_record._data
194+ record = copy.deepcopy(cached_record)
195+ # Loop until either failure or success has been determined
196 while True:
197- record = self.db[record_id]
198- record.update(fields)
199- try:
200- self.db[record_id] = record
201- except ResourceConflict:
202- continue
203+ modified = False
204+ conflicts = {}
205+ # loop through all the fields that need to be modified
206+ for path, new_value in fields.items():
207+ if not isinstance(path, tuple):
208+ path = (path,)
209+ # Walk down in both copies of the record to the leaf
210+ # node that represents the field, creating the path in
211+ # the in memory record if necessary.
212+ db_parent = record
213+ cached_parent = cached_record
214+ for field in path[:-1]:
215+ db_parent = db_parent.setdefault(field, {})
216+ cached_parent = cached_parent.get(field, {})
217+ # Get the values of the fields in the two copies.
218+ db_value = db_parent.get(path[-1])
219+ cached_value = cached_parent.get(path[-1])
220+ # If the value we intend to store is already in the
221+ # database, we need do nothing, which is our favorite.
222+ if db_value == new_value:
223+ continue
224+ # If the value in the db is different than the value
225+ # our copy holds, we have a conflict. We could bail
226+ # here, but we can give better feedback if we gather
227+ # all the conflicts, so we continue the for loop
228+ if db_value != cached_value:
229+ conflicts[path] = (db_value, new_value)
230+ continue
231+ # Otherwise, it is safe to update the field with the
232+ # new value.
233+ modified = True
234+ db_parent[path[-1]] = new_value
235+ # If we had conflicts, we can bail.
236+ if conflicts:
237+ raise FieldsConflict(conflicts)
238+ # If we made changes to the document, we'll need to save
239+ # it.
240+ if modified:
241+ try:
242+ self.db[record_id] = record
243+ except ResourceConflict:
244+ # We got a conflict, meaning the record has
245+ # changed in the database since we last loaded it
246+ # into memory. Let's get a fresh copy and try
247+ # again.
248+ record = self.db[record_id]
249+ continue
250+ # If we get here, nothing remains to be done, and we can
251+ # take a well deserved break.
252 break
253
254 def delete_record(self, record_id):
255@@ -212,7 +299,7 @@
256 if record_id not in self.db:
257 return False
258 record = self.db[record_id]
259- return not row_is_deleted(record)
260+ return not row_is_deleted(record)
261
262 def delete_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT):
263 """Remove a view, given its name. Raises a KeyError on a unknown
264@@ -257,7 +344,7 @@
265
266 return deleted_data
267
268- def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT,
269+ def execute_view(self, view_name, design_doc=DEFAULT_DESIGN_DOCUMENT,
270 params=None):
271 """Execute view and return results."""
272 if design_doc is None:
273@@ -373,10 +460,10 @@
274 """Check to see if there are any changes on this database since last
275 call (or since this object was instantiated), call a function for each,
276 and return the number of changes reported.
277-
278+
279 The callback function is called for every single change, with the
280 keyword parameters of the dictionary of values returned from couchdb.
281-
282+
283 >>> def f(seq=None, id=None, changes=None):
284 ... pass
285
286@@ -400,7 +487,7 @@
287
288 # Can't use self._server.resource.get() directly because it encodes "/".
289 uri = couchdburi(self._server.resource.uri, self.db.name, "_changes",
290- since=self._changes_since)
291+ since=self._changes_since)
292 resp, data = self._server.resource.http.request(uri, "GET", "", {})
293 structure = json.loads(data)
294 for change in structure.get("results"):
295
296=== modified file 'desktopcouch/records/tests/test_couchgrid.py'
297--- desktopcouch/records/tests/test_couchgrid.py 2009-11-11 16:24:22 +0000
298+++ desktopcouch/records/tests/test_couchgrid.py 2010-01-29 22:17:14 +0000
299@@ -31,6 +31,7 @@
300 """Test the CouchGrid functionality"""
301
302 def setUp(self):
303+ super(TestCouchGrid, self).setUp()
304 self.dbname = self._testMethodName
305 self.db = CouchDatabase(self.dbname, create=True,
306 ctx=test_environment.test_context)
307@@ -38,6 +39,7 @@
308
309 def tearDown(self):
310 """tear down each test"""
311+ super(TestCouchGrid, self).tearDown()
312 #delete the database
313 del self.db._server[self.dbname]
314
315@@ -46,7 +48,7 @@
316 database name.
317 """
318 try:
319- cw = CouchGrid(None, ctx=test_environment.test_context)
320+ CouchGrid(None, ctx=test_environment.test_context)
321 except TypeError, inst:
322 self.assertEqual(
323 inst.args[0],"database_name is required and must be a string")
324@@ -104,7 +106,7 @@
325 cw.append_row([])
326
327 #if this all worked, there should be three rows in the model
328- model = cw.get_model()
329+ cw.get_model()
330
331 #should be catching the following exception
332 except RuntimeError, inst:
333
334=== modified file 'desktopcouch/records/tests/test_field_registry.py'
335--- desktopcouch/records/tests/test_field_registry.py 2009-10-02 23:47:26 +0000
336+++ desktopcouch/records/tests/test_field_registry.py 2010-01-29 22:17:14 +0000
337@@ -57,6 +57,7 @@
338 """Test Case for FieldMapping objects."""
339
340 def setUp(self):
341+ super(TestFieldMapping, self).setUp()
342 self.test_record = copy.deepcopy(TEST_RECORD)
343
344 def test_simple_field_mapping(self):
345@@ -90,6 +91,7 @@
346
347 def setUp(self):
348 """setup test fixtures"""
349+ super(TestTransformer, self).setUp()
350 self.transformer = AppTransformer()
351
352 def test_from_app(self):
353
354=== modified file 'desktopcouch/records/tests/test_record.py'
355--- desktopcouch/records/tests/test_record.py 2010-01-22 20:52:35 +0000
356+++ desktopcouch/records/tests/test_record.py 2010-01-29 22:17:14 +0000
357@@ -34,6 +34,7 @@
358
359 def setUp(self):
360 """Test setup."""
361+ super(TestRecords, self).setUp()
362 self.dict = {
363 "a": "A",
364 "b": "B",
365@@ -62,13 +63,13 @@
366 db = CouchDatabase('testing', create=True, ctx=ctx)
367 record_id = db.put_record(self.record)
368
369- retreived_record = db.get_record(record_id)
370+ db.get_record(record_id)
371 self.assertNotEquals(self.record.record_revision, None)
372
373 first = self.record.record_revision
374
375 record_id = db.put_record(self.record)
376- retreived_record = db.get_record(record_id)
377+ db.get_record(record_id)
378 second = self.record.record_revision
379
380 self.assertTrue(first < second)
381@@ -81,16 +82,16 @@
382 self.assertRaises(KeyError, f, self.record)
383
384 del self.record["a"]
385-
386+
387 def test_iter(self):
388- self.assertEquals(sorted(list(iter(self.record))),
389+ self.assertEquals(sorted(list(iter(self.record))),
390 ['a', 'b', 'record_type', 'subfield', 'subfield_uuid'])
391-
392+
393 def test_setitem_internal(self):
394 def f(r):
395 r["_id"] = "new!"
396 self.assertRaises(IllegalKeyException, f, self.record)
397-
398+
399 def test_no_record_type(self):
400 self.assertRaises(NoRecordTypeSpecified, Record, {})
401
402@@ -232,7 +233,7 @@
403 globs = { "db": CouchDatabase('testing', create=True, ctx=ctx) }
404 results = doctest.testfile('../doc/records.txt', globs=globs)
405 self.assertEqual(0, results.failed)
406-
407+
408 def test_record_id(self):
409 data = {"_id":"recordid"}
410 record = Record(data, record_type="url")
411@@ -242,14 +243,16 @@
412 record = Record(data, record_type="url", record_id=record_id)
413 self.assertEqual(record_id, record.record_id)
414 data = {"_id":"differentid"}
415- self.assertRaises(ValueError,
416+ self.assertRaises(ValueError,
417 Record, data, record_id=record_id, record_type="url")
418
419+
420 class TestRecordFactory(TestCase):
421 """Test Record/Mergeable List factories."""
422
423 def setUp(self):
424 """Test setup."""
425+ super(TestRecordFactory, self).setUp()
426 self.dict = {
427 "a": "A",
428 "b": "B",
429
430=== modified file 'desktopcouch/records/tests/test_server.py'
431--- desktopcouch/records/tests/test_server.py 2009-12-15 20:59:18 +0000
432+++ desktopcouch/records/tests/test_server.py 2010-01-29 22:17:14 +0000
433@@ -22,13 +22,17 @@
434
435 import desktopcouch.tests as test_environment
436 from desktopcouch.records.server import CouchDatabase
437-from desktopcouch.records.server_base import row_is_deleted, NoSuchDatabase
438+from desktopcouch.records.server_base import (
439+ row_is_deleted, NoSuchDatabase, FieldsConflict)
440 from desktopcouch.records.record import Record
441
442+# pylint can't deal with failing imports even when they're handled
443+# pylint: disable-msg=F0401
444 try:
445 from io import StringIO
446 except ImportError:
447 from cStringIO import StringIO as StringIO
448+# pylint: enable-msg=F0401
449
450 FAKE_RECORD_TYPE = "http://example.org/test"
451
452@@ -43,9 +47,9 @@
453 class TestCouchDatabase(testtools.TestCase):
454 """tests specific for CouchDatabase"""
455
456-
457 def setUp(self):
458 """setup each test"""
459+ super(TestCouchDatabase, self).setUp()
460 # Connect to CouchDB server
461 self.dbname = self._testMethodName
462 self.database = CouchDatabase(self.dbname, create=True,
463@@ -63,10 +67,12 @@
464
465 def tearDown(self):
466 """tear down each test"""
467+ super(TestCouchDatabase, self).tearDown()
468 del self.database._server[self.dbname]
469
470 def test_database_not_exists(self):
471- self.assertRaises(NoSuchDatabase, CouchDatabase, "this-must-not-exist", create=False)
472+ self.assertRaises(
473+ NoSuchDatabase, CouchDatabase, "this-must-not-exist", create=False)
474
475 def test_get_records_by_record_type_save_view(self):
476 """Test getting mutliple records by type"""
477@@ -136,7 +142,6 @@
478 design_doc = "design"
479 view1_name = "unit_tests_are_wonderful"
480 view2_name = "unit_tests_are_marvelous"
481- view3_name = "unit_tests_are_fantastic"
482
483 map_js = """function(doc) { emit(doc._id, null) }"""
484 reduce_js = """\
485@@ -242,7 +247,8 @@
486
487 self.database.report_changes(rep) # Make sure nothing horks.
488
489- count = self.database.report_changes(lambda **kw: self.fail()) # Too soon to try again.
490+ # Too soon to try again.
491+ count = self.database.report_changes(lambda **kw: self.fail())
492 self.assertEqual(0, count)
493
494 def test_report_changes_exceptions(self):
495@@ -250,48 +256,66 @@
496 self.failUnless("changes" in kwargs)
497 self.failUnless("id" in kwargs)
498
499- self.database.report_changes(rep) # Consume pending.
500+ # Consume pending.
501+ self.database.report_changes(rep)
502 self.database._changes_last_used = 0
503
504- saved_time = self.database._changes_last_used # Store time.
505- saved_position = self.database._changes_since # Store position.
506- self.test_put_record() # Queue new changes. This is 1 event!
507+ # Store time.
508+ saved_time = self.database._changes_last_used
509+ # Store position.
510+ saved_position = self.database._changes_since
511+ # Queue new changes. This is 1 event!
512+ self.test_put_record()
513
514 # Exceptions in our callbacks do not consume an event.
515- self.assertRaises(ZeroDivisionError, self.database.report_changes, lambda **kw: 1/0)
516+ self.assertRaises(
517+ ZeroDivisionError, self.database.report_changes, lambda **kw: 1/0)
518
519- self.assertEqual(saved_position, self.database._changes_since) # Ensure pos'n is same.
520- self.assertEqual(saved_time, self.database._changes_last_used) # Ensure time is same.
521+ # Ensure pos'n is same.
522+ self.assertEqual(saved_position, self.database._changes_since)
523+ # Ensure time is same.
524+ self.assertEqual(saved_time, self.database._changes_last_used)
525
526 # Next time we run, we get the same event again.
527- count = self.database.report_changes(rep) # Consume queued changes.
528- self.assertEquals(1, count) # Ensure position different.
529- self.assertEqual(saved_position + 1, self.database._changes_since) # Ensure position different.
530+ # Consume queued changes.
531+ count = self.database.report_changes(rep)
532+ # Ensure position different.
533+ self.assertEquals(1, count)
534+ # Ensure position different.
535+ self.assertEqual(saved_position + 1, self.database._changes_since)
536
537 def test_report_changes_all_ops_give_known_keys(self):
538 def rep(**kwargs):
539 self.failUnless("changes" in kwargs)
540 self.failUnless("id" in kwargs)
541
542- self.database._changes_last_used = 0 # Permit immediate run.
543- count = self.database.report_changes(rep) # Test expected kw args.
544+ # Permit immediate run.
545+ self.database._changes_last_used = 0
546+ # Test expected kw args.
547+ self.database.report_changes(rep)
548
549 def test_report_changes_nochanges(self):
550 def rep(**kwargs):
551 self.failUnless("changes" in kwargs)
552 self.failUnless("id" in kwargs)
553
554- count = self.database.report_changes(rep) # Consume queue.
555- self.database._changes_last_used = 0 # Permit immediate run.
556- saved_position = self.database._changes_since # Store position.
557- count = self.database.report_changes(rep)
558- self.assertEquals(0, count) # Ensure event count is zero.
559- self.assertEqual(saved_position, self.database._changes_since) # Pos'n is same.
560+ # Consume queue.
561+ count = self.database.report_changes(rep)
562+ # Permit immediate run.
563+ self.database._changes_last_used = 0
564+ # Store position.
565+ saved_position = self.database._changes_since
566+ count = self.database.report_changes(rep)
567+ # Ensure event count is zero.
568+ self.assertEquals(0, count)
569+ # Pos'n is same.
570+ self.assertEqual(saved_position, self.database._changes_since)
571
572 def test_attachments(self):
573 content = StringIO("0123456789\n==========\n\n" * 5)
574
575- constructed_record = Record({'record_number': 0}, record_type="http://example.com/")
576+ constructed_record = Record(
577+ {'record_number': 0}, record_type="http://example.com/")
578
579 # Before anything is attached, there are no attachments.
580 self.assertEqual(constructed_record.list_attachments(), [])
581@@ -307,7 +331,7 @@
582 self.assertEqual(out_content_type, "text/plain")
583
584 # One can not put another document of the same name.
585- self.assertRaises(KeyError, constructed_record.attach, content,
586+ self.assertRaises(KeyError, constructed_record.attach, content,
587 "another document", "text/x-rst")
588
589 record_id = self.database.put_record(constructed_record)
590@@ -315,37 +339,44 @@
591
592 # We can add attachments after a document is put in the DB.
593 retrieved_record.attach(content, "Document", "text/x-rst")
594- record_id = self.database.put_record(retrieved_record) # push new version
595- retrieved_record = self.database.get_record(record_id) # get new
596+ # push new version
597+ record_id = self.database.put_record(retrieved_record)
598+ # get new
599+ retrieved_record = self.database.get_record(record_id)
600
601 # To replace, one must remove first.
602 retrieved_record.detach("another document")
603 retrieved_record.attach(content, "another document", "text/plain")
604
605- record_id = self.database.put_record(retrieved_record) # push new version
606- retrieved_record = self.database.get_record(record_id) # get new
607+ # push new version
608+ record_id = self.database.put_record(retrieved_record)
609+ # get new
610+ retrieved_record = self.database.get_record(record_id)
611
612 # We can get a list of attachments.
613- self.assertEqual(set(retrieved_record.list_attachments()),
614+ self.assertEqual(set(retrieved_record.list_attachments()),
615 set(["nu/mbe/rs", "Document", "another document"]))
616
617 # We can read from a document that we retrieved.
618- out_data, out_content_type = retrieved_record.attachment_data("nu/mbe/rs")
619+ out_data, out_content_type = retrieved_record.attachment_data(
620+ "nu/mbe/rs")
621 self.assertEqual(out_data, content.getvalue())
622 self.assertEqual(out_content_type, "text/plain")
623
624 # Asking for a named document that does not exist causes KeyError.
625- self.assertRaises(KeyError, retrieved_record.attachment_data,
626+ self.assertRaises(KeyError, retrieved_record.attachment_data,
627 "NoExist")
628- self.assertRaises(KeyError, constructed_record.attachment_data,
629+ self.assertRaises(KeyError, constructed_record.attachment_data,
630 "No Exist")
631- self.assertRaises(KeyError, retrieved_record.detach,
632+ self.assertRaises(KeyError, retrieved_record.detach,
633 "NoExist")
634
635 for i, name in enumerate(retrieved_record.list_attachments()):
636 if i != 0:
637- retrieved_record.detach(name) # delete all but one.
638- record_id = self.database.put_record(retrieved_record) # push new version.
639+ # delete all but one.
640+ retrieved_record.detach(name)
641+ # push new version.
642+ record_id = self.database.put_record(retrieved_record)
643
644 # We can remove records with attachments.
645 self.database.delete_record(record_id)
646@@ -361,12 +392,63 @@
647 list(self.database.execute_view(view1_name, design_doc))]
648 # ordinary requests are in key order
649 self.assertEqual(data, sorted(data))
650-
651+
652 # now request descending order and confirm that it *is* descending
653 descdata = [i.key for i in
654- list(self.database.execute_view(view1_name, design_doc,
655+ list(self.database.execute_view(view1_name, design_doc,
656 {"descending": True}))]
657 self.assertEqual(descdata, list(reversed(sorted(data))))
658
659 self.database.delete_view(view1_name, design_doc)
660
661+ def test_update_fields_success(self):
662+ """Test update_fields method"""
663+ dictionary = {
664+ 'record_number': 0,
665+ 'field1': 1,
666+ 'field2': 2,
667+ 'nested': {
668+ 'sub1': 's1',
669+ 'sub2': 's2'}}
670+ record = Record(dictionary, record_type="http://example.com/")
671+ record_id = self.database.put_record(record)
672+ # manipulate the database 'out of view'
673+ non_working_copy = self.database.get_record(record_id)
674+ non_working_copy['field2'] = 22
675+ non_working_copy['field3'] = 3
676+ self.database.put_record(non_working_copy)
677+ self.database.update_fields(
678+ record_id, {'field1': 11,('nested', 'sub2'): 's2-changed'},
679+ cached_record=record)
680+ working_copy = self.database.get_record(record_id)
681+ self.assertEqual(0, working_copy['record_number'])
682+ self.assertEqual(11, working_copy['field1'])
683+ self.assertEqual(22, working_copy['field2'])
684+ self.assertEqual(3, working_copy['field3'])
685+ self.assertEqual('s2-changed', working_copy['nested']['sub2'])
686+ self.assertEqual('s1', working_copy['nested']['sub1'])
687+
688+ def test_update_fields_failure(self):
689+ """Test update_fields method"""
690+ dictionary = {
691+ 'record_number': 0,
692+ 'field1': 1,
693+ 'field2': 2,
694+ 'nested': {
695+ 'sub1': 's1',
696+ 'sub2': 's2'}}
697+ record = Record(dictionary, record_type="http://example.com/")
698+ record_id = self.database.put_record(record)
699+ # manipulate the database 'out of view'
700+ non_working_copy = self.database.get_record(record_id)
701+ non_working_copy['field1'] = 22
702+ non_working_copy['field3'] = 3
703+ self.database.put_record(non_working_copy)
704+ try:
705+ self.database.update_fields(
706+ record_id, {'field1': 11, ('nested', 'sub2'): 's2-changed'},
707+ cached_record=record)
708+ # we want the exception
709+ self.fail()
710+ except FieldsConflict, e:
711+ self.assertEqual({('field1',): (22, 11)}, e.conflicts)
712
713=== modified file 'desktopcouch/tests/test_local_files.py'
714--- desktopcouch/tests/test_local_files.py 2009-11-22 02:36:00 +0000
715+++ desktopcouch/tests/test_local_files.py 2010-01-29 22:17:14 +0000
716@@ -3,14 +3,16 @@
717 import testtools
718 import desktopcouch.tests as test_environment
719 import desktopcouch
720-import os
721+
722
723 class TestLocalFiles(testtools.TestCase):
724 """Testing that local files returns the right things"""
725
726 def setUp(self):
727+ super(TestLocalFiles, self).setUp()
728 cf = test_environment.test_context.configuration
729- cf._fill_from_file(test_environment.test_context.file_ini) # Test loading from file.
730+ # Test loading from file.
731+ cf._fill_from_file(test_environment.test_context.file_ini)
732
733 def test_all_files_returned(self):
734 "Does local_files list all the files that it needs to?"
735@@ -23,12 +25,13 @@
736
737 def test_xdg_overwrite_works(self):
738 # this should really check that it's in os.environ["TMP"]
739- self.assertTrue(test_environment.test_context.file_ini.startswith("/tmp"))
740+ self.assertTrue(
741+ test_environment.test_context.file_ini.startswith("/tmp"))
742
743 def test_couch_chain_ini_files(self):
744 "Is compulsory-auth.ini picked up by the ini file finder?"
745 import desktopcouch.local_files
746- ok = [x for x
747+ ok = [x for x
748 in test_environment.test_context.couch_chain_ini_files().split()
749 if x.endswith("compulsory-auth.ini")]
750 self.assertTrue(len(ok) > 0)
751
752=== modified file 'desktopcouch/tests/test_replication.py'
753--- desktopcouch/tests/test_replication.py 2009-11-12 22:17:52 +0000
754+++ desktopcouch/tests/test_replication.py 2010-01-29 22:17:14 +0000
755@@ -11,6 +11,7 @@
756 """Testing that the database/designdoc filesystem loader works"""
757
758 def setUp(self):
759+ super(TestReplication, self).setUp()
760 self.db_apple = desktopcouch.records.server.CouchDatabase("apple",
761 create=True, ctx=test_environment.test_context)
762 banana = test_environment.create_new_test_environment()
763@@ -18,4 +19,5 @@
764 create=True, ctx=banana)
765
766 def test_creation(self):
767+ # XXX uhm?
768 pass
769
770=== modified file 'desktopcouch/tests/test_start_local_couchdb.py'
771--- desktopcouch/tests/test_start_local_couchdb.py 2009-11-12 22:05:09 +0000
772+++ desktopcouch/tests/test_start_local_couchdb.py 2010-01-29 22:17:14 +0000
773@@ -71,6 +71,7 @@
774
775 def setUp(self):
776 # create temp folder with databases and design documents in
777+ super(TestUpdateDesignDocuments, self).setUp()
778 xdg_data = os.path.split(test_environment.test_context.db_dir)[0]
779 try:
780 os.mkdir(os.path.join(xdg_data, "desktop-couch"))
781@@ -92,21 +93,33 @@
782
783 # databases that should be created
784 couchdb("cfg", create=True, ctx=test_environment.test_context)
785- couchdb("cfg_and_empty_design", create=True, ctx=test_environment.test_context)
786- couchdb("cfg_and_design_no_views", create=True, ctx=test_environment.test_context)
787- couchdb("cfg_and_design_one_view_no_map", create=True, ctx=test_environment.test_context)
788- couchdb("cfg_and_design_one_view_map_no_reduce", create=True, ctx=test_environment.test_context)
789+ couchdb(
790+ "cfg_and_empty_design", create=True,
791+ ctx=test_environment.test_context)
792+ couchdb(
793+ "cfg_and_design_no_views", create=True,
794+ ctx=test_environment.test_context)
795+ couchdb(
796+ "cfg_and_design_one_view_no_map", create=True,
797+ ctx=test_environment.test_context)
798+ couchdb(
799+ "cfg_and_design_one_view_map_no_reduce", create=True,
800+ ctx=test_environment.test_context)
801
802 dbmock1 = mocker.mock()
803 mocker.result(dbmock1)
804 dbmock1.add_view("view1", "cfg_and_design_one_view_map_no_reduce:map",
805 None, "doc1")
806- couchdb("cfg_and_design_one_view_map_reduce", create=True, ctx=test_environment.test_context)
807+ couchdb(
808+ "cfg_and_design_one_view_map_reduce", create=True,
809+ ctx=test_environment.test_context)
810 dbmock2 = mocker.mock()
811 mocker.result(dbmock2)
812 dbmock2.add_view("view1", "cfg_and_design_one_view_map_reduce:map",
813 "cfg_and_design_one_view_map_reduce:reduce", "doc1")
814- couchdb("cfg_and_design_two_views_map_reduce", create=True, ctx=test_environment.test_context)
815+ couchdb(
816+ "cfg_and_design_two_views_map_reduce", create=True,
817+ ctx=test_environment.test_context)
818 dbmock3 = mocker.mock()
819 mocker.result(dbmock3)
820 dbmock3.add_view("view1", "cfg_and_design_two_views_map_reduce:map1",

Subscribers

People subscribed via source and target branches