Bug 1286915 - Implement a validator for the sync addons engine. r=markh

MozReview-Commit-ID: 2sGNs9BUoXt

--HG--
extra : transplant_source : %FBJ%F9%D5%B3%95%E9%A6%10M%CBV%3A%08y%A1%DFsC%DD
This commit is contained in:
Thom Chiovoloni 2016-09-22 17:29:01 -04:00
Родитель 919d732c59
Коммит 3e4ccfdb57
6 изменённых файлов: 148 добавлений и 53 удалений

Просмотреть файл

@ -44,6 +44,7 @@ Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js"); Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/util.js"); Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js"); Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/collection_validator.js");
Cu.import("resource://services-common/async.js"); Cu.import("resource://services-common/async.js");
Cu.import("resource://gre/modules/Preferences.jsm"); Cu.import("resource://gre/modules/Preferences.jsm");
@ -53,7 +54,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
"resource://gre/modules/addons/AddonRepository.jsm"); "resource://gre/modules/addons/AddonRepository.jsm");
this.EXPORTED_SYMBOLS = ["AddonsEngine"]; this.EXPORTED_SYMBOLS = ["AddonsEngine", "AddonValidator"];
// 7 days in milliseconds. // 7 days in milliseconds.
const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000; const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
@ -176,7 +177,7 @@ AddonsEngine.prototype = {
continue; continue;
} }
if (!this._store.isAddonSyncable(addons[id])) { if (!this.isAddonSyncable(addons[id])) {
continue; continue;
} }
@ -234,6 +235,10 @@ AddonsEngine.prototype = {
let cb = Async.makeSpinningCallback(); let cb = Async.makeSpinningCallback();
this._reconciler.refreshGlobalState(cb); this._reconciler.refreshGlobalState(cb);
cb.wait(); cb.wait();
},
isAddonSyncable(addon, ignoreRepoCheck) {
return this._store.isAddonSyncable(addon, ignoreRepoCheck);
} }
}; };
@ -533,9 +538,12 @@ AddonsStore.prototype = {
* *
* @param addon * @param addon
* Addon instance * Addon instance
* @param ignoreRepoCheck
* Should we skip checking the Addons repository (primarially useful
* for testing and validation).
* @return Boolean indicating whether it is appropriate for Sync * @return Boolean indicating whether it is appropriate for Sync
*/ */
isAddonSyncable: function isAddonSyncable(addon) { isAddonSyncable: function isAddonSyncable(addon, ignoreRepoCheck = false) {
// Currently, we limit syncable add-ons to those that are: // Currently, we limit syncable add-ons to those that are:
// 1) In a well-defined set of types // 1) In a well-defined set of types
// 2) Installed in the current profile // 2) Installed in the current profile
@ -592,8 +600,9 @@ AddonsStore.prototype = {
// If the AddonRepository's cache isn't enabled (which it typically isn't // If the AddonRepository's cache isn't enabled (which it typically isn't
// in tests), getCachedAddonByID always returns null - so skip the check // in tests), getCachedAddonByID always returns null - so skip the check
// in that case. // in that case. We also provide a way to specifically opt-out of the check
if (!AddonRepository.cacheEnabled) { // even if the cache is enabled, which is used by the validators.
if (ignoreRepoCheck || !AddonRepository.cacheEnabled) {
return true; return true;
} }
@ -736,3 +745,69 @@ AddonsTracker.prototype = {
this.reconciler.stopListening(); this.reconciler.stopListening();
}, },
}; };
class AddonValidator extends CollectionValidator {
constructor(engine = null) {
super("addons", "id", [
"addonID",
"enabled",
"applicationID",
"source"
]);
this.engine = engine;
}
getClientItems() {
return Promise.all([
new Promise(resolve =>
AddonManager.getAllAddons(resolve)),
new Promise(resolve =>
AddonManager.getAddonsWithOperationsByTypes(["extension", "theme"], resolve)),
]).then(([installed, addonsWithPendingOperation]) => {
// Addons pending install won't be in the first list, but addons pending
// uninstall/enable/disable will be in both lists.
let all = new Map(installed.map(addon => [addon.id, addon]));
for (let addon of addonsWithPendingOperation) {
all.set(addon.id, addon);
}
// Convert to an array since Map.prototype.values returns an iterable
return [...all.values()];
});
}
normalizeClientItem(item) {
let enabled = !item.userDisabled;
if (item.pendingOperations & AddonManager.PENDING_ENABLE) {
enabled = true;
} else if (item.pendingOperations & AddonManager.PENDING_DISABLE) {
enabled = false;
}
return {
enabled,
id: item.syncGUID,
addonID: item.id,
applicationID: Services.appinfo.ID,
source: "amo", // check item.foreignInstall?
original: item
};
}
normalizeServerItem(item) {
let guid = this.engine._findDupe(item);
if (guid) {
item.id = guid;
}
return item;
}
clientUnderstands(item) {
return item.applicationID === Services.appinfo.ID;
}
syncedByClient(item) {
return !item.original.hidden &&
!item.original.isSystem &&
!(item.original.pendingOperations & AddonManager.PENDING_UNINSTALL) &&
this.engine.isAddonSyncable(item.original, true);
}
}

Просмотреть файл

@ -33,7 +33,8 @@ Phase("phase01", [
[Sync] [Sync]
]); ]);
Phase("phase02", [ Phase("phase02", [
[Addons.verify, [id], STATE_ENABLED] [Addons.verify, [id], STATE_ENABLED],
[Sync]
]); ]);
Phase("phase03", [ Phase("phase03", [
[Addons.verifyNot, [id]], [Addons.verifyNot, [id]],
@ -41,6 +42,7 @@ Phase("phase03", [
]); ]);
Phase("phase04", [ Phase("phase04", [
[Addons.verify, [id], STATE_ENABLED], [Addons.verify, [id], STATE_ENABLED],
[Sync]
]); ]);
// Now we disable the add-on // Now we disable the add-on
@ -51,13 +53,15 @@ Phase("phase05", [
]); ]);
Phase("phase06", [ Phase("phase06", [
[Addons.verify, [id], STATE_DISABLED], [Addons.verify, [id], STATE_DISABLED],
[Sync]
]); ]);
Phase("phase07", [ Phase("phase07", [
[Addons.verify, [id], STATE_ENABLED], [Addons.verify, [id], STATE_ENABLED],
[Sync] [Sync]
]); ]);
Phase("phase08", [ Phase("phase08", [
[Addons.verify, [id], STATE_DISABLED] [Addons.verify, [id], STATE_DISABLED],
[Sync]
]); ]);
// Now we re-enable it again. // Now we re-enable it again.
@ -68,13 +72,15 @@ Phase("phase09", [
]); ]);
Phase("phase10", [ Phase("phase10", [
[Addons.verify, [id], STATE_ENABLED], [Addons.verify, [id], STATE_ENABLED],
[Sync]
]); ]);
Phase("phase11", [ Phase("phase11", [
[Addons.verify, [id], STATE_DISABLED], [Addons.verify, [id], STATE_DISABLED],
[Sync] [Sync]
]); ]);
Phase("phase12", [ Phase("phase12", [
[Addons.verify, [id], STATE_ENABLED] [Addons.verify, [id], STATE_ENABLED],
[Sync]
]); ]);
// And we uninstall it // And we uninstall it
@ -86,12 +92,14 @@ Phase("phase13", [
[Sync] [Sync]
]); ]);
Phase("phase14", [ Phase("phase14", [
[Addons.verifyNot, [id]] [Addons.verifyNot, [id]],
[Sync]
]); ]);
Phase("phase15", [ Phase("phase15", [
[Addons.verify, [id], STATE_ENABLED], [Addons.verify, [id], STATE_ENABLED],
[Sync] [Sync]
]); ]);
Phase("phase16", [ Phase("phase16", [
[Addons.verifyNot, [id]] [Addons.verifyNot, [id]],
[Sync]
]); ]);

Просмотреть файл

@ -34,6 +34,9 @@ Phase("phase02", [
Phase("phase03", [ Phase("phase03", [
[Sync], // Get GUID updates, potentially. [Sync], // Get GUID updates, potentially.
[Addons.setEnabled, [id], STATE_DISABLED], [Addons.setEnabled, [id], STATE_DISABLED],
// We've changed the state, but don't want this profile to sync until phase5,
// so if we ran a validation now we'd be expecting to find errors.
[Addons.skipValidation]
]); ]);
Phase("phase04", [ Phase("phase04", [
[EnsureTracking], [EnsureTracking],

Просмотреть файл

@ -25,5 +25,6 @@ Phase("phase1", [
Phase("phase2", [ Phase("phase2", [
// Add-on should be present after restart // Add-on should be present after restart
[Addons.verify, [id], STATE_ENABLED] [Addons.verify, [id], STATE_ENABLED],
[Sync] // Sync to ensure everything is initialized enough for the addon validator to run
]); ]);

Просмотреть файл

@ -30,5 +30,6 @@ Phase("phase02", [
]); ]);
Phase("phase03", [ Phase("phase03", [
[Addons.verify, [id1], STATE_ENABLED], [Addons.verify, [id1], STATE_ENABLED],
[Addons.verify, [id2], STATE_ENABLED] [Addons.verify, [id2], STATE_ENABLED],
[Sync] // Sync to ensure that the addon validator can run without error
]); ]);

Просмотреть файл

@ -26,6 +26,7 @@ Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/bookmark_validator.js"); Cu.import("resource://services-sync/bookmark_validator.js");
Cu.import("resource://services-sync/engines/passwords.js"); Cu.import("resource://services-sync/engines/passwords.js");
Cu.import("resource://services-sync/engines/forms.js"); Cu.import("resource://services-sync/engines/forms.js");
Cu.import("resource://services-sync/engines/addons.js");
// TPS modules // TPS modules
Cu.import("resource://tps/logger.jsm"); Cu.import("resource://tps/logger.jsm");
@ -113,6 +114,7 @@ var TPS = {
_triggeredSync: false, _triggeredSync: false,
_usSinceEpoch: 0, _usSinceEpoch: 0,
_requestedQuit: false, _requestedQuit: false,
shouldValidateAddons: false,
shouldValidateBookmarks: false, shouldValidateBookmarks: false,
shouldValidatePasswords: false, shouldValidatePasswords: false,
shouldValidateForms: false, shouldValidateForms: false,
@ -464,6 +466,7 @@ var TPS = {
}, },
HandleAddons: function (addons, action, state) { HandleAddons: function (addons, action, state) {
this.shouldValidateAddons = true;
for (let entry of addons) { for (let entry of addons) {
Logger.logInfo("executing action " + action.toUpperCase() + Logger.logInfo("executing action " + action.toUpperCase() +
" on addon " + JSON.stringify(entry)); " on addon " + JSON.stringify(entry));
@ -661,66 +664,64 @@ var TPS = {
Logger.logInfo("Bookmark validation finished"); Logger.logInfo("Bookmark validation finished");
}, },
ValidatePasswords() { ValidateCollection(engineName, ValidatorType) {
let serverRecordDumpStr;
try {
Logger.logInfo("About to perform password validation");
let pwEngine = Weave.Service.engineManager.get("passwords");
let validator = new PasswordValidator();
let serverRecords = validator.getServerItems(pwEngine);
let clientRecords = Async.promiseSpinningly(validator.getClientItems());
serverRecordDumpStr = JSON.stringify(serverRecords);
let { problemData } = validator.compareClientWithServer(clientRecords, serverRecords);
for (let { name, count } of problemData.getSummary()) {
if (count) {
Logger.logInfo(`Validation problem: "${name}": ${JSON.stringify(problemData[name])}`);
}
Logger.AssertEqual(count, 0, `Password validation error of type ${name}`);
}
} catch (e) {
// Dump the client records (should always be doable)
DumpPasswords();
// Dump the server records if gotten them already.
if (serverRecordDumpStr) {
Logger.logInfo("Server password records:\n" + serverRecordDumpStr + "\n");
}
this.DumpError("Password validation failed", e);
}
Logger.logInfo("Password validation finished");
},
ValidateForms() {
let serverRecordDumpStr; let serverRecordDumpStr;
let clientRecordDumpStr; let clientRecordDumpStr;
try { try {
Logger.logInfo("About to perform form validation"); Logger.logInfo(`About to perform validation for "${engineName}"`);
let engine = Weave.Service.engineManager.get("forms"); let engine = Weave.Service.engineManager.get(engineName);
let validator = new FormValidator(); let validator = new ValidatorType(engine);
let serverRecords = validator.getServerItems(engine); let serverRecords = validator.getServerItems(engine);
let clientRecords = Async.promiseSpinningly(validator.getClientItems()); let clientRecords = Async.promiseSpinningly(validator.getClientItems());
clientRecordDumpStr = JSON.stringify(clientRecords, undefined, 2); try {
serverRecordDumpStr = JSON.stringify(serverRecords, undefined, 2); // This substantially improves the logs for addons while not making a
// substantial difference for the other two
clientRecordDumpStr = JSON.stringify(clientRecords.map(r => {
let res = validator.normalizeClientItem(r);
delete res.original; // Try and prevent cyclic references
return res;
}));
} catch (e) {
// ignore the error, the dump string is just here to make debugging easier.
clientRecordDumpStr = "<Cyclic value>";
}
try {
serverRecordDumpStr = JSON.stringify(serverRecords);
} catch (e) {
// as above
serverRecordDumpStr = "<Cyclic value>";
}
let { problemData } = validator.compareClientWithServer(clientRecords, serverRecords); let { problemData } = validator.compareClientWithServer(clientRecords, serverRecords);
for (let { name, count } of problemData.getSummary()) { for (let { name, count } of problemData.getSummary()) {
if (count) { if (count) {
Logger.logInfo(`Validation problem: "${name}": ${JSON.stringify(problemData[name])}`); Logger.logInfo(`Validation problem: "${name}": ${JSON.stringify(problemData[name])}`);
} }
Logger.AssertEqual(count, 0, `Form validation error of type ${name}`); Logger.AssertEqual(count, 0, `Validation error for "${engineName}" of type "${name}"`);
} }
} catch (e) { } catch (e) {
// Dump the client records if possible // Dump the client records if possible
if (clientRecordDumpStr) { if (clientRecordDumpStr) {
Logger.logInfo("Client forms records:\n" + clientRecordDumpStr + "\n"); Logger.logInfo(`Client state for ${engineName}:\n${clientRecordDumpStr}\n`);
} }
// Dump the server records if gotten them already. // Dump the server records if gotten them already.
if (serverRecordDumpStr) { if (serverRecordDumpStr) {
Logger.logInfo("Server forms records:\n" + serverRecordDumpStr + "\n"); Logger.logInfo(`Server state for ${engineName}:\n${serverRecordDumpStr}\n`);
} }
this.DumpError("Form validation failed", e); this.DumpError(`Validation failed for ${engineName}`, e);
} }
Logger.logInfo("Form validation finished"); Logger.logInfo(`Validation finished for ${engineName}`);
},
ValidatePasswords() {
return this.ValidateCollection("passwords", PasswordValidator);
},
ValidateForms() {
return this.ValidateCollection("forms", FormValidator);
},
ValidateAddons() {
return this.ValidateCollection("addons", AddonValidator);
}, },
RunNextTestAction: function() { RunNextTestAction: function() {
@ -737,6 +738,9 @@ var TPS = {
if (this.shouldValidateForms) { if (this.shouldValidateForms) {
this.ValidateForms(); this.ValidateForms();
} }
if (this.shouldValidateAddons) {
this.ValidateAddons();
}
// we're all done // we're all done
Logger.logInfo("test phase " + this._currentPhase + ": " + Logger.logInfo("test phase " + this._currentPhase + ": " +
(this._errors ? "FAIL" : "PASS")); (this._errors ? "FAIL" : "PASS"));
@ -1137,6 +1141,9 @@ var Addons = {
verifyNot: function Addons__verifyNot(addons) { verifyNot: function Addons__verifyNot(addons) {
TPS.HandleAddons(addons, ACTION_VERIFY_NOT); TPS.HandleAddons(addons, ACTION_VERIFY_NOT);
}, },
skipValidation() {
TPS.shouldValidateAddons = false;
}
}; };
var Bookmarks = { var Bookmarks = {