Bug 1286923 - Implement sync validator for forms and integrate with TPS r=markh
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Thu, 08 Sep 2016 11:14:30 -0400
changeset 357648 887a66cc1f7749cb5d354ea0600f0b493d2b9c19
parent 357647 367e18f42d597ebe2f93ec7681ce48fa43ab66e2
child 357649 2c7ba40559c6d8e0b2e30abcee5c225fe5b57577
push id1324
push usermtabara@mozilla.com
push dateMon, 16 Jan 2017 13:07:44 +0000
treeherdermozilla-release@a01c49833940 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1286923
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1286923 - Implement sync validator for forms and integrate with TPS r=markh MozReview-Commit-ID: psZteJajT
services/sync/modules/engines/forms.js
services/sync/tps/extensions/tps/resource/tps.jsm
--- 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;
       }