Merge lp:~jamesh/bindwood/sync-all into lp:bindwood
- sync-all
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | James Henstridge |
Approved revision: | 44 |
Merged at revision: | 38 |
Proposed branch: | lp:~jamesh/bindwood/sync-all |
Merge into: | lp:bindwood |
Diff against target: |
954 lines (+578/-58) 5 files modified
modules/sync.jsm (+142/-28) mozmill/tests/test_sync_all.js (+195/-0) mozmill/tests/test_sync_from_couch.js (+144/-30) mozmill/tests/test_sync_guid_map.js (+36/-0) mozmill/tests/test_sync_to_couch.js (+61/-0) |
To merge this branch: | bzr merge lp:~jamesh/bindwood/sync-all |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Manuel de la Peña (community) | Approve | ||
Eric Casteleijn (community) | Approve | ||
Review via email: mp+51129@code.launchpad.net |
Commit message
Tie together the new synchroniser code to provide an interface for doing regular two-way synchronisation.
Description of the change
Pull together the recent work to perform a two way sync.
The basic algorithm is as follows:
* pull changes from CouchDB, and create or update local items. If we're updating items, discard the change if the local item's modification time is greater than or equal to the CouchDB record.
* Determine the list of local items that have been changed (and clear the change list in the observer in the process).
* For each changed local item, push changes back to CouchDB. Don't actually save to couch for null-changes (as may be the case for items changed in the first step).
In addition to this, there is some first time behaviour:
* If we've never synced with Couch, treat every record as changed. We'll keep track of this over multiple runs through a preference as current Bindwood does.
* On start up, treat all local items as changed.
- 42. By James Henstridge
-
Add missing onItemRemoved callback to observer.
- 43. By James Henstridge
-
Add a test demonstrating problem when importing a bookmark from CouchDB.
We create the bookmark as normal, and then annotate it with the CouchDB
record ID. Unfortunately, the bookmarks observer picks up the new
bookmark and assigns an ID too. Normally this wouldn't be a problem,
but our GUID->ID cache keeps the bogus GUID alive causing us to export
the bookmark twice. - 44. By James Henstridge
-
When setting a GUID on an item, first check if it has already been annotated.
If there is an existing annotation, clear it from our GUID->ID cache
since it will soon be out of date.
James Henstridge (jamesh) wrote : | # |
One of the problems with relying on timestamps is that it opens us up to problems when there is clock skew between two systems.
If the user modifies the same bookmark on two systems at once, I think it is fine if one of the edits wins out completely, rather than e.g. picking the title from one and the url from the other.
With the new schema, we don't rely on the children property on folders to locate children (only to order them), so having one winner is acceptable in the case where two systems add a bookmark to the same folder too.
Combine this with the fact that we're using the changes feed to decide which documents to sync, and I think we should be fine.
Manuel de la Peña (mandel) wrote : | # |
Tests pass and code looks ok
Preview Diff
1 | === modified file 'modules/sync.jsm' |
2 | --- modules/sync.jsm 2011-02-21 15:04:37 +0000 |
3 | +++ modules/sync.jsm 2011-02-25 07:36:30 +0000 |
4 | @@ -16,6 +16,13 @@ |
5 | |
6 | const EXPORTED_SYMBOLS = ["Synchroniser", "BookmarksObserver"]; |
7 | |
8 | +const Cc = Components.classes; |
9 | +const Ci = Components.interfaces; |
10 | +const Cr = Components.results; |
11 | +const Cu = Components.utils; |
12 | + |
13 | +Cu.import("resource://bindwood/logging.jsm"); |
14 | + |
15 | const GUID_ANNOTATION = "bindwood/uuid"; |
16 | const PARENT_ANNOTATION = "bindwood/parent"; |
17 | |
18 | @@ -26,10 +33,6 @@ |
19 | const TYPE_FEED = RECORD_PREFIX + "feed"; |
20 | const TYPE_SEPARATOR = RECORD_PREFIX + "separator"; |
21 | |
22 | -const Cc = Components.classes; |
23 | -const Ci = Components.interfaces; |
24 | -const Cr = Components.results; |
25 | - |
26 | var bookmarksService = Cc["@mozilla.org/browser/nav-bookmarks-service;1"] |
27 | .getService(Ci.nsINavBookmarksService); |
28 | var livemarkService = Cc["@mozilla.org/browser/livemark-service;2"] |
29 | @@ -50,16 +53,34 @@ |
30 | this.last_seq = 0; |
31 | this.guid_item_map = {}; |
32 | this.folders_to_reorder = {}; |
33 | + this.first_push = true; |
34 | + this.observer = null; |
35 | } |
36 | |
37 | Synchroniser.prototype = { |
38 | + init: function() { |
39 | + Log.debug("Initialising synchroniser."); |
40 | + this.ensureCouchViews(); |
41 | + this.first_push = true; |
42 | + this.observer = new BookmarksObserver(this); |
43 | + bookmarksService.addObserver(this.observer, false); |
44 | + }, |
45 | + |
46 | + uninit: function() { |
47 | + Log.debug("Uninitialising synchroniser."); |
48 | + if (this.observer) { |
49 | + bookmarksService.removeObserver(this.observer); |
50 | + this.observer = null; |
51 | + } |
52 | + }, |
53 | + |
54 | ensureCouchViews: function() { |
55 | var design_doc_id = "_design/bindwood", doc; |
56 | |
57 | try { |
58 | doc = this.couch.open(design_doc_id); |
59 | } catch (e) { |
60 | - dump("Problem retrieving view view:" + e + "\n"); |
61 | + Log.exception("Problem retrieving view view"); |
62 | return; |
63 | } |
64 | if (!doc) { |
65 | @@ -87,14 +108,26 @@ |
66 | " req.query.profile);\n" + |
67 | " } catch (e) { /* ignore error */ }\n" + |
68 | "}") |
69 | + // XXX: should we check to see whether we've changed the |
70 | + // document before saving it? |
71 | try { |
72 | this.couch.save(doc); |
73 | } catch (e) { |
74 | - dump("Problem saving view:" + e + "\n"); |
75 | + Log.exception("Problem saving view"); |
76 | } |
77 | }, |
78 | |
79 | set_guid: function(item_id, guid) { |
80 | + // If the item has already been annotated with a GUID, clear |
81 | + // the old GUID mapping from our cache. |
82 | + try { |
83 | + var old_guid = annotationService.getItemAnnotation( |
84 | + item_id, GUID_ANNOTATION); |
85 | + delete this.guid_item_map[old_guid]; |
86 | + } catch (e) { |
87 | + // Ignore errors. |
88 | + } |
89 | + |
90 | if (!guid) { |
91 | guid = uuidService.generateUUID().toString(); |
92 | } |
93 | @@ -108,7 +141,9 @@ |
94 | |
95 | guid_from_id: function(item_id) { |
96 | // Handle special items. |
97 | - if (item_id == bookmarksService.toolbarFolder) |
98 | + if (item_id == bookmarksService.placesRoot) |
99 | + return "root_" + this.profile; |
100 | + else if (item_id == bookmarksService.toolbarFolder) |
101 | return "toolbar_" + this.profile; |
102 | else if (item_id == bookmarksService.bookmarksMenuFolder) |
103 | return "menu_" + this.profile; |
104 | @@ -132,7 +167,9 @@ |
105 | |
106 | guid_to_id: function(guid) { |
107 | // Handle special items. |
108 | - if (guid == "toolbar_" + this.profile) |
109 | + if (guid == "root_" + this.profile) |
110 | + return bookmarksService.placesRoot; |
111 | + else if (guid == "toolbar_" + this.profile) |
112 | return bookmarksService.toolbarFolder; |
113 | else if (guid == "menu_" + this.profile) |
114 | return bookmarksService.bookmarksMenuFolder; |
115 | @@ -159,15 +196,71 @@ |
116 | return null; |
117 | }, |
118 | |
119 | + _get_folder_root: function(folder_id) { |
120 | + var query = historyService.getNewQuery(); |
121 | + var options = historyService.getNewQueryOptions(); |
122 | + query.setFolders([folder_id], 1); |
123 | + return historyService.executeQuery(query, options).root; |
124 | + }, |
125 | + |
126 | + get_folder_children: function(parent_id) { |
127 | + if (parent_id == bookmarksService.placesRoot) { |
128 | + // Special case the root folder, returning the three trees |
129 | + // we're interested in. |
130 | + return [ |
131 | + bookmarksService.toolbarFolder, |
132 | + bookmarksService.bookmarksMenuFolder, |
133 | + bookmarksService.unfiledBookmarksFolder]; |
134 | + } |
135 | + var query = historyService.getNewQuery(); |
136 | + var options = historyService.getNewQueryOptions(); |
137 | + query.setFolders([parent_id], 1); |
138 | + var root = this._get_folder_root(parent_id); |
139 | + |
140 | + var children = []; |
141 | + root.containerOpen = true; |
142 | + for (var i = 0; i < root.childCount; i++) { |
143 | + var child = root.getChild(i); |
144 | + children.push(child.itemId); |
145 | + } |
146 | + return children; |
147 | + }, |
148 | + |
149 | + get_all_bookmarks: function() { |
150 | + var bookmarks = {} |
151 | + bookmarks[this.guid_from_id(bookmarksService.placesRoot)] = true; |
152 | + var sync = this; |
153 | + var add_bookmarks = function(node) { |
154 | + bookmarks[sync.guid_from_id(node.itemId)] = true; |
155 | + |
156 | + if (node.type != node.RESULT_TYPE_FOLDER || |
157 | + livemarkService.isLivemark(node.itemId)) |
158 | + return; |
159 | + node.QueryInterface(Ci.nsINavHistoryQueryResultNode); |
160 | + node.containerOpen = true; |
161 | + for (var i = 0; i < node.childCount; i++) { |
162 | + add_bookmarks(node.getChild(i)); |
163 | + } |
164 | + }; |
165 | + |
166 | + add_bookmarks(this._get_folder_root( |
167 | + bookmarksService.toolbarFolder)); |
168 | + add_bookmarks(this._get_folder_root( |
169 | + bookmarksService.bookmarksMenuFolder)); |
170 | + add_bookmarks(this._get_folder_root( |
171 | + bookmarksService.unfiledBookmarksFolder)); |
172 | + |
173 | + return bookmarks; |
174 | + }, |
175 | + |
176 | sync: function() { |
177 | - //this.recordChangedIds(); |
178 | + Log.debug("Starting two-way synchronisation."); |
179 | this.pullChanges(); |
180 | - //this.pushChanges(); |
181 | + this.pushChanges(); |
182 | //this.cleanup(); |
183 | }, |
184 | |
185 | pullChanges: function() { |
186 | - var new_last_seq; |
187 | var info = this.couch.info(); |
188 | if (this.last_seq <= info.purge_seq) { |
189 | // We haven't pulled any records before, or the database |
190 | @@ -202,10 +295,15 @@ |
191 | this.processRecord(row.doc); |
192 | } |
193 | } |
194 | + this.reorder_children(); |
195 | }, |
196 | |
197 | processRecord: function (doc) { |
198 | var item_id = this.guid_to_id(doc._id); |
199 | + if (item_id == bookmarksService.placesRoot) { |
200 | + Log.debug("Ignoring change to places root.\n"); |
201 | + return; |
202 | + } |
203 | // XXX: We should perform duplicate detection here if item_id |
204 | // is null. |
205 | if (item_id != null) { |
206 | @@ -266,12 +364,23 @@ |
207 | }, |
208 | |
209 | updateItem: function(item_id, doc) { |
210 | + Log.debug("Updating local item " + item_id + " from document " + |
211 | + doc._id); |
212 | if (doc._deleted) { |
213 | // Document has been deleted in Couch: propagate to local. |
214 | + Log.debug("Removing local item."); |
215 | bookmarksService.removeItem(item_id); |
216 | return; |
217 | } |
218 | |
219 | + // If the local item is newer than the version in Couch, |
220 | + // ignore. |
221 | + if (bookmarksService.getItemLastModified(item_id) >= |
222 | + doc.application_annotations.Firefox.last_modified) { |
223 | + Log.debug("Local item is newer than CouchDB version."); |
224 | + return; |
225 | + } |
226 | + |
227 | var parent_id = this.guid_to_id(doc.parent_guid); |
228 | if (parent_id == null) { |
229 | this.set_orphan(item_id, doc.parent_guid); |
230 | @@ -333,21 +442,6 @@ |
231 | } |
232 | }, |
233 | |
234 | - get_folder_children: function(parent_id) { |
235 | - var query = historyService.getNewQuery(); |
236 | - var options = historyService.getNewQueryOptions(); |
237 | - query.setFolders([parent_id], 1); |
238 | - var root = historyService.executeQuery(query, options).root; |
239 | - |
240 | - var children = []; |
241 | - root.containerOpen = true; |
242 | - for (var i = 0; i < root.childCount; i++) { |
243 | - var child = root.getChild(i); |
244 | - children.push(child.itemId); |
245 | - } |
246 | - return children; |
247 | - }, |
248 | - |
249 | reorder_children: function() { |
250 | for (var parent_guid in this.folders_to_reorder) { |
251 | var parent_id = this.guid_to_id(parent_guid); |
252 | @@ -377,8 +471,23 @@ |
253 | this.folders_to_reorder = {} |
254 | }, |
255 | |
256 | + pushChanges: function() { |
257 | + Log.debug("Pushing changes. first_push = " + this.first_push); |
258 | + var changed_guids = this.observer.clear_changes(); |
259 | + if (this.first_push) { |
260 | + // First time: process all bookmarks. |
261 | + changed_guids = this.get_all_bookmarks(); |
262 | + this.first_push = false; |
263 | + } |
264 | + for (var item_guid in changed_guids) { |
265 | + this.exportItem(item_guid); |
266 | + } |
267 | + }, |
268 | + |
269 | exportItem: function(item_guid) { |
270 | var item_id = this.guid_to_id(item_guid); |
271 | + Log.debug("Exporting item " + item_guid + |
272 | + " (local ID " + item_id + ")"); |
273 | var item_type = null, deleted = false; |
274 | if (item_id) { |
275 | // Get the item type, which also checks whether the item exists. |
276 | @@ -457,11 +566,13 @@ |
277 | _setattr(doc, "record_type", TYPE_SEPARATOR); |
278 | break; |
279 | default: |
280 | - dump("Can not handle item " + item_id + " of type " + |
281 | - item_type + "\n"); |
282 | + Log.error("Can not handle item " + item_id + " of type " + |
283 | + item_type); |
284 | return; |
285 | } |
286 | if (changed) { |
287 | + doc.application_annotations.Firefox.last_modified = ( |
288 | + bookmarksService.getItemLastModified(item_id)); |
289 | this.couch.save(doc); |
290 | } |
291 | }, |
292 | @@ -503,7 +614,9 @@ |
293 | }, |
294 | |
295 | clear_changes: function() { |
296 | + var changes = this.changed_guids; |
297 | this.changed_guids = {}; |
298 | + return changes; |
299 | }, |
300 | |
301 | onItemAdded: function(item_id, folder_id, child_index) { |
302 | @@ -555,5 +668,6 @@ |
303 | // Currently unhandled |
304 | onBeginUpdateBatch: function() {}, |
305 | onEndUpdateBatch: function() {}, |
306 | + onItemRemoved: function(item_id, folder_id, index) {}, |
307 | onItemVisited: function(aBookmarkId, aVisitID, time) {}, |
308 | }; |
309 | |
310 | === added file 'mozmill/tests/test_sync_all.js' |
311 | --- mozmill/tests/test_sync_all.js 1970-01-01 00:00:00 +0000 |
312 | +++ mozmill/tests/test_sync_all.js 2011-02-25 07:36:30 +0000 |
313 | @@ -0,0 +1,195 @@ |
314 | +var bm = require("../shared-modules/bookmarks"); |
315 | + |
316 | +const TIMEOUT = 5000; |
317 | + |
318 | +const LOCAL_TEST_FOLDER = collector.addHttpResource('../test-files/'); |
319 | +const LOCAL_TEST_PAGE = LOCAL_TEST_FOLDER + 'test.html'; |
320 | +const LOCAL_TEST_FEED = LOCAL_TEST_FOLDER + 'feed.atom'; |
321 | + |
322 | +var setupModule = function(module) { |
323 | + module.controller = mozmill.getBrowserController(); |
324 | + module.jum = {}; |
325 | + module.desktopcouch = {}; |
326 | + module.sync = {}; |
327 | + Cu.import("resource://mozmill/modules/jum.js", module.jum); |
328 | + Cu.import("resource://bindwood/desktopcouch.jsm", module.desktopcouch); |
329 | + Cu.import("resource://bindwood/sync.jsm", module.sync); |
330 | + bm.clearBookmarks(); |
331 | + module.couch = null; |
332 | + module.synchroniser = null; |
333 | +}; |
334 | + |
335 | + |
336 | +var setupTest = function(test) { |
337 | + var done = false; |
338 | + desktopcouch.connect_desktopcouch("test_bookmarks", function(db) { |
339 | + couch = db; |
340 | + done = true; |
341 | + }, function (message) {}); |
342 | + controller.waitFor( |
343 | + function() { return done; }, "Could not connect to CouchDB", TIMEOUT); |
344 | + jum.assertNotEquals(couch, null); |
345 | + |
346 | + try { |
347 | + couch.createDb(); |
348 | + } catch (e) { |
349 | + if (e.error != 'file_exists') |
350 | + throw(e); |
351 | + } |
352 | +}; |
353 | + |
354 | + |
355 | +var teardownTest = function(test) { |
356 | + if (synchroniser) { |
357 | + synchroniser.uninit(); |
358 | + synchroniser = null; |
359 | + } |
360 | + bm.clearBookmarks(); |
361 | + couch.deleteDb(); |
362 | +}; |
363 | + |
364 | + |
365 | +var test_sync_local = function() { |
366 | + var item_id = bm.bookmarksService.insertBookmark( |
367 | + bm.bookmarksService.toolbarFolder, bm.createURI(LOCAL_TEST_PAGE), |
368 | + bm.bookmarksService.DEFAULT_INDEX, "Bookmark title"); |
369 | + |
370 | + // Perform synchronisation. |
371 | + synchroniser = new sync.Synchroniser(couch, "profile_name"); |
372 | + synchroniser.init(); |
373 | + synchroniser.sync(); |
374 | + |
375 | + // The root and toolbar have been synchronised. |
376 | + var root_guid = synchroniser.guid_from_id( |
377 | + bm.bookmarksService.placesRoot); |
378 | + var toolbar_guid = synchroniser.guid_from_id( |
379 | + bm.bookmarksService.toolbarFolder) |
380 | + var doc = couch.open(root_guid); |
381 | + jum.assertNotNull(doc); |
382 | + var old_root_rev = doc._rev; |
383 | + |
384 | + doc = couch.open(toolbar_guid); |
385 | + jum.assertNotNull(doc); |
386 | + var old_toolbar_rev = doc._rev; |
387 | + |
388 | + // Our bookmark has also been synchronised. |
389 | + var item_guid = synchroniser.guid_from_id(item_id); |
390 | + doc = couch.open(item_guid); |
391 | + jum.assertNotNull(doc); |
392 | + jum.assertEquals(doc.title, "Bookmark title"); |
393 | + |
394 | + // Making a local change to the bookmark. |
395 | + bm.bookmarksService.setItemTitle(item_id, "New title"); |
396 | + synchroniser.sync(); |
397 | + |
398 | + // The parent folders should be unchanged. |
399 | + doc = couch.open(root_guid); |
400 | + jum.assertEquals(doc._rev, old_root_rev); |
401 | + doc = couch.open(toolbar_guid); |
402 | + jum.assertEquals(doc._rev, old_toolbar_rev); |
403 | + |
404 | + // Our bookmark should have been updated. |
405 | + doc = couch.open(item_guid); |
406 | + jum.assertEquals(doc.title, "New title"); |
407 | + var old_item_rev = doc._rev; |
408 | + |
409 | + // Performing another synchronisation should leave the item unchanged. |
410 | + synchroniser.sync(); |
411 | + doc = couch.open(item_guid); |
412 | + jum.assertEquals(doc._rev, old_item_rev); |
413 | +}; |
414 | + |
415 | +var test_sync_remote = function() { |
416 | + var item_guid = '12345'; |
417 | + var doc = { |
418 | + _id: item_guid, |
419 | + record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark', |
420 | + parent_guid: 'toolbar_profile_name', |
421 | + title: 'Bookmark title', |
422 | + uri: LOCAL_TEST_PAGE, |
423 | + application_annotations: { |
424 | + Firefox: { |
425 | + profile: 'profile_name', |
426 | + last_modified: 1, |
427 | + } |
428 | + } |
429 | + }; |
430 | + couch.save(doc); |
431 | + var old_item_rev = doc._rev; |
432 | + |
433 | + // Perform synchronisation. |
434 | + synchroniser = new sync.Synchroniser(couch, "profile_name"); |
435 | + synchroniser.init(); |
436 | + synchroniser.sync(); |
437 | + |
438 | + // Item has been created locally. |
439 | + var item_id = synchroniser.guid_to_id(item_guid); |
440 | + jum.assertEquals( |
441 | + bm.bookmarksService.getItemTitle(item_id), 'Bookmark title'); |
442 | + |
443 | + // CouchDB record has not been modified. |
444 | + doc = couch.open(item_guid); |
445 | + jum.assertEquals(doc._rev, old_item_rev); |
446 | + |
447 | + // Modify the item and save it back. |
448 | + doc.title = 'New title'; |
449 | + doc.application_annotations.Firefox.last_modified = ( |
450 | + bm.bookmarksService.getItemLastModified(item_id) + 1); |
451 | + couch.save(doc); |
452 | + old_item_rev = doc._rev; |
453 | + |
454 | + // After synchronisation, the local bookmark has changed. |
455 | + synchroniser.sync(); |
456 | + jum.assertEquals( |
457 | + bm.bookmarksService.getItemTitle(item_id), 'New title'); |
458 | + |
459 | + // Again, the CouchDB record hasn't been modified by the sync. |
460 | + doc = couch.open(item_guid); |
461 | + jum.assertEquals(doc._rev, old_item_rev); |
462 | +}; |
463 | + |
464 | +function get_bookmark_ids(profile) { |
465 | + var result = couch.view("bindwood/bookmarks", {key: profile}); |
466 | + return [row.value for each (row in result.rows)]; |
467 | +} |
468 | + |
469 | +var test_sync_remote_add_no_duplicate = function() { |
470 | + // Perform synchronisation. |
471 | + synchroniser = new sync.Synchroniser(couch, "profile_name"); |
472 | + synchroniser.init(); |
473 | + synchroniser.sync(); |
474 | + |
475 | + var expected_bookmarks = [ |
476 | + synchroniser.guid_from_id(bm.bookmarksService.placesRoot), |
477 | + synchroniser.guid_from_id(bm.bookmarksService.toolbarFolder), |
478 | + synchroniser.guid_from_id(bm.bookmarksService.bookmarksMenuFolder), |
479 | + synchroniser.guid_from_id(bm.bookmarksService.unfiledBookmarksFolder), |
480 | + ]; |
481 | + var actual_bookmarks = get_bookmark_ids("profile_name"); |
482 | + jum.assertEquals(expected_bookmarks.sort().join("\n"), |
483 | + actual_bookmarks.sort().join("\n")) |
484 | + |
485 | + var item_guid = '12345'; |
486 | + var doc = { |
487 | + _id: item_guid, |
488 | + record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark', |
489 | + parent_guid: 'toolbar_profile_name', |
490 | + title: 'Bookmark title', |
491 | + uri: LOCAL_TEST_PAGE, |
492 | + application_annotations: { |
493 | + Firefox: { |
494 | + profile: 'profile_name', |
495 | + last_modified: 1, |
496 | + } |
497 | + } |
498 | + }; |
499 | + couch.save(doc); |
500 | + |
501 | + // After synchronisation, we expect to only find the one added |
502 | + // bookmark. |
503 | + synchroniser.sync(); |
504 | + expected_bookmarks.push(item_guid); |
505 | + var actual_bookmarks = get_bookmark_ids("profile_name"); |
506 | + jum.assertEquals(expected_bookmarks.sort().join("\n"), |
507 | + actual_bookmarks.sort().join("\n")) |
508 | +}; |
509 | |
510 | === modified file 'mozmill/tests/test_sync_from_couch.js' |
511 | --- mozmill/tests/test_sync_from_couch.js 2011-02-22 11:02:10 +0000 |
512 | +++ mozmill/tests/test_sync_from_couch.js 2011-02-25 07:36:30 +0000 |
513 | @@ -32,6 +32,12 @@ |
514 | parent_guid: 'toolbar_profile_name', |
515 | title: 'Bookmark title', |
516 | uri: LOCAL_TEST_PAGE, |
517 | + application_annotations: { |
518 | + Firefox: { |
519 | + profile: 'profile_name', |
520 | + last_modified: 1, |
521 | + } |
522 | + } |
523 | }); |
524 | var bookmark_id = synchroniser.guid_to_id('12345'); |
525 | jum.assertEquals(bm.bookmarksService.getItemType(bookmark_id), |
526 | @@ -51,13 +57,21 @@ |
527 | parent_guid: 'toolbar_profile_name', |
528 | title: 'Bookmark title', |
529 | uri: LOCAL_TEST_PAGE, |
530 | + application_annotations: { |
531 | + Firefox: { |
532 | + profile: 'profile_name', |
533 | + last_modified: 1, |
534 | + } |
535 | + } |
536 | }; |
537 | synchroniser.processRecord(doc); |
538 | + var bookmark_id = synchroniser.guid_to_id('12345'); |
539 | doc.title = 'New title'; |
540 | doc.uri = LOCAL_TEST_PAGE + "#updated"; |
541 | + doc.application_annotations.Firefox.last_modified = ( |
542 | + bm.bookmarksService.getItemLastModified(bookmark_id) + 1); |
543 | synchroniser.processRecord(doc); |
544 | |
545 | - var bookmark_id = synchroniser.guid_to_id('12345'); |
546 | jum.assertEquals(bm.bookmarksService.getItemTitle(bookmark_id), |
547 | 'New title'); |
548 | jum.assertEquals(bm.bookmarksService.getBookmarkURI(bookmark_id).spec, |
549 | @@ -71,6 +85,12 @@ |
550 | parent_guid: 'toolbar_profile_name', |
551 | title: 'Folder', |
552 | children: [], |
553 | + application_annotations: { |
554 | + Firefox: { |
555 | + profile: 'profile_name', |
556 | + last_modified: 1, |
557 | + } |
558 | + } |
559 | }); |
560 | var bookmark_id = synchroniser.guid_to_id('12345'); |
561 | jum.assertEquals(bm.bookmarksService.getItemType(bookmark_id), |
562 | @@ -90,12 +110,20 @@ |
563 | parent_guid: 'toolbar_profile_name', |
564 | title: 'Folder', |
565 | children: [], |
566 | + application_annotations: { |
567 | + Firefox: { |
568 | + profile: 'profile_name', |
569 | + last_modified: 1, |
570 | + } |
571 | + } |
572 | }; |
573 | synchroniser.processRecord(doc); |
574 | + var bookmark_id = synchroniser.guid_to_id('12345'); |
575 | doc.title = 'Updated folder'; |
576 | + doc.application_annotations.Firefox.last_modified = ( |
577 | + bm.bookmarksService.getItemLastModified(bookmark_id) + 1); |
578 | synchroniser.processRecord(doc); |
579 | |
580 | - var bookmark_id = synchroniser.guid_to_id('12345'); |
581 | jum.assertEquals(bm.bookmarksService.getItemTitle(bookmark_id), |
582 | 'Updated folder'); |
583 | // Reordering of folder's children has been scheduled. |
584 | @@ -110,6 +138,12 @@ |
585 | title: 'Livemark', |
586 | site_uri: "http://www.example.com/", |
587 | feed_uri: LOCAL_TEST_FEED, |
588 | + application_annotations: { |
589 | + Firefox: { |
590 | + profile: 'profile_name', |
591 | + last_modified: 1, |
592 | + } |
593 | + } |
594 | }); |
595 | var bookmark_id = synchroniser.guid_to_id('12345'); |
596 | jum.assertTrue(bm.livemarkService.isLivemark(bookmark_id)) |
597 | @@ -131,13 +165,21 @@ |
598 | title: 'Livemark', |
599 | site_uri: "http://www.example.com/", |
600 | feed_uri: LOCAL_TEST_FEED, |
601 | + application_annotations: { |
602 | + Firefox: { |
603 | + profile: 'profile_name', |
604 | + last_modified: 1, |
605 | + } |
606 | + } |
607 | }; |
608 | synchroniser.processRecord(doc); |
609 | + var bookmark_id = synchroniser.guid_to_id('12345'); |
610 | doc.title = 'New title'; |
611 | doc.feed_uri = LOCAL_TEST_FEED + "#updated"; |
612 | + doc.application_annotations.Firefox.last_modified = ( |
613 | + bm.bookmarksService.getItemLastModified(bookmark_id) + 1); |
614 | synchroniser.processRecord(doc); |
615 | |
616 | - var bookmark_id = synchroniser.guid_to_id('12345'); |
617 | jum.assertEquals(bm.bookmarksService.getItemTitle(bookmark_id), |
618 | 'New title'); |
619 | jum.assertEquals(bm.livemarkService.getFeedURI(bookmark_id).spec, |
620 | @@ -149,6 +191,12 @@ |
621 | _id: '12345', |
622 | record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/separator', |
623 | parent_guid: 'toolbar_profile_name', |
624 | + application_annotations: { |
625 | + Firefox: { |
626 | + profile: 'profile_name', |
627 | + last_modified: 1, |
628 | + } |
629 | + } |
630 | }); |
631 | var bookmark_id = synchroniser.guid_to_id('12345'); |
632 | jum.assertEquals(bm.bookmarksService.getItemType(bookmark_id), |
633 | @@ -162,12 +210,21 @@ |
634 | _id: '12345', |
635 | record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/separator', |
636 | parent_guid: 'toolbar_profile_name', |
637 | + application_annotations: { |
638 | + Firefox: { |
639 | + profile: 'profile_name', |
640 | + last_modified: 1, |
641 | + } |
642 | + } |
643 | }; |
644 | synchroniser.processRecord(doc); |
645 | + var bookmark_id = synchroniser.guid_to_id('12345'); |
646 | // No properties to change on the separator, but verify that the |
647 | // update code path runs anyway. |
648 | + doc.application_annotations.Firefox.last_modified = ( |
649 | + bm.bookmarksService.getItemLastModified(bookmark_id) + 1); |
650 | synchroniser.processRecord(doc); |
651 | - var bookmark_id = synchroniser.guid_to_id('12345'); |
652 | + |
653 | jum.assertEquals(bm.bookmarksService.getItemType(bookmark_id), |
654 | bm.bookmarksService.TYPE_SEPARATOR); |
655 | }; |
656 | @@ -179,6 +236,12 @@ |
657 | parent_guid: 'toolbar_profile_name', |
658 | title: 'Bookmark title', |
659 | uri: LOCAL_TEST_PAGE, |
660 | + application_annotations: { |
661 | + Firefox: { |
662 | + profile: 'profile_name', |
663 | + last_modified: 1, |
664 | + } |
665 | + } |
666 | }; |
667 | synchroniser.processRecord(doc); |
668 | var bookmark_id = synchroniser.guid_to_id('12345'); |
669 | @@ -187,7 +250,10 @@ |
670 | |
671 | // Move to a new parent folder. |
672 | doc.parent_guid = "menu_profile_name"; |
673 | + doc.application_annotations.Firefox.last_modified = ( |
674 | + bm.bookmarksService.getItemLastModified(bookmark_id) + 1); |
675 | synchroniser.processRecord(doc); |
676 | + |
677 | jum.assertEquals(bm.bookmarksService.getFolderIdForItem(bookmark_id), |
678 | bm.bookmarksService.bookmarksMenuFolder); |
679 | }; |
680 | @@ -199,6 +265,12 @@ |
681 | parent_guid: 'toolbar_profile_name', |
682 | title: 'Folder', |
683 | children: ['67890'], |
684 | + application_annotations: { |
685 | + Firefox: { |
686 | + profile: 'profile_name', |
687 | + last_modified: 1, |
688 | + } |
689 | + } |
690 | }); |
691 | var parent_id = synchroniser.guid_to_id('12345'); |
692 | jum.assertEquals(bm.bookmarksService.getFolderIdForItem(parent_id), |
693 | @@ -212,6 +284,12 @@ |
694 | parent_guid: '12345', |
695 | title: 'Bookmark title', |
696 | uri: LOCAL_TEST_PAGE, |
697 | + application_annotations: { |
698 | + Firefox: { |
699 | + profile: 'profile_name', |
700 | + last_modified: 1, |
701 | + } |
702 | + } |
703 | }); |
704 | var child_id = synchroniser.guid_to_id('67890'); |
705 | jum.assertEquals(bm.bookmarksService.getFolderIdForItem(child_id), |
706 | @@ -225,6 +303,12 @@ |
707 | parent_guid: '12345', |
708 | title: 'Bookmark title', |
709 | uri: LOCAL_TEST_PAGE, |
710 | + application_annotations: { |
711 | + Firefox: { |
712 | + profile: 'profile_name', |
713 | + last_modified: 1, |
714 | + } |
715 | + } |
716 | }); |
717 | var child_id = synchroniser.guid_to_id('67890'); |
718 | // The bookmark is temporarily stashed in unfiled bookmarks. |
719 | @@ -237,6 +321,12 @@ |
720 | parent_guid: 'toolbar_profile_name', |
721 | title: 'Folder', |
722 | children: ['67890'], |
723 | + application_annotations: { |
724 | + Firefox: { |
725 | + profile: 'profile_name', |
726 | + last_modified: 1, |
727 | + } |
728 | + } |
729 | }); |
730 | var parent_id = synchroniser.guid_to_id('12345'); |
731 | jum.assertEquals(bm.bookmarksService.getFolderIdForItem(parent_id), |
732 | @@ -250,12 +340,18 @@ |
733 | |
734 | var test_reorder_children = function() { |
735 | synchroniser.processRecord({ |
736 | - _id: 'parent', |
737 | - record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder', |
738 | - parent_guid: 'toolbar_profile_name', |
739 | - title: 'Folder', |
740 | - children: ['child1', 'child2', 'child3', 'missing_child'], |
741 | - }); |
742 | + _id: 'parent', |
743 | + record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder', |
744 | + parent_guid: 'toolbar_profile_name', |
745 | + title: 'Folder', |
746 | + children: ['child1', 'child2', 'child3', 'missing_child'], |
747 | + application_annotations: { |
748 | + Firefox: { |
749 | + profile: 'profile_name', |
750 | + last_modified: 1, |
751 | + } |
752 | + } |
753 | + }); |
754 | var parent_id = synchroniser.guid_to_id('parent'); |
755 | // Add an untracked bookmark to the folder. |
756 | var untracked_child = bm.bookmarksService.insertBookmark( |
757 | @@ -265,26 +361,44 @@ |
758 | |
759 | // Add expected children. |
760 | synchroniser.processRecord({ |
761 | - _id: 'child1', |
762 | - record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark', |
763 | - parent_guid: 'parent', |
764 | - title: 'Child 1', |
765 | - uri: LOCAL_TEST_PAGE + "#child1", |
766 | - }); |
767 | - synchroniser.processRecord({ |
768 | - _id: 'child3', |
769 | - record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark', |
770 | - parent_guid: 'parent', |
771 | - title: 'Child 3', |
772 | - uri: LOCAL_TEST_PAGE + "#child1", |
773 | - }); |
774 | - synchroniser.processRecord({ |
775 | - _id: 'child2', |
776 | - record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark', |
777 | - parent_guid: 'parent', |
778 | - title: 'Child 2', |
779 | - uri: LOCAL_TEST_PAGE + "#child1", |
780 | - }); |
781 | + _id: 'child1', |
782 | + record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark', |
783 | + parent_guid: 'parent', |
784 | + title: 'Child 1', |
785 | + uri: LOCAL_TEST_PAGE + "#child1", |
786 | + application_annotations: { |
787 | + Firefox: { |
788 | + profile: 'profile_name', |
789 | + last_modified: 1, |
790 | + } |
791 | + } |
792 | + }); |
793 | + synchroniser.processRecord({ |
794 | + _id: 'child3', |
795 | + record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark', |
796 | + parent_guid: 'parent', |
797 | + title: 'Child 3', |
798 | + uri: LOCAL_TEST_PAGE + "#child1", |
799 | + application_annotations: { |
800 | + Firefox: { |
801 | + profile: 'profile_name', |
802 | + last_modified: 1, |
803 | + } |
804 | + } |
805 | + }); |
806 | + synchroniser.processRecord({ |
807 | + _id: 'child2', |
808 | + record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark', |
809 | + parent_guid: 'parent', |
810 | + title: 'Child 2', |
811 | + uri: LOCAL_TEST_PAGE + "#child1", |
812 | + application_annotations: { |
813 | + Firefox: { |
814 | + profile: 'profile_name', |
815 | + last_modified: 1, |
816 | + } |
817 | + } |
818 | + }); |
819 | |
820 | // The parent has been scheduled for reordering, so trigger |
821 | // reordering. |
822 | |
823 | === modified file 'mozmill/tests/test_sync_guid_map.js' |
824 | --- mozmill/tests/test_sync_guid_map.js 2011-02-15 09:37:54 +0000 |
825 | +++ mozmill/tests/test_sync_guid_map.js 2011-02-25 07:36:30 +0000 |
826 | @@ -25,12 +25,15 @@ |
827 | }; |
828 | |
829 | var test_special_ids = function() { |
830 | + var placesRoot = bm.bookmarksService.placesRoot; |
831 | var toolbarFolder = bm.bookmarksService.toolbarFolder; |
832 | var bookmarksMenuFolder = bm.bookmarksService.bookmarksMenuFolder; |
833 | var unfiledBookmarksFolder = bm.bookmarksService.unfiledBookmarksFolder; |
834 | |
835 | // Special folders get mapped to fixed GUIDs |
836 | jum.assertEquals( |
837 | + synchroniser.guid_from_id(placesRoot), "root_profile_name"); |
838 | + jum.assertEquals( |
839 | synchroniser.guid_from_id(toolbarFolder), "toolbar_profile_name"); |
840 | jum.assertEquals( |
841 | synchroniser.guid_from_id(bookmarksMenuFolder), "menu_profile_name"); |
842 | @@ -40,6 +43,8 @@ |
843 | |
844 | // Special fixed GUIDs get mapped to special folders |
845 | jum.assertEquals( |
846 | + synchroniser.guid_to_id("root_profile_name"), placesRoot); |
847 | + jum.assertEquals( |
848 | synchroniser.guid_to_id("toolbar_profile_name"), toolbarFolder); |
849 | jum.assertEquals( |
850 | synchroniser.guid_to_id("menu_profile_name"), bookmarksMenuFolder); |
851 | @@ -79,3 +84,34 @@ |
852 | // Non-existent GUIDs result in a null result. |
853 | jum.assertEquals(synchroniser.guid_to_id("non-existent"), null); |
854 | }; |
855 | + |
856 | +var test_get_all_bookmarks = function() { |
857 | + var first_id = bm.bookmarksService.insertBookmark( |
858 | + bm.bookmarksService.toolbarFolder, |
859 | + bm.createURI(LOCAL_TEST_PAGE + "#one"), |
860 | + bm.bookmarksService.DEFAULT_INDEX, |
861 | + "Bookmark 1"); |
862 | + var second_id = bm.bookmarksService.createFolder( |
863 | + bm.bookmarksService.bookmarksMenuFolder, |
864 | + "Folder", bm.bookmarksService.DEFAULT_INDEX); |
865 | + var third_id = bm.bookmarksService.insertBookmark( |
866 | + second_id, |
867 | + bm.createURI(LOCAL_TEST_PAGE + "#three"), |
868 | + bm.bookmarksService.DEFAULT_INDEX, |
869 | + "Bookmark 3"); |
870 | + var fourth_id = bm.bookmarksService.insertBookmark( |
871 | + bm.bookmarksService.unfiledBookmarksFolder, |
872 | + bm.createURI(LOCAL_TEST_PAGE + "#four"), |
873 | + bm.bookmarksService.DEFAULT_INDEX, |
874 | + "Bookmark 4"); |
875 | + |
876 | + var bookmarks = synchroniser.get_all_bookmarks(); |
877 | + jum.assertNotUndefined(bookmarks["root_profile_name"]); |
878 | + jum.assertNotUndefined(bookmarks["toolbar_profile_name"]); |
879 | + jum.assertNotUndefined(bookmarks[synchroniser.guid_from_id(first_id)]); |
880 | + jum.assertNotUndefined(bookmarks["menu_profile_name"]); |
881 | + jum.assertNotUndefined(bookmarks[synchroniser.guid_from_id(second_id)]); |
882 | + jum.assertNotUndefined(bookmarks[synchroniser.guid_from_id(third_id)]); |
883 | + jum.assertNotUndefined(bookmarks["unfiled_profile_name"]); |
884 | + jum.assertNotUndefined(bookmarks[synchroniser.guid_from_id(fourth_id)]); |
885 | +}; |
886 | |
887 | === modified file 'mozmill/tests/test_sync_to_couch.js' |
888 | --- mozmill/tests/test_sync_to_couch.js 2011-02-22 11:09:37 +0000 |
889 | +++ mozmill/tests/test_sync_to_couch.js 2011-02-25 07:36:30 +0000 |
890 | @@ -183,3 +183,64 @@ |
891 | var doc = couch.open(item_guid); |
892 | jum.assertNull(doc); |
893 | }; |
894 | + |
895 | +var test_export_root = function() { |
896 | + var root_guid = synchroniser.guid_from_id(bm.bookmarksService.placesRoot); |
897 | + synchroniser.exportItem(root_guid); |
898 | + |
899 | + var doc = couch.open(root_guid); |
900 | + jum.assertEquals(doc.record_type, "http://www.freedesktop.org/wiki/" + |
901 | + "Specifications/desktopcouch/folder"); |
902 | + jum.assertUndefined(doc.parent_guid); |
903 | + jum.assertEquals(doc.children.length, 3); |
904 | + jum.assertEquals(doc.children[0], synchroniser.guid_from_id( |
905 | + bm.bookmarksService.toolbarFolder)); |
906 | + jum.assertEquals(doc.children[1], synchroniser.guid_from_id( |
907 | + bm.bookmarksService.bookmarksMenuFolder)); |
908 | + jum.assertEquals(doc.children[2], synchroniser.guid_from_id( |
909 | + bm.bookmarksService.unfiledBookmarksFolder)); |
910 | +}; |
911 | + |
912 | +var test_push_changes = function() { |
913 | + var item_id = bm.bookmarksService.insertBookmark( |
914 | + bm.bookmarksService.toolbarFolder, bm.createURI(LOCAL_TEST_PAGE), |
915 | + bm.bookmarksService.DEFAULT_INDEX, "Bookmark title"); |
916 | + |
917 | + // Trap calls to exportItem, to see what bookmarks are exported. |
918 | + var bookmarks = [] |
919 | + synchroniser.exportItem = function(item_id) { |
920 | + bookmarks.push(item_id); |
921 | + } |
922 | + |
923 | + // Fake observer that returns a fixed set of changes. |
924 | + synchroniser.observer = { |
925 | + clear_changes: function() { |
926 | + var changes = {} |
927 | + changes[synchroniser.guid_from_id( |
928 | + bm.bookmarksService.toolbarFolder)] = true; |
929 | + changes[synchroniser.guid_from_id(item_id)] = true; |
930 | + return changes; |
931 | + } |
932 | + }; |
933 | + |
934 | + // First call to pushItems() will export everything. |
935 | + synchroniser.pushChanges(); |
936 | + bookmarks.sort(); |
937 | + jum.assertEquals(bookmarks.length, 5); |
938 | + // This relies on the fact that the genrated IDs '{...}' sort |
939 | + // after the special ones. If we change the generated IDs, we'll |
940 | + // need to change how this test works. |
941 | + jum.assertEquals(bookmarks[0], "menu_profile_name"); |
942 | + jum.assertEquals(bookmarks[1], "root_profile_name"); |
943 | + jum.assertEquals(bookmarks[2], "toolbar_profile_name"); |
944 | + jum.assertEquals(bookmarks[3], "unfiled_profile_name"); |
945 | + jum.assertEquals(bookmarks[4], synchroniser.guid_from_id(item_id)); |
946 | + |
947 | + // Subsequent calls get the ID list from the bookmarks observer. |
948 | + bookmarks = []; |
949 | + synchroniser.pushChanges(); |
950 | + bookmarks.sort() |
951 | + jum.assertEquals(bookmarks.length, 2); |
952 | + jum.assertEquals(bookmarks[0], "toolbar_profile_name"); |
953 | + jum.assertEquals(bookmarks[1], synchroniser.guid_from_id(item_id)); |
954 | +}; |
I wonder if we can actually safely do this:
* pull changes from CouchDB, and create or update local items. If we're updating items, discard the change if the local item's modification time is greater than or equal to the CouchDB record.
Say a user modifies bookmarks on their laptop while offline, and only connects that machine *after* working (and possibly modifying the same bookmark) on another machine?
In general, relying on timestamps is extremely tricky in the face of couchdb replication as well, as timestamps may actually decrease on replication.
Not sure if it's a (big) problem in this case, but in general, document revision numbers are safer to rely on as always increasing on updates (at least in the current implementation.) I hope that doesn't mean we need to store a mapping of timestamps to revision numbers somewhere, but I've had to do something similar for the funambol integration...
Anyway, approving, with the above caveat.