Bug 1258127 - Update the bookmarks engine to pull changes from Places. r=markh

MozReview-Commit-ID: 4YESuxP2rRf

--HG--
extra : rebase_source : 9c2f5830d10ba280e45c30076f19f498e6913fd0
This commit is contained in:
Kit Cambridge 2016-11-17 15:39:15 -08:00
parent 308045ed35
commit ccf45973d0
5 changed files with 316 additions and 337 deletions

View File

@ -1303,8 +1303,11 @@ SyncEngine.prototype = {
_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
@ -1631,9 +1634,12 @@ SyncEngine.prototype = {
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 () {

View File

@ -40,8 +40,6 @@ const {
SOURCE_IMPORT_REPLACE,
} = Ci.nsINavBookmarksService;
const SQLITE_MAX_VARIABLE_NUMBER = 999;
const ORGANIZERQUERY_ANNO = "PlacesOrganizer/OrganizerQuery";
const ALLBOOKMARKS_ANNO = "AllBookmarks";
const MOBILE_ANNO = "MobileBookmarks";
@ -63,6 +61,13 @@ const FORBIDDEN_INCOMING_PARENT_IDS = ["pinned", "readinglist"];
// 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) {
@ -302,9 +307,8 @@ BookmarksEngine.prototype = {
_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
@ -320,19 +324,15 @@ BookmarksEngine.prototype = {
}
}
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;
@ -570,6 +570,11 @@ BookmarksEngine.prototype = {
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;
},
@ -590,83 +595,26 @@ BookmarksEngine.prototype = {
},
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
@ -1054,50 +1002,28 @@ BookmarksStore.prototype = {
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 = {
@ -1113,6 +1039,10 @@ BookmarksTracker.prototype = {
// 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);
@ -1127,6 +1057,46 @@ BookmarksTracker.prototype = {
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);
@ -1154,52 +1124,6 @@ BookmarksTracker.prototype = {
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() {
@ -1218,8 +1142,7 @@ BookmarksTracker.prototype = {
}
this._log.trace("onItemAdded: " + itemId);
this._add(itemId, guid);
this._add(folder, parentGuid);
this._upScore();
},
onItemRemoved: function (itemId, parentId, index, type, uri,
@ -1228,47 +1151,8 @@ BookmarksTracker.prototype = {
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() {
@ -1340,7 +1224,7 @@ BookmarksTracker.prototype = {
this._log.trace("onItemChanged: " + itemId +
(", " + property + (isAnno? " (anno)" : "")) +
(value ? (" = \"" + value + "\"") : ""));
this._add(itemId, guid);
this._upScore();
},
onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex,
@ -1352,15 +1236,7 @@ BookmarksTracker.prototype = {
}
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 () {
@ -1375,21 +1251,34 @@ BookmarksTracker.prototype = {
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;
}
}
}

View File

@ -23,6 +23,28 @@ function* assertChildGuids(folderGuid, expectedChildGuids, message) {
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.");
@ -97,6 +119,7 @@ add_task(function* test_change_during_sync() {
let bz_guid = yield PlacesUtils.promiseItemGuid(bz_id);
_(`Bugzilla GUID: ${bz_guid}`);
yield PlacesTestUtils.markBookmarksAsSynced();
Svc.Obs.notify("weave:engine:start-tracking");
try {
@ -250,14 +273,15 @@ add_task(function* bad_record_allIDs() {
_("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));
});
@ -335,6 +359,7 @@ add_task(function* test_processIncoming_error_orderChildren() {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(resolve => server.stop(resolve));
}
});
@ -407,12 +432,12 @@ add_task(function* test_restorePromptsReupload() {
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!
@ -466,9 +491,8 @@ add_task(function* test_restorePromptsReupload() {
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));
}
});
@ -547,6 +571,7 @@ add_task(function* test_mismatched_types() {
store.wipe();
Svc.Prefs.resetBranch("");
Service.recordManager.clearCache();
yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(r => server.stop(r));
}
});
@ -600,6 +625,7 @@ add_task(function* test_bookmark_guidMap_fail() {
do_check_eq(err, "Nooo");
PlacesUtils.promiseBookmarksTree = pbt;
yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(r => server.stop(r));
});
@ -712,6 +738,7 @@ add_task(function* test_misreconciled_root() {
do_check_eq(parentGUIDBefore, parentGUIDAfter);
do_check_eq(parentIDBefore, parentIDAfter);
yield PlacesSyncUtils.bookmarks.reset();
yield new Promise(r => server.stop(r));
});

View File

@ -440,6 +440,8 @@ function assertDeleted(id) {
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");

View File

@ -2,13 +2,19 @@
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);
@ -23,13 +29,13 @@ 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();
}
@ -43,6 +49,7 @@ function* cleanup() {
// 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() {
@ -50,12 +57,12 @@ function* stopTracking() {
}
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: ${
@ -63,23 +70,78 @@ function* verifyTrackedItems(tracked) {
}
function* verifyTrackedCount(expected) {
let changes = engine.pullNewChanges();
equal(changes.count(), expected);
let changedIDs = yield tracker.promiseChangedIDs();
do_check_attribute_count(changedIDs, expected);
}
// Copied from PlacesSyncUtils.jsm.
function findAnnoItems(anno, val) {
let annos = PlacesUtils.annotations;
return annos.getItemsWithAnnotation(anno, {}).filter(id =>
annos.getItemAnnotation(id, anno) == val);
// 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));
});
})
}
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"),
@ -87,37 +149,18 @@ add_task(function* test_tracking() {
}
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.");
@ -205,6 +248,11 @@ add_task(function* test_tracker_sql_batching() {
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();
});
@ -220,7 +268,7 @@ add_task(function* test_onItemAdded() {
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();
@ -232,7 +280,7 @@ add_task(function* test_onItemAdded() {
"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();
@ -243,7 +291,7 @@ add_task(function* test_onItemAdded() {
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();
@ -263,7 +311,7 @@ add_task(function* test_async_onItemAdded() {
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();
@ -276,7 +324,7 @@ add_task(function* test_async_onItemAdded() {
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();
@ -288,7 +336,7 @@ add_task(function* test_async_onItemAdded() {
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();
@ -429,7 +477,7 @@ add_task(function* test_onItemTagged() {
// 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();
@ -461,7 +509,7 @@ add_task(function* test_onItemUntagged() {
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();
@ -504,7 +552,7 @@ add_task(function* test_async_onItemUntagged() {
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();
@ -562,7 +610,7 @@ add_task(function* test_async_onItemTagged() {
});
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();
@ -697,7 +745,8 @@ add_task(function* test_onItemPostDataChanged() {
// 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();
@ -773,9 +822,7 @@ add_task(function* test_onItemAdded_filtered_root() {
_("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();
@ -783,7 +830,7 @@ add_task(function* test_onItemAdded_filtered_root() {
});
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();
@ -800,13 +847,9 @@ add_task(function* test_onItemDeleted_filtered_root() {
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();
@ -832,13 +875,15 @@ add_task(function* test_onPageAnnoChanged() {
_("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();
@ -877,7 +922,8 @@ add_task(function* test_onFaviconChanged() {
},
Services.scriptSecurityManager.getSystemPrincipal());
});
yield verifyTrackerEmpty();
yield verifyTrackedItems([]);
do_check_eq(tracker.score, 0);
} finally {
_("Clean up.");
yield cleanup();
@ -901,9 +947,9 @@ add_task(function* test_onLivemarkAdded() {
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();
@ -931,7 +977,7 @@ add_task(function* test_onLivemarkDeleted() {
});
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();
@ -965,13 +1011,14 @@ add_task(function* test_onItemMoved() {
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.");
@ -1017,7 +1064,7 @@ add_task(function* test_async_onItemMoved_update() {
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();
@ -1170,7 +1217,7 @@ add_task(function* test_onItemDeleted_removeFolderTransaction() {
_("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");
@ -1180,13 +1227,13 @@ add_task(function* test_onItemDeleted_removeFolderTransaction() {
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();
@ -1232,7 +1279,7 @@ add_task(function* test_treeMoved() {
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();
@ -1262,7 +1309,7 @@ add_task(function* test_onItemDeleted() {
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();
@ -1294,7 +1341,7 @@ add_task(function* test_async_onItemDeleted() {
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();
@ -1363,18 +1410,22 @@ add_task(function* test_async_onItemDeleted_eraseEverything() {
_(`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();
@ -1416,7 +1467,7 @@ add_task(function* test_onItemDeleted_removeFolderChildren() {
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();
@ -1461,7 +1512,7 @@ add_task(function* test_onItemDeleted_tree() {
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();
@ -1472,30 +1523,34 @@ 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");
@ -1528,8 +1583,8 @@ add_task(function* test_mobile_query() {
"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();