Merge lp://qastaging/~thisfred/desktopcouch/backport-update-fields into lp://qastaging/desktopcouch
- backport-update-fields
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guillermo Gonzalez | Approve | ||
Chad Miller (community) | Approve | ||
Review via email:
|
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
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Casteleijn (thisfred) wrote : | # |
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Chad Miller (cmiller) wrote : | # |
Looks good. Thanks!
review:
Approve
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guillermo Gonzalez (verterok) wrote : | # |
looks good, all tests pass
review:
Approve
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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", |
- 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