Bug 1274496 - Filter excluded bookmarks at sync time based on their root. r=markh,rnewman

MozReview-Commit-ID: 6xWohLeIMha

--HG--
extra : rebase_source : 122b3026ff9608f5f528665e3c529aa92c010b5a
This commit is contained in:
Kit Cambridge 2016-09-06 11:39:13 -07:00
parent f9e6d19361
commit be1b540390
4 changed files with 421 additions and 117 deletions

View File

@ -7,7 +7,8 @@ this.EXPORTED_SYMBOLS = [
"Engine",
"SyncEngine",
"Tracker",
"Store"
"Store",
"Changeset"
];
var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
@ -131,26 +132,30 @@ Tracker.prototype = {
this._ignored.splice(index, 1);
},
_saveChangedID(id, when) {
this._log.trace(`Adding changed ID: ${id}, ${JSON.stringify(when)}`);
this.changedIDs[id] = when;
this.saveChangedIDs(this.onSavedChangedIDs);
},
addChangedID: function (id, when) {
if (!id) {
this._log.warn("Attempted to add undefined ID to tracker");
return false;
}
if (this.ignoreAll || (id in this._ignored)) {
if (this.ignoreAll || this._ignored.includes(id)) {
return false;
}
// Default to the current time in seconds if no time is provided.
if (when == null) {
when = Math.floor(Date.now() / 1000);
when = Date.now() / 1000;
}
// Add/update the entry if we have a newer time.
if ((this.changedIDs[id] || -Infinity) < when) {
this._log.trace("Adding changed ID: " + id + ", " + when);
this.changedIDs[id] = when;
this.saveChangedIDs(this.onSavedChangedIDs);
this._saveChangedID(id, when);
}
return true;
@ -161,8 +166,9 @@ Tracker.prototype = {
this._log.warn("Attempted to remove undefined ID to tracker");
return false;
}
if (this.ignoreAll || (id in this._ignored))
if (this.ignoreAll || this._ignored.includes(id)) {
return false;
}
if (this.changedIDs[id] != null) {
this._log.trace("Removing changed ID " + id);
delete this.changedIDs[id];
@ -862,9 +868,8 @@ SyncEngine.prototype = {
},
/*
* Returns a mapping of IDs -> changed timestamp. Engine implementations
* can override this method to bypass the tracker for certain or all
* changed items.
* Returns a changeset for this sync. Engine implementations can override this
* method to bypass the tracker for certain or all changed items.
*/
getChangedIDs: function () {
return this._tracker.changedIDs;
@ -932,20 +937,16 @@ SyncEngine.prototype = {
// this._modified to the tracker.
this.lastSyncLocal = Date.now();
if (this.lastSync) {
this._modified = this.getChangedIDs();
this._modified = this.pullNewChanges();
} else {
// Mark all items to be uploaded, but treat them as changed from long ago
this._log.debug("First sync, uploading all items");
this._modified = {};
for (let id in this._store.getAllIDs()) {
this._modified[id] = 0;
}
this._modified = this.pullAllChanges();
}
// Clear the tracker now. If the sync fails we'll add the ones we failed
// to upload back.
this._tracker.clearChangedIDs();
this._log.info(Object.keys(this._modified).length +
this._log.info(this._modified.count() +
" outgoing items pre-reconciliation");
// Keep track of what to delete at the end of sync
@ -1293,12 +1294,12 @@ SyncEngine.prototype = {
// because some state may change during the course of this function and we
// need to operate on the original values.
let existsLocally = this._store.itemExists(item.id);
let locallyModified = item.id in this._modified;
let locallyModified = this._modified.has(item.id);
// TODO Handle clock drift better. Tracked in bug 721181.
let remoteAge = AsyncResource.serverTime - item.modified;
let localAge = locallyModified ?
(Date.now() / 1000 - this._modified[item.id]) : null;
(Date.now() / 1000 - this._modified.getModifiedTimestamp(item.id)) : null;
let remoteIsNewer = remoteAge < localAge;
this._log.trace("Reconciling " + item.id + ". exists=" +
@ -1369,13 +1370,13 @@ SyncEngine.prototype = {
// If the local item was modified, we carry its metadata forward so
// appropriate reconciling can be performed.
if (dupeID in this._modified) {
if (this._modified.has(dupeID)) {
locallyModified = true;
localAge = Date.now() / 1000 - this._modified[dupeID];
localAge = Date.now() / 1000 -
this._modified.getModifiedTimestamp(dupeID);
remoteIsNewer = remoteAge < localAge;
this._modified[item.id] = this._modified[dupeID];
delete this._modified[dupeID];
this._modified.swap(dupeID, item.id);
} else {
locallyModified = false;
localAge = null;
@ -1409,7 +1410,7 @@ SyncEngine.prototype = {
if (remoteIsNewer) {
this._log.trace("Applying incoming because local item was deleted " +
"before the incoming item was changed.");
delete this._modified[item.id];
this._modified.delete(item.id);
return true;
}
@ -1435,7 +1436,7 @@ SyncEngine.prototype = {
this._log.trace("Ignoring incoming item because the local item is " +
"identical.");
delete this._modified[item.id];
this._modified.delete(item.id);
return false;
}
@ -1460,7 +1461,7 @@ SyncEngine.prototype = {
_uploadOutgoing: function () {
this._log.trace("Uploading local changes to server.");
let modifiedIDs = Object.keys(this._modified);
let modifiedIDs = this._modified.ids();
if (modifiedIDs.length) {
this._log.trace("Preparing " + modifiedIDs.length +
" outgoing records");
@ -1504,7 +1505,7 @@ SyncEngine.prototype = {
counts.failed += failed.length;
for (let id of successful) {
delete this._modified[id];
this._modified.delete(id);
}
this._onRecordsWritten(successful, failed);
@ -1588,10 +1589,8 @@ SyncEngine.prototype = {
}
// Mark failed WBOs as changed again so they are reuploaded next time.
for (let [id, when] of Object.entries(this._modified)) {
this._tracker.addChangedID(id, when);
}
this._modified = {};
this.trackRemainingChanges();
this._modified.clear();
},
_sync: function () {
@ -1677,5 +1676,108 @@ SyncEngine.prototype = {
return (this.service.handleHMACEvent() && mayRetry) ?
SyncEngine.kRecoveryStrategy.retry :
SyncEngine.kRecoveryStrategy.error;
}
},
/**
* Returns a changeset containing all items in the store. The default
* implementation returns a changeset with timestamps from long ago, to
* ensure we always use the remote version if one exists.
*
* This function is only called for the first sync. Subsequent syncs call
* `pullNewChanges`.
*
* @return A `Changeset` object.
*/
pullAllChanges() {
let changeset = new Changeset();
for (let id in this._store.getAllIDs()) {
changeset.set(id, 0);
}
return changeset;
},
/*
* Returns a changeset containing entries for all currently tracked items.
* The default implementation returns a changeset with timestamps indicating
* when the item was added to the tracker.
*
* @return A `Changeset` object.
*/
pullNewChanges() {
return new Changeset(this.getChangedIDs());
},
/**
* Adds all remaining changeset entries back to the tracker, typically for
* items that failed to upload. This method is called at the end of each sync.
*
*/
trackRemainingChanges() {
for (let [id, change] of this._modified.entries()) {
this._tracker.addChangedID(id, change);
}
},
};
/**
* A changeset is created for each sync in `Engine::get{Changed, All}IDs`,
* and stores opaque change data for tracked IDs. The default implementation
* only records timestamps, though engines can extend this to store additional
* data for each entry.
*/
class Changeset {
// Creates a changeset with an initial set of tracked entries.
constructor(changes = {}) {
this.changes = changes;
}
// Returns the last modified time, in seconds, for an entry in the changeset.
// `id` is guaranteed to be in the set.
getModifiedTimestamp(id) {
return this.changes[id];
}
// Adds a change for a tracked ID to the changeset.
set(id, change) {
this.changes[id] = change;
}
// Indicates whether an entry is in the changeset.
has(id) {
return id in this.changes;
}
// Deletes an entry from the changeset. Used to clean up entries for
// reconciled and successfully uploaded records.
delete(id) {
delete this.changes[id];
}
// Swaps two entries in the changeset. Used when reconciling duplicates that
// have local changes.
swap(oldID, newID) {
this.changes[newID] = this.changes[oldID];
delete this.changes[oldID];
}
// Returns an array of all tracked IDs in this changeset.
ids() {
return Object.keys(this.changes);
}
// Returns an array of `[id, change]` tuples. Used to repopulate the tracker
// with entries for failed uploads at the end of a sync.
entries() {
return Object.entries(this.changes);
}
// Returns the number of entries in this changeset.
count() {
return this.ids().length;
}
// Clears the changeset.
clear() {
this.changes = {};
}
}

View File

@ -33,6 +33,8 @@ const {
SOURCE_IMPORT_REPLACE,
} = Ci.nsINavBookmarksService;
const SQLITE_MAX_VARIABLE_NUMBER = 999;
// Maps Sync record property names to `PlacesSyncUtils` bookmark properties.
const RECORD_PROPS_TO_BOOKMARK_PROPS = {
title: "title",
@ -426,7 +428,93 @@ BookmarksEngine.prototype = {
// 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() {
let changeset = new BookmarksChangeset();
for (let id in this._store.getAllIDs()) {
changeset.set(id, { modified: 0, deleted: false });
}
return changeset;
},
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(startIndex + SQLITE_MAX_VARIABLE_NUMBER,
modifiedGUIDs.length);
let query = `
WITH RECURSIVE
modifiedGuids(guid) AS (
VALUES ${new Array(chunkLength).fill("(?)").join(", ")}
),
syncedItems(id) AS (
SELECT b.id
FROM moz_bookmarks b
WHERE b.id IN (${getChangeRootIds().join(", ")})
UNION ALL
SELECT b.id
FROM moz_bookmarks b
JOIN syncedItems s ON b.parent = s.id
)
SELECT b.guid, b.id
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, ["id", "guid"]);
for (let { id, guid } of results) {
let syncID = BookmarkSpecialIds.specialGUIDForId(id) || guid;
this._tracker.removeChangedID(syncID);
}
} finally {
statement.finalize();
}
}
return new BookmarksChangeset(this._tracker.changedIDs);
},
// 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 = BookmarkSpecialIds.syncIDToPlacesGUID(syncID);
guids.push(guid);
}
return guids;
},
};
function BookmarksStore(name, engine) {
@ -964,16 +1052,50 @@ 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 itemGuid
* GUID of the bookmark to upload.
* @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) {
_add: function BMT__add(itemId, guid, isTombstone = false) {
guid = BookmarkSpecialIds.specialGUIDForId(itemId) || guid;
if (this.addChangedID(guid))
let info = { modified: Date.now() / 1000, deleted: isTombstone };
if (this.addChangedID(guid, info)) {
this._upScore();
}
},
/* Every add/remove/change will trigger a sync for MULTI_DEVICE (except in
@ -986,59 +1108,10 @@ BookmarksTracker.prototype = {
}
},
/**
* Determine if a change should be ignored.
*
* @param itemId
* Item under consideration to ignore
* @param folder (optional)
* Folder of the item being changed
* @param guid
* Places GUID of the item being changed
* @param source
* A change source constant from `nsINavBookmarksService::SOURCE_*`.
*/
_ignore: function BMT__ignore(itemId, folder, guid, source) {
if (IGNORED_SOURCES.includes(source)) {
return true;
}
// Get the folder id if we weren't given one.
if (folder == null) {
try {
folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
} catch (ex) {
this._log.debug("getFolderIdForItem(" + itemId +
") threw; calling _ensureMobileQuery.");
// I'm guessing that gFIFI can throw, and perhaps that's why
// _ensureMobileQuery is here at all. Try not to call it.
this._ensureMobileQuery();
folder = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
}
}
// Ignore changes to tags (folders under the tags folder).
let tags = BookmarkSpecialIds.tags;
if (folder == tags)
return true;
// Ignore tag items (the actual instance of a tag for a bookmark).
if (PlacesUtils.bookmarks.getFolderIdForItem(folder) == tags)
return true;
// Make sure to remove items that have the exclude annotation.
if (PlacesUtils.annotations.itemHasAnnotation(itemId, BookmarkAnnos.EXCLUDEBACKUP_ANNO)) {
this.removeChangedID(guid);
return true;
}
return false;
},
onItemAdded: function BMT_onItemAdded(itemId, folder, index,
itemType, uri, title, dateAdded,
guid, parentGuid, source) {
if (this._ignore(itemId, folder, guid, source)) {
if (IGNORED_SOURCES.includes(source)) {
return;
}
@ -1049,12 +1122,50 @@ BookmarksTracker.prototype = {
onItemRemoved: function (itemId, parentId, index, type, uri,
guid, parentGuid, source) {
if (this._ignore(itemId, parentId, guid, 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);
this._add(itemId, guid, /* isTombstone */ true);
this._add(parentId, parentGuid);
},
@ -1098,6 +1209,10 @@ BookmarksTracker.prototype = {
onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value,
lastModified, itemType, parentId,
guid, parentGuid, source) {
if (IGNORED_SOURCES.includes(source)) {
return;
}
if (isAnno && (ANNOS_TO_TRACK.indexOf(property) == -1))
// Ignore annotations except for the ones that we sync.
return;
@ -1106,10 +1221,6 @@ BookmarksTracker.prototype = {
if (property == "favicon")
return;
if (this._ignore(itemId, parentId, guid, source)) {
return;
}
this._log.trace("onItemChanged: " + itemId +
(", " + property + (isAnno? " (anno)" : "")) +
(value ? (" = \"" + value + "\"") : ""));
@ -1120,7 +1231,7 @@ BookmarksTracker.prototype = {
newParent, newIndex, itemType,
guid, oldParentGuid, newParentGuid,
source) {
if (this._ignore(itemId, newParent, guid, source)) {
if (IGNORED_SOURCES.includes(source)) {
return;
}
@ -1146,3 +1257,26 @@ 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() {
let rootIds = [
PlacesUtils.bookmarksMenuFolderId,
PlacesUtils.toolbarFolderId,
PlacesUtils.unfiledBookmarksFolderId,
];
let mobileRootId = BookmarkSpecialIds.findMobileRoot(false);
if (mobileRootId) {
rootIds.push(mobileRootId);
}
return rootIds;
}
class BookmarksChangeset extends Changeset {
getModifiedTimestamp(id) {
let change = this.changes[id];
return change ? change.modified : Number.NaN;
}
}

View File

@ -311,7 +311,7 @@ ClientEngine.prototype = {
const clientWithPendingCommands = Object.keys(this._currentlySyncingCommands);
for (let clientId of clientWithPendingCommands) {
if (this._store._remoteClients[clientId] || this.localID == clientId) {
this._modified[clientId] = 0;
this._modified.set(clientId, 0);
}
}
SyncEngine.prototype._uploadOutgoing.call(this);

View File

@ -22,8 +22,9 @@ const DAY_IN_MS = 24 * 60 * 60 * 1000;
// Test helpers.
function* verifyTrackerEmpty() {
do_check_empty(tracker.changedIDs);
do_check_eq(tracker.score, 0);
let changes = engine.pullNewChanges();
equal(changes.count(), 0);
equal(tracker.score, 0);
}
function* resetTracker() {
@ -48,9 +49,12 @@ function* stopTracking() {
}
function* verifyTrackedItems(tracked) {
let trackedIDs = new Set(Object.keys(tracker.changedIDs));
let changes = engine.pullNewChanges();
let trackedIDs = new Set(changes.ids());
for (let guid of tracked) {
ok(tracker.changedIDs[guid] > 0, `${guid} should be tracked`);
ok(changes.has(guid), `${guid} should be tracked`);
ok(changes.getModifiedTimestamp(guid) > 0,
`${guid} should have a modified time`);
trackedIDs.delete(guid);
}
equal(trackedIDs.size, 0, `Unhandled tracked IDs: ${
@ -58,7 +62,8 @@ function* verifyTrackedItems(tracked) {
}
function* verifyTrackedCount(expected) {
do_check_attribute_count(tracker.changedIDs, expected);
let changes = engine.pullNewChanges();
equal(changes.count(), expected);
}
add_task(function* test_tracking() {
@ -389,7 +394,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);
do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 5);
} finally {
_("Clean up.");
yield cleanup();
@ -522,7 +527,7 @@ add_task(function* test_async_onItemTagged() {
});
yield verifyTrackedItems([fxBmk1.guid, fxBmk2.guid]);
do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);
} finally {
_("Clean up.");
yield cleanup();
@ -700,31 +705,73 @@ add_task(function* test_onItemAnnoChanged() {
}
});
add_task(function* test_onItemExcluded() {
_("Items excluded from backups should not be tracked");
add_task(function* test_onItemAdded_filtered_root() {
_("Items outside the change roots should not be tracked");
try {
yield startTracking();
_("Create a new root");
let rootID = PlacesUtils.bookmarks.createFolder(
PlacesUtils.bookmarks.placesRoot,
"New root",
PlacesUtils.bookmarks.DEFAULT_INDEX);
let rootGUID = engine._store.GUIDForId(rootID);
_(`New root GUID: ${rootGUID}`);
_("Insert a bookmark underneath the new root");
let untrackedBmkID = PlacesUtils.bookmarks.insertBookmark(
rootID,
Utils.makeURI("http://getthunderbird.com"),
PlacesUtils.bookmarks.DEFAULT_INDEX,
"Get Thunderbird!");
let untrackedBmkGUID = engine._store.GUIDForId(untrackedBmkID);
_(`New untracked bookmark GUID: ${untrackedBmkGUID}`);
_("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}`);
_("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);
} finally {
_("Clean up.");
yield cleanup();
}
});
add_task(function* test_onItemDeleted_filtered_root() {
_("Deleted items outside the change roots should be tracked");
try {
yield stopTracking();
_("Create a bookmark");
let b = PlacesUtils.bookmarks.insertBookmark(
PlacesUtils.bookmarks.bookmarksMenuFolder,
_("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 bGUID = engine._store.GUIDForId(b);
let rootBmkGUID = engine._store.GUIDForId(rootBmkID);
_(`New Places root bookmark GUID: ${rootBmkGUID}`);
yield startTracking();
_("Exclude the bookmark from backups");
PlacesUtils.annotations.setItemAnnotation(
b, BookmarkAnnos.EXCLUDEBACKUP_ANNO, "Don't back this up", 0,
PlacesUtils.annotations.EXPIRE_NEVER);
PlacesUtils.bookmarks.removeItem(rootBmkID);
_("Modify the bookmark");
PlacesUtils.bookmarks.setItemTitle(b, "Download Firefox");
_("Excluded items should be ignored");
yield verifyTrackerEmpty();
// 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);
} finally {
_("Clean up.");
yield cleanup();
@ -1254,22 +1301,43 @@ add_task(function* test_async_onItemDeleted_eraseEverything() {
url: "https://developer.mozilla.org",
title: "MDN",
});
_(`MDN GUID: ${mdnBmk.guid}`);
let bugsFolder = yield PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
title: "Bugs",
});
_(`Bugs folder GUID: ${bugsFolder.guid}`);
let bzBmk = yield PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: bugsFolder.guid,
url: "https://bugzilla.mozilla.org",
title: "Bugzilla",
});
_(`Bugzilla GUID: ${bzBmk.guid}`);
let bugsChildFolder = yield PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: bugsFolder.guid,
title: "Bugs child",
});
_(`Bugs child GUID: ${bugsChildFolder.guid}`);
let bugsGrandChildBmk = yield PlacesUtils.bookmarks.insert({
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: bugsChildFolder.guid,
url: "https://example.com",
title: "Bugs grandchild",
});
_(`Bugs grandchild GUID: ${bugsGrandChildBmk.guid}`);
yield startTracking();
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.
yield verifyTrackedItems(["menu", mozBmk.guid, mdnBmk.guid, "toolbar",
bugsFolder.guid]);
do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 6);