Merge lp:~jamesh/bindwood/sync-all into lp:bindwood

Proposed by James Henstridge
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
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.

To post a comment you must log in.
lp:~jamesh/bindwood/sync-all updated
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.

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

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.

review: Approve
Revision history for this message
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.

Revision history for this message
Manuel de la Peña (mandel) wrote :

Tests pass and code looks ok

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+};

Subscribers

People subscribed via source and target branches