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