Bug 1286923 - Implement sync validator for forms and integrate with TPS r?markh draft
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Thu, 08 Sep 2016 11:14:30 -0400
changeset 413835 6640e2d1ca96156ef979d33517be8a4cfe90c68e
parent 413698 e95f8d487fbcc97766b5456ae12eb524563f4900
child 531318 ca4fd48be2bfcd69036763f3968b2f5984f36428
push id29530
push userbmo:tchiovoloni@mozilla.com
push dateThu, 15 Sep 2016 00:21:40 +0000
reviewersmarkh
bugs1286923
milestone51.0a1
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;
       }