Bug 1286923 - Implement sync validator for forms and integrate with TPS r?markh
MozReview-Commit-ID: psZteJajT
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -1,24 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-this.EXPORTED_SYMBOLS = ['FormEngine', 'FormRec'];
+this.EXPORTED_SYMBOLS = ['FormEngine', 'FormRec', 'FormValidator'];
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/collection_validator.js");
Cu.import("resource://gre/modules/Log.jsm");
const FORMS_TTL = 3 * 365 * 24 * 60 * 60; // Three years in seconds.
this.FormRec = function FormRec(collection, id) {
CryptoWrapper.call(this, collection, id);
}
FormRec.prototype = {
@@ -31,30 +32,34 @@ Utils.deferGetSet(FormRec, "cleartext",
var FormWrapper = {
_log: Log.repository.getLogger("Sync.Engine.Forms"),
_getEntryCols: ["fieldname", "value"],
_guidCols: ["guid"],
+ _promiseSearch: function(terms, searchData) {
+ return new Promise(resolve => {
+ let results = [];
+ let callbacks = {
+ handleResult(result) {
+ results.push(result);
+ },
+ handleCompletion(reason) {
+ resolve(results);
+ }
+ };
+ Svc.FormHistory.search(terms, searchData, callbacks);
+ })
+ },
+
// Do a "sync" search by spinning the event loop until it completes.
_searchSpinningly: function(terms, searchData) {
- let results = [];
- let cb = Async.makeSpinningCallback();
- let callbacks = {
- handleResult: function(result) {
- results.push(result);
- },
- handleCompletion: function(reason) {
- cb(null, results);
- }
- };
- Svc.FormHistory.search(terms, searchData, callbacks);
- return cb.wait();
+ return Async.promiseSpinningly(this._promiseSearch(terms, searchData));
},
_updateSpinningly: function(changes) {
if (!Svc.FormHistory.enabled) {
return; // update isn't going to do anything.
}
let cb = Async.makeSpinningCallback();
let callbacks = {
@@ -239,8 +244,61 @@ FormTracker.prototype = {
}
},
trackEntry: function (guid) {
this.addChangedID(guid);
this.score += SCORE_INCREMENT_MEDIUM;
},
};
+
+
+class FormsProblemData extends CollectionProblemData {
+ getSummary() {
+ // We don't support syncing deleted form data, so "clientMissing" isn't a problem
+ return super.getSummary().filter(entry =>
+ entry.name !== "clientMissing");
+ }
+}
+
+class FormValidator extends CollectionValidator {
+ constructor() {
+ super("forms", "id", ["name", "value"]);
+ }
+
+ emptyProblemData() {
+ return new FormsProblemData();
+ }
+
+ getClientItems() {
+ return FormWrapper._promiseSearch(["guid", "fieldname", "value"], {});
+ }
+
+ normalizeClientItem(item) {
+ return {
+ id: item.guid,
+ guid: item.guid,
+ name: item.fieldname,
+ fieldname: item.fieldname,
+ value: item.value,
+ original: item,
+ };
+ }
+
+ normalizeServerItem(item) {
+ let res = Object.assign({
+ guid: item.id,
+ fieldname: item.name,
+ original: item,
+ }, item);
+ // Missing `name` or `value` causes the getGUID call to throw
+ if (item.name !== undefined && item.value !== undefined) {
+ let guid = FormWrapper.getGUID(item.name, item.value);
+ if (guid) {
+ res.guid = guid;
+ res.id = guid;
+ res.duped = true;
+ }
+ }
+
+ return res;
+ }
+}
\ No newline at end of file
--- a/services/sync/tps/extensions/tps/resource/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -20,16 +20,17 @@ Cu.import("resource://gre/modules/Servic
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/main.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/bookmark_validator.js");
Cu.import("resource://services-sync/engines/passwords.js");
+Cu.import("resource://services-sync/engines/forms.js");
// TPS modules
Cu.import("resource://tps/logger.jsm");
// Module wrappers for tests
Cu.import("resource://tps/modules/addons.jsm");
Cu.import("resource://tps/modules/bookmarks.jsm");
Cu.import("resource://tps/modules/forms.jsm");
Cu.import("resource://tps/modules/history.jsm");
@@ -109,16 +110,17 @@ var TPS = {
_tabsAdded: 0,
_tabsFinished: 0,
_test: null,
_triggeredSync: false,
_usSinceEpoch: 0,
_requestedQuit: false,
shouldValidateBookmarks: false,
shouldValidatePasswords: false,
+ shouldValidateForms: false,
_init: function TPS__init() {
// Check if Firefox Accounts is enabled
let service = Cc["@mozilla.org/weave/service;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
this.fxaccounts_enabled = service.fxAccountsEnabled;
@@ -353,16 +355,17 @@ var TPS = {
default:
Logger.AssertTrue(false, "invalid action: " + action);
}
}
Logger.logPass("executing action " + action.toUpperCase() + " on pref");
},
HandleForms: function (data, action) {
+ this.shouldValidateForms = true;
for (let datum of data) {
Logger.logInfo("executing action " + action.toUpperCase() +
" on form entry " + JSON.stringify(datum));
let formdata = new FormData(datum, this._usSinceEpoch);
switch(action) {
case ACTION_ADD:
formdata.Create();
break;
@@ -684,27 +687,62 @@ var TPS = {
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 clientRecordDumpStr;
+ try {
+ Logger.logInfo("About to perform form validation");
+ let engine = Weave.Service.engineManager.get("forms");
+ let validator = new FormValidator();
+ let serverRecords = validator.getServerItems(engine);
+ let clientRecords = Async.promiseSpinningly(validator.getClientItems());
+ clientRecordDumpStr = JSON.stringify(clientRecords);
+ 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, `Form validation error of type ${name}`);
+ }
+ } catch (e) {
+ // Dump the client records if possible
+ if (clientRecordDumpStr) {
+ Logger.logInfo("Client forms records:\n" + clientRecordDumpStr + "\n");
+ }
+ // Dump the server records if gotten them already.
+ if (serverRecordDumpStr) {
+ Logger.logInfo("Server forms records:\n" + serverRecordDumpStr + "\n");
+ }
+ this.DumpError("Form validation failed", e);
+ }
+ Logger.logInfo("Form validation finished");
+ },
+
RunNextTestAction: function() {
try {
if (this._currentAction >=
this._phaselist[this._currentPhase].length) {
// Run necessary validations and then finish up
if (this.shouldValidateBookmarks) {
this.ValidateBookmarks();
}
if (this.shouldValidatePasswords) {
this.ValidatePasswords();
}
+ if (this.shouldValidateForms) {
+ this.ValidateForms();
+ }
// we're all done
Logger.logInfo("test phase " + this._currentPhase + ": " +
(this._errors ? "FAIL" : "PASS"));
this._phaseFinished = true;
this.quit();
return;
}