--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -1298,18 +1298,21 @@ SyncEngine.prototype = {
// process should be done at this point (such as mark the record's parent
// for reuploading in the case of bookmarks).
_shouldReviveRemotelyDeletedRecord(remoteItem) {
return true;
},
_deleteId: function (id) {
this._tracker.removeChangedID(id);
+ this._noteDeletedId(id);
+ },
- // Remember this id to delete at the end of sync
+ // Marks an ID for deletion at the end of the sync.
+ _noteDeletedId(id) {
if (this._delete.ids == null)
this._delete.ids = [id];
else
this._delete.ids.push(id);
},
_switchItemToDupe(localDupeGUID, incomingItem) {
// The local, duplicate ID is always deleted on the server.
@@ -1626,19 +1629,22 @@ SyncEngine.prototype = {
}
},
_syncCleanup: function () {
if (!this._modified) {
return;
}
- // Mark failed WBOs as changed again so they are reuploaded next time.
- this.trackRemainingChanges();
- this._modified.clear();
+ try {
+ // Mark failed WBOs as changed again so they are reuploaded next time.
+ this.trackRemainingChanges();
+ } finally {
+ this._modified.clear();
+ }
},
_sync: function () {
try {
this._syncStartup();
Observers.notify("weave:engine:sync:status", "process-incoming");
this._processIncoming();
Observers.notify("weave:engine:sync:status", "upload-outgoing");
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -35,18 +35,16 @@ const ANNOS_TO_TRACK = [PlacesSyncUtils.
const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
const FOLDER_SORTINDEX = 1000000;
const {
SOURCE_SYNC,
SOURCE_IMPORT,
SOURCE_IMPORT_REPLACE,
} = Ci.nsINavBookmarksService;
-const SQLITE_MAX_VARIABLE_NUMBER = 999;
-
const ORGANIZERQUERY_ANNO = "PlacesOrganizer/OrganizerQuery";
const ALLBOOKMARKS_ANNO = "AllBookmarks";
const MOBILE_ANNO = "MobileBookmarks";
// Roots that should be deleted from the server, instead of applied locally.
// This matches `AndroidBrowserBookmarksRepositorySession::forbiddenGUID`,
// but allows tags because we don't want to reparent tag folders or tag items
// to "unfiled".
@@ -58,16 +56,23 @@ const FORBIDDEN_INCOMING_IDS = ["pinned"
const FORBIDDEN_INCOMING_PARENT_IDS = ["pinned", "readinglist"];
// The tracker ignores changes made by bookmark import and restore, and
// changes made by Sync. We don't need to exclude `SOURCE_IMPORT`, but both
// import and restore fire `bookmarks-restore-*` observer notifications, and
// the tracker doesn't currently distinguish between the two.
const IGNORED_SOURCES = [SOURCE_SYNC, SOURCE_IMPORT, SOURCE_IMPORT_REPLACE];
+function isSyncedRootNode(node) {
+ return node.root == "bookmarksMenuFolder" ||
+ node.root == "unfiledBookmarksFolder" ||
+ node.root == "toolbarFolder" ||
+ node.root == "mobileFolder";
+}
+
// Returns the constructor for a bookmark record type.
function getTypeObject(type) {
switch (type) {
case "bookmark":
case "microsummary":
return Bookmark;
case "query":
return BookmarkQuery;
@@ -297,47 +302,42 @@ BookmarksEngine.prototype = {
}
return url;
},
_guidMapFailed: false,
_buildGUIDMap: function _buildGUIDMap() {
let store = this._store;
let guidMap = {};
- let tree = Async.promiseSpinningly(PlacesUtils.promiseBookmarksTree("", {
- includeItemIds: true
- }));
+ let tree = Async.promiseSpinningly(PlacesUtils.promiseBookmarksTree(""));
+
function* walkBookmarksTree(tree, parent=null) {
if (tree) {
// Skip root node
if (parent) {
yield [tree, parent];
}
if (tree.children) {
for (let child of tree.children) {
store._sleep(0); // avoid jank while looping.
yield* walkBookmarksTree(child, tree);
}
}
}
}
- function* walkBookmarksRoots(tree, rootIDs) {
- for (let id of rootIDs) {
- let bookmarkRoot = tree.children.find(child => child.id === id);
- if (bookmarkRoot === null) {
- continue;
+ function* walkBookmarksRoots(tree) {
+ for (let child of tree.children) {
+ if (isSyncedRootNode(child)) {
+ yield* walkBookmarksTree(child, tree);
}
- yield* walkBookmarksTree(bookmarkRoot, tree);
}
}
- let rootsToWalk = getChangeRootIds();
-
- for (let [node, parent] of walkBookmarksRoots(tree, rootsToWalk)) {
+ for (let [node, parent] of walkBookmarksRoots(tree)) {
let {guid, id, type: placeType} = node;
guid = PlacesSyncUtils.bookmarks.guidToSyncId(guid);
let key;
switch (placeType) {
case PlacesUtils.TYPE_X_MOZ_PLACE:
// Bookmark
let query = null;
if (node.annos && node.uri.startsWith("place:")) {
@@ -565,16 +565,21 @@ BookmarksEngine.prototype = {
_createRecord: function _createRecord(id) {
// Create the record as usual, but mark it as having dupes if necessary.
let record = SyncEngine.prototype._createRecord.call(this, id);
let entry = this._mapDupe(record);
if (entry != null && entry.hasDupe) {
record.hasDupe = true;
}
+ if (record.deleted) {
+ // Make sure deleted items are marked as tombstones. This handles the
+ // case where a changed item is deleted during a sync.
+ this._modified.setTombstone(record.id);
+ }
return record;
},
_findDupe: function _findDupe(item) {
this._log.trace("Finding dupe for " + item.id +
" (already duped: " + item.hasDupe + ").");
// Don't bother finding a dupe if the incoming item has duplicates.
@@ -585,93 +590,36 @@ BookmarksEngine.prototype = {
let mapped = this._mapDupe(item);
this._log.debug(item.id + " mapped to " + mapped);
// We must return a string, not an object, and the entries in the GUIDMap
// are created via "new String()" making them an object.
return mapped ? mapped.toString() : mapped;
},
pullAllChanges() {
- return new BookmarksChangeset(this._store.getAllIDs());
+ return this.pullNewChanges();
},
pullNewChanges() {
- let modifiedGUIDs = this._getModifiedGUIDs();
- if (!modifiedGUIDs.length) {
- return new BookmarksChangeset(this._tracker.changedIDs);
- }
-
- // We don't use `PlacesUtils.promiseDBConnection` here because
- // `getChangedIDs` might be called while we're in a batch, meaning we
- // won't see any changes until the batch finishes and the transaction
- // commits.
- let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
- .DBConnection;
-
- // Filter out tags, organizer queries, and other descendants that we're
- // not tracking. We chunk `modifiedGUIDs` because SQLite limits the number
- // of bound parameters per query.
- for (let startIndex = 0;
- startIndex < modifiedGUIDs.length;
- startIndex += SQLITE_MAX_VARIABLE_NUMBER) {
-
- let chunkLength = Math.min(SQLITE_MAX_VARIABLE_NUMBER,
- modifiedGUIDs.length - startIndex);
-
- let query = `
- WITH RECURSIVE
- modifiedGuids(guid) AS (
- VALUES ${new Array(chunkLength).fill("(?)").join(", ")}
- ),
- syncedItems(id) AS (
- VALUES ${getChangeRootIds().map(id => `(${id})`).join(", ")}
- UNION ALL
- SELECT b.id
- FROM moz_bookmarks b
- JOIN syncedItems s ON b.parent = s.id
- )
- SELECT b.guid
- FROM modifiedGuids m
- JOIN moz_bookmarks b ON b.guid = m.guid
- LEFT JOIN syncedItems s ON b.id = s.id
- WHERE s.id IS NULL
- `;
-
- let statement = db.createAsyncStatement(query);
- try {
- for (let i = 0; i < chunkLength; i++) {
- statement.bindByIndex(i, modifiedGUIDs[startIndex + i]);
- }
- let results = Async.querySpinningly(statement, ["guid"]);
- for (let { guid } of results) {
- let syncID = PlacesSyncUtils.bookmarks.guidToSyncId(guid);
- this._tracker.removeChangedID(syncID);
- }
- } finally {
- statement.finalize();
- }
- }
-
- return new BookmarksChangeset(this._tracker.changedIDs);
+ let changes = Async.promiseSpinningly(this._tracker.promiseChangedIDs());
+ return new BookmarksChangeset(changes);
},
- // Returns an array of Places GUIDs for all changed items. Ignores deletions,
- // which won't exist in the DB and shouldn't be removed from the tracker.
- _getModifiedGUIDs() {
- let guids = [];
- for (let syncID in this._tracker.changedIDs) {
- if (this._tracker.changedIDs[syncID].deleted === true) {
- // The `===` check also filters out old persisted timestamps,
- // which won't have a `deleted` property.
- continue;
- }
- let guid = PlacesSyncUtils.bookmarks.syncIdToGuid(syncID);
- guids.push(guid);
- }
- return guids;
+ trackRemainingChanges() {
+ let changes = this._modified.changes;
+ Async.promiseSpinningly(PlacesSyncUtils.bookmarks.pushChanges(changes));
+ },
+
+ _deleteId(id) {
+ this._noteDeletedId(id);
+ },
+
+ resetClient() {
+ SyncEngine.prototype.resetClient.call(this);
+ Async.promiseSpinningly(PlacesSyncUtils.bookmarks.reset());
},
// Called when _findDupe returns a dupe item and the engine has decided to
// switch the existing item to the new incoming item.
_switchItemToDupe(localDupeGUID, incomingItem) {
// We unconditionally change the item's ID in case the engine knows of
// an item but doesn't expose it through itemExists. If the API
// contract were stronger, this could be changed.
@@ -1049,89 +997,111 @@ BookmarksStore.prototype = {
let result = Async.querySpinningly(this._frecencyStm, this._frecencyCols);
if (result.length)
index += result[0].frecency;
}
return index;
},
- getAllIDs: function BStore_getAllIDs() {
- let items = {};
-
- let query = `
- WITH RECURSIVE
- changeRootContents(id) AS (
- VALUES ${getChangeRootIds().map(id => `(${id})`).join(", ")}
- UNION ALL
- SELECT b.id
- FROM moz_bookmarks b
- JOIN changeRootContents c ON b.parent = c.id
- )
- SELECT guid
- FROM changeRootContents
- JOIN moz_bookmarks USING (id)
- `;
-
- let statement = this._getStmt(query);
- let results = Async.querySpinningly(statement, ["guid"]);
- for (let { guid } of results) {
- let syncID = PlacesSyncUtils.bookmarks.guidToSyncId(guid);
- items[syncID] = { modified: 0, deleted: false };
- }
-
- return items;
- },
-
wipe: function BStore_wipe() {
this.clearPendingDeletions();
Async.promiseSpinningly(Task.spawn(function* () {
// Save a backup before clearing out all bookmarks.
yield PlacesBackups.create(null, true);
- yield PlacesUtils.bookmarks.eraseEverything({
- source: SOURCE_SYNC,
- });
+ yield PlacesSyncUtils.bookmarks.wipe();
}));
}
};
+// The bookmarks tracker is a special flower. Instead of listening for changes
+// via observer notifications, it queries Places for the set of items that have
+// changed since the last sync. Because it's a "pull-based" tracker, it ignores
+// all concepts of "add a changed ID." However, it still registers an observer
+// to bump the score, so that changed bookmarks are synced immediately.
function BookmarksTracker(name, engine) {
this._batchDepth = 0;
this._batchSawScoreIncrement = false;
Tracker.call(this, name, engine);
+ delete this.changedIDs; // so our getter/setter takes effect.
+
Svc.Obs.add("places-shutdown", this);
}
BookmarksTracker.prototype = {
__proto__: Tracker.prototype,
//`_ignore` checks the change source for each observer notification, so we
// don't want to let the engine ignore all changes during a sync.
get ignoreAll() {
return false;
},
// Define an empty setter so that the engine doesn't throw a `TypeError`
// setting a read-only property.
set ignoreAll(value) {},
+ // We never want to persist changed IDs, as the changes are already stored
+ // in Places.
+ persistChangedIDs: false,
+
startTracking: function() {
PlacesUtils.bookmarks.addObserver(this, true);
Svc.Obs.add("bookmarks-restore-begin", this);
Svc.Obs.add("bookmarks-restore-success", this);
Svc.Obs.add("bookmarks-restore-failed", this);
},
stopTracking: function() {
PlacesUtils.bookmarks.removeObserver(this);
Svc.Obs.remove("bookmarks-restore-begin", this);
Svc.Obs.remove("bookmarks-restore-success", this);
Svc.Obs.remove("bookmarks-restore-failed", this);
},
+ // Ensure we aren't accidentally using the base persistence.
+ addChangedID(id, when) {
+ throw new Error("Don't add IDs to the bookmarks tracker");
+ },
+
+ removeChangedID(id) {
+ throw new Error("Don't remove IDs from the bookmarks tracker");
+ },
+
+ // This method is called at various times, so we override with a no-op
+ // instead of throwing.
+ clearChangedIDs() {},
+
+ saveChangedIDs(cb) {
+ if (cb) {
+ cb();
+ }
+ },
+
+ loadChangedIDs(cb) {
+ if (cb) {
+ cb();
+ }
+ },
+
+ promiseChangedIDs() {
+ return PlacesSyncUtils.bookmarks.pullChanges();
+ },
+
+ get changedIDs() {
+ throw new Error("Use promiseChangedIDs");
+ },
+
+ set changedIDs(obj) {
+ // let engine init set it to nothing.
+ if (Object.keys(obj).length != 0) {
+ throw new Error("Don't set initial changed bookmark IDs");
+ }
+ },
+
observe: function observe(subject, topic, data) {
Tracker.prototype.observe.call(this, subject, topic, data);
switch (topic) {
case "bookmarks-restore-begin":
this._log.debug("Ignoring changes from importing bookmarks.");
break;
case "bookmarks-restore-success":
@@ -1149,62 +1119,16 @@ BookmarksTracker.prototype = {
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsINavBookmarkObserver,
Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS,
Ci.nsISupportsWeakReference
]),
- addChangedID(id, change) {
- if (!id) {
- this._log.warn("Attempted to add undefined ID to tracker");
- return false;
- }
- if (this._ignored.includes(id)) {
- return false;
- }
- let shouldSaveChange = false;
- let currentChange = this.changedIDs[id];
- if (currentChange) {
- if (typeof currentChange == "number") {
- // Allow raw timestamps for backward-compatibility with persisted
- // changed IDs. The new format uses tuples to track deleted items.
- shouldSaveChange = currentChange < change.modified;
- } else {
- shouldSaveChange = currentChange.modified < change.modified ||
- currentChange.deleted != change.deleted;
- }
- } else {
- shouldSaveChange = true;
- }
- if (shouldSaveChange) {
- this._saveChangedID(id, change);
- }
- return true;
- },
-
- /**
- * Add a bookmark GUID to be uploaded and bump up the sync score.
- *
- * @param itemId
- * The Places item ID of the bookmark to upload.
- * @param guid
- * The Places GUID of the bookmark to upload.
- * @param isTombstone
- * Whether we're uploading a tombstone for a removed bookmark.
- */
- _add: function BMT__add(itemId, guid, isTombstone = false) {
- let syncID = PlacesSyncUtils.bookmarks.guidToSyncId(guid);
- let info = { modified: Date.now() / 1000, deleted: isTombstone };
- if (this.addChangedID(syncID, info)) {
- this._upScore();
- }
- },
-
/* Every add/remove/change will trigger a sync for MULTI_DEVICE (except in
a batch operation, where we do it at the end of the batch) */
_upScore: function BMT__upScore() {
if (this._batchDepth == 0) {
this.score += SCORE_INCREMENT_XLARGE;
} else {
this._batchSawScoreIncrement = true;
}
@@ -1213,67 +1137,27 @@ BookmarksTracker.prototype = {
onItemAdded: function BMT_onItemAdded(itemId, folder, index,
itemType, uri, title, dateAdded,
guid, parentGuid, source) {
if (IGNORED_SOURCES.includes(source)) {
return;
}
this._log.trace("onItemAdded: " + itemId);
- this._add(itemId, guid);
- this._add(folder, parentGuid);
+ this._upScore();
},
onItemRemoved: function (itemId, parentId, index, type, uri,
guid, parentGuid, source) {
if (IGNORED_SOURCES.includes(source)) {
return;
}
- // Ignore changes to tags (folders under the tags folder).
- if (parentId == PlacesUtils.tagsFolderId) {
- return;
- }
-
- let grandParentId = -1;
- try {
- grandParentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
- } catch (ex) {
- // `getFolderIdForItem` can throw if the item no longer exists, such as
- // when we've removed a subtree using `removeFolderChildren`.
- return;
- }
-
- // Ignore tag items (the actual instance of a tag for a bookmark).
- if (grandParentId == PlacesUtils.tagsFolderId) {
- return;
- }
-
- /**
- * The above checks are incomplete: we can still write tombstones for
- * items that we don't track, and upload extraneous roots.
- *
- * Consider the left pane root: it's a child of the Places root, and has
- * children and grandchildren. `PlacesUIUtils` can create, delete, and
- * recreate it as needed. We can't determine ancestors when the root or its
- * children are deleted, because they've already been removed from the
- * database when `onItemRemoved` is called. Likewise, we can't check their
- * "exclude from backup" annos, because they've *also* been removed.
- *
- * So, we end up writing tombstones for the left pane queries and left
- * pane root. For good measure, we'll also upload the Places root, because
- * it's the parent of the left pane root.
- *
- * As a workaround, we can track the parent GUID and reconstruct the item's
- * ancestry at sync time. This is complicated, and the previous behavior was
- * already wrong, so we'll wait for bug 1258127 to fix this generally.
- */
this._log.trace("onItemRemoved: " + itemId);
- this._add(itemId, guid, /* isTombstone */ true);
- this._add(parentId, parentGuid);
+ this._upScore();
},
_ensureMobileQuery: function _ensureMobileQuery() {
let find = val =>
PlacesUtils.annotations.getItemsWithAnnotation(ORGANIZERQUERY_ANNO, {}).filter(
id => PlacesUtils.annotations.getItemAnnotation(id, ORGANIZERQUERY_ANNO) == val
);
@@ -1335,61 +1219,66 @@ BookmarksTracker.prototype = {
// Ignore favicon changes to avoid unnecessary churn.
if (property == "favicon")
return;
this._log.trace("onItemChanged: " + itemId +
(", " + property + (isAnno? " (anno)" : "")) +
(value ? (" = \"" + value + "\"") : ""));
- this._add(itemId, guid);
+ this._upScore();
},
onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex,
newParent, newIndex, itemType,
guid, oldParentGuid, newParentGuid,
source) {
if (IGNORED_SOURCES.includes(source)) {
return;
}
this._log.trace("onItemMoved: " + itemId);
- this._add(oldParent, oldParentGuid);
- if (oldParent != newParent) {
- this._add(itemId, guid);
- this._add(newParent, newParentGuid);
- }
-
- // Remove any position annotations now that the user moved the item
- PlacesUtils.annotations.removeItemAnnotation(itemId,
- PlacesSyncUtils.bookmarks.SYNC_PARENT_ANNO, SOURCE_SYNC);
+ this._upScore();
},
onBeginUpdateBatch: function () {
++this._batchDepth;
},
onEndUpdateBatch: function () {
if (--this._batchDepth === 0 && this._batchSawScoreIncrement) {
this.score += SCORE_INCREMENT_XLARGE;
this._batchSawScoreIncrement = false;
}
},
onItemVisited: function () {}
};
-// Returns an array of root IDs to recursively query for synced bookmarks.
-// Items in other roots, including tags and organizer queries, will be
-// ignored.
-function getChangeRootIds() {
- return [
- PlacesUtils.bookmarksMenuFolderId,
- PlacesUtils.toolbarFolderId,
- PlacesUtils.unfiledBookmarksFolderId,
- PlacesUtils.mobileFolderId,
- ];
-}
-
class BookmarksChangeset extends Changeset {
getModifiedTimestamp(id) {
let change = this.changes[id];
- return change ? change.modified : Number.NaN;
+ if (!change || change.synced) {
+ // Pretend the change doesn't exist if we've already synced or
+ // reconciled it.
+ return Number.NaN;
+ }
+ return change.modified;
+ }
+
+ has(id) {
+ return id in this.changes && !this.changes[id].synced;
+ }
+
+ setTombstone(id) {
+ let change = this.changes[id];
+ if (change) {
+ change.tombstone = true;
+ }
+ }
+
+ delete(id) {
+ let change = this.changes[id];
+ if (change) {
+ // Mark the change as synced without removing it from the set. We do this
+ // so that we can update Places in `trackRemainingChanges`.
+ change.synced = true;
+ }
}
}
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -18,16 +18,38 @@ initTestLogging("Trace");
Service.engineManager.register(BookmarksEngine);
function* assertChildGuids(folderGuid, expectedChildGuids, message) {
let tree = yield PlacesUtils.promiseBookmarksTree(folderGuid);
let childGuids = tree.children.map(child => child.guid);
deepEqual(childGuids, expectedChildGuids, message);
}
+function* fetchAllSyncIds() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.executeCached(`
+ WITH RECURSIVE
+ syncedItems(id, guid) AS (
+ SELECT b.id, b.guid FROM moz_bookmarks b
+ WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
+ 'mobile______')
+ UNION ALL
+ SELECT b.id, b.guid FROM moz_bookmarks b
+ JOIN syncedItems s ON b.parent = s.id
+ )
+ SELECT guid FROM syncedItems`);
+ let syncIds = new Set();
+ for (let row of rows) {
+ let syncId = PlacesSyncUtils.bookmarks.guidToSyncId(
+ row.getResultByName("guid"));
+ syncIds.add(syncId);
+ }
+ return syncIds;
+}
+
add_task(function* test_delete_invalid_roots_from_server() {
_("Ensure that we delete the Places and Reading List roots from the server.");
let engine = new BookmarksEngine(Service);
let store = engine._store;
let tracker = engine._tracker;
let server = serverForFoo(engine);
new SyncTestingInfrastructure(server.server);
@@ -92,16 +114,17 @@ add_task(function* test_change_during_sy
let collection = server.user("foo").collection("bookmarks");
let bz_id = PlacesUtils.bookmarks.insertBookmark(
PlacesUtils.bookmarksMenuFolderId, Utils.makeURI("https://bugzilla.mozilla.org/"),
PlacesUtils.bookmarks.DEFAULT_INDEX, "Bugzilla");
let bz_guid = yield PlacesUtils.promiseItemGuid(bz_id);
_(`Bugzilla GUID: ${bz_guid}`);
+ yield PlacesTestUtils.markBookmarksAsSynced();
Svc.Obs.notify("weave:engine:start-tracking");
try {
let folder1_id = PlacesUtils.bookmarks.createFolder(
PlacesUtils.bookmarks.toolbarFolder, "Folder 1", 0);
let folder1_guid = store.GUIDForId(folder1_id);
_(`Folder GUID: ${folder1_guid}`);
@@ -245,24 +268,25 @@ add_task(function* bad_record_allIDs() {
PlacesUtils.bookmarks.DEFAULT_INDEX,
null);
do_check_true(badRecordID > 0);
_("Record is " + badRecordID);
_("Type: " + PlacesUtils.bookmarks.getItemType(badRecordID));
_("Fetching all IDs.");
- let all = store.getAllIDs();
+ let all = yield* fetchAllSyncIds();
- _("All IDs: " + JSON.stringify(all));
- do_check_true("menu" in all);
- do_check_true("toolbar" in all);
+ _("All IDs: " + JSON.stringify([...all]));
+ do_check_true(all.has("menu"));
+ do_check_true(all.has("toolbar"));
_("Clean up.");
PlacesUtils.bookmarks.removeItem(badRecordID);
+ yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(r => server.stop(r));
});
function serverForFoo(engine) {
return serverForUsers({"foo": "password"}, {
meta: {global: {engines: {bookmarks: {version: engine.version,
syncID: engine.syncID}}}},
bookmarks: {}
@@ -330,16 +354,17 @@ add_task(function* test_processIncoming_
do_check_eq(PlacesUtils.bookmarks.getItemIndex(bmk1_id), 1);
do_check_eq(PlacesUtils.bookmarks.getItemIndex(bmk2_id), 0);
} finally {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
+ yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(resolve => server.stop(resolve));
}
});
add_task(function* test_restorePromptsReupload() {
_("Ensure that restoring from a backup will reupload all records.");
let engine = new BookmarksEngine(Service);
let store = engine._store;
@@ -402,22 +427,22 @@ add_task(function* test_restorePromptsRe
});
do_check_eq(wbos.length, 1);
do_check_eq(wbos[0], bmk2_guid);
_("Now restore from a backup.");
yield BookmarkJSONUtils.importFromFile(backupFile, true);
_("Ensure we have the bookmarks we expect locally.");
- let guids = store.getAllIDs();
- _("GUIDs: " + JSON.stringify(guids));
+ let guids = yield* fetchAllSyncIds();
+ _("GUIDs: " + JSON.stringify([...guids]));
let found = false;
let count = 0;
let newFX;
- for (let guid in guids) {
+ for (let guid of guids) {
count++;
let id = store.idForGUID(guid, true);
// Only one bookmark, so _all_ should be Firefox!
if (PlacesUtils.bookmarks.getItemType(id) == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
let uri = PlacesUtils.bookmarks.getBookmarkURI(id);
_("Found URI " + uri.spec + " for GUID " + guid);
do_check_eq(uri.spec, fxuri.spec);
newFX = guid; // Save the new GUID after restore.
@@ -461,19 +486,18 @@ add_task(function* test_restorePromptsRe
_("Our old friend Folder 1 is still in play.");
do_check_eq(folderWBOs.length, 1);
do_check_eq(folderWBOs[0].title, "Folder 1");
} finally {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
- let deferred = Promise.defer();
- server.stop(deferred.resolve);
- yield deferred.promise;
+ yield PlacesSyncUtils.bookmarks.reset();
+ yield new Promise(r => server.stop(r));
}
});
function FakeRecord(constructor, r) {
constructor.call(this, "bookmarks", r.id);
for (let x in r) {
this[x] = r[x];
}
@@ -542,16 +566,17 @@ add_task(function* test_mismatched_types
do_check_eq(bms.getItemType(newID), bms.TYPE_FOLDER);
do_check_true(PlacesUtils.annotations
.itemHasAnnotation(newID, PlacesUtils.LMANNO_FEEDURI));
} finally {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
+ yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(r => server.stop(r));
}
});
add_task(function* test_bookmark_guidMap_fail() {
_("Ensure that failures building the GUID map cause early death.");
let engine = new BookmarksEngine(Service);
@@ -595,16 +620,17 @@ add_task(function* test_bookmark_guidMap
try {
engine._processIncoming();
} catch (ex) {
err = ex;
}
do_check_eq(err, "Nooo");
PlacesUtils.promiseBookmarksTree = pbt;
+ yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(r => server.stop(r));
});
add_task(function* test_bookmark_tag_but_no_uri() {
_("Ensure that a bookmark record with tags, but no URI, doesn't throw an exception.");
let engine = new BookmarksEngine(Service);
let store = engine._store;
@@ -707,16 +733,17 @@ add_task(function* test_misreconciled_ro
// the real GUID, instead using a generated one. Sync does the translation.
let toolbarAfter = store.createRecord("toolbar", "bookmarks");
let parentGUIDAfter = toolbarAfter.parentid;
let parentIDAfter = store.idForGUID(parentGUIDAfter);
do_check_eq(store.GUIDForId(toolbarIDBefore), "toolbar");
do_check_eq(parentGUIDBefore, parentGUIDAfter);
do_check_eq(parentIDBefore, parentIDAfter);
+ yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(r => server.stop(r));
});
function run_test() {
initTestLogging("Trace");
generateNewKeys(Service.collectionKeys);
run_next_test();
}
--- a/services/sync/tests/unit/test_bookmark_store.js
+++ b/services/sync/tests/unit/test_bookmark_store.js
@@ -435,16 +435,18 @@ function assertDeleted(id) {
} catch (e) {
error = e;
}
equal(error.result, Cr.NS_ERROR_ILLEGAL_VALUE)
}
add_task(function* test_delete_buffering() {
store.wipe();
+ yield PlacesTestUtils.markBookmarksAsSynced();
+
try {
_("Create a folder with two bookmarks.");
let folder = new BookmarkFolder("bookmarks", "testfolder-1");
folder.parentName = "Bookmarks Toolbar";
folder.parentid = "toolbar";
folder.title = "Test Folder";
store.applyIncoming(folder);
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -1,128 +1,171 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
+const {
+ // `fetchGuidsWithAnno` isn't exported, but we can still access it here via a
+ // backstage pass.
+ fetchGuidsWithAnno,
+} = Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines/bookmarks.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://testing-common/PlacesTestUtils.jsm");
Cu.import("resource:///modules/PlacesUIUtils.jsm");
Service.engineManager.register(BookmarksEngine);
var engine = Service.engineManager.get("bookmarks");
var store = engine._store;
var tracker = engine._tracker;
store.wipe();
tracker.persistChangedIDs = false;
const DAY_IN_MS = 24 * 60 * 60 * 1000;
// Test helpers.
function* verifyTrackerEmpty() {
- let changes = engine.pullNewChanges();
- equal(changes.count(), 0);
+ let changes = yield tracker.promiseChangedIDs();
+ deepEqual(changes, {});
equal(tracker.score, 0);
}
function* resetTracker() {
- tracker.clearChangedIDs();
+ yield PlacesTestUtils.markBookmarksAsSynced();
tracker.resetScore();
}
function* cleanup() {
store.wipe();
yield resetTracker();
yield stopTracking();
}
// startTracking is a signal that the test wants to notice things that happen
// after this is called (ie, things already tracked should be discarded.)
function* startTracking() {
Svc.Obs.notify("weave:engine:start-tracking");
+ yield PlacesTestUtils.markBookmarksAsSynced();
}
function* stopTracking() {
Svc.Obs.notify("weave:engine:stop-tracking");
}
function* verifyTrackedItems(tracked) {
- let changes = engine.pullNewChanges();
- let trackedIDs = new Set(changes.ids());
+ let changedIDs = yield tracker.promiseChangedIDs();
+ let trackedIDs = new Set(Object.keys(changedIDs));
for (let guid of tracked) {
- ok(changes.has(guid), `${guid} should be tracked`);
- ok(changes.getModifiedTimestamp(guid) > 0,
- `${guid} should have a modified time`);
+ ok(guid in changedIDs, `${guid} should be tracked`);
+ ok(changedIDs[guid].modified > 0, `${guid} should have a modified time`);
+ ok(changedIDs[guid].counter >= -1, `${guid} should have a change counter`);
trackedIDs.delete(guid);
}
equal(trackedIDs.size, 0, `Unhandled tracked IDs: ${
JSON.stringify(Array.from(trackedIDs))}`);
}
function* verifyTrackedCount(expected) {
- let changes = engine.pullNewChanges();
- equal(changes.count(), expected);
+ let changedIDs = yield tracker.promiseChangedIDs();
+ do_check_attribute_count(changedIDs, expected);
+}
+
+// A debugging helper that dumps the full bookmarks tree.
+function* dumpBookmarks() {
+ let columns = ["id", "title", "guid", "syncStatus", "syncChangeCounter", "position"];
+ return PlacesUtils.promiseDBConnection().then(connection => {
+ let all = [];
+ return connection.executeCached(`SELECT ${columns.join(", ")} FROM moz_bookmarks;`,
+ {},
+ row => {
+ let repr = {};
+ for (let column of columns) {
+ repr[column] = row.getResultByName(column);
+ }
+ all.push(repr);
+ }
+ ).then(() => {
+ dump("All bookmarks:\n");
+ dump(JSON.stringify(all, undefined, 2));
+ });
+ })
}
-// Copied from PlacesSyncUtils.jsm.
-function findAnnoItems(anno, val) {
- let annos = PlacesUtils.annotations;
- return annos.getItemsWithAnnotation(anno, {}).filter(id =>
- annos.getItemAnnotation(id, anno) == val);
-}
+var populateTree = Task.async(function* populate(parentId, ...items) {
+ let guids = {};
+ for (let item of items) {
+ let itemId;
+ switch (item.type) {
+ case PlacesUtils.bookmarks.TYPE_BOOKMARK:
+ itemId = PlacesUtils.bookmarks.insertBookmark(parentId,
+ Utils.makeURI(item.url),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, item.title);
+ break;
+
+ case PlacesUtils.bookmarks.TYPE_FOLDER: {
+ itemId = PlacesUtils.bookmarks.createFolder(parentId,
+ item.title, PlacesUtils.bookmarks.DEFAULT_INDEX);
+ Object.assign(guids, yield* populate(itemId, ...item.children));
+ break;
+ }
+
+ default:
+ throw new Error(`Unsupported item type: ${item.type}`);
+ }
+ if (item.exclude) {
+ PlacesUtils.annotations.setItemAnnotation(
+ itemId, BookmarkAnnos.EXCLUDEBACKUP_ANNO, "Don't back this up", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ }
+ guids[item.title] = yield PlacesUtils.promiseItemGuid(itemId);
+ }
+ return guids;
+});
add_task(function* test_tracking() {
_("Test starting and stopping the tracker");
+ // Remove existing tracking information for roots.
+ yield startTracking();
+
let folder = PlacesUtils.bookmarks.createFolder(
PlacesUtils.bookmarks.bookmarksMenuFolder,
"Test Folder", PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ // creating the folder should have made 2 changes - the folder itself and
+ // the parent of the folder.
+ yield verifyTrackedCount(2);
+ // Reset the changes as the rest of the test doesn't want to see these.
+ yield resetTracker();
+
function createBmk() {
return PlacesUtils.bookmarks.insertBookmark(
folder, Utils.makeURI("http://getfirefox.com"),
PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
}
try {
- _("Create bookmark. Won't show because we haven't started tracking yet");
- createBmk();
- yield verifyTrackedCount(0);
- do_check_eq(tracker.score, 0);
-
_("Tell the tracker to start tracking changes.");
yield startTracking();
createBmk();
// We expect two changed items because the containing folder
// changed as well (new child).
yield verifyTrackedCount(2);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
_("Notifying twice won't do any harm.");
- yield startTracking();
createBmk();
yield verifyTrackedCount(3);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 4);
-
- _("Let's stop tracking again.");
- yield resetTracker();
- yield stopTracking();
- createBmk();
- yield verifyTrackedCount(0);
- do_check_eq(tracker.score, 0);
-
- _("Notifying twice won't do any harm.");
- yield stopTracking();
- createBmk();
- yield verifyTrackedCount(0);
- do_check_eq(tracker.score, 0);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_batch_tracking() {
@@ -200,55 +243,60 @@ add_task(function* test_tracker_sql_batc
"Sync Bookmark " + i);
createdIDs.push(syncBmkID);
}
}
}, null);
do_check_eq(createdIDs.length, numItems);
yield verifyTrackedCount(numItems + 1); // the folder is also tracked.
+ yield resetTracker();
+
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.bookmarks.unfiledBookmarksFolder);
+ yield verifyTrackedCount(numItems + 1);
+
yield cleanup();
});
add_task(function* test_onItemAdded() {
_("Items inserted via the synchronous bookmarks API should be tracked");
try {
yield startTracking();
_("Insert a folder using the sync API");
let syncFolderID = PlacesUtils.bookmarks.createFolder(
PlacesUtils.bookmarks.bookmarksMenuFolder, "Sync Folder",
PlacesUtils.bookmarks.DEFAULT_INDEX);
let syncFolderGUID = engine._store.GUIDForId(syncFolderID);
yield verifyTrackedItems(["menu", syncFolderGUID]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
yield resetTracker();
yield startTracking();
_("Insert a bookmark using the sync API");
let syncBmkID = PlacesUtils.bookmarks.insertBookmark(syncFolderID,
Utils.makeURI("https://example.org/sync"),
PlacesUtils.bookmarks.DEFAULT_INDEX,
"Sync Bookmark");
let syncBmkGUID = engine._store.GUIDForId(syncBmkID);
yield verifyTrackedItems([syncFolderGUID, syncBmkGUID]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
yield resetTracker();
yield startTracking();
_("Insert a separator using the sync API");
let syncSepID = PlacesUtils.bookmarks.insertSeparator(
PlacesUtils.bookmarks.bookmarksMenuFolder,
PlacesUtils.bookmarks.getItemIndex(syncFolderID));
let syncSepGUID = engine._store.GUIDForId(syncSepID);
yield verifyTrackedItems(["menu", syncSepGUID]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_async_onItemAdded() {
_("Items inserted via the asynchronous bookmarks API should be tracked");
@@ -258,42 +306,42 @@ add_task(function* test_async_onItemAdde
_("Insert a folder using the async API");
let asyncFolder = yield PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: PlacesUtils.bookmarks.menuGuid,
title: "Async Folder",
});
yield verifyTrackedItems(["menu", asyncFolder.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
yield resetTracker();
yield startTracking();
_("Insert a bookmark using the async API");
let asyncBmk = yield PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: asyncFolder.guid,
url: "https://example.org/async",
title: "Async Bookmark",
});
yield verifyTrackedItems([asyncFolder.guid, asyncBmk.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
yield resetTracker();
yield startTracking();
_("Insert a separator using the async API");
let asyncSep = yield PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
parentGuid: PlacesUtils.bookmarks.menuGuid,
index: asyncFolder.index,
});
yield verifyTrackedItems(["menu", asyncSep.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_async_onItemChanged() {
_("Items updated using the asynchronous bookmarks API should be tracked");
@@ -424,17 +472,17 @@ add_task(function* test_onItemTagged() {
yield startTracking();
_("Tag the item");
PlacesUtils.tagging.tagURI(uri, ["foo"]);
// bookmark should be tracked, folder should not be.
yield verifyTrackedItems([bGUID]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 5);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemUntagged() {
_("Items untagged using the synchronous API should be tracked");
@@ -456,17 +504,17 @@ add_task(function* test_onItemUntagged()
PlacesUtils.tagging.tagURI(uri, ["foo"]);
yield startTracking();
_("Remove the tag");
PlacesUtils.tagging.untagURI(uri, ["foo"]);
yield verifyTrackedItems([fx1GUID, fx2GUID]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 4);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_async_onItemUntagged() {
_("Items untagged using the asynchronous API should be tracked");
@@ -499,17 +547,17 @@ add_task(function* test_async_onItemUnta
});
yield startTracking();
_("Remove the tag using the async bookmarks API");
yield PlacesUtils.bookmarks.remove(fxTag.guid);
yield verifyTrackedItems([fxBmk1.guid, fxBmk2.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 4);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_async_onItemTagged() {
_("Items tagged using the asynchronous API should be tracked");
@@ -557,17 +605,17 @@ add_task(function* test_async_onItemTagg
_("Tag an item using the async bookmarks API");
yield PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: tag.guid,
url: "http://getfirefox.com",
});
yield verifyTrackedItems([fxBmk1.guid, fxBmk2.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 4);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemKeywordChanged() {
_("Keyword changes via the synchronous API should be tracked");
@@ -692,17 +740,18 @@ add_task(function* test_onItemPostDataCh
_(`Firefox GUID: ${fx_guid}`);
yield startTracking();
// PlacesUtils.setPostDataForBookmark is deprecated, but still used by
// PlacesTransactions.NewBookmark.
_("Post data for the bookmark should be ignored");
yield PlacesUtils.setPostDataForBookmark(fx_id, "postData");
- yield verifyTrackerEmpty();
+ yield verifyTrackedItems([]);
+ do_check_eq(tracker.score, 0);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemAnnoChanged() {
_("Item annotations should be tracked");
@@ -768,50 +817,44 @@ add_task(function* test_onItemAdded_filt
PlacesUtils.bookmarks.placesRoot,
Utils.makeURI("http://getfirefox.com"),
PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
let rootBmkGUID = engine._store.GUIDForId(rootBmkID);
_(`New Places root bookmark GUID: ${rootBmkGUID}`);
_("New root and bookmark should be ignored");
yield verifyTrackedItems([]);
- // ...But we'll still increment the score and filter out the changes at
- // sync time.
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemDeleted_filtered_root() {
- _("Deleted items outside the change roots should be tracked");
+ _("Deleted items outside the change roots should not be tracked");
try {
yield stopTracking();
_("Insert a bookmark underneath the Places root");
let rootBmkID = PlacesUtils.bookmarks.insertBookmark(
PlacesUtils.bookmarks.placesRoot,
Utils.makeURI("http://getfirefox.com"),
PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
let rootBmkGUID = engine._store.GUIDForId(rootBmkID);
_(`New Places root bookmark GUID: ${rootBmkGUID}`);
yield startTracking();
PlacesUtils.bookmarks.removeItem(rootBmkID);
- // We shouldn't upload tombstones for items in filtered roots, but the
- // `onItemRemoved` observer doesn't have enough context to determine
- // the root, so we'll end up uploading it.
- yield verifyTrackedItems([rootBmkGUID]);
- // We'll increment the counter twice (once for the removed item, and once
- // for the Places root), then filter out the root.
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ yield verifyTrackedItems([]);
+ // We'll still increment the counter for the removed item.
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onPageAnnoChanged() {
_("Page annotations should not be tracked");
@@ -827,23 +870,25 @@ add_task(function* test_onPageAnnoChange
PlacesUtils.bookmarks.DEFAULT_INDEX,
"Get Firefox!");
yield startTracking();
_("Add a page annotation");
PlacesUtils.annotations.setPageAnnotation(pageURI, "URIProperties/characterSet",
"UTF-8", 0, PlacesUtils.annotations.EXPIRE_NEVER);
- yield verifyTrackerEmpty();
+ yield verifyTrackedItems([]);
+ do_check_eq(tracker.score, 0);
yield resetTracker();
_("Remove the page annotation");
PlacesUtils.annotations.removePageAnnotation(pageURI,
"URIProperties/characterSet");
- yield verifyTrackerEmpty();
+ yield verifyTrackedItems([]);
+ do_check_eq(tracker.score, 0);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onFaviconChanged() {
_("Favicon changes should not be tracked");
@@ -872,17 +917,18 @@ add_task(function* test_onFaviconChanged
yield new Promise(resolve => {
PlacesUtils.favicons.setAndFetchFaviconForPage(pageURI, iconURI, true,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, (iconURI, dataLen, data, mimeType) => {
resolve();
},
Services.scriptSecurityManager.getSystemPrincipal());
});
- yield verifyTrackerEmpty();
+ yield verifyTrackedItems([]);
+ do_check_eq(tracker.score, 0);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onLivemarkAdded() {
_("New livemarks should be tracked");
@@ -896,19 +942,19 @@ add_task(function* test_onLivemarkAdded(
// Use a local address just in case, to avoid potential aborts for
// non-local connections.
feedURI: Utils.makeURI("http://localhost:0"),
});
// Prevent the livemark refresh timer from requesting the URI.
livemark.terminate();
yield verifyTrackedItems(["menu", livemark.guid]);
- // Three changes: one for the parent, one for creating the livemark
- // folder, and one for setting the "livemark/feedURI" anno on the folder.
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
+ // Two observer notifications: one for creating the livemark folder, and
+ // one for setting the "livemark/feedURI" anno on the folder.
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onLivemarkDeleted() {
_("Deleted livemarks should be tracked");
@@ -926,17 +972,17 @@ add_task(function* test_onLivemarkDelete
yield startTracking();
_("Remove a livemark");
yield PlacesUtils.livemarks.removeLivemark({
guid: livemark.guid,
});
yield verifyTrackedItems(["menu", livemark.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemMoved() {
_("Items moved via the synchronous API should be tracked");
@@ -960,23 +1006,24 @@ add_task(function* test_onItemMoved() {
yield startTracking();
// Moving within the folder will just track the folder.
PlacesUtils.bookmarks.moveItem(
tb_id, PlacesUtils.bookmarks.bookmarksMenuFolder, 0);
yield verifyTrackedItems(['menu']);
do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
yield resetTracker();
+ yield PlacesTestUtils.markBookmarksAsSynced();
// Moving a bookmark to a different folder will track the old
// folder, the new folder and the bookmark.
PlacesUtils.bookmarks.moveItem(fx_id, PlacesUtils.bookmarks.toolbarFolder,
PlacesUtils.bookmarks.DEFAULT_INDEX);
yield verifyTrackedItems(['menu', 'toolbar', fx_guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_async_onItemMoved_update() {
@@ -1012,17 +1059,17 @@ add_task(function* test_async_onItemMove
_("Reparenting a bookmark should track both folders and the bookmark");
yield PlacesUtils.bookmarks.update({
guid: tbBmk.guid,
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
index: PlacesUtils.bookmarks.DEFAULT_INDEX,
});
yield verifyTrackedItems(['menu', 'toolbar', tbBmk.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_async_onItemMoved_reorder() {
_("Items reordered via the asynchronous API should be tracked");
@@ -1165,33 +1212,33 @@ add_task(function* test_onItemDeleted_re
let txn = PlacesUtils.bookmarks.getRemoveFolderTransaction(folder_id);
// We haven't executed the transaction yet.
yield verifyTrackerEmpty();
_("Execute the remove folder transaction");
txn.doTransaction();
yield verifyTrackedItems(["menu", folder_guid, fx_guid, tb_guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
yield resetTracker();
_("Undo the remove folder transaction");
txn.undoTransaction();
// At this point, the restored folder has the same ID, but a different GUID.
let new_folder_guid = yield PlacesUtils.promiseItemGuid(folder_id);
yield verifyTrackedItems(["menu", new_folder_guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
yield resetTracker();
_("Redo the transaction");
txn.redoTransaction();
yield verifyTrackedItems(["menu", new_folder_guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_treeMoved() {
_("Moving an entire tree of bookmarks should track the parents");
@@ -1227,17 +1274,17 @@ add_task(function* test_treeMoved() {
yield startTracking();
// Move folder 2 to be a sibling of folder1.
PlacesUtils.bookmarks.moveItem(
folder2_id, PlacesUtils.bookmarks.bookmarksMenuFolder, 0);
// the menu and both folders should be tracked, the children should not be.
yield verifyTrackedItems(['menu', folder1_guid, folder2_guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemDeleted() {
_("Bookmarks deleted via the synchronous API should be tracked");
@@ -1257,17 +1304,17 @@ add_task(function* test_onItemDeleted()
let tb_guid = engine._store.GUIDForId(tb_id);
yield startTracking();
// Delete the last item - the item and parent should be tracked.
PlacesUtils.bookmarks.removeItem(tb_id);
yield verifyTrackedItems(['menu', tb_guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_async_onItemDeleted() {
_("Bookmarks deleted via the asynchronous API should be tracked");
@@ -1289,17 +1336,17 @@ add_task(function* test_async_onItemDele
});
yield startTracking();
_("Delete the first item");
yield PlacesUtils.bookmarks.remove(fxBmk.guid);
yield verifyTrackedItems(["menu", fxBmk.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_async_onItemDeleted_eraseEverything() {
_("Erasing everything should track all deleted items");
@@ -1358,28 +1405,32 @@ add_task(function* test_async_onItemDele
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: bugsChildFolder.guid,
url: "https://example.com",
title: "Bugs grandchild",
});
_(`Bugs grandchild GUID: ${bugsGrandChildBmk.guid}`);
yield startTracking();
-
+ // Simulate moving a synced item into a new folder. Deleting the folder
+ // should write a tombstone for the item, but not the folder.
+ yield PlacesTestUtils.setBookmarkSyncFields({
+ guid: bugsChildFolder.guid,
+ syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
+ });
yield PlacesUtils.bookmarks.eraseEverything();
- // `eraseEverything` removes all items from the database before notifying
- // observers. Because of this, grandchild lookup in the tracker's
- // `onItemRemoved` observer will fail. That means we won't track
- // (bzBmk.guid, bugsGrandChildBmk.guid, bugsChildFolder.guid), even
- // though we should.
+ // bugsChildFolder's sync status is still "NEW", so it shouldn't be
+ // tracked. bugsGrandChildBmk is "NORMAL", so we *should* write a
+ // tombstone and track it.
yield verifyTrackedItems(["menu", mozBmk.guid, mdnBmk.guid, "toolbar",
bugsFolder.guid, "mobile", fxBmk.guid,
- tbBmk.guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 10);
+ tbBmk.guid, "unfiled", bzBmk.guid,
+ bugsGrandChildBmk.guid]);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 8);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemDeleted_removeFolderChildren() {
_("Removing a folder's children should track the folder and its children");
@@ -1411,17 +1462,17 @@ add_task(function* test_onItemDeleted_re
_(`Mozilla GUID: ${moz_guid}`);
yield startTracking();
_(`Mobile root ID: ${PlacesUtils.mobileFolderId}`);
PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.mobileFolderId);
yield verifyTrackedItems(["mobile", fx_guid, tb_guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 4);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemDeleted_tree() {
_("Deleting a tree of bookmarks should track all items");
@@ -1456,51 +1507,55 @@ add_task(function* test_onItemDeleted_tr
let tb_guid = engine._store.GUIDForId(tb_id);
yield startTracking();
// Delete folder2 - everything we created should be tracked.
PlacesUtils.bookmarks.removeItem(folder2_id);
yield verifyTrackedItems([fx_guid, tb_guid, folder1_guid, folder2_guid]);
- do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 3);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_mobile_query() {
_("Ensure we correctly create the mobile query");
try {
+ yield startTracking();
+
// Creates the organizer queries as a side effect.
let leftPaneId = PlacesUIUtils.leftPaneFolderId;
_(`Left pane root ID: ${leftPaneId}`);
- let allBookmarksIds = findAnnoItems("PlacesOrganizer/OrganizerQuery", "AllBookmarks");
- equal(allBookmarksIds.length, 1, "Should create folder with all bookmarks queries");
- let allBookmarkGuid = yield PlacesUtils.promiseItemGuid(allBookmarksIds[0]);
+ let allBookmarksGuids = yield fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
+ "AllBookmarks");
+ equal(allBookmarksGuids.length, 1, "Should create folder with all bookmarks queries");
+ let allBookmarkGuid = allBookmarksGuids[0];
_("Try creating query after organizer is ready");
tracker._ensureMobileQuery();
- let queryIds = findAnnoItems("PlacesOrganizer/OrganizerQuery", "MobileBookmarks");
- equal(queryIds.length, 0, "Should not create query without any mobile bookmarks");
+ let queryGuids = yield fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
+ "MobileBookmarks");
+ equal(queryGuids.length, 0, "Should not create query without any mobile bookmarks");
_("Insert mobile bookmark, then create query");
- yield PlacesUtils.bookmarks.insert({
+ let mozBmk = yield PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.mobileGuid,
url: "https://mozilla.org",
});
tracker._ensureMobileQuery();
- queryIds = findAnnoItems("PlacesOrganizer/OrganizerQuery", "MobileBookmarks", {});
- equal(queryIds.length, 1, "Should create query once mobile bookmarks exist");
+ queryGuids = yield fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
+ "MobileBookmarks");
+ equal(queryGuids.length, 1, "Should create query once mobile bookmarks exist");
- let queryId = queryIds[0];
- let queryGuid = yield PlacesUtils.promiseItemGuid(queryId);
+ let queryGuid = queryGuids[0];
let queryInfo = yield PlacesUtils.bookmarks.fetch(queryGuid);
equal(queryInfo.url, `place:folder=${PlacesUtils.mobileFolderId}`, "Query should point to mobile root");
equal(queryInfo.title, "Mobile Bookmarks", "Query title should be localized");
equal(queryInfo.parentGuid, allBookmarkGuid, "Should append mobile query to all bookmarks queries");
_("Rename root and query, then recreate");
yield PlacesUtils.bookmarks.update({
@@ -1523,18 +1578,18 @@ add_task(function* test_mobile_query() {
url: "place:folder=BOOKMARKS_MENU",
});
tracker._ensureMobileQuery();
queryInfo = yield PlacesUtils.bookmarks.fetch(queryGuid);
equal(queryInfo.url.href, `place:folder=${PlacesUtils.mobileFolderId}`,
"Should fix query URL to point to mobile root");
_("We shouldn't track the query or the left pane root");
- yield verifyTrackedCount(0);
- do_check_eq(tracker.score, 0);
+ yield verifyTrackedItems([mozBmk.guid, "mobile"]);
+ do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 5);
} finally {
_("Clean up.");
yield cleanup();
}
});
function run_test() {
initTestLogging("Trace");