Bug 966434 - Refactor tps for fxaccounts and old sync authentication support. r=jgriffin
authorHenrik Skupin <mail@hskupin.info>
Fri, 14 Mar 2014 21:13:38 +0100
changeset 191928 500bcf9f9d3a5986a096f98c4bf74560a5b2231d
parent 191927 893b864b4b187390cf5eb2488b979ec706652832
child 191929 a582eefb45b211e65d24df5f2af9ea5da27bb78b
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjgriffin
bugs966434
milestone30.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 966434 - Refactor tps for fxaccounts and old sync authentication support. r=jgriffin
services/sync/modules/service.js
services/sync/tests/tps/mozmill_sanity.js
services/sync/tps/extensions/tps/chrome.manifest
services/sync/tps/extensions/tps/components/tps-cmdline.js
services/sync/tps/extensions/tps/install.rdf
services/sync/tps/extensions/tps/modules/addons.jsm
services/sync/tps/extensions/tps/modules/bookmarks.jsm
services/sync/tps/extensions/tps/modules/forms.jsm
services/sync/tps/extensions/tps/modules/fxaccounts.jsm
services/sync/tps/extensions/tps/modules/history.jsm
services/sync/tps/extensions/tps/modules/logger.jsm
services/sync/tps/extensions/tps/modules/passwords.jsm
services/sync/tps/extensions/tps/modules/prefs.jsm
services/sync/tps/extensions/tps/modules/quit.js
services/sync/tps/extensions/tps/modules/sync.jsm
services/sync/tps/extensions/tps/modules/tabs.jsm
services/sync/tps/extensions/tps/modules/tps.jsm
services/sync/tps/extensions/tps/modules/windows.jsm
services/sync/tps/extensions/tps/resource/fxaccounts.jsm
services/sync/tps/extensions/tps/resource/logger.jsm
services/sync/tps/extensions/tps/resource/modules/addons.jsm
services/sync/tps/extensions/tps/resource/modules/bookmarks.jsm
services/sync/tps/extensions/tps/resource/modules/forms.jsm
services/sync/tps/extensions/tps/resource/modules/history.jsm
services/sync/tps/extensions/tps/resource/modules/passwords.jsm
services/sync/tps/extensions/tps/resource/modules/prefs.jsm
services/sync/tps/extensions/tps/resource/modules/tabs.jsm
services/sync/tps/extensions/tps/resource/modules/windows.jsm
services/sync/tps/extensions/tps/resource/mozmill/sync.jsm
services/sync/tps/extensions/tps/resource/quit.js
services/sync/tps/extensions/tps/resource/tps.jsm
testing/tps/README
testing/tps/config/README.txt
testing/tps/config/config.json.in
testing/tps/setup.py
testing/tps/tps/__init__.py
testing/tps/tps/cli.py
testing/tps/tps/firefoxrunner.py
testing/tps/tps/mozhttpd.py
testing/tps/tps/phase.py
testing/tps/tps/testrunner.py
testing/tps/tps/thread.py
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -965,17 +965,16 @@ Sync11Service.prototype = {
       cb.wait();
 
       // Calling login() with parameters when the client was
       // previously not configured means setup was completed.
       if (initialStatus == CLIENT_NOT_CONFIGURED
           && (username || password || passphrase)) {
         Svc.Obs.notify("weave:service:setup-complete");
       }
-
       this._log.info("Logging in user " + this.identity.username);
       this._updateCachedURLs();
 
       if (!this.verifyLogin()) {
         // verifyLogin sets the failure states here.
         throw "Login failed: " + this.status.login;
       }
 
--- a/services/sync/tests/tps/mozmill_sanity.js
+++ b/services/sync/tests/tps/mozmill_sanity.js
@@ -1,13 +1,13 @@
 /* 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/. */
 
-Components.utils.import('resource://tps/sync.jsm');
+Components.utils.import('resource://tps/mozmill/sync.jsm');
 
 var setupModule = function(module) {
   controller = mozmill.getBrowserController();
   assert.ok(true, "SetupModule passes");
 }
 
 var setupTest = function(module) {
   assert.ok(true, "SetupTest passes");
--- a/services/sync/tps/extensions/tps/chrome.manifest
+++ b/services/sync/tps/extensions/tps/chrome.manifest
@@ -1,4 +1,5 @@
-resource tps modules/
+resource tps resource/
+
 component {4e5bd3f0-41d3-11df-9879-0800200c9a66} components/tps-cmdline.js
 contract @mozilla.org/commandlinehandler/general-startup;1?type=tps {4e5bd3f0-41d3-11df-9879-0800200c9a66}
 category command-line-handler m-tps @mozilla.org/commandlinehandler/general-startup;1?type=tps
--- a/services/sync/tps/extensions/tps/components/tps-cmdline.js
+++ b/services/sync/tps/extensions/tps/components/tps-cmdline.js
@@ -17,18 +17,18 @@ const nsICommandLine                 = C
 const nsICommandLineHandler          = Components.interfaces.nsICommandLineHandler;
 const nsIComponentRegistrar          = Components.interfaces.nsIComponentRegistrar;
 const nsISupportsString              = Components.interfaces.nsISupportsString;
 const nsIWindowWatcher               = Components.interfaces.nsIWindowWatcher;
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 function TPSCmdLineHandler() {}
-TPSCmdLineHandler.prototype =
-{
+
+TPSCmdLineHandler.prototype = {
   classDescription: "TPSCmdLineHandler",
   classID         : TPS_CMDLINE_CLSID,
   contractID      : TPS_CMDLINE_CONTRACTID,
 
   QueryInterface: XPCOMUtils.generateQI([nsISupports,
                                          nsICommandLineHandler,
                                          nsICmdLineHandler]),   /* nsISupports */
 
@@ -44,17 +44,17 @@ TPSCmdLineHandler.prototype =
   handle : function handler_handle(cmdLine) {
     let options = {};
 
     let uristr = cmdLine.handleFlagWithParam("tps", false);
     if (uristr == null)
         return;
     let phase = cmdLine.handleFlagWithParam("tpsphase", false);
     if (phase == null)
-        throw("must specify --tpsphase with --tps");
+        throw Error("must specify --tpsphase with --tps");
     let logfile = cmdLine.handleFlagWithParam("tpslogfile", false);
     if (logfile == null)
         logfile = "";
 
     options.ignoreUnusedEngines = cmdLine.handleFlag("ignore-unused-engines",
                                                      false);
 
 
@@ -74,33 +74,29 @@ TPSCmdLineHandler.prototype =
 
   helpInfo : "  -tps <file>               Run TPS tests with the given test file.\n" +
              "  -tpsphase <phase>         Run the specified phase in the TPS test.\n" +
              "  -tpslogfile <file>        Logfile for TPS output.\n" +
              "  --ignore-unused-engines   Don't load engines not used in tests.\n",
 };
 
 
-var TPSCmdLineFactory =
-{
-  createInstance : function(outer, iid)
-  {
+var TPSCmdLineFactory = {
+  createInstance : function(outer, iid) {
     if (outer != null) {
       throw Components.results.NS_ERROR_NO_AGGREGATION;
     }
 
     return new TPSCmdLineHandler().QueryInterface(iid);
   }
 };
 
 
-var TPSCmdLineModule =
-{
-  registerSelf : function(compMgr, fileSpec, location, type)
-  {
+var TPSCmdLineModule = {
+  registerSelf : function(compMgr, fileSpec, location, type) {
     compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
 
     compMgr.registerFactoryLocation(TPS_CMDLINE_CLSID,
                                     "TPS CommandLine Service",
                                     TPS_CMDLINE_CONTRACTID,
                                     fileSpec,
                                     location,
                                     type);
@@ -109,43 +105,40 @@ var TPSCmdLineModule =
     catman.addCategoryEntry("command-line-argument-handlers",
                             "TPS command line handler",
                             TPS_CMDLINE_CONTRACTID, true, true);
     catman.addCategoryEntry("command-line-handler",
                             "m-tps",
                             TPS_CMDLINE_CONTRACTID, true, true);
   },
 
-  unregisterSelf : function(compMgr, fileSpec, location)
-  {
+  unregisterSelf : function(compMgr, fileSpec, location) {
     compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
 
     compMgr.unregisterFactoryLocation(TPS_CMDLINE_CLSID, fileSpec);
     catman = Components.classes[CATMAN_CONTRACTID].getService(nsICategoryManager);
     catman.deleteCategoryEntry("command-line-argument-handlers",
                                "TPS command line handler", true);
     catman.deleteCategoryEntry("command-line-handler",
                                "m-tps", true);
   },
 
-  getClassObject : function(compMgr, cid, iid)
-  {
+  getClassObject : function(compMgr, cid, iid) {
     if (cid.equals(TPS_CMDLINE_CLSID)) {
       return TPSCmdLineFactory;
     }
 
     if (!iid.equals(Components.interfaces.nsIFactory)) {
       throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
     }
 
     throw Components.results.NS_ERROR_NO_INTERFACE;
   },
 
-  canUnload : function(compMgr)
-  {
+  canUnload : function(compMgr) {
     return true;
   }
 };
 
 /**
 * XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4).
 * XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6).
 */
--- a/services/sync/tps/extensions/tps/install.rdf
+++ b/services/sync/tps/extensions/tps/install.rdf
@@ -2,26 +2,27 @@
 <!-- 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/. -->
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
      xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>tps@mozilla.org</em:id>
-    <em:version>0.2</em:version>
+    <em:version>0.5</em:version>
 
     <em:targetApplication>
       <!-- Firefox -->
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
-        <em:minVersion>3.5.0</em:minVersion>
-        <em:maxVersion>12.0.*</em:maxVersion>
+        <em:minVersion>24.0.*</em:minVersion>
+        <em:maxVersion>31.0.*</em:maxVersion>
       </Description>
     </em:targetApplication>
 
     <!-- front-end metadata -->
     <em:name>TPS</em:name>
     <em:description>Sync test extension</em:description>
     <em:creator>Jonathan Griffin</em:creator>
-    <em:homepageURL>http://www.mozilla.org/</em:homepageURL>
+    <em:contributor>Henrik Skupin</em:contributor>
+    <em:homepageURL>https://developer.mozilla.org/en-US/docs/TPS</em:homepageURL>
   </Description>
 </RDF>
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/addons.jsm
+++ /dev/null
@@ -1,125 +0,0 @@
-/* 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/. */
-"use strict";
-
-let EXPORTED_SYMBOLS = ["Addon", "STATE_ENABLED", "STATE_DISABLED"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/AddonManager.jsm");
-Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-sync/addonutils.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://tps/logger.jsm");
-
-const ADDONSGETURL = 'http://127.0.0.1:4567/';
-const STATE_ENABLED = 1;
-const STATE_DISABLED = 2;
-
-function GetFileAsText(file)
-{
-  let channel = Services.io.newChannel(file, null, null);
-  let inputStream = channel.open();
-  if (channel instanceof Ci.nsIHttpChannel &&
-      channel.responseStatus != 200) {
-    return "";
-  }
-
-  let streamBuf = "";
-  let sis = Cc["@mozilla.org/scriptableinputstream;1"]
-            .createInstance(Ci.nsIScriptableInputStream);
-  sis.init(inputStream);
-
-  let available;
-  while ((available = sis.available()) != 0) {
-    streamBuf += sis.read(available);
-  }
-
-  inputStream.close();
-  return streamBuf;
-}
-
-function Addon(TPS, id) {
-  this.TPS = TPS;
-  this.id = id;
-}
-
-Addon.prototype = {
-  addon: null,
-
-  uninstall: function uninstall() {
-    // find our addon locally
-    let cb = Async.makeSyncCallback();
-    AddonManager.getAddonByID(this.id, cb);
-    let addon = Async.waitForSyncCallback(cb);
-
-    Logger.AssertTrue(!!addon, 'could not find addon ' + this.id + ' to uninstall');
-
-    cb = Async.makeSpinningCallback();
-    AddonUtils.uninstallAddon(addon, cb);
-    cb.wait();
-  },
-
-  find: function find(state) {
-    let cb = Async.makeSyncCallback();
-    AddonManager.getAddonByID(this.id, cb);
-    let addon = Async.waitForSyncCallback(cb);
-
-    if (!addon) {
-      Logger.logInfo("Could not find add-on with ID: " + this.id);
-      return false;
-    }
-
-    this.addon = addon;
-
-    Logger.logInfo("add-on found: " + addon.id + ", enabled: " +
-                   !addon.userDisabled);
-    if (state == STATE_ENABLED) {
-      Logger.AssertFalse(addon.userDisabled, "add-on is disabled: " + addon.id);
-      return true;
-    } else if (state == STATE_DISABLED) {
-      Logger.AssertTrue(addon.userDisabled, "add-on is enabled: " + addon.id);
-      return true;
-    } else if (state) {
-      throw Error("Don't know how to handle state: " + state);
-    } else {
-      // No state, so just checking that it exists.
-      return true;
-    }
-  },
-
-  install: function install() {
-    // For Install, the id parameter initially passed is really the filename
-    // for the addon's install .xml; we'll read the actual id from the .xml.
-
-    let cb = Async.makeSpinningCallback();
-    AddonUtils.installAddons([{id: this.id, requireSecureURI: false}], cb);
-    let result = cb.wait();
-
-    Logger.AssertEqual(1, result.installedIDs.length, "Exactly 1 add-on was installed.");
-    Logger.AssertEqual(this.id, result.installedIDs[0],
-                       "Add-on was installed successfully: " + this.id);
-  },
-
-  setEnabled: function setEnabled(flag) {
-    Logger.AssertTrue(this.find(), "Add-on is available.");
-
-    let userDisabled;
-    if (flag == STATE_ENABLED) {
-      userDisabled = false;
-    } else if (flag == STATE_DISABLED) {
-      userDisabled = true;
-    } else {
-      throw new Error("Unknown flag to setEnabled: " + flag);
-    }
-
-    let cb = Async.makeSpinningCallback();
-    AddonUtils.updateUserDisabled(this.addon, userDisabled, cb);
-    cb.wait();
-
-    return true;
-  }
-};
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/bookmarks.jsm
+++ /dev/null
@@ -1,998 +0,0 @@
-/* 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 is a JavaScript module (JSM) to be imported via
-  * Components.utils.import() and acts as a singleton. Only the following
-  * listed symbols will exposed on import, and only when and where imported.
-  */
-
-var EXPORTED_SYMBOLS = ["PlacesItem", "Bookmark", "Separator", "Livemark",
-                        "BookmarkFolder", "DumpBookmarks"];
-
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
-
-CU.import("resource://tps/logger.jsm");
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://gre/modules/PlacesUtils.jsm");
-CU.import("resource://gre/modules/BookmarkJSONUtils.jsm");
-CU.import("resource://gre/modules/Task.jsm");
-CU.import("resource://services-common/async.js");
-
-var DumpBookmarks = function TPS_Bookmarks__DumpBookmarks() {
-  let writer = {
-    value: "",
-    write: function PlacesItem__dump__write(aStr, aLen) {
-      this.value += aStr;
-    }
-  };
-
-  let options = PlacesUtils.history.getNewQueryOptions();
-  options.queryType = options.QUERY_TYPE_BOOKMARKS;
-  let query = PlacesUtils.history.getNewQuery();
-  query.setFolders([PlacesUtils.placesRootId], 1);
-  let root = PlacesUtils.history.executeQuery(query, options).root;
-  root.containerOpen = true;
-  let cb = Async.makeSpinningCallback();
-  Task.spawn(function() {
-    yield BookmarkJSONUtils.serializeNodeAsJSONToOutputStream(root, writer, true, false);
-    let value = JSON.parse(writer.value);
-    Logger.logInfo("dumping bookmarks\n\n" + JSON.stringify(value, null, ' ') + "\n\n");
-    cb();
-  });
-  cb.wait();
-};
-
-/**
- * extend, causes a child object to inherit from a parent
- */
-function extend(child, supertype)
-{
-   child.prototype.__proto__ = supertype.prototype;
-}
-
-/**
- * PlacesItemProps object, holds properties for places items
- */
-function PlacesItemProps(props) {
-  this.location = null;
-  this.uri = null;
-  this.loadInSidebar = null;
-  this.keyword = null;
-  this.title = null;
-  this.description = null;
-  this.after = null;
-  this.before = null;
-  this.folder = null;
-  this.position = null;
-  this.delete = false;
-  this.siteUri = null;
-  this.feedUri = null;
-  this.livemark = null;
-  this.tags = null;
-  this.last_item_pos = null;
-  this.type = null;
-
-  for (var prop in props) {
-    if (prop in this)
-      this[prop] = props[prop];
-  }
-}
-
-/**
- * PlacesItem object.  Base class for places items.
- */
-function PlacesItem(props) {
-  this.props = new PlacesItemProps(props);
-  if (this.props.location == null)
-    this.props.location = "menu";
-  if ("changes" in props)
-    this.updateProps = new PlacesItemProps(props.changes);
-  else
-    this.updateProps = null;
-}
-
-/**
- * Instance methods for generic places items.
- */
-PlacesItem.prototype = {
-  // an array of possible root folders for places items
-  _bookmarkFolders: {
-    "places": "placesRoot",
-    "menu": "bookmarksMenuFolder",
-    "tags": "tagFolder",
-    "unfiled": "unfiledBookmarksFolder",
-    "toolbar": "toolbarFolder",
-  },
-
-  toString: function() {
-    var that = this;
-    var props = ['uri', 'title', 'location', 'folder', 'feedUri', 'siteUri', 'livemark'];
-    var string = (this.props.type ? this.props.type + " " : "") +
-      "(" +
-      (function() {
-        var ret = [];
-        for (var i in props) {
-          if (that.props[props[i]]) {
-            ret.push(props[i] + ": " + that.props[props[i]])
-          }
-        }
-        return ret;
-      })().join(", ") + ")";
-    return string;
-  },
-
-  /**
-   * GetPlacesNodeId
-   *
-   * Finds the id of the an item with the specified properties in the places
-   * database.
-   *
-   * @param folder The id of the folder to search
-   * @param type The type of the item to find, or null to match any item;
-   *        this is one of the values listed at
-   *        https://developer.mozilla.org/en/nsINavHistoryResultNode#Constants
-   * @param title The title of the item to find, or null to match any title
-   * @param uri The uri of the item to find, or null to match any uri
-   *
-   * @return the node id if the item was found, otherwise -1
-   */
-  GetPlacesNodeId: function (folder, type, title, uri) {
-    let node_id = -1;
-
-    let options = PlacesUtils.history.getNewQueryOptions();
-    let query = PlacesUtils.history.getNewQuery();
-    query.setFolders([folder], 1);
-    let result = PlacesUtils.history.executeQuery(query, options);
-    let rootNode = result.root;
-    rootNode.containerOpen = true;
-
-    for (let j = 0; j < rootNode.childCount; j ++) {
-      let node = rootNode.getChild(j);
-      if (node.title == title) {
-        if (type == null || type == undefined || node.type == type)
-          if (uri == undefined || uri == null || node.uri.spec == uri.spec)
-            node_id = node.itemId;
-      }
-    }
-    rootNode.containerOpen = false;
-
-    return node_id;
-  },
-
-  /**
-   * IsAdjacentTo
-   *
-   * Determines if this object is immediately adjacent to another.
-   *
-   * @param itemName The name of the other object; this may be any kind of
-   *        places item
-   * @param relativePos The relative position of the other object.  If -1,
-   *        it means the other object should precede this one, if +1,
-   *        the other object should come after this one
-   * @return true if this object is immediately adjacent to the other object,
-   *         otherwise false
-   */
-  IsAdjacentTo: function(itemName, relativePos) {
-    Logger.AssertTrue(this.props.folder_id != -1 && this.props.item_id != -1,
-      "Either folder_id or item_id was invalid");
-    let other_id = this.GetPlacesNodeId(this.props.folder_id, null, itemName);
-    Logger.AssertTrue(other_id != -1, "item " + itemName + " not found");
-    let other_pos = PlacesUtils.bookmarks.getItemIndex(other_id);
-    let this_pos = PlacesUtils.bookmarks.getItemIndex(this.props.item_id);
-    if (other_pos + relativePos != this_pos) {
-      Logger.logPotentialError("Invalid position - " +
-       (this.props.title ? this.props.title : this.props.folder) +
-      " not " + (relativePos == 1 ? "after " : "before ") + itemName +
-      " for " + this.toString());
-      return false;
-    }
-    return true;
-  },
-
-  /**
-   * GetItemIndex
-   *
-   * Gets the item index for this places item.
-   *
-   * @return the item index, or -1 if there's an error
-   */
-  GetItemIndex: function() {
-    if (this.props.item_id == -1)
-      return -1;
-    return PlacesUtils.bookmarks.getItemIndex(this.props.item_id);
-  },
-
-  /**
-   * GetFolder
-   *
-   * Gets the folder id for the specified bookmark folder
-   *
-   * @param location The full path of the folder, which must begin
-   *        with one of the bookmark root folders
-   * @return the folder id if the folder is found, otherwise -1
-   */
-  GetFolder: function(location) {
-    let folder_parts = location.split("/");
-    if (!(folder_parts[0] in this._bookmarkFolders)) {
-      return -1;
-    }
-    let folder_id = PlacesUtils.bookmarks[this._bookmarkFolders[folder_parts[0]]];
-    for (let i = 1; i < folder_parts.length; i++) {
-      let subfolder_id = this.GetPlacesNodeId(
-        folder_id,
-        CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
-        folder_parts[i]);
-      if (subfolder_id == -1) {
-        return -1;
-      }
-      else {
-        folder_id = subfolder_id;
-      }
-    }
-    return folder_id;
-  },
-
-  /**
-   * CreateFolder
-   *
-   * Creates a bookmark folder.
-   *
-   * @param location The full path of the folder, which must begin
-   *        with one of the bookmark root folders
-   * @return the folder id if the folder was created, otherwise -1
-   */
-  CreateFolder: function(location) {
-    let folder_parts = location.split("/");
-    if (!(folder_parts[0] in this._bookmarkFolders)) {
-      return -1;
-    }
-    let folder_id = PlacesUtils.bookmarks[this._bookmarkFolders[folder_parts[0]]];
-    for (let i = 1; i < folder_parts.length; i++) {
-      let subfolder_id = this.GetPlacesNodeId(
-        folder_id,
-        CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
-        folder_parts[i]);
-      if (subfolder_id == -1) {
-        folder_id = PlacesUtils.bookmarks.createFolder(folder_id,
-                                                 folder_parts[i], -1);
-      }
-      else {
-        folder_id = subfolder_id;
-      }
-    }
-    return folder_id;
-  },
-
-  /**
-   * GetOrCreateFolder
-   *
-   * Locates the specified folder; if not found it is created.
-   *
-   * @param location The full path of the folder, which must begin
-   *        with one of the bookmark root folders
-   * @return the folder id if the folder was found or created, otherwise -1
-   */
-  GetOrCreateFolder: function(location) {
-    folder_id = this.GetFolder(location);
-    if (folder_id == -1)
-      folder_id = this.CreateFolder(location);
-    return folder_id;
-  },
-
-  /**
-   * CheckDescription
-   *
-   * Compares the description of this places item with an expected
-   * description.
-   *
-   * @param expectedDescription The description this places item is
-   *        expected to have
-   * @return true if the actual and expected descriptions match, or if
-   *         there is no expected description; otherwise false
-   */
-  CheckDescription: function(expectedDescription) {
-    if (expectedDescription != null) {
-      let description = "";
-      if (PlacesUtils.annotations.itemHasAnnotation(this.props.item_id,
-          "bookmarkProperties/description")) {
-        description = PlacesUtils.annotations.getItemAnnotation(
-          this.props.item_id, "bookmarkProperties/description");
-      }
-      if (description != expectedDescription) {
-        Logger.logPotentialError("Invalid description, expected: " +
-          expectedDescription + ", actual: " + description + " for " +
-          this.toString());
-        return false;
-      }
-    }
-    return true;
-  },
-
-  /**
-   * CheckPosition
-   *
-   * Verifies the position of this places item.
-   *
-   * @param before The name of the places item that this item should be
-            before, or null if this check should be skipped
-   * @param after The name of the places item that this item should be
-            after, or null if this check should be skipped
-   * @param last_item_pos The index of the places item above this one,
-   *        or null if this check should be skipped
-   * @return true if this item is in the correct position, otherwise false
-   */
-  CheckPosition: function(before, after, last_item_pos) {
-    if (after)
-      if (!this.IsAdjacentTo(after, 1)) return false;
-    if (before)
-      if (!this.IsAdjacentTo(before, -1)) return false;
-    if (last_item_pos != null && last_item_pos > -1) {
-      if (this.GetItemIndex() != last_item_pos + 1) {
-        Logger.logPotentialError("Item not found at the expected index, got " +
-          this.GetItemIndex() + ", expected " + (last_item_pos + 1) + " for " +
-          this.toString());
-        return false;
-      }
-    }
-    return true;
-  },
-
-  /**
-   * SetLocation
-   *
-   * Moves this places item to a different folder.
-   *
-   * @param location The full path of the folder to which to move this
-   *        places item, which must begin with one of the bookmark root
-   *        folders; if null, no changes are made
-   * @return nothing if successful, otherwise an exception is thrown
-   */
-  SetLocation: function(location) {
-    if (location != null) {
-      let newfolder_id = this.GetOrCreateFolder(location);
-      Logger.AssertTrue(newfolder_id != -1, "Location " + location +
-                        " doesn't exist; can't change item's location");
-      PlacesUtils.bookmarks.moveItem(this.props.item_id, newfolder_id, -1);
-      this.props.folder_id = newfolder_id;
-    }
-  },
-
-  /**
-   * SetDescription
-   *
-   * Updates the description for this places item.
-   *
-   * @param description The new description to set; if null, no changes are
-   *        made
-   * @return nothing
-   */
-  SetDescription: function(description) {
-    if (description != null) {
-      if (description != "")
-        PlacesUtils.annotations.setItemAnnotation(this.props.item_id,
-                                      "bookmarkProperties/description",
-                                      description,
-                                      0,
-                                      PlacesUtils.annotations.EXPIRE_NEVER);
-      else
-        PlacesUtils.annotations.removeItemAnnotation(this.props.item_id,
-                                         "bookmarkProperties/description");
-    }
-  },
-
-  /**
-   * SetPosition
-   *
-   * Updates the position of this places item within this item's current
-   * folder.  Use SetLocation to change folders.
-   *
-   * @param position The new index this item should be moved to; if null,
-   *        no changes are made; if -1, this item is moved to the bottom of
-   *        the current folder
-   * @return nothing if successful, otherwise an exception is thrown
-   */
-  SetPosition: function(position) {
-    if (position != null) {
-      let newposition = -1;
-      if (position != -1) {
-        newposition = this.GetPlacesNodeId(this.props.folder_id,
-                                           null, position);
-        Logger.AssertTrue(newposition != -1, "position " + position +
-                          " is invalid; unable to change position");
-        newposition = PlacesUtils.bookmarks.getItemIndex(newposition);
-      }
-      PlacesUtils.bookmarks.moveItem(this.props.item_id,
-                               this.props.folder_id, newposition);
-    }
-  },
-
-  /**
-   * Update the title of this places item
-   *
-   * @param title The new title to set for this item; if null, no changes
-   *        are made
-   * @return nothing
-   */
-  SetTitle: function(title) {
-    if (title != null) {
-      PlacesUtils.bookmarks.setItemTitle(this.props.item_id, title);
-    }
-  },
-};
-
-/**
- * Bookmark class constructor.  Initializes instance properties.
- */
-function Bookmark(props) {
-  PlacesItem.call(this, props);
-  if (this.props.title == null)
-    this.props.title = this.props.uri;
-  this.props.type = "bookmark";
-}
-
-/**
- * Bookmark instance methods.
- */
-Bookmark.prototype = {
-  /**
-   * SetKeyword
-   *
-   * Update this bookmark's keyword.
-   *
-   * @param keyword The keyword to set for this bookmark; if null, no
-   *        changes are made
-   * @return nothing
-   */
-  SetKeyword: function(keyword) {
-    if (keyword != null)
-      PlacesUtils.bookmarks.setKeywordForBookmark(this.props.item_id, keyword);
-  },
-
-  /**
-   * SetLoadInSidebar
-   *
-   * Updates this bookmark's loadInSidebar property.
-   *
-   * @param loadInSidebar if true, the loadInSidebar property will be set,
-   *        if false, it will be cleared, and any other value will result
-   *        in no change
-   * @return nothing
-   */
-  SetLoadInSidebar: function(loadInSidebar) {
-    if (loadInSidebar == true)
-      PlacesUtils.annotations.setItemAnnotation(this.props.item_id,
-                                    "bookmarkProperties/loadInSidebar",
-                                    true,
-                                    0,
-                                    PlacesUtils.annotations.EXPIRE_NEVER);
-    else if (loadInSidebar == false)
-      PlacesUtils.annotations.removeItemAnnotation(this.props.item_id,
-                                       "bookmarkProperties/loadInSidebar");
-  },
-
-  /**
-   * SetTitle
-   *
-   * Updates this bookmark's title.
-   *
-   * @param title The new title to set for this boomark; if null, no changes
-   *        are made
-   * @return nothing
-   */
-  SetTitle: function(title) {
-    if (title)
-      PlacesUtils.bookmarks.setItemTitle(this.props.item_id, title);
-  },
-
-  /**
-   * SetUri
-   *
-   * Updates this bookmark's URI.
-   *
-   * @param uri The new URI to set for this boomark; if null, no changes
-   *        are made
-   * @return nothing
-   */
-  SetUri: function(uri) {
-    if (uri) {
-      let newURI = Services.io.newURI(uri, null, null);
-      PlacesUtils.bookmarks.changeBookmarkURI(this.props.item_id, newURI);
-    }
-  },
-
-  /**
-   * SetTags
-   *
-   * Updates this bookmark's tags.
-   *
-   * @param tags An array of tags which should be associated with this
-   *        bookmark; any previous tags are removed; if this param is null,
-   *        no changes are made.  If this param is an empty array, all
-   *        tags are removed from this bookmark.
-   * @return nothing
-   */
-  SetTags: function(tags) {
-    if (tags != null) {
-      let URI = Services.io.newURI(this.props.uri, null, null);
-      PlacesUtils.tagging.untagURI(URI, null);
-      if (tags.length > 0)
-        PlacesUtils.tagging.tagURI(URI, tags);
-    }
-  },
-
-  /**
-   * Create
-   *
-   * Creates the bookmark described by this object's properties.
-   *
-   * @return the id of the created bookmark
-   */
-  Create: function() {
-    this.props.folder_id = this.GetOrCreateFolder(this.props.location);
-    Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
-      "bookmark, error creating folder " + this.props.location);
-    let bookmarkURI = Services.io.newURI(this.props.uri, null, null);
-    this.props.item_id = PlacesUtils.bookmarks.insertBookmark(this.props.folder_id,
-                                                        bookmarkURI,
-                                                        -1,
-                                                        this.props.title);
-    this.SetKeyword(this.props.keyword);
-    this.SetDescription(this.props.description);
-    this.SetLoadInSidebar(this.props.loadInSidebar);
-    this.SetTags(this.props.tags);
-    return this.props.item_id;
-  },
-
-  /**
-   * Update
-   *
-   * Updates this bookmark's properties according the properties on this
-   * object's 'updateProps' property.
-   *
-   * @return nothing
-   */
-  Update: function() {
-    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
-      "Invalid item_id during Remove");
-    this.SetKeyword(this.updateProps.keyword);
-    this.SetDescription(this.updateProps.description);
-    this.SetLoadInSidebar(this.updateProps.loadInSidebar);
-    this.SetTitle(this.updateProps.title);
-    this.SetUri(this.updateProps.uri);
-    this.SetTags(this.updateProps.tags);
-    this.SetLocation(this.updateProps.location);
-    this.SetPosition(this.updateProps.position);
-  },
-
-  /**
-   * Find
-   *
-   * Locates the bookmark which corresponds to this object's properties.
-   *
-   * @return the bookmark id if the bookmark was found, otherwise -1
-   */
-  Find: function() {
-    this.props.folder_id = this.GetFolder(this.props.location);
-    if (this.props.folder_id == -1) {
-      Logger.logError("Unable to find folder " + this.props.location);
-      return -1;
-    }
-    let bookmarkTitle = this.props.title;
-    this.props.item_id = this.GetPlacesNodeId(this.props.folder_id,
-                                              null,
-                                              bookmarkTitle,
-                                              this.props.uri);
-
-    if (this.props.item_id == -1) {
-      Logger.logPotentialError(this.toString() + " not found");
-      return -1;
-    }
-    if (!this.CheckDescription(this.props.description))
-      return -1;
-    if (this.props.keyword != null) {
-      let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.props.item_id);
-      if (keyword != this.props.keyword) {
-        Logger.logPotentialError("Incorrect keyword - expected: " +
-          this.props.keyword + ", actual: " + keyword +
-          " for " + this.toString());
-        return -1;
-      }
-    }
-    let loadInSidebar = PlacesUtils.annotations.itemHasAnnotation(
-      this.props.item_id,
-      "bookmarkProperties/loadInSidebar");
-    if (loadInSidebar)
-      loadInSidebar = PlacesUtils.annotations.getItemAnnotation(
-        this.props.item_id,
-        "bookmarkProperties/loadInSidebar");
-    if (this.props.loadInSidebar != null &&
-        loadInSidebar != this.props.loadInSidebar) {
-      Logger.logPotentialError("Incorrect loadInSidebar setting - expected: " +
-        this.props.loadInSidebar + ", actual: " + loadInSidebar +
-        " for " + this.toString());
-      return -1;
-    }
-    if (this.props.tags != null) {
-      try {
-        let URI = Services.io.newURI(this.props.uri, null, null);
-        let tags = PlacesUtils.tagging.getTagsForURI(URI, {});
-        tags.sort();
-        this.props.tags.sort();
-        if (JSON.stringify(tags) != JSON.stringify(this.props.tags)) {
-          Logger.logPotentialError("Wrong tags - expected: " +
-            JSON.stringify(this.props.tags) + ", actual: " +
-            JSON.stringify(tags) + " for " + this.toString());
-          return -1;
-        }
-      }
-      catch (e) {
-        Logger.logPotentialError("error processing tags " + e);
-        return -1;
-      }
-    }
-    if (!this.CheckPosition(this.props.before,
-                            this.props.after,
-                            this.props.last_item_pos))
-      return -1;
-    return this.props.item_id;
-  },
-
-  /**
-   * Remove
-   *
-   * Removes this bookmark.  The bookmark should have been located previously
-   * by a call to Find.
-   *
-   * @return nothing
-   */
-  Remove: function() {
-    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
-      "Invalid item_id during Remove");
-    PlacesUtils.bookmarks.removeItem(this.props.item_id);
-  },
-};
-
-extend(Bookmark, PlacesItem);
-
-/**
- * BookmarkFolder class constructor. Initializes instance properties.
- */
-function BookmarkFolder(props) {
-  PlacesItem.call(this, props);
-  this.props.type = "folder";
-}
-
-/**
- * BookmarkFolder instance methods
- */
-BookmarkFolder.prototype = {
-  /**
-   * Create
-   *
-   * Creates the bookmark folder described by this object's properties.
-   *
-   * @return the id of the created bookmark folder
-   */
-  Create: function() {
-    this.props.folder_id = this.GetOrCreateFolder(this.props.location);
-    Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
-      "folder, error creating parent folder " + this.props.location);
-    this.props.item_id = PlacesUtils.bookmarks.createFolder(this.props.folder_id,
-                                                      this.props.folder,
-                                                      -1);
-    this.SetDescription(this.props.description);
-    return this.props.folder_id;
-  },
-
-  /**
-   * Find
-   *
-   * Locates the bookmark folder which corresponds to this object's
-   * properties.
-   *
-   * @return the folder id if the folder was found, otherwise -1
-   */
-  Find: function() {
-    this.props.folder_id = this.GetFolder(this.props.location);
-    if (this.props.folder_id == -1) {
-      Logger.logError("Unable to find folder " + this.props.location);
-      return -1;
-    }
-    this.props.item_id = this.GetPlacesNodeId(
-                              this.props.folder_id,
-                              CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
-                              this.props.folder);
-    if (!this.CheckDescription(this.props.description))
-      return -1;
-    if (!this.CheckPosition(this.props.before,
-                            this.props.after,
-                            this.props.last_item_pos))
-      return -1;
-    return this.props.item_id;
-  },
-
-  /**
-   * Remove
-   *
-   * Removes this folder.  The folder should have been located previously
-   * by a call to Find.
-   *
-   * @return nothing
-   */
-  Remove: function() {
-    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
-      "Invalid item_id during Remove");
-    PlacesUtils.bookmarks.removeFolderChildren(this.props.item_id);
-    PlacesUtils.bookmarks.removeItem(this.props.item_id);
-  },
-
-  /**
-   * Update
-   *
-   * Updates this bookmark's properties according the properties on this
-   * object's 'updateProps' property.
-   *
-   * @return nothing
-   */
-  Update: function() {
-    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
-      "Invalid item_id during Update");
-    this.SetLocation(this.updateProps.location);
-    this.SetPosition(this.updateProps.position);
-    this.SetTitle(this.updateProps.folder);
-    this.SetDescription(this.updateProps.description);
-  },
-};
-
-extend(BookmarkFolder, PlacesItem);
-
-/**
- * Livemark class constructor. Initialzes instance properties.
- */
-function Livemark(props) {
-  PlacesItem.call(this, props);
-  this.props.type = "livemark";
-}
-
-/**
- * Livemark instance methods
- */
-Livemark.prototype = {
-  /**
-   * Create
-   *
-   * Creates the livemark described by this object's properties.
-   *
-   * @return the id of the created livemark
-   */
-  Create: function() {
-    this.props.folder_id = this.GetOrCreateFolder(this.props.location);
-    Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
-      "folder, error creating parent folder " + this.props.location);
-    let siteURI = null;
-    if (this.props.siteUri != null)
-      siteURI = Services.io.newURI(this.props.siteUri, null, null);
-    let livemarkObj = {parentId: this.props.folder_id,
-                       title: this.props.livemark,
-                       siteURI: siteURI,
-                       feedURI: Services.io.newURI(this.props.feedUri, null, null),
-                       index: PlacesUtils.bookmarks.DEFAULT_INDEX};
-
-    // Until this can handle asynchronous creation, we need to spin.
-    let spinningCb = Async.makeSpinningCallback();
-
-    PlacesUtils.livemarks.addLivemark(livemarkObj).then(
-      aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
-      () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
-    );
-
-    let [status, livemark] = spinningCb.wait();
-    if (!Components.isSuccessCode(status)) {
-      throw status;
-    }
-
-    this.props.item_id = livemark.id;
-    return this.props.item_id;
-  },
-
-  /**
-   * Find
-   *
-   * Locates the livemark which corresponds to this object's
-   * properties.
-   *
-   * @return the item id if the livemark was found, otherwise -1
-   */
-  Find: function() {
-    this.props.folder_id = this.GetFolder(this.props.location);
-    if (this.props.folder_id == -1) {
-      Logger.logError("Unable to find folder " + this.props.location);
-      return -1;
-    }
-    this.props.item_id = this.GetPlacesNodeId(
-                              this.props.folder_id,
-                              CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
-                              this.props.livemark);
-    if (!PlacesUtils.annotations
-                    .itemHasAnnotation(this.props.item_id, PlacesUtils.LMANNO_FEEDURI)) {
-      Logger.logPotentialError("livemark folder found, but it's just a regular folder, for " +
-        this.toString());
-      this.props.item_id = -1;
-      return -1;
-    }
-    let feedURI = Services.io.newURI(this.props.feedUri, null, null);
-    let lmFeedURISpec =
-      PlacesUtils.annotations.getItemAnnotation(this.props.item_id,
-                                                PlacesUtils.LMANNO_FEEDURI);
-    if (feedURI.spec != lmFeedURISpec) {
-      Logger.logPotentialError("livemark feed uri not correct, expected: " +
-        this.props.feedUri + ", actual: " + lmFeedURISpec +
-        " for " + this.toString());
-      return -1;
-    }
-    if (this.props.siteUri != null) {
-      let siteURI = Services.io.newURI(this.props.siteUri, null, null);
-      let lmSiteURISpec =
-        PlacesUtils.annotations.getItemAnnotation(this.props.item_id,
-                                                  PlacesUtils.LMANNO_SITEURI);
-      if (siteURI.spec != lmSiteURISpec) {
-        Logger.logPotentialError("livemark site uri not correct, expected: " +
-        this.props.siteUri + ", actual: " + lmSiteURISpec + " for " +
-        this.toString());
-        return -1;
-      }
-    }
-    if (!this.CheckPosition(this.props.before,
-                            this.props.after,
-                            this.props.last_item_pos))
-      return -1;
-    return this.props.item_id;
-  },
-
-  /**
-   * Update
-   *
-   * Updates this livemark's properties according the properties on this
-   * object's 'updateProps' property.
-   *
-   * @return nothing
-   */
-  Update: function() {
-    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
-      "Invalid item_id during Update");
-    this.SetLocation(this.updateProps.location);
-    this.SetPosition(this.updateProps.position);
-    this.SetTitle(this.updateProps.livemark);
-    return true;
-  },
-
-  /**
-   * Remove
-   *
-   * Removes this livemark.  The livemark should have been located previously
-   * by a call to Find.
-   *
-   * @return nothing
-   */
-  Remove: function() {
-    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
-      "Invalid item_id during Remove");
-    PlacesUtils.bookmarks.removeItem(this.props.item_id);
-  },
-};
-
-extend(Livemark, PlacesItem);
-
-/**
- * Separator class constructor. Initializes instance properties.
- */
-function Separator(props) {
-  PlacesItem.call(this, props);
-  this.props.type = "separator";
-}
-
-/**
- * Separator instance methods.
- */
-Separator.prototype = {
-  /**
-   * Create
-   *
-   * Creates the bookmark separator described by this object's properties.
-   *
-   * @return the id of the created separator
-   */
-  Create: function () {
-    this.props.folder_id = this.GetOrCreateFolder(this.props.location);
-    Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
-      "folder, error creating parent folder " + this.props.location);
-    this.props.item_id = PlacesUtils.bookmarks.insertSeparator(this.props.folder_id,
-                                                         -1);
-    return this.props.item_id;
-  },
-
-  /**
-   * Find
-   *
-   * Locates the bookmark separator which corresponds to this object's
-   * properties.
-   *
-   * @return the item id if the separator was found, otherwise -1
-   */
-  Find: function () {
-    this.props.folder_id = this.GetFolder(this.props.location);
-    if (this.props.folder_id == -1) {
-      Logger.logError("Unable to find folder " + this.props.location);
-      return -1;
-    }
-    if (this.props.before == null && this.props.last_item_pos == null) {
-      Logger.logPotentialError("Separator requires 'before' attribute if it's the" +
-        "first item in the list");
-      return -1;
-    }
-    let expected_pos = -1;
-    if (this.props.before) {
-      other_id = this.GetPlacesNodeId(this.props.folder_id,
-                                      null,
-                                      this.props.before);
-      if (other_id == -1) {
-        Logger.logPotentialError("Can't find places item " + this.props.before +
-          " for locating separator");
-        return -1;
-      }
-      expected_pos = PlacesUtils.bookmarks.getItemIndex(other_id) - 1;
-    }
-    else {
-      expected_pos = this.props.last_item_pos + 1;
-    }
-    this.props.item_id = PlacesUtils.bookmarks.getIdForItemAt(this.props.folder_id,
-                                                        expected_pos);
-    if (this.props.item_id == -1) {
-      Logger.logPotentialError("No separator found at position " + expected_pos);
-    }
-    else {
-      if (PlacesUtils.bookmarks.getItemType(this.props.item_id) !=
-          PlacesUtils.bookmarks.TYPE_SEPARATOR) {
-        Logger.logPotentialError("Places item at position " + expected_pos +
-          " is not a separator");
-        return -1;
-      }
-    }
-    return this.props.item_id;
-  },
-
-  /**
-   * Update
-   *
-   * Updates this separator's properties according the properties on this
-   * object's 'updateProps' property.
-   *
-   * @return nothing
-   */
-  Update: function() {
-    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
-      "Invalid item_id during Update");
-    this.SetLocation(this.updateProps.location);
-    this.SetPosition(this.updateProps.position);
-    return true;
-  },
-
-  /**
-   * Remove
-   *
-   * Removes this separator.  The separator should have been located
-   * previously by a call to Find.
-   *
-   * @return nothing
-   */
-  Remove: function() {
-    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
-      "Invalid item_id during Update");
-    PlacesUtils.bookmarks.removeItem(this.props.item_id);
-  },
-};
-
-extend(Separator, PlacesItem);
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/forms.jsm
+++ /dev/null
@@ -1,263 +0,0 @@
-/* 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 is a JavaScript module (JSM) to be imported via
-   Components.utils.import() and acts as a singleton. Only the following
-   listed symbols will exposed on import, and only when and where imported.
-  */
-
-var EXPORTED_SYMBOLS = ["FormData"];
-
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
-
-CU.import("resource://tps/logger.jsm");
-
-let formService = CC["@mozilla.org/satchel/form-history;1"]
-                  .getService(CI.nsIFormHistory2);
-
-/**
- * FormDB
- *
- * Helper object containing methods to interact with the moz_formhistory
- * SQLite table.
- */
-let FormDB = {
-  /**
-   * makeGUID
-   *
-   * Generates a brand-new globally unique identifier (GUID).  Borrowed
-   * from Weave's utils.js.
-   *
-   * @return the new guid
-   */
-  makeGUID: function makeGUID() {
-    // 70 characters that are not-escaped URL-friendly
-    const code =
-      "!()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~";
-
-    let guid = "";
-    let num = 0;
-    let val;
-
-    // Generate ten 70-value characters for a 70^10 (~61.29-bit) GUID
-    for (let i = 0; i < 10; i++) {
-      // Refresh the number source after using it a few times
-      if (i == 0 || i == 5)
-        num = Math.random();
-
-      // Figure out which code to use for the next GUID character
-      num *= 70;
-      val = Math.floor(num);
-      guid += code[val];
-      num -= val;
-    }
-
-    return guid;
-  },
-
-  /**
-   * insertValue
-   *
-   * Inserts the specified value for the specified fieldname into the
-   * moz_formhistory table.
-   *
-   * @param fieldname The form fieldname to insert
-   * @param value The form value to insert
-   * @param us The time, in microseconds, to use for the lastUsed
-   *        and firstUsed columns
-   * @return nothing
-   */
-  insertValue: function (fieldname, value, us) {
-    let query = this.createStatement(
-      "INSERT INTO moz_formhistory " +
-      "(fieldname, value, timesUsed, firstUsed, lastUsed, guid) VALUES " +
-      "(:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)");
-    query.params.fieldname = fieldname;
-    query.params.value = value;
-    query.params.timesUsed = 1;
-    query.params.firstUsed = us;
-    query.params.lastUsed = us;
-    query.params.guid = this.makeGUID();
-    query.execute();
-    query.reset();
-  },
-
-  /**
-   * updateValue
-   *
-   * Updates a row in the moz_formhistory table with a new value.
-   *
-   * @param id The id of the row to update
-   * @param newvalue The new value to set
-   * @return nothing
-   */
-  updateValue: function (id, newvalue) {
-    let query = this.createStatement(
-      "UPDATE moz_formhistory SET value = :value WHERE id = :id");
-    query.params.id = id;
-    query.params.value = newvalue;
-    query.execute();
-    query.reset();
-  },
-
-  /**
-   * getDataForValue
-   *
-   * Retrieves a set of values for a row in the database that
-   * corresponds to the given fieldname and value.
-   *
-   * @param fieldname The fieldname of the row to query
-   * @param value The value of the row to query
-   * @return null if no row is found with the specified fieldname and value,
-   *         or an object containing the row's id, lastUsed, and firstUsed
-   *         values
-   */
-  getDataForValue: function (fieldname, value) {
-    let query = this.createStatement(
-      "SELECT id, lastUsed, firstUsed FROM moz_formhistory WHERE " +
-      "fieldname = :fieldname AND value = :value");
-    query.params.fieldname = fieldname;
-    query.params.value = value;
-    if (!query.executeStep())
-      return null;
-
-    return {
-      id: query.row.id,
-      lastUsed: query.row.lastUsed,
-      firstUsed: query.row.firstUsed
-    };
-  },
-
-  /**
-   * createStatement
-   *
-   * Creates a statement from a SQL string.  This function is borrowed
-   * from Weave's forms.js.
-   *
-   * @param query The SQL query string
-   * @return the mozIStorageStatement created from the specified SQL
-   */
-  createStatement: function createStatement(query) {
-    try {
-      // Just return the statement right away if it's okay
-      return formService.DBConnection.createStatement(query);
-    }
-    catch(ex) {
-      // Assume guid column must not exist yet, so add it with an index
-      formService.DBConnection.executeSimpleSQL(
-        "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT");
-      formService.DBConnection.executeSimpleSQL(
-        "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index " +
-        "ON moz_formhistory (guid)");
-    }
-
-    // Try creating the query now that the column exists
-    return formService.DBConnection.createStatement(query);
-  }
-};
-
-/**
- * FormData class constructor
- *
- * Initializes instance properties.
- */
-function FormData(props, usSinceEpoch) {
-  this.fieldname = null;
-  this.value = null;
-  this.date = 0;
-  this.newvalue = null;
-  this.usSinceEpoch = usSinceEpoch;
-
-  for (var prop in props) {
-    if (prop in this)
-      this[prop] = props[prop];
-  }
-}
-
-/**
- * FormData instance methods
- */
-FormData.prototype = {
-  /**
-   * hours_to_us
-   *
-   * Converts hours since present to microseconds since epoch.
-   *
-   * @param hours The number of hours since the present time (e.g., 0 is
-   *        'now', and -1 is 1 hour ago)
-   * @return the corresponding number of microseconds since the epoch
-   */
-  hours_to_us: function(hours) {
-    return this.usSinceEpoch + (hours * 60 * 60 * 1000 * 1000);
-  },
-
-  /**
-   * Create
-   *
-   * If this FormData object doesn't exist in the moz_formhistory database,
-   * add it.  Throws on error.
-   *
-   * @return nothing
-   */
-  Create: function() {
-    Logger.AssertTrue(this.fieldname != null && this.value != null,
-      "Must specify both fieldname and value");
-
-    let formdata = FormDB.getDataForValue(this.fieldname, this.value);
-    if (!formdata) {
-      // this item doesn't exist yet in the db, so we need to insert it
-      FormDB.insertValue(this.fieldname, this.value,
-                         this.hours_to_us(this.date));
-    }
-    else {
-      /* Right now, we ignore this case.  If bug 552531 is ever fixed,
-         we might need to add code here to update the firstUsed or
-         lastUsed fields, as appropriate.
-       */
-    }
-  },
-
-  /**
-   * Find
-   *
-   * Attempts to locate an entry in the moz_formhistory database that
-   * matches the fieldname and value for this FormData object.
-   *
-   * @return true if this entry exists in the database, otherwise false
-   */
-  Find: function() {
-    let formdata = FormDB.getDataForValue(this.fieldname, this.value);
-    let status = formdata != null;
-    if (status) {
-      /*
-      //form history dates currently not synced!  bug 552531
-      let us = this.hours_to_us(this.date);
-      status = Logger.AssertTrue(
-        us >= formdata.firstUsed && us <= formdata.lastUsed,
-        "No match for with that date value");
-
-      if (status)
-      */
-        this.id = formdata.id;
-    }
-    return status;
-  },
-
-  /**
-   * Remove
-   *
-   * Removes the row represented by this FormData instance from the
-   * moz_formhistory database.
-   *
-   * @return nothing
-   */
-  Remove: function() {
-    /* Right now Weave doesn't handle this correctly, see bug 568363.
-     */
-    formService.removeEntry(this.fieldname, this.value);
-    return true;
-  },
-};
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/fxaccounts.jsm
+++ /dev/null
@@ -1,57 +0,0 @@
-/* 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/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = [
-  "FxAccountsHelper",
-];
-
-const { utils: Cu } = Components;
-
-Cu.import("resource://gre/modules/FxAccountsClient.jsm");
-Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-sync/main.js");
-Cu.import("resource://tps/logger.jsm");
-
-
-/**
- * Helper object for Firefox Accounts authentication
- */
-var FxAccountsHelper = {
-
-  /**
-   * Wrapper to synchronize the login of a user
-   *
-   * @param email
-   *        The email address for the account (utf8)
-   * @param password
-   *        The user's password
-   */
-  signIn: function signIn(email, password) {
-    let cb = Async.makeSpinningCallback();
-
-    var client = new FxAccountsClient();
-    client.signIn(email, password).then(credentials => {
-      // Add keys because without those setSignedInUser() will fail
-      credentials.kA = 'foo';
-      credentials.kB = 'bar';
-
-      Weave.Service.identity._fxaService.setSignedInUser(credentials).then(() => {
-        cb(null);
-      }, err => {
-        cb(err);
-      });
-    }, (err) => {
-      cb(err);
-    });
-
-    try {
-      cb.wait();
-    } catch (err) {
-      Logger.logError("signIn() failed with: " + JSON.stringify(err));
-      throw err;
-    }
-  }
-};
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/history.jsm
+++ /dev/null
@@ -1,203 +0,0 @@
-/* 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 is a JavaScript module (JSM) to be imported via
-  * Components.utils.import() and acts as a singleton. Only the following
-  * listed symbols will exposed on import, and only when and where imported.
-  */
-
-var EXPORTED_SYMBOLS = ["HistoryEntry", "DumpHistory"];
-
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
-
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://gre/modules/PlacesUtils.jsm");
-CU.import("resource://tps/logger.jsm");
-CU.import("resource://services-common/async.js");
-
-var DumpHistory = function TPS_History__DumpHistory() {
-  let writer = {
-    value: "",
-    write: function PlacesItem__dump__write(aStr, aLen) {
-      this.value += aStr;
-    }
-  };
-
-  let query = PlacesUtils.history.getNewQuery();
-  let options = PlacesUtils.history.getNewQueryOptions();
-  let root = PlacesUtils.history.executeQuery(query, options).root;
-  root.containerOpen = true;
-  Logger.logInfo("\n\ndumping history\n", true);
-  for (var i = 0; i < root.childCount; i++) {
-    let node = root.getChild(i);
-    let uri = node.uri;
-    let curvisits = HistoryEntry._getVisits(uri);
-    for each (var visit in curvisits) {
-      Logger.logInfo("URI: " + uri + ", type=" + visit.type + ", date=" + visit.date, true);
-    }
-  }
-  root.containerOpen = false;
-  Logger.logInfo("\nend history dump\n", true);
-};
-
-/**
- * HistoryEntry object
- *
- * Contains methods for manipulating browser history entries.
- */
-var HistoryEntry = {
-  /**
-   * _db
-   *
-   * Returns the DBConnection object for the history service.
-   */
-  get _db() {
-    return PlacesUtils.history.QueryInterface(CI.nsPIPlacesDatabase).DBConnection;
-  },
-
-  /**
-   * _visitStm
-   *
-   * Return the SQL statement for getting history visit information
-   * from the moz_historyvisits table.  Borrowed from Weave's
-   * history.js.
-   */
-  get _visitStm() {
-    let stm = this._db.createStatement(
-      "SELECT visit_type type, visit_date date " +
-      "FROM moz_historyvisits " +
-      "WHERE place_id = (" +
-        "SELECT id " +
-        "FROM moz_places " +
-        "WHERE url = :url) " +
-      "ORDER BY date DESC LIMIT 10");
-    this.__defineGetter__("_visitStm", function() stm);
-    return stm;
-  },
-
-  /**
-   * _getVisits
-   *
-   * Gets history information about visits to a given uri.
-   *
-   * @param uri The uri to get visits for
-   * @return an array of objects with 'date' and 'type' properties,
-   * corresponding to the visits in the history database for the
-   * given uri
-   */
-  _getVisits: function HistStore__getVisits(uri) {
-    this._visitStm.params.url = uri;
-    return Async.querySpinningly(this._visitStm, ["date", "type"]);
-  },
-
-  /**
-   * Add
-   *
-   * Adds visits for a uri to the history database.  Throws on error.
-   *
-   * @param item An object representing one or more visits to a specific uri
-   * @param usSinceEpoch The number of microseconds from Epoch to
-   *        the time the current Crossweave run was started
-   * @return nothing
-   */
-  Add: function(item, usSinceEpoch) {
-    Logger.AssertTrue("visits" in item && "uri" in item,
-      "History entry in test file must have both 'visits' " +
-      "and 'uri' properties");
-    let uri = Services.io.newURI(item.uri, null, null);
-    let place = {
-      uri: uri,
-      visits: []
-    };
-    for each (visit in item.visits) {
-      place.visits.push({
-        visitDate: usSinceEpoch + (visit.date * 60 * 60 * 1000 * 1000),
-        transitionType: visit.type
-      });
-    }
-    if ("title" in item) {
-      place.title = item.title;
-    }
-    let cb = Async.makeSpinningCallback();
-    PlacesUtils.asyncHistory.updatePlaces(place, {
-        handleError: function Add_handleError() {
-          cb(new Error("Error adding history entry"));
-        },
-        handleResult: function Add_handleResult() {
-          cb();
-        },
-        handleCompletion: function Add_handleCompletion() {
-          // Nothing to do
-        }
-    });
-    // Spin the event loop to embed this async call in a sync API
-    cb.wait();
-  },
-
-  /**
-   * Find
-   *
-   * Finds visits for a uri to the history database.  Throws on error.
-   *
-   * @param item An object representing one or more visits to a specific uri
-   * @param usSinceEpoch The number of microseconds from Epoch to
-   *        the time the current Crossweave run was started
-   * @return true if all the visits for the uri are found, otherwise false
-   */
-  Find: function(item, usSinceEpoch) {
-    Logger.AssertTrue("visits" in item && "uri" in item,
-      "History entry in test file must have both 'visits' " +
-      "and 'uri' properties");
-    let curvisits = this._getVisits(item.uri);
-    for each (visit in curvisits) {
-      for each (itemvisit in item.visits) {
-        let expectedDate = itemvisit.date * 60 * 60 * 1000 * 1000
-            + usSinceEpoch;
-        if (visit.type == itemvisit.type && visit.date == expectedDate) {
-          itemvisit.found = true;
-        }
-      }
-    }
-
-    let all_items_found = true;
-    for each (itemvisit in item.visits) {
-      all_items_found = all_items_found && "found" in itemvisit;
-      Logger.logInfo("History entry for " + item.uri + ", type:" +
-              itemvisit.type + ", date:" + itemvisit.date +
-              ("found" in itemvisit ? " is present" : " is not present"));
-    }
-    return all_items_found;
-  },
-
-  /**
-   * Delete
-   *
-   * Removes visits from the history database. Throws on error.
-   *
-   * @param item An object representing items to delete
-   * @param usSinceEpoch The number of microseconds from Epoch to
-   *        the time the current Crossweave run was started
-   * @return nothing
-   */
-  Delete: function(item, usSinceEpoch) {
-    if ("uri" in item) {
-      let uri = Services.io.newURI(item.uri, null, null);
-      PlacesUtils.history.removePage(uri);
-    }
-    else if ("host" in item) {
-      PlacesUtils.history.removePagesFromHost(item.host, false);
-    }
-    else if ("begin" in item && "end" in item) {
-      PlacesUtils.history.removeVisitsByTimeframe(
-          usSinceEpoch + (item.begin * 60 * 60 * 1000 * 1000),
-          usSinceEpoch + (item.end * 60 * 60 * 1000 * 1000));
-    }
-    else {
-      Logger.AssertTrue(false, "invalid entry in delete history");
-    }
-  },
-};
-
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/logger.jsm
+++ /dev/null
@@ -1,151 +0,0 @@
-/* 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 is a JavaScript module (JSM) to be imported via
-    Components.utils.import() and acts as a singleton.
-    Only the following listed symbols will exposed on import, and only when
-    and where imported. */
-
-var EXPORTED_SYMBOLS = ["Logger"];
-
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
-
-var Logger =
-{
-  _foStream: null,
-  _converter: null,
-  _potentialError: null,
-
-  init: function (path) {
-    if (this._converter != null) {
-      // we're already open!
-      return;
-    }
-
-    let prefs = CC["@mozilla.org/preferences-service;1"]
-                .getService(CI.nsIPrefBranch);
-    if (path) {
-      prefs.setCharPref("tps.logfile", path);
-    }
-    else {
-      path = prefs.getCharPref("tps.logfile");
-    }
-
-    this._file = CC["@mozilla.org/file/local;1"]
-                 .createInstance(CI.nsILocalFile);
-    this._file.initWithPath(path);
-    var exists = this._file.exists();
-
-    // Make a file output stream and converter to handle it.
-    this._foStream = CC["@mozilla.org/network/file-output-stream;1"]
-                     .createInstance(CI.nsIFileOutputStream);
-    // If the file already exists, append it, otherwise create it.
-    var fileflags = exists ? 0x02 | 0x08 | 0x10 : 0x02 | 0x08 | 0x20;
-
-    this._foStream.init(this._file, fileflags, 0666, 0);
-    this._converter = CC["@mozilla.org/intl/converter-output-stream;1"]
-                      .createInstance(CI.nsIConverterOutputStream);
-    this._converter.init(this._foStream, "UTF-8", 0, 0);
-  },
-
-  write: function (data) {
-    if (this._converter == null) {
-      CU.reportError(
-          "TPS Logger.write called with _converter == null!");
-      return;
-    }
-    this._converter.writeString(data);
-  },
-
-  close: function () {
-    if (this._converter != null) {
-      this._converter.close();
-      this._converter = null;
-      this._foStream = null;
-    }
-  },
-
-  AssertTrue: function(bool, msg, showPotentialError) {
-    if (bool) {
-      return;
-    }
-
-    if (showPotentialError && this._potentialError) {
-      msg += "; " + this._potentialError;
-      this._potentialError = null;
-    }
-    throw("ASSERTION FAILED! " + msg);
-  },
-
-  AssertFalse: function(bool, msg, showPotentialError) {
-    return this.AssertTrue(!bool, msg, showPotentialError);
-  },
-
-  AssertEqual: function(val1, val2, msg) {
-    if (val1 != val2)
-      throw("ASSERTION FAILED! " + msg + "; expected " +
-            JSON.stringify(val2) + ", got " + JSON.stringify(val1));
-  },
-
-  log: function (msg, withoutPrefix) {
-    dump(msg + "\n");
-    if (withoutPrefix) {
-      this.write(msg + "\n");
-    }
-    else {
-      function pad(n, len) {
-        let s = "0000" + n;
-        return s.slice(-len);
-      }
-
-      let now = new Date();
-      let year    = pad(now.getFullYear(),     4);
-      let month   = pad(now.getMonth() + 1,    2);
-      let day     = pad(now.getDate(),         2);
-      let hour    = pad(now.getHours(),        2);
-      let minutes = pad(now.getMinutes(),      2);
-      let seconds = pad(now.getSeconds(),      2);
-      let ms      = pad(now.getMilliseconds(), 3);
-
-      this.write(year + "-" + month + "-" + day + " " +
-                 hour + ":" + minutes + ":" + seconds + "." + ms + " " +
-                 msg + "\n");
-    }
-  },
-
-  clearPotentialError: function() {
-    this._potentialError = null;
-  },
-
-  logPotentialError: function(msg) {
-    this._potentialError = msg;
-  },
-
-  logLastPotentialError: function(msg) {
-    var message = msg;
-    if (this._potentialError) {
-      message = this._poentialError;
-      this._potentialError = null;
-    }
-    this.log("CROSSWEAVE ERROR: " + message);
-  },
-
-  logError: function (msg) {
-    this.log("CROSSWEAVE ERROR: " + msg);
-  },
-
-  logInfo: function (msg, withoutPrefix) {
-    if (withoutPrefix)
-      this.log(msg, true);
-    else
-      this.log("CROSSWEAVE INFO: " + msg);
-  },
-
-  logPass: function (msg) {
-    this.log("CROSSWEAVE TEST PASS: " + msg);
-  },
-};
-
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/passwords.jsm
+++ /dev/null
@@ -1,165 +0,0 @@
-/* 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 is a JavaScript module (JSM) to be imported via
-  * Components.utils.import() and acts as a singleton. Only the following
-  * listed symbols will exposed on import, and only when and where imported.
-  */
-
-var EXPORTED_SYMBOLS = ["Password", "DumpPasswords"];
-
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
-
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://tps/logger.jsm");
-
-let nsLoginInfo = new Components.Constructor(
-                      "@mozilla.org/login-manager/loginInfo;1",
-                      CI.nsILoginInfo,
-                      "init");
-
-var DumpPasswords = function TPS__Passwords__DumpPasswords() {
-  let logins = Services.logins.getAllLogins();
-  Logger.logInfo("\ndumping password list\n", true);
-  for (var i = 0; i < logins.length; i++) {
-    Logger.logInfo("* host=" + logins[i].hostname + ", submitURL=" + logins[i].formSubmitURL +
-                   ", realm=" + logins[i].httpRealm + ", password=" + logins[i].password +
-                   ", passwordField=" + logins[i].passwordField + ", username=" +
-                   logins[i].username + ", usernameField=" + logins[i].usernameField, true);
-  }
-  Logger.logInfo("\n\nend password list\n", true);
-};
-
-/**
- * PasswordProps object; holds password properties.
- */
-function PasswordProps(props) {
-  this.hostname = null;
-  this.submitURL = null;
-  this.realm = null;
-  this.username = "";
-  this.password = "";
-  this.usernameField = "";
-  this.passwordField = "";
-  this.delete = false;
-
-  for (var prop in props) {
-    if (prop in this)
-      this[prop] = props[prop];
-  }
-}
-
-/**
- * Password class constructor. Initializes instance properties.
- */
-function Password(props) {
-  this.props = new PasswordProps(props);
-  if ("changes" in props) {
-    this.updateProps = new PasswordProps(props);
-    for (var prop in props.changes)
-      if (prop in this.updateProps)
-        this.updateProps[prop] = props.changes[prop];
-  }
-  else {
-    this.updateProps = null;
-  }
-}
-
-/**
- * Password instance methods.
- */
-Password.prototype = {
-  /**
-   * Create
-   *
-   * Adds a password entry to the login manager for the password
-   * represented by this object's properties. Throws on error.
-   *
-   * @return the new login guid
-   */
-  Create: function() {
-    let login = new nsLoginInfo(this.props.hostname, this.props.submitURL,
-                                this.props.realm, this.props.username,
-                                this.props.password,
-                                this.props.usernameField,
-                                this.props.passwordField);
-    Services.logins.addLogin(login);
-    login.QueryInterface(CI.nsILoginMetaInfo);
-    return login.guid;
-  },
-
-  /**
-   * Find
-   *
-   * Finds a password entry in the login manager, for the password
-   * represented by this object's properties.
-   *
-   * @return the guid of the password if found, otherwise -1
-   */
-  Find: function() {
-    let logins = Services.logins.findLogins({},
-                                            this.props.hostname,
-                                            this.props.submitURL,
-                                            this.props.realm);
-    for (var i = 0; i < logins.length; i++) {
-      if (logins[i].username == this.props.username &&
-          logins[i].password == this.props.password &&
-          logins[i].usernameField == this.props.usernameField &&
-          logins[i].passwordField == this.props.passwordField) {
-        logins[i].QueryInterface(CI.nsILoginMetaInfo);
-        return logins[i].guid;
-      }
-    }
-    return -1;
-  },
-
-  /**
-   * Update
-   *
-   * Updates an existing password entry in the login manager with
-   * new properties. Throws on error.  The 'old' properties are this
-   * object's properties, the 'new' properties are the properties in
-   * this object's 'updateProps' object.
-   *
-   * @return nothing
-   */
-  Update: function() {
-    let oldlogin = new nsLoginInfo(this.props.hostname,
-                                   this.props.submitURL,
-                                   this.props.realm,
-                                   this.props.username,
-                                   this.props.password,
-                                   this.props.usernameField,
-                                   this.props.passwordField);
-    let newlogin = new nsLoginInfo(this.updateProps.hostname,
-                                   this.updateProps.submitURL,
-                                   this.updateProps.realm,
-                                   this.updateProps.username,
-                                   this.updateProps.password,
-                                   this.updateProps.usernameField,
-                                   this.updateProps.passwordField);
-    Services.logins.modifyLogin(oldlogin, newlogin);
-  },
-
-  /**
-   * Remove
-   *
-   * Removes an entry from the login manager for a password which
-   * matches this object's properties. Throws on error.
-   *
-   * @return nothing
-   */
-  Remove: function() {
-    let login = new nsLoginInfo(this.props.hostname,
-                                this.props.submitURL,
-                                this.props.realm,
-                                this.props.username,
-                                this.props.password,
-                                this.props.usernameField,
-                                this.props.passwordField);
-    Services.logins.removeLogin(login);
-  },
-};
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/prefs.jsm
+++ /dev/null
@@ -1,118 +0,0 @@
-/* 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 is a JavaScript module (JSM) to be imported via
-   Components.utils.import() and acts as a singleton.
-   Only the following listed symbols will exposed on import, and only when
-   and where imported. */
-
-var EXPORTED_SYMBOLS = ["Preference"];
-
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
-const WEAVE_PREF_PREFIX = "services.sync.prefs.sync.";
-
-let prefs = CC["@mozilla.org/preferences-service;1"]
-           .getService(CI.nsIPrefBranch);
-
-CU.import("resource://tps/logger.jsm");
-
-/**
- * Preference class constructor
- *
- * Initializes instance properties.
- */
-function Preference (props) {
-  Logger.AssertTrue("name" in props && "value" in props,
-    "Preference must have both name and value");
-
-  this.name = props.name;
-  this.value = props.value;
-}
-
-/**
- * Preference instance methods
- */
-Preference.prototype = {
-  /**
-   * Modify
-   *
-   * Sets the value of the preference this.name to this.value.
-   * Throws on error.
-   *
-   * @return nothing
-   */
-  Modify: function() {
-    // Determine if this pref is actually something Weave even looks at.
-    let weavepref = WEAVE_PREF_PREFIX + this.name;
-    try {
-      let syncPref = prefs.getBoolPref(weavepref);
-      if (!syncPref)
-        prefs.setBoolPref(weavepref, true);
-    }
-    catch(e) {
-      Logger.AssertTrue(false, "Weave doesn't sync pref " + this.name);
-    }
-
-    // Modify the pref; throw an exception if the pref type is different
-    // than the value type specified in the test.
-    let prefType = prefs.getPrefType(this.name);
-    switch (prefType) {
-      case CI.nsIPrefBranch.PREF_INT:
-        Logger.AssertEqual(typeof(this.value), "number",
-          "Wrong type used for preference value");
-        prefs.setIntPref(this.name, this.value);
-        break;
-      case CI.nsIPrefBranch.PREF_STRING:
-        Logger.AssertEqual(typeof(this.value), "string",
-          "Wrong type used for preference value");
-        prefs.setCharPref(this.name, this.value);
-        break;
-      case CI.nsIPrefBranch.PREF_BOOL:
-        Logger.AssertEqual(typeof(this.value), "boolean",
-          "Wrong type used for preference value");
-        prefs.setBoolPref(this.name, this.value);
-        break;
-    }
-  },
-
-  /**
-   * Find
-   *
-   * Verifies that the preference this.name has the value
-   * this.value. Throws on error, or if the pref's type or value
-   * doesn't match.
-   *
-   * @return nothing
-   */
-  Find: function() {
-    // Read the pref value.
-    let value;
-    try {
-      let prefType = prefs.getPrefType(this.name);
-      switch(prefType) {
-        case CI.nsIPrefBranch.PREF_INT:
-          value = prefs.getIntPref(this.name);
-          break;
-        case CI.nsIPrefBranch.PREF_STRING:
-          value = prefs.getCharPref(this.name);
-          break;
-        case CI.nsIPrefBranch.PREF_BOOL:
-          value = prefs.getBoolPref(this.name);
-          break;
-      }
-    }
-    catch (e) {
-      Logger.AssertTrue(false, "Error accessing pref " + this.name);
-    }
-
-    // Throw an exception if the current and expected values aren't of
-    // the same type, or don't have the same values.
-    Logger.AssertEqual(typeof(value), typeof(this.value),
-      "Value types don't match");
-    Logger.AssertEqual(value, this.value, "Preference values don't match");
-  },
-};
-
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/quit.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; -*- */
-/* 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/. */
-
-/*
-  From mozilla/toolkit/content
-  These files did not have a license
-*/
-var EXPORTED_SYMBOLS = ["goQuitApplication"];
-
-Components.utils.import("resource://gre/modules/Services.jsm");
-
-function canQuitApplication()
-{
-  try
-  {
-    var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
-      .createInstance(Components.interfaces.nsISupportsPRBool);
-    Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
-
-    // Something aborted the quit process.
-    if (cancelQuit.data)
-    {
-      return false;
-    }
-  }
-  catch (ex)
-  {
-  }
-  return true;
-}
-
-function goQuitApplication()
-{
-  if (!canQuitApplication())
-  {
-    return false;
-  }
-
-  const kAppStartup = '@mozilla.org/toolkit/app-startup;1';
-  const kAppShell   = '@mozilla.org/appshell/appShellService;1';
-  var   appService;
-  var   forceQuit;
-
-  if (kAppStartup in Components.classes)
-  {
-    appService = Components.classes[kAppStartup].
-      getService(Components.interfaces.nsIAppStartup);
-    forceQuit  = Components.interfaces.nsIAppStartup.eForceQuit;
-  }
-  else if (kAppShell in Components.classes)
-  {
-    appService = Components.classes[kAppShell].
-      getService(Components.interfaces.nsIAppShellService);
-    forceQuit = Components.interfaces.nsIAppShellService.eForceQuit;
-  }
-  else
-  {
-    throw 'goQuitApplication: no AppStartup/appShell';
-  }
-
-  try
-  {
-    appService.quit(forceQuit);
-  }
-  catch(ex)
-  {
-    throw('goQuitApplication: ' + ex);
-  }
-
-  return true;
-}
-
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/sync.jsm
+++ /dev/null
@@ -1,116 +0,0 @@
-/* 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/. */
-
-
-var EXPORTED_SYMBOLS = ["TPS", "SYNC_WIPE_SERVER", "SYNC_RESET_CLIENT",
-                        "SYNC_WIPE_CLIENT"];
-
-const CC = Components.classes;
-const CI = Components.interfaces;
-const CU = Components.utils;
-
-CU.import("resource://gre/modules/XPCOMUtils.jsm");
-CU.import("resource://gre/modules/Services.jsm");
-CU.import("resource://services-sync/util.js");
-CU.import("resource://tps/logger.jsm");
-var utils = {}; CU.import('resource://mozmill/modules/utils.js', utils);
-
-const SYNC_RESET_CLIENT = "reset-client";
-const SYNC_WIPE_CLIENT  = "wipe-client";
-const SYNC_WIPE_REMOTE  = "wipe-remote";
-const SYNC_WIPE_SERVER  = "wipe-server";
-
-var prefs = CC["@mozilla.org/preferences-service;1"]
-            .getService(CI.nsIPrefBranch);
-
-var syncFinishedCallback = function() {
-  Logger.logInfo('syncFinishedCallback returned ' + !TPS._waitingForSync);
-  return !TPS._waitingForSync;
-};
-
-var TPS = {
-  _waitingForSync: false,
-  _syncErrors: 0,
-
-  QueryInterface: XPCOMUtils.generateQI([CI.nsIObserver,
-                                         CI.nsISupportsWeakReference]),
-
-  observe: function TPS__observe(subject, topic, data) {
-    Logger.logInfo('Mozmill observed: ' + topic);
-    switch(topic) {
-      case "weave:service:sync:error":
-        if (this._waitingForSync && this._syncErrors == 0) {
-          Logger.logInfo("sync error; retrying...");
-          this._syncErrors++;
-          Utils.namedTimer(function() {
-            Weave.service.sync();
-          }, 1000, this, "resync");
-        }
-        else if (this._waitingForSync) {
-          this._syncErrors = "sync error, see log";
-          this._waitingForSync = false;
-        }
-        break;
-      case "weave:service:sync:finish":
-        if (this._waitingForSync) {
-          this._syncErrors = 0;
-          this._waitingForSync = false;
-        }
-        break;
-    }
-  },
-
-  SetupSyncAccount: function TPS__SetupSyncAccount() {
-    try {
-      let serverURL = prefs.getCharPref('tps.account.serverURL');
-      if (serverURL) {
-        Weave.Service.serverURL = serverURL;
-      }
-    }
-    catch(e) {}
-
-    Weave.Service.identity.account       = prefs.getCharPref('tps.account.username');
-    Weave.Service.Identity.basicPassword = prefs.getCharPref('tps.account.password');
-    Weave.Service.identity.syncKey       = prefs.getCharPref('tps.account.passphrase');
-    Weave.Svc.Obs.notify("weave:service:setup-complete");
-  },
-
-  Sync: function TPS__Sync(options) {
-    Logger.logInfo('Mozmill starting sync operation: ' + options);
-    switch(options) {
-      case SYNC_WIPE_REMOTE:
-        Weave.Svc.Prefs.set("firstSync", "wipeRemote");
-        break;
-      case SYNC_WIPE_CLIENT:
-        Weave.Svc.Prefs.set("firstSync", "wipeClient");
-        break;
-      case SYNC_RESET_CLIENT:
-        Weave.Svc.Prefs.set("firstSync", "resetClient");
-        break;
-      default:
-        Weave.Svc.Prefs.reset("firstSync");
-    }
-
-    if (Weave.Status.service != Weave.STATUS_OK) {
-      return "Sync status not ok: " + Weave.Status.service;
-    }
-
-    this._syncErrors = 0;
-
-    if (options == SYNC_WIPE_SERVER) {
-      Weave.Service.wipeServer();
-    } else {
-      this._waitingForSync = true;
-      Weave.Service.sync();
-      utils.waitFor(syncFinishedCallback, null, 20000, 500, TPS);
-    }
-    return this._syncErrors;
-  },
-};
-
-Services.obs.addObserver(TPS, "weave:service:sync:finish", true);
-Services.obs.addObserver(TPS, "weave:service:sync:error", true);
-Logger.init();
-
-
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/tabs.jsm
+++ /dev/null
@@ -1,63 +0,0 @@
-/* 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 is a JavaScript module (JSM) to be imported via
-   Components.utils.import() and acts as a singleton.
-   Only the following listed symbols will exposed on import, and only when
-   and where imported. */
-
-const EXPORTED_SYMBOLS = ["BrowserTabs"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://services-sync/main.js");
-
-let BrowserTabs = {
-  /**
-   * Add
-   *
-   * Opens a new tab in the current browser window for the
-   * given uri.  Throws on error.
-   *
-   * @param uri The uri to load in the new tab
-   * @return nothing
-   */
-  Add: function(uri, fn) {
-    // Open the uri in a new tab in the current browser window, and calls
-    // the callback fn from the tab's onload handler.
-    let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
-               .getService(Ci.nsIWindowMediator);
-    let mainWindow = wm.getMostRecentWindow("navigator:browser");
-    let newtab = mainWindow.getBrowser().addTab(uri);
-    mainWindow.getBrowser().selectedTab = newtab;
-    let win = mainWindow.getBrowser().getBrowserForTab(newtab);
-    win.addEventListener("load", function() { fn.call(); }, true);
-  },
-
-  /**
-   * Find
-   *
-   * Finds the specified uri and title in Weave's list of remote tabs
-   * for the specified profile.
-   *
-   * @param uri The uri of the tab to find
-   * @param title The page title of the tab to find
-   * @param profile The profile to search for tabs
-   * @return true if the specified tab could be found, otherwise false
-   */
-  Find: function(uri, title, profile) {
-    // Find the uri in Weave's list of tabs for the given profile.
-    let engine = Weave.Service.engineManager.get("tabs");
-    for (let [guid, client] in Iterator(engine.getAllClients())) {
-      for each (tab in client.tabs) {
-        let weaveTabUrl = tab.urlHistory[0];
-        if (uri == weaveTabUrl && profile == client.clientName)
-          if (title == undefined || title == tab.title)
-            return true;
-      }
-    }
-    return false;
-  },
-};
-
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/tps.jsm
+++ /dev/null
@@ -1,981 +0,0 @@
-/* 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 is a JavaScript module (JSM) to be imported via
-  * Components.utils.import() and acts as a singleton. Only the following
-  * listed symbols will exposed on import, and only when and where imported.
-  */
-
-let EXPORTED_SYMBOLS = ["TPS"];
-
-const {classes: CC, interfaces: CI, utils: CU} = Components;
-
-CU.import("resource://gre/modules/XPCOMUtils.jsm");
-CU.import("resource://gre/modules/Services.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://tps/addons.jsm");
-CU.import("resource://tps/bookmarks.jsm");
-CU.import("resource://tps/logger.jsm");
-CU.import("resource://tps/passwords.jsm");
-CU.import("resource://tps/history.jsm");
-CU.import("resource://tps/forms.jsm");
-CU.import("resource://tps/fxaccounts.jsm");
-CU.import("resource://tps/prefs.jsm");
-CU.import("resource://tps/tabs.jsm");
-CU.import("resource://tps/windows.jsm");
-
-var hh = CC["@mozilla.org/network/protocol;1?name=http"]
-         .getService(CI.nsIHttpProtocolHandler);
-var prefs = CC["@mozilla.org/preferences-service;1"]
-            .getService(CI.nsIPrefBranch);
-
-var mozmillInit = {};
-CU.import('resource://mozmill/modules/init.js', mozmillInit);
-
-const ACTION_ADD              = "add";
-const ACTION_VERIFY           = "verify";
-const ACTION_VERIFY_NOT       = "verify-not";
-const ACTION_MODIFY           = "modify";
-const ACTION_SYNC             = "sync";
-const ACTION_DELETE           = "delete";
-const ACTION_PRIVATE_BROWSING = "private-browsing";
-const ACTION_WIPE_REMOTE      = "wipe-remote";
-const ACTION_WIPE_SERVER      = "wipe-server";
-const ACTION_SET_ENABLED      = "set-enabled";
-
-const ACTIONS = [ACTION_ADD, ACTION_VERIFY, ACTION_VERIFY_NOT,
-                 ACTION_MODIFY, ACTION_SYNC, ACTION_DELETE,
-                 ACTION_PRIVATE_BROWSING, ACTION_WIPE_REMOTE,
-                 ACTION_WIPE_SERVER, ACTION_SET_ENABLED];
-
-const SYNC_WIPE_CLIENT  = "wipe-client";
-const SYNC_WIPE_REMOTE  = "wipe-remote";
-const SYNC_WIPE_SERVER  = "wipe-server";
-const SYNC_RESET_CLIENT = "reset-client";
-const SYNC_START_OVER   = "start-over";
-
-const OBSERVER_TOPICS = ["fxaccounts:onlogin",
-                         "fxaccounts:onlogout",
-                         "weave:engine:start-tracking",
-                         "weave:engine:stop-tracking",
-                         "weave:service:setup-complete",
-                         "weave:service:sync:finish",
-                         "weave:service:sync:error",
-                         "sessionstore-windows-restored",
-                         "private-browsing"];
-
-let TPS = {
-  _waitingForSync: false,
-  _isTracking: false,
-  _test: null,
-  _currentAction: -1,
-  _currentPhase: -1,
-  _errors: 0,
-  _setupComplete: false,
-  _syncErrors: 0,
-  _usSinceEpoch: 0,
-  _tabsAdded: 0,
-  _tabsFinished: 0,
-  _phaselist: {},
-  _operations_pending: 0,
-  _loggedIn: false,
-  _enabledEngines: null,
-
-  DumpError: function (msg) {
-    this._errors++;
-    Logger.logError("[phase" + this._currentPhase + "] " + msg);
-    this.quit();
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([CI.nsIObserver,
-                                         CI.nsISupportsWeakReference]),
-
-  observe: function TPS__observe(subject, topic, data) {
-    try {
-      Logger.logInfo("----------event observed: " + topic);
-      switch(topic) {
-        case "private-browsing":
-          Logger.logInfo("private browsing " + data);
-          break;
-
-        case "fxaccounts:onlogin":
-          this._loggedIn = true;
-          break;
-
-        case "fxaccounts:onlogout":
-          this._loggedIn = false;
-          break;
-
-        case "weave:service:sync:error":
-          if (this._waitingForSync && this._syncErrors == 0) {
-            // if this is the first sync error, retry...
-            Logger.logInfo("sync error; retrying...");
-            this._syncErrors++;
-            this._waitingForSync = false;
-            Utils.nextTick(this.RunNextTestAction, this);
-          }
-          else if (this._waitingForSync) {
-            // ...otherwise abort the test
-            this.DumpError("sync error; aborting test");
-            return;
-          }
-          break;
-
-        case "weave:service:setup-complete":
-          this._setupComplete = true;
-          break;
-
-        case "weave:service:sync:finish":
-          if (this._waitingForSync) {
-            this._syncErrors = 0;
-            this._waitingForSync = false;
-            // Wait a second before continuing, otherwise we can get
-            // 'sync not complete' errors.
-            Utils.namedTimer(function() {
-              this.FinishAsyncOperation();
-            }, 1000, this, "postsync");
-          }
-          break;
-
-        case "weave:engine:start-tracking":
-          this._isTracking = true;
-          break;
-
-        case "weave:engine:stop-tracking":
-          this._isTracking = false;
-          break;
-
-        case "sessionstore-windows-restored":
-          Utils.nextTick(this.RunNextTestAction, this);
-          break;
-      }
-    }
-    catch(e) {
-      this.DumpError("Exception caught: " + Utils.exceptionStr(e));
-      return;
-    }
-  },
-
-  StartAsyncOperation: function TPS__StartAsyncOperation() {
-    this._operations_pending++;
-  },
-
-  FinishAsyncOperation: function TPS__FinishAsyncOperation() {
-    this._operations_pending--;
-    if (!this.operations_pending) {
-      this._currentAction++;
-      Utils.nextTick(function() {
-        this.RunNextTestAction();
-      }, this);
-    }
-  },
-
-  quit: function () {
-    OBSERVER_TOPICS.forEach(function(topic) {
-      Services.obs.removeObserver(this, topic);
-    }, this);
-    Logger.close();
-    this.goQuitApplication();
-  },
-
-  HandleWindows: function (aWindow, action) {
-    Logger.logInfo("executing action " + action.toUpperCase() +
-                   " on window " + JSON.stringify(aWindow));
-    switch(action) {
-      case ACTION_ADD:
-        BrowserWindows.Add(aWindow.private, function(win) {
-          Logger.logInfo("window finished loading");
-          this.FinishAsyncOperation();
-        }.bind(this));
-        break;
-    }
-    Logger.logPass("executing action " + action.toUpperCase() + " on windows");
-  },
-
-  HandleTabs: function (tabs, action) {
-    this._tabsAdded = tabs.length;
-    this._tabsFinished = 0;
-    for each (let tab in tabs) {
-      Logger.logInfo("executing action " + action.toUpperCase() +
-                     " on tab " + JSON.stringify(tab));
-      switch(action) {
-        case ACTION_ADD:
-          // When adding tabs, we keep track of how many tabs we're adding,
-          // and wait until we've received that many onload events from our
-          // new tabs before continuing
-          let that = this;
-          let taburi = tab.uri;
-          BrowserTabs.Add(tab.uri, function() {
-            that._tabsFinished++;
-            Logger.logInfo("tab for " + taburi + " finished loading");
-            if (that._tabsFinished == that._tabsAdded) {
-              Logger.logInfo("all tabs loaded, continuing...");
-              that.FinishAsyncOperation();
-            }
-          });
-          break;
-        case ACTION_VERIFY:
-          Logger.AssertTrue(typeof(tab.profile) != "undefined",
-            "profile must be defined when verifying tabs");
-          Logger.AssertTrue(
-            BrowserTabs.Find(tab.uri, tab.title, tab.profile), "error locating tab");
-          break;
-        case ACTION_VERIFY_NOT:
-          Logger.AssertTrue(typeof(tab.profile) != "undefined",
-            "profile must be defined when verifying tabs");
-          Logger.AssertTrue(
-            !BrowserTabs.Find(tab.uri, tab.title, tab.profile),
-            "tab found which was expected to be absent");
-          break;
-        default:
-          Logger.AssertTrue(false, "invalid action: " + action);
-      }
-    }
-    Logger.logPass("executing action " + action.toUpperCase() + " on tabs");
-  },
-
-  HandlePrefs: function (prefs, action) {
-    for each (pref in prefs) {
-      Logger.logInfo("executing action " + action.toUpperCase() +
-                     " on pref " + JSON.stringify(pref));
-      let preference = new Preference(pref);
-      switch(action) {
-        case ACTION_MODIFY:
-          preference.Modify();
-          break;
-        case ACTION_VERIFY:
-          preference.Find();
-          break;
-        default:
-          Logger.AssertTrue(false, "invalid action: " + action);
-      }
-    }
-    Logger.logPass("executing action " + action.toUpperCase() + " on pref");
-  },
-
-  HandleForms: function (data, action) {
-    for each (datum in 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;
-        case ACTION_DELETE:
-          formdata.Remove();
-          break;
-        case ACTION_VERIFY:
-          Logger.AssertTrue(formdata.Find(), "form data not found");
-          break;
-        case ACTION_VERIFY_NOT:
-          Logger.AssertTrue(!formdata.Find(),
-            "form data found, but it shouldn't be present");
-          break;
-        default:
-          Logger.AssertTrue(false, "invalid action: " + action);
-      }
-    }
-    Logger.logPass("executing action " + action.toUpperCase() +
-                   " on formdata");
-  },
-
-  HandleHistory: function (entries, action) {
-    try {
-      for each (entry in entries) {
-        Logger.logInfo("executing action " + action.toUpperCase() +
-                       " on history entry " + JSON.stringify(entry));
-        switch(action) {
-          case ACTION_ADD:
-            HistoryEntry.Add(entry, this._usSinceEpoch);
-            break;
-          case ACTION_DELETE:
-            HistoryEntry.Delete(entry, this._usSinceEpoch);
-            break;
-          case ACTION_VERIFY:
-            Logger.AssertTrue(HistoryEntry.Find(entry, this._usSinceEpoch),
-              "Uri visits not found in history database");
-            break;
-          case ACTION_VERIFY_NOT:
-            Logger.AssertTrue(!HistoryEntry.Find(entry, this._usSinceEpoch),
-              "Uri visits found in history database, but they shouldn't be");
-            break;
-          default:
-            Logger.AssertTrue(false, "invalid action: " + action);
-        }
-      }
-      Logger.logPass("executing action " + action.toUpperCase() +
-                     " on history");
-    }
-    catch(e) {
-      DumpHistory();
-      throw(e);
-    }
-  },
-
-  HandlePasswords: function (passwords, action) {
-    try {
-      for each (password in passwords) {
-        let password_id = -1;
-        Logger.logInfo("executing action " + action.toUpperCase() +
-                      " on password " + JSON.stringify(password));
-        var password = new Password(password);
-        switch (action) {
-          case ACTION_ADD:
-            Logger.AssertTrue(password.Create() > -1, "error adding password");
-            break;
-          case ACTION_VERIFY:
-            Logger.AssertTrue(password.Find() != -1, "password not found");
-            break;
-          case ACTION_VERIFY_NOT:
-            Logger.AssertTrue(password.Find() == -1,
-              "password found, but it shouldn't exist");
-            break;
-          case ACTION_DELETE:
-            Logger.AssertTrue(password.Find() != -1, "password not found");
-            password.Remove();
-            break;
-          case ACTION_MODIFY:
-            if (password.updateProps != null) {
-              Logger.AssertTrue(password.Find() != -1, "password not found");
-              password.Update();
-            }
-            break;
-          default:
-            Logger.AssertTrue(false, "invalid action: " + action);
-        }
-      }
-      Logger.logPass("executing action " + action.toUpperCase() +
-                     " on passwords");
-    }
-    catch(e) {
-      DumpPasswords();
-      throw(e);
-    }
-  },
-
-  HandleAddons: function (addons, action, state) {
-    for each (let entry in addons) {
-      Logger.logInfo("executing action " + action.toUpperCase() +
-                     " on addon " + JSON.stringify(entry));
-      let addon = new Addon(this, entry);
-      switch(action) {
-        case ACTION_ADD:
-          addon.install();
-          break;
-        case ACTION_DELETE:
-          addon.uninstall();
-          break;
-        case ACTION_VERIFY:
-          Logger.AssertTrue(addon.find(state), 'addon ' + addon.id + ' not found');
-          break;
-        case ACTION_VERIFY_NOT:
-          Logger.AssertFalse(addon.find(state), 'addon ' + addon.id + " is present, but it shouldn't be");
-          break;
-        case ACTION_SET_ENABLED:
-          Logger.AssertTrue(addon.setEnabled(state), 'addon ' + addon.id + ' not found');
-          break;
-        default:
-          throw new Error("Unknown action for add-on: " + action);
-      }
-    }
-    Logger.logPass("executing action " + action.toUpperCase() +
-                   " on addons");
-  },
-
-  HandleBookmarks: function (bookmarks, action) {
-    try {
-      let items = [];
-      for (folder in bookmarks) {
-        let last_item_pos = -1;
-        for each (bookmark in bookmarks[folder]) {
-          Logger.clearPotentialError();
-          let placesItem;
-          bookmark['location'] = folder;
-          if (last_item_pos != -1)
-            bookmark['last_item_pos'] = last_item_pos;
-          let item_id = -1;
-          if (action != ACTION_MODIFY && action != ACTION_DELETE)
-            Logger.logInfo("executing action " + action.toUpperCase() +
-                           " on bookmark " + JSON.stringify(bookmark));
-          if ("uri" in bookmark)
-            placesItem = new Bookmark(bookmark);
-          else if ("folder" in bookmark)
-            placesItem = new BookmarkFolder(bookmark);
-          else if ("livemark" in bookmark)
-            placesItem = new Livemark(bookmark);
-          else if ("separator" in bookmark)
-            placesItem = new Separator(bookmark);
-          if (action == ACTION_ADD) {
-            item_id = placesItem.Create();
-          }
-          else {
-            item_id = placesItem.Find();
-            if (action == ACTION_VERIFY_NOT) {
-              Logger.AssertTrue(item_id == -1,
-                "places item exists but it shouldn't: " +
-                JSON.stringify(bookmark));
-            }
-            else
-              Logger.AssertTrue(item_id != -1, "places item not found", true);
-          }
-
-          last_item_pos = placesItem.GetItemIndex();
-          items.push(placesItem);
-        }
-      }
-
-      if (action == ACTION_DELETE || action == ACTION_MODIFY) {
-        for each (item in items) {
-          Logger.logInfo("executing action " + action.toUpperCase() +
-                         " on bookmark " + JSON.stringify(item));
-          switch(action) {
-            case ACTION_DELETE:
-              item.Remove();
-              break;
-            case ACTION_MODIFY:
-              if (item.updateProps != null)
-                item.Update();
-              break;
-          }
-        }
-      }
-
-      Logger.logPass("executing action " + action.toUpperCase() +
-        " on bookmarks");
-    }
-    catch(e) {
-      DumpBookmarks();
-      throw(e);
-    }
-  },
-
-  MozmillEndTestListener: function TPS__MozmillEndTestListener(obj) {
-    Logger.logInfo("mozmill endTest: " + JSON.stringify(obj));
-    if (obj.failed > 0) {
-      this.DumpError('mozmill test failed, name: ' + obj.name + ', reason: ' + JSON.stringify(obj.fails));
-      return;
-    }
-    else if ('skipped' in obj && obj.skipped) {
-      this.DumpError('mozmill test failed, name: ' + obj.name + ', reason: ' + obj.skipped_reason);
-      return;
-    }
-    else {
-      Utils.namedTimer(function() {
-        this.FinishAsyncOperation();
-      }, 2000, this, "postmozmilltest");
-    }
-  },
-
-  MozmillSetTestListener: function TPS__MozmillSetTestListener(obj) {
-    Logger.logInfo("mozmill setTest: " + obj.name);
-  },
-
-  RunNextTestAction: function() {
-    try {
-      if (this._currentAction >=
-          this._phaselist["phase" + this._currentPhase].length) {
-        // we're all done
-        Logger.logInfo("test phase " + this._currentPhase + ": " +
-          (this._errors ? "FAIL" : "PASS"));
-        this.quit();
-        return;
-      }
-
-      if (this.seconds_since_epoch)
-        this._usSinceEpoch = this.seconds_since_epoch * 1000 * 1000;
-      else {
-        this.DumpError("seconds-since-epoch not set");
-        return;
-      }
-
-      let phase = this._phaselist["phase" + this._currentPhase];
-      let action = phase[this._currentAction];
-      Logger.logInfo("starting action: " + JSON.stringify(action));
-      action[0].apply(this, action.slice(1));
-
-      // if we're in an async operation, don't continue on to the next action
-      if (this._operations_pending)
-        return;
-
-      this._currentAction++;
-    }
-    catch(e) {
-      this.DumpError("Exception caught: " + Utils.exceptionStr(e));
-      return;
-    }
-    this.RunNextTestAction();
-  },
-
-  /**
-   * Runs a single test phase.
-   *
-   * This is the main entry point for each phase of a test. The TPS command
-   * line driver loads this module and calls into the function with the
-   * arguments from the command line.
-   *
-   * When a phase is executed, the file is loaded as JavaScript into the
-   * current object.
-   *
-   * The following keys in the options argument have meaning:
-   *
-   *   - ignoreUnusedEngines  If true, unused engines will be unloaded from
-   *                          Sync. This makes output easier to parse and is
-   *                          useful for debugging test failures.
-   *
-   * @param  file
-   *         String URI of the file to open.
-   * @param  phase
-   *         String name of the phase to run.
-   * @param  logpath
-   *         String path of the log file to write to.
-   * @param  options
-   *         Object defining addition run-time options.
-   */
-  RunTestPhase: function (file, phase, logpath, options) {
-    try {
-      let settings = options || {};
-
-      Logger.init(logpath);
-      Logger.logInfo("Sync version: " + WEAVE_VERSION);
-      Logger.logInfo("Firefox builddate: " + Services.appinfo.appBuildID);
-      Logger.logInfo("Firefox version: " + Services.appinfo.version);
-
-      // do some sync housekeeping
-      if (Weave.Service.isLoggedIn) {
-        this.DumpError("Sync logged in on startup...profile may be dirty");
-        return;
-      }
-
-      // Wait for Sync service to become ready.
-      if (!Weave.Status.ready) {
-        this.waitForEvent("weave:service:ready");
-      }
-
-      // Always give Sync an extra tick to initialize. If we waited for the
-      // service:ready event, this is required to ensure all handlers have
-      // executed.
-      Utils.nextTick(this._executeTestPhase.bind(this, file, phase, settings));
-    } catch(e) {
-      this.DumpError("Exception caught: " + Utils.exceptionStr(e));
-      return;
-    }
-  },
-
-  /**
-   * Executes a single test phase.
-   *
-   * This is called by RunTestPhase() after the environment is validated.
-   */
-  _executeTestPhase: function _executeTestPhase(file, phase, settings) {
-    try {
-      OBSERVER_TOPICS.forEach(function(topic) {
-        Services.obs.addObserver(this, topic, true);
-      }, this);
-
-      // parse the test file
-      Services.scriptloader.loadSubScript(file, this);
-      this._currentPhase = phase;
-      let this_phase = this._phaselist["phase" + this._currentPhase];
-
-      if (this_phase == undefined) {
-        this.DumpError("invalid phase " + this._currentPhase);
-        return;
-      }
-
-      if (this.phases["phase" + this._currentPhase] == undefined) {
-        this.DumpError("no profile defined for phase " + this._currentPhase);
-        return;
-      }
-
-      // If we have restricted the active engines, unregister engines we don't
-      // care about.
-      if (settings.ignoreUnusedEngines && Array.isArray(this._enabledEngines)) {
-        let names = {};
-        for each (let name in this._enabledEngines) {
-          names[name] = true;
-        }
-
-        for (let engine of Weave.Service.engineManager.getEnabled()) {
-          if (!(engine.name in names)) {
-            Logger.logInfo("Unregistering unused engine: " + engine.name);
-            Weave.Service.engineManager.unregister(engine);
-          }
-        }
-      }
-
-      Logger.logInfo("Starting phase " + parseInt(phase, 10) + "/" +
-                     Object.keys(this._phaselist).length);
-
-      Logger.logInfo("setting client.name to " + this.phases["phase" + this._currentPhase]);
-      Weave.Svc.Prefs.set("client.name", this.phases["phase" + this._currentPhase]);
-
-      // TODO Phases should be defined in a data type that has strong
-      // ordering, not by lexical sorting.
-      let currentPhase = parseInt(this._currentPhase, 10);
-      // Reset everything at the beginning of the test.
-      if (currentPhase <= 1) {
-        this_phase.unshift([this.ResetData]);
-      }
-
-      // Wipe the server at the end of the final test phase.
-      if (currentPhase >= Object.keys(this.phases).length) {
-        this_phase.push([this.WipeServer]);
-      }
-
-      // Store account details as prefs so they're accessible to the mozmill
-      // framework.
-      prefs.setCharPref('tps.account.username', this.config.fx_account.username);
-      prefs.setCharPref('tps.account.password', this.config.fx_account.password);
-      // old sync
-      // prefs.setCharPref('tps.account.passphrase', this.config.fx_account.passphrase);
-      if (this.config["serverURL"]) {
-        prefs.setCharPref('tps.account.serverURL', this.config.serverURL);
-      }
-      // start processing the test actions
-      this._currentAction = 0;
-    }
-    catch(e) {
-      this.DumpError("Exception caught: " + Utils.exceptionStr(e));
-      return;
-    }
-  },
-
-  /**
-   * Register a single phase with the test harness.
-   *
-   * This is called when loading individual test files.
-   *
-   * @param  phasename
-   *         String name of the phase being loaded.
-   * @param  fnlist
-   *         Array of functions/actions to perform.
-   */
-  Phase: function Test__Phase(phasename, fnlist) {
-    this._phaselist[phasename] = fnlist;
-  },
-
-  /**
-   * Restrict enabled Sync engines to a specified set.
-   *
-   * This can be called by a test to limit what engines are enabled. It is
-   * recommended to call it to reduce the overhead and log clutter for the
-   * test.
-   *
-   * The "clients" engine is special and is always enabled, so there is no
-   * need to specify it.
-   *
-   * @param  names
-   *         Array of Strings for engines to make active during the test.
-   */
-  EnableEngines: function EnableEngines(names) {
-    if (!Array.isArray(names)) {
-      throw new Error("Argument to RestrictEngines() is not an array: "
-                      + typeof(names));
-    }
-
-    this._enabledEngines = names;
-  },
-
-  RunMozmillTest: function TPS__RunMozmillTest(testfile) {
-    var mozmillfile = CC["@mozilla.org/file/local;1"]
-                      .createInstance(CI.nsILocalFile);
-    if (hh.oscpu.toLowerCase().indexOf('windows') > -1) {
-      let re = /\/(\w)\/(.*)/;
-      this.config.testdir = this.config.testdir.replace(re, "$1://$2").replace(/\//g, "\\");
-    }
-    mozmillfile.initWithPath(this.config.testdir);
-    mozmillfile.appendRelativePath(testfile);
-    Logger.logInfo("Running mozmill test " + mozmillfile.path);
-
-    var frame = {};
-    CU.import('resource://mozmill/modules/frame.js', frame);
-    frame.events.addListener('setTest', this.MozmillSetTestListener.bind(this));
-    frame.events.addListener('endTest', this.MozmillEndTestListener.bind(this));
-    this.StartAsyncOperation();
-    frame.runTestFile(mozmillfile.path, false);
-  },
-
-  /**
-   * Synchronously wait for the named event to be observed.
-   *
-   * When the event is observed, the function will wait an extra tick before
-   * returning.
-   *
-   * @param name
-   *        String event to wait for.
-   */
-  waitForEvent:function waitForEvent(name) {
-    Logger.logInfo("Waiting for " + name + "...");
-    let cb = Async.makeSpinningCallback();
-    Svc.Obs.add(name, cb);
-    cb.wait();
-    Svc.Obs.remove(name, cb);
-    Logger.logInfo(name + " observed!");
-
-    let cb = Async.makeSpinningCallback();
-    Utils.nextTick(cb);
-    cb.wait();
-  },
-
-
-  /**
-   * Waits for Sync to logged in before returning
-   */
-  waitForLoggedIn: function waitForLoggedIn() {
-    if (!this._loggedIn) {
-      this.waitForEvent("fxaccount:onlogin");
-    }
-
-    let cb = Async.makeSyncCallback();
-    Utils.nextTick(cb);
-    Async.waitForSyncCallback(cb);
-  },
-
-  /**
-   * Waits for Sync to logged in before returning
-   */
-  waitForSetupComplete: function waitForSetup() {
-    if (!this._setupComplete) {
-      this.waitForEvent("weave:service:setup-complete");
-    }
-
-    let cb = Async.makeSyncCallback();
-    Utils.nextTick(cb);
-    Async.waitForSyncCallback(cb);
-  },
-
-  /**
-   * Waits for Sync to start tracking before returning.
-   */
-  waitForTracking: function waitForTracking() {
-    if (!this._isTracking) {
-      this.waitForEvent("weave:engine:start-tracking");
-    }
-
-    let cb = Async.makeSyncCallback();
-    Utils.nextTick(cb);
-    Async.waitForSyncCallback(cb);
-  },
-
-  /**
-   * Reset the client and server to an empty/pure state.
-   *
-   * All data on the server is wiped and replaced with new keys and local
-   * client data. The local client is configured such that it is in sync
-   * with the server and ready to handle changes.
-   *
-   * This is typically called at the beginning of every test to set up a clean
-   * slate.
-   *
-   * This executes synchronously and doesn't return until things are in a good
-   * state.
-   */
-  ResetData: function ResetData() {
-    this.Login(true);
-
-    Weave.Service.login();
-    Weave.Service.wipeServer();
-    Weave.Service.resetClient();
-    Weave.Service.login();
-
-    this.waitForTracking();
-  },
-
-  Login: function Login(force) {
-    if (this._loggedIn && !force) {
-      return;
-    }
-
-    // old sync: have to add handling for this.config.sync_account
-    let account = this.config.fx_account;
-
-    if (!account) {
-      this.DumperError("No account information found! Did you use a valid " +
-                       "config file?");
-      return;
-    }
-
-    if (this.config["serverURL"]) {
-      Weave.Service.serverURL = this.config.serverURL;
-    }
-
-    Logger.logInfo("Setting client credentials.");
-    if (account["username"] && account["password"]) { // && account["passphrase"]) {
-      FxAccountsHelper.signIn(account["username"], account["password"]);
-      this.waitForSetupComplete();
-
-      // Old sync code - has to be reactivated later for fallback
-      //Weave.Service.identity.account = account["username"];
-      //Weave.Service.identity.basicPassword = account["password"];
-      //Weave.Service.identity.syncKey = account["passphrase"];
-    } else {
-      this.DumpError("Must specify username/password in the config file");
-      return;
-    }
-
-    //Weave.Service.login();
-    //this._loggedIn = true;
-    //Weave.Svc.Obs.notify("weave:service:setup-complete");
-    Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status OK");
-
-    this.waitForTracking();
-  },
-
-  Sync: function TPS__Sync(options) {
-    Logger.logInfo("executing Sync " + (options ? options : ""));
-
-    if (options == SYNC_WIPE_REMOTE) {
-      Weave.Svc.Prefs.set("firstSync", "wipeRemote");
-    }
-    else if (options == SYNC_WIPE_CLIENT) {
-      Weave.Svc.Prefs.set("firstSync", "wipeClient");
-    }
-    else if (options == SYNC_RESET_CLIENT) {
-      Weave.Svc.Prefs.set("firstSync", "resetClient");
-    }
-    else if (options) {
-      throw new Error("Unhandled options to Sync(): " + options);
-    } else {
-      Weave.Svc.Prefs.reset("firstSync");
-    }
-
-    this.Login(false);
-
-    this._waitingForSync = true;
-    this.StartAsyncOperation();
-
-    Weave.Service.sync();
-  },
-
-  WipeServer: function TPS__WipeServer() {
-    Logger.logInfo("WipeServer()");
-    this.Login();
-    Weave.Service.wipeServer();
-  },
-
-  /**
-   * Action which ensures changes are being tracked before returning.
-   */
-  EnsureTracking: function EnsureTracking() {
-    this.Login(false);
-    this.waitForTracking();
-  }
-};
-
-var Addons = {
-  install: function Addons__install(addons) {
-    TPS.HandleAddons(addons, ACTION_ADD);
-  },
-  setEnabled: function Addons__setEnabled(addons, state) {
-    TPS.HandleAddons(addons, ACTION_SET_ENABLED, state);
-  },
-  uninstall: function Addons__uninstall(addons) {
-    TPS.HandleAddons(addons, ACTION_DELETE);
-  },
-  verify: function Addons__verify(addons, state) {
-    TPS.HandleAddons(addons, ACTION_VERIFY, state);
-  },
-  verifyNot: function Addons__verifyNot(addons) {
-    TPS.HandleAddons(addons, ACTION_VERIFY_NOT);
-  },
-};
-
-var Bookmarks = {
-  add: function Bookmarks__add(bookmarks) {
-    TPS.HandleBookmarks(bookmarks, ACTION_ADD);
-  },
-  modify: function Bookmarks__modify(bookmarks) {
-    TPS.HandleBookmarks(bookmarks, ACTION_MODIFY);
-  },
-  delete: function Bookmarks__delete(bookmarks) {
-    TPS.HandleBookmarks(bookmarks, ACTION_DELETE);
-  },
-  verify: function Bookmarks__verify(bookmarks) {
-    TPS.HandleBookmarks(bookmarks, ACTION_VERIFY);
-  },
-  verifyNot: function Bookmarks__verifyNot(bookmarks) {
-    TPS.HandleBookmarks(bookmarks, ACTION_VERIFY_NOT);
-  }
-};
-
-var Formdata = {
-  add: function Formdata__add(formdata) {
-    this.HandleForms(formdata, ACTION_ADD);
-  },
-  delete: function Formdata__delete(formdata) {
-    this.HandleForms(formdata, ACTION_DELETE);
-  },
-  verify: function Formdata__verify(formdata) {
-    this.HandleForms(formdata, ACTION_VERIFY);
-  },
-  verifyNot: function Formdata__verifyNot(formdata) {
-    this.HandleForms(formdata, ACTION_VERIFY_NOT);
-  }
-};
-
-var History = {
-  add: function History__add(history) {
-    this.HandleHistory(history, ACTION_ADD);
-  },
-  delete: function History__delete(history) {
-    this.HandleHistory(history, ACTION_DELETE);
-  },
-  verify: function History__verify(history) {
-    this.HandleHistory(history, ACTION_VERIFY);
-  },
-  verifyNot: function History__verifyNot(history) {
-    this.HandleHistory(history, ACTION_VERIFY_NOT);
-  }
-};
-
-var Passwords = {
-  add: function Passwords__add(passwords) {
-    this.HandlePasswords(passwords, ACTION_ADD);
-  },
-  modify: function Passwords__modify(passwords) {
-    this.HandlePasswords(passwords, ACTION_MODIFY);
-  },
-  delete: function Passwords__delete(passwords) {
-    this.HandlePasswords(passwords, ACTION_DELETE);
-  },
-  verify: function Passwords__verify(passwords) {
-    this.HandlePasswords(passwords, ACTION_VERIFY);
-  },
-  verifyNot: function Passwords__verifyNot(passwords) {
-    this.HandlePasswords(passwords, ACTION_VERIFY_NOT);
-  }
-};
-
-var Prefs = {
-  modify: function Prefs__modify(prefs) {
-    TPS.HandlePrefs(prefs, ACTION_MODIFY);
-  },
-  verify: function Prefs__verify(prefs) {
-    TPS.HandlePrefs(prefs, ACTION_VERIFY);
-  }
-};
-
-var Tabs = {
-  add: function Tabs__add(tabs) {
-    TPS.StartAsyncOperation();
-    TPS.HandleTabs(tabs, ACTION_ADD);
-  },
-  verify: function Tabs__verify(tabs) {
-    TPS.HandleTabs(tabs, ACTION_VERIFY);
-  },
-  verifyNot: function Tabs__verifyNot(tabs) {
-    TPS.HandleTabs(tabs, ACTION_VERIFY_NOT);
-  }
-};
-
-var Windows = {
-  add: function Window__add(aWindow) {
-    TPS.StartAsyncOperation();
-    TPS.HandleWindows(aWindow, ACTION_ADD);
-  },
-};
deleted file mode 100644
--- a/services/sync/tps/extensions/tps/modules/windows.jsm
+++ /dev/null
@@ -1,36 +0,0 @@
-/* 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/. */
-"use strict";
-
- /* This is a JavaScript module (JSM) to be imported via
-   Components.utils.import() and acts as a singleton.
-   Only the following listed symbols will exposed on import, and only when
-   and where imported. */
-
-const EXPORTED_SYMBOLS = ["BrowserWindows"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://services-sync/main.js");
-
-let BrowserWindows = {
-  /**
-   * Add
-   *
-   * Opens a new window. Throws on error.
-   *
-   * @param aPrivate The private option.
-   * @return nothing
-   */
-  Add: function(aPrivate, fn) {
-    let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
-               .getService(Ci.nsIWindowMediator);
-    let mainWindow = wm.getMostRecentWindow("navigator:browser");
-    let win = mainWindow.OpenBrowserWindow({private: aPrivate});
-    win.addEventListener("load", function onLoad() {
-      win.removeEventListener("load", onLoad, false);
-      fn.call(win);
-    }, false);
-  }
-};
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/fxaccounts.jsm
@@ -0,0 +1,57 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "FxAccountsHelper",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-sync/main.js");
+Cu.import("resource://tps/logger.jsm");
+
+
+/**
+ * Helper object for Firefox Accounts authentication
+ */
+var FxAccountsHelper = {
+
+  /**
+   * Wrapper to synchronize the login of a user
+   *
+   * @param email
+   *        The email address for the account (utf8)
+   * @param password
+   *        The user's password
+   */
+  signIn: function signIn(email, password) {
+    let cb = Async.makeSpinningCallback();
+
+    var client = new FxAccountsClient();
+    client.signIn(email, password).then(credentials => {
+      // Add keys because without those setSignedInUser() will fail
+      credentials.kA = 'foo';
+      credentials.kB = 'bar';
+
+      Weave.Service.identity._fxaService.setSignedInUser(credentials).then(() => {
+        cb(null);
+      }, err => {
+        cb(err);
+      });
+    }, (err) => {
+      cb(err);
+    });
+
+    try {
+      cb.wait();
+    } catch (err) {
+      Logger.logError("signIn() failed with: " + JSON.stringify(err));
+      throw err;
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/logger.jsm
@@ -0,0 +1,148 @@
+/* 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 is a JavaScript module (JSM) to be imported via
+    Components.utils.import() and acts as a singleton.
+    Only the following listed symbols will exposed on import, and only when
+    and where imported. */
+
+var EXPORTED_SYMBOLS = ["Logger"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+var Logger = {
+  _foStream: null,
+  _converter: null,
+  _potentialError: null,
+
+  init: function (path) {
+    if (this._converter != null) {
+      // we're already open!
+      return;
+    }
+
+    let prefs = Cc["@mozilla.org/preferences-service;1"]
+                .getService(Ci.nsIPrefBranch);
+    if (path) {
+      prefs.setCharPref("tps.logfile", path);
+    }
+    else {
+      path = prefs.getCharPref("tps.logfile");
+    }
+
+    this._file = Cc["@mozilla.org/file/local;1"]
+                 .createInstance(Ci.nsILocalFile);
+    this._file.initWithPath(path);
+    var exists = this._file.exists();
+
+    // Make a file output stream and converter to handle it.
+    this._foStream = Cc["@mozilla.org/network/file-output-stream;1"]
+                     .createInstance(Ci.nsIFileOutputStream);
+    // If the file already exists, append it, otherwise create it.
+    var fileflags = exists ? 0x02 | 0x08 | 0x10 : 0x02 | 0x08 | 0x20;
+
+    this._foStream.init(this._file, fileflags, 0666, 0);
+    this._converter = Cc["@mozilla.org/intl/converter-output-stream;1"]
+                      .createInstance(Ci.nsIConverterOutputStream);
+    this._converter.init(this._foStream, "UTF-8", 0, 0);
+  },
+
+  write: function (data) {
+    if (this._converter == null) {
+      Cu.reportError(
+          "TPS Logger.write called with _converter == null!");
+      return;
+    }
+    this._converter.writeString(data);
+  },
+
+  close: function () {
+    if (this._converter != null) {
+      this._converter.close();
+      this._converter = null;
+      this._foStream = null;
+    }
+  },
+
+  AssertTrue: function(bool, msg, showPotentialError) {
+    if (bool) {
+      return;
+    }
+
+    if (showPotentialError && this._potentialError) {
+      msg += "; " + this._potentialError;
+      this._potentialError = null;
+    }
+    throw("ASSERTION FAILED! " + msg);
+  },
+
+  AssertFalse: function(bool, msg, showPotentialError) {
+    return this.AssertTrue(!bool, msg, showPotentialError);
+  },
+
+  AssertEqual: function(val1, val2, msg) {
+    if (val1 != val2)
+      throw("ASSERTION FAILED! " + msg + "; expected " +
+            JSON.stringify(val2) + ", got " + JSON.stringify(val1));
+  },
+
+  log: function (msg, withoutPrefix) {
+    dump(msg + "\n");
+    if (withoutPrefix) {
+      this.write(msg + "\n");
+    }
+    else {
+      function pad(n, len) {
+        let s = "0000" + n;
+        return s.slice(-len);
+      }
+
+      let now = new Date();
+      let year    = pad(now.getFullYear(),     4);
+      let month   = pad(now.getMonth() + 1,    2);
+      let day     = pad(now.getDate(),         2);
+      let hour    = pad(now.getHours(),        2);
+      let minutes = pad(now.getMinutes(),      2);
+      let seconds = pad(now.getSeconds(),      2);
+      let ms      = pad(now.getMilliseconds(), 3);
+
+      this.write(year + "-" + month + "-" + day + " " +
+                 hour + ":" + minutes + ":" + seconds + "." + ms + " " +
+                 msg + "\n");
+    }
+  },
+
+  clearPotentialError: function() {
+    this._potentialError = null;
+  },
+
+  logPotentialError: function(msg) {
+    this._potentialError = msg;
+  },
+
+  logLastPotentialError: function(msg) {
+    var message = msg;
+    if (this._potentialError) {
+      message = this._poentialError;
+      this._potentialError = null;
+    }
+    this.log("CROSSWEAVE ERROR: " + message);
+  },
+
+  logError: function (msg) {
+    this.log("CROSSWEAVE ERROR: " + msg);
+  },
+
+  logInfo: function (msg, withoutPrefix) {
+    if (withoutPrefix)
+      this.log(msg, true);
+    else
+      this.log("CROSSWEAVE INFO: " + msg);
+  },
+
+  logPass: function (msg) {
+    this.log("CROSSWEAVE TEST PASS: " + msg);
+  },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/addons.jsm
@@ -0,0 +1,124 @@
+/* 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/. */
+"use strict";
+
+let EXPORTED_SYMBOLS = ["Addon", "STATE_ENABLED", "STATE_DISABLED"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-sync/addonutils.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://tps/logger.jsm");
+
+const ADDONSGETURL = "http://127.0.0.1:4567/";
+const STATE_ENABLED = 1;
+const STATE_DISABLED = 2;
+
+function GetFileAsText(file) {
+  let channel = Services.io.newChannel(file, null, null);
+  let inputStream = channel.open();
+  if (channel instanceof Ci.nsIHttpChannel &&
+      channel.responseStatus != 200) {
+    return "";
+  }
+
+  let streamBuf = "";
+  let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+            .createInstance(Ci.nsIScriptableInputStream);
+  sis.init(inputStream);
+
+  let available;
+  while ((available = sis.available()) != 0) {
+    streamBuf += sis.read(available);
+  }
+
+  inputStream.close();
+  return streamBuf;
+}
+
+function Addon(TPS, id) {
+  this.TPS = TPS;
+  this.id = id;
+}
+
+Addon.prototype = {
+  addon: null,
+
+  uninstall: function uninstall() {
+    // find our addon locally
+    let cb = Async.makeSyncCallback();
+    AddonManager.getAddonByID(this.id, cb);
+    let addon = Async.waitForSyncCallback(cb);
+
+    Logger.AssertTrue(!!addon, 'could not find addon ' + this.id + ' to uninstall');
+
+    cb = Async.makeSpinningCallback();
+    AddonUtils.uninstallAddon(addon, cb);
+    cb.wait();
+  },
+
+  find: function find(state) {
+    let cb = Async.makeSyncCallback();
+    AddonManager.getAddonByID(this.id, cb);
+    let addon = Async.waitForSyncCallback(cb);
+
+    if (!addon) {
+      Logger.logInfo("Could not find add-on with ID: " + this.id);
+      return false;
+    }
+
+    this.addon = addon;
+
+    Logger.logInfo("add-on found: " + addon.id + ", enabled: " +
+                   !addon.userDisabled);
+    if (state == STATE_ENABLED) {
+      Logger.AssertFalse(addon.userDisabled, "add-on is disabled: " + addon.id);
+      return true;
+    } else if (state == STATE_DISABLED) {
+      Logger.AssertTrue(addon.userDisabled, "add-on is enabled: " + addon.id);
+      return true;
+    } else if (state) {
+      throw Error("Don't know how to handle state: " + state);
+    } else {
+      // No state, so just checking that it exists.
+      return true;
+    }
+  },
+
+  install: function install() {
+    // For Install, the id parameter initially passed is really the filename
+    // for the addon's install .xml; we'll read the actual id from the .xml.
+
+    let cb = Async.makeSpinningCallback();
+    AddonUtils.installAddons([{id: this.id, requireSecureURI: false}], cb);
+    let result = cb.wait();
+
+    Logger.AssertEqual(1, result.installedIDs.length, "Exactly 1 add-on was installed.");
+    Logger.AssertEqual(this.id, result.installedIDs[0],
+                       "Add-on was installed successfully: " + this.id);
+  },
+
+  setEnabled: function setEnabled(flag) {
+    Logger.AssertTrue(this.find(), "Add-on is available.");
+
+    let userDisabled;
+    if (flag == STATE_ENABLED) {
+      userDisabled = false;
+    } else if (flag == STATE_DISABLED) {
+      userDisabled = true;
+    } else {
+      throw new Error("Unknown flag to setEnabled: " + flag);
+    }
+
+    let cb = Async.makeSpinningCallback();
+    AddonUtils.updateUserDisabled(this.addon, userDisabled, cb);
+    cb.wait();
+
+    return true;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/bookmarks.jsm
@@ -0,0 +1,997 @@
+/* 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 is a JavaScript module (JSM) to be imported via
+  * Components.utils.import() and acts as a singleton. Only the following
+  * listed symbols will exposed on import, and only when and where imported.
+  */
+
+var EXPORTED_SYMBOLS = ["PlacesItem", "Bookmark", "Separator", "Livemark",
+                        "BookmarkFolder", "DumpBookmarks"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://services-common/async.js");
+
+Cu.import("resource://tps/logger.jsm");
+
+var DumpBookmarks = function TPS_Bookmarks__DumpBookmarks() {
+  let writer = {
+    value: "",
+    write: function PlacesItem__dump__write(aStr, aLen) {
+      this.value += aStr;
+    }
+  };
+
+  let options = PlacesUtils.history.getNewQueryOptions();
+  options.queryType = options.QUERY_TYPE_BOOKMARKS;
+  let query = PlacesUtils.history.getNewQuery();
+  query.setFolders([PlacesUtils.placesRootId], 1);
+  let root = PlacesUtils.history.executeQuery(query, options).root;
+  root.containerOpen = true;
+  let cb = Async.makeSpinningCallback();
+  Task.spawn(function() {
+    yield BookmarkJSONUtils.serializeNodeAsJSONToOutputStream(root, writer, true, false);
+    let value = JSON.parse(writer.value);
+    Logger.logInfo("dumping bookmarks\n\n" + JSON.stringify(value, null, ' ') + "\n\n");
+    cb();
+  });
+  cb.wait();
+};
+
+/**
+ * extend, causes a child object to inherit from a parent
+ */
+function extend(child, supertype)
+{
+   child.prototype.__proto__ = supertype.prototype;
+}
+
+/**
+ * PlacesItemProps object, holds properties for places items
+ */
+function PlacesItemProps(props) {
+  this.location = null;
+  this.uri = null;
+  this.loadInSidebar = null;
+  this.keyword = null;
+  this.title = null;
+  this.description = null;
+  this.after = null;
+  this.before = null;
+  this.folder = null;
+  this.position = null;
+  this.delete = false;
+  this.siteUri = null;
+  this.feedUri = null;
+  this.livemark = null;
+  this.tags = null;
+  this.last_item_pos = null;
+  this.type = null;
+
+  for (var prop in props) {
+    if (prop in this)
+      this[prop] = props[prop];
+  }
+}
+
+/**
+ * PlacesItem object.  Base class for places items.
+ */
+function PlacesItem(props) {
+  this.props = new PlacesItemProps(props);
+  if (this.props.location == null)
+    this.props.location = "menu";
+  if ("changes" in props)
+    this.updateProps = new PlacesItemProps(props.changes);
+  else
+    this.updateProps = null;
+}
+
+/**
+ * Instance methods for generic places items.
+ */
+PlacesItem.prototype = {
+  // an array of possible root folders for places items
+  _bookmarkFolders: {
+    "places": "placesRoot",
+    "menu": "bookmarksMenuFolder",
+    "tags": "tagFolder",
+    "unfiled": "unfiledBookmarksFolder",
+    "toolbar": "toolbarFolder",
+  },
+
+  toString: function() {
+    var that = this;
+    var props = ['uri', 'title', 'location', 'folder', 'feedUri', 'siteUri', 'livemark'];
+    var string = (this.props.type ? this.props.type + " " : "") +
+      "(" +
+      (function() {
+        var ret = [];
+        for (var i in props) {
+          if (that.props[props[i]]) {
+            ret.push(props[i] + ": " + that.props[props[i]])
+          }
+        }
+        return ret;
+      })().join(", ") + ")";
+    return string;
+  },
+
+  /**
+   * GetPlacesNodeId
+   *
+   * Finds the id of the an item with the specified properties in the places
+   * database.
+   *
+   * @param folder The id of the folder to search
+   * @param type The type of the item to find, or null to match any item;
+   *        this is one of the values listed at
+   *        https://developer.mozilla.org/en/nsINavHistoryResultNode#Constants
+   * @param title The title of the item to find, or null to match any title
+   * @param uri The uri of the item to find, or null to match any uri
+   *
+   * @return the node id if the item was found, otherwise -1
+   */
+  GetPlacesNodeId: function (folder, type, title, uri) {
+    let node_id = -1;
+
+    let options = PlacesUtils.history.getNewQueryOptions();
+    let query = PlacesUtils.history.getNewQuery();
+    query.setFolders([folder], 1);
+    let result = PlacesUtils.history.executeQuery(query, options);
+    let rootNode = result.root;
+    rootNode.containerOpen = true;
+
+    for (let j = 0; j < rootNode.childCount; j ++) {
+      let node = rootNode.getChild(j);
+      if (node.title == title) {
+        if (type == null || type == undefined || node.type == type)
+          if (uri == undefined || uri == null || node.uri.spec == uri.spec)
+            node_id = node.itemId;
+      }
+    }
+    rootNode.containerOpen = false;
+
+    return node_id;
+  },
+
+  /**
+   * IsAdjacentTo
+   *
+   * Determines if this object is immediately adjacent to another.
+   *
+   * @param itemName The name of the other object; this may be any kind of
+   *        places item
+   * @param relativePos The relative position of the other object.  If -1,
+   *        it means the other object should precede this one, if +1,
+   *        the other object should come after this one
+   * @return true if this object is immediately adjacent to the other object,
+   *         otherwise false
+   */
+  IsAdjacentTo: function(itemName, relativePos) {
+    Logger.AssertTrue(this.props.folder_id != -1 && this.props.item_id != -1,
+      "Either folder_id or item_id was invalid");
+    let other_id = this.GetPlacesNodeId(this.props.folder_id, null, itemName);
+    Logger.AssertTrue(other_id != -1, "item " + itemName + " not found");
+    let other_pos = PlacesUtils.bookmarks.getItemIndex(other_id);
+    let this_pos = PlacesUtils.bookmarks.getItemIndex(this.props.item_id);
+    if (other_pos + relativePos != this_pos) {
+      Logger.logPotentialError("Invalid position - " +
+       (this.props.title ? this.props.title : this.props.folder) +
+      " not " + (relativePos == 1 ? "after " : "before ") + itemName +
+      " for " + this.toString());
+      return false;
+    }
+    return true;
+  },
+
+  /**
+   * GetItemIndex
+   *
+   * Gets the item index for this places item.
+   *
+   * @return the item index, or -1 if there's an error
+   */
+  GetItemIndex: function() {
+    if (this.props.item_id == -1)
+      return -1;
+    return PlacesUtils.bookmarks.getItemIndex(this.props.item_id);
+  },
+
+  /**
+   * GetFolder
+   *
+   * Gets the folder id for the specified bookmark folder
+   *
+   * @param location The full path of the folder, which must begin
+   *        with one of the bookmark root folders
+   * @return the folder id if the folder is found, otherwise -1
+   */
+  GetFolder: function(location) {
+    let folder_parts = location.split("/");
+    if (!(folder_parts[0] in this._bookmarkFolders)) {
+      return -1;
+    }
+    let folder_id = PlacesUtils.bookmarks[this._bookmarkFolders[folder_parts[0]]];
+    for (let i = 1; i < folder_parts.length; i++) {
+      let subfolder_id = this.GetPlacesNodeId(
+        folder_id,
+        Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+        folder_parts[i]);
+      if (subfolder_id == -1) {
+        return -1;
+      }
+      else {
+        folder_id = subfolder_id;
+      }
+    }
+    return folder_id;
+  },
+
+  /**
+   * CreateFolder
+   *
+   * Creates a bookmark folder.
+   *
+   * @param location The full path of the folder, which must begin
+   *        with one of the bookmark root folders
+   * @return the folder id if the folder was created, otherwise -1
+   */
+  CreateFolder: function(location) {
+    let folder_parts = location.split("/");
+    if (!(folder_parts[0] in this._bookmarkFolders)) {
+      return -1;
+    }
+    let folder_id = PlacesUtils.bookmarks[this._bookmarkFolders[folder_parts[0]]];
+    for (let i = 1; i < folder_parts.length; i++) {
+      let subfolder_id = this.GetPlacesNodeId(
+        folder_id,
+        Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+        folder_parts[i]);
+      if (subfolder_id == -1) {
+        folder_id = PlacesUtils.bookmarks.createFolder(folder_id,
+                                                 folder_parts[i], -1);
+      }
+      else {
+        folder_id = subfolder_id;
+      }
+    }
+    return folder_id;
+  },
+
+  /**
+   * GetOrCreateFolder
+   *
+   * Locates the specified folder; if not found it is created.
+   *
+   * @param location The full path of the folder, which must begin
+   *        with one of the bookmark root folders
+   * @return the folder id if the folder was found or created, otherwise -1
+   */
+  GetOrCreateFolder: function(location) {
+    folder_id = this.GetFolder(location);
+    if (folder_id == -1)
+      folder_id = this.CreateFolder(location);
+    return folder_id;
+  },
+
+  /**
+   * CheckDescription
+   *
+   * Compares the description of this places item with an expected
+   * description.
+   *
+   * @param expectedDescription The description this places item is
+   *        expected to have
+   * @return true if the actual and expected descriptions match, or if
+   *         there is no expected description; otherwise false
+   */
+  CheckDescription: function(expectedDescription) {
+    if (expectedDescription != null) {
+      let description = "";
+      if (PlacesUtils.annotations.itemHasAnnotation(this.props.item_id,
+          "bookmarkProperties/description")) {
+        description = PlacesUtils.annotations.getItemAnnotation(
+          this.props.item_id, "bookmarkProperties/description");
+      }
+      if (description != expectedDescription) {
+        Logger.logPotentialError("Invalid description, expected: " +
+          expectedDescription + ", actual: " + description + " for " +
+          this.toString());
+        return false;
+      }
+    }
+    return true;
+  },
+
+  /**
+   * CheckPosition
+   *
+   * Verifies the position of this places item.
+   *
+   * @param before The name of the places item that this item should be
+            before, or null if this check should be skipped
+   * @param after The name of the places item that this item should be
+            after, or null if this check should be skipped
+   * @param last_item_pos The index of the places item above this one,
+   *        or null if this check should be skipped
+   * @return true if this item is in the correct position, otherwise false
+   */
+  CheckPosition: function(before, after, last_item_pos) {
+    if (after)
+      if (!this.IsAdjacentTo(after, 1)) return false;
+    if (before)
+      if (!this.IsAdjacentTo(before, -1)) return false;
+    if (last_item_pos != null && last_item_pos > -1) {
+      if (this.GetItemIndex() != last_item_pos + 1) {
+        Logger.logPotentialError("Item not found at the expected index, got " +
+          this.GetItemIndex() + ", expected " + (last_item_pos + 1) + " for " +
+          this.toString());
+        return false;
+      }
+    }
+    return true;
+  },
+
+  /**
+   * SetLocation
+   *
+   * Moves this places item to a different folder.
+   *
+   * @param location The full path of the folder to which to move this
+   *        places item, which must begin with one of the bookmark root
+   *        folders; if null, no changes are made
+   * @return nothing if successful, otherwise an exception is thrown
+   */
+  SetLocation: function(location) {
+    if (location != null) {
+      let newfolder_id = this.GetOrCreateFolder(location);
+      Logger.AssertTrue(newfolder_id != -1, "Location " + location +
+                        " doesn't exist; can't change item's location");
+      PlacesUtils.bookmarks.moveItem(this.props.item_id, newfolder_id, -1);
+      this.props.folder_id = newfolder_id;
+    }
+  },
+
+  /**
+   * SetDescription
+   *
+   * Updates the description for this places item.
+   *
+   * @param description The new description to set; if null, no changes are
+   *        made
+   * @return nothing
+   */
+  SetDescription: function(description) {
+    if (description != null) {
+      if (description != "")
+        PlacesUtils.annotations.setItemAnnotation(this.props.item_id,
+                                      "bookmarkProperties/description",
+                                      description,
+                                      0,
+                                      PlacesUtils.annotations.EXPIRE_NEVER);
+      else
+        PlacesUtils.annotations.removeItemAnnotation(this.props.item_id,
+                                         "bookmarkProperties/description");
+    }
+  },
+
+  /**
+   * SetPosition
+   *
+   * Updates the position of this places item within this item's current
+   * folder.  Use SetLocation to change folders.
+   *
+   * @param position The new index this item should be moved to; if null,
+   *        no changes are made; if -1, this item is moved to the bottom of
+   *        the current folder
+   * @return nothing if successful, otherwise an exception is thrown
+   */
+  SetPosition: function(position) {
+    if (position != null) {
+      let newposition = -1;
+      if (position != -1) {
+        newposition = this.GetPlacesNodeId(this.props.folder_id,
+                                           null, position);
+        Logger.AssertTrue(newposition != -1, "position " + position +
+                          " is invalid; unable to change position");
+        newposition = PlacesUtils.bookmarks.getItemIndex(newposition);
+      }
+      PlacesUtils.bookmarks.moveItem(this.props.item_id,
+                               this.props.folder_id, newposition);
+    }
+  },
+
+  /**
+   * Update the title of this places item
+   *
+   * @param title The new title to set for this item; if null, no changes
+   *        are made
+   * @return nothing
+   */
+  SetTitle: function(title) {
+    if (title != null) {
+      PlacesUtils.bookmarks.setItemTitle(this.props.item_id, title);
+    }
+  },
+};
+
+/**
+ * Bookmark class constructor.  Initializes instance properties.
+ */
+function Bookmark(props) {
+  PlacesItem.call(this, props);
+  if (this.props.title == null)
+    this.props.title = this.props.uri;
+  this.props.type = "bookmark";
+}
+
+/**
+ * Bookmark instance methods.
+ */
+Bookmark.prototype = {
+  /**
+   * SetKeyword
+   *
+   * Update this bookmark's keyword.
+   *
+   * @param keyword The keyword to set for this bookmark; if null, no
+   *        changes are made
+   * @return nothing
+   */
+  SetKeyword: function(keyword) {
+    if (keyword != null)
+      PlacesUtils.bookmarks.setKeywordForBookmark(this.props.item_id, keyword);
+  },
+
+  /**
+   * SetLoadInSidebar
+   *
+   * Updates this bookmark's loadInSidebar property.
+   *
+   * @param loadInSidebar if true, the loadInSidebar property will be set,
+   *        if false, it will be cleared, and any other value will result
+   *        in no change
+   * @return nothing
+   */
+  SetLoadInSidebar: function(loadInSidebar) {
+    if (loadInSidebar == true)
+      PlacesUtils.annotations.setItemAnnotation(this.props.item_id,
+                                    "bookmarkProperties/loadInSidebar",
+                                    true,
+                                    0,
+                                    PlacesUtils.annotations.EXPIRE_NEVER);
+    else if (loadInSidebar == false)
+      PlacesUtils.annotations.removeItemAnnotation(this.props.item_id,
+                                       "bookmarkProperties/loadInSidebar");
+  },
+
+  /**
+   * SetTitle
+   *
+   * Updates this bookmark's title.
+   *
+   * @param title The new title to set for this boomark; if null, no changes
+   *        are made
+   * @return nothing
+   */
+  SetTitle: function(title) {
+    if (title)
+      PlacesUtils.bookmarks.setItemTitle(this.props.item_id, title);
+  },
+
+  /**
+   * SetUri
+   *
+   * Updates this bookmark's URI.
+   *
+   * @param uri The new URI to set for this boomark; if null, no changes
+   *        are made
+   * @return nothing
+   */
+  SetUri: function(uri) {
+    if (uri) {
+      let newURI = Services.io.newURI(uri, null, null);
+      PlacesUtils.bookmarks.changeBookmarkURI(this.props.item_id, newURI);
+    }
+  },
+
+  /**
+   * SetTags
+   *
+   * Updates this bookmark's tags.
+   *
+   * @param tags An array of tags which should be associated with this
+   *        bookmark; any previous tags are removed; if this param is null,
+   *        no changes are made.  If this param is an empty array, all
+   *        tags are removed from this bookmark.
+   * @return nothing
+   */
+  SetTags: function(tags) {
+    if (tags != null) {
+      let URI = Services.io.newURI(this.props.uri, null, null);
+      PlacesUtils.tagging.untagURI(URI, null);
+      if (tags.length > 0)
+        PlacesUtils.tagging.tagURI(URI, tags);
+    }
+  },
+
+  /**
+   * Create
+   *
+   * Creates the bookmark described by this object's properties.
+   *
+   * @return the id of the created bookmark
+   */
+  Create: function() {
+    this.props.folder_id = this.GetOrCreateFolder(this.props.location);
+    Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
+      "bookmark, error creating folder " + this.props.location);
+    let bookmarkURI = Services.io.newURI(this.props.uri, null, null);
+    this.props.item_id = PlacesUtils.bookmarks.insertBookmark(this.props.folder_id,
+                                                        bookmarkURI,
+                                                        -1,
+                                                        this.props.title);
+    this.SetKeyword(this.props.keyword);
+    this.SetDescription(this.props.description);
+    this.SetLoadInSidebar(this.props.loadInSidebar);
+    this.SetTags(this.props.tags);
+    return this.props.item_id;
+  },
+
+  /**
+   * Update
+   *
+   * Updates this bookmark's properties according the properties on this
+   * object's 'updateProps' property.
+   *
+   * @return nothing
+   */
+  Update: function() {
+    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+      "Invalid item_id during Remove");
+    this.SetKeyword(this.updateProps.keyword);
+    this.SetDescription(this.updateProps.description);
+    this.SetLoadInSidebar(this.updateProps.loadInSidebar);
+    this.SetTitle(this.updateProps.title);
+    this.SetUri(this.updateProps.uri);
+    this.SetTags(this.updateProps.tags);
+    this.SetLocation(this.updateProps.location);
+    this.SetPosition(this.updateProps.position);
+  },
+
+  /**
+   * Find
+   *
+   * Locates the bookmark which corresponds to this object's properties.
+   *
+   * @return the bookmark id if the bookmark was found, otherwise -1
+   */
+  Find: function() {
+    this.props.folder_id = this.GetFolder(this.props.location);
+    if (this.props.folder_id == -1) {
+      Logger.logError("Unable to find folder " + this.props.location);
+      return -1;
+    }
+    let bookmarkTitle = this.props.title;
+    this.props.item_id = this.GetPlacesNodeId(this.props.folder_id,
+                                              null,
+                                              bookmarkTitle,
+                                              this.props.uri);
+
+    if (this.props.item_id == -1) {
+      Logger.logPotentialError(this.toString() + " not found");
+      return -1;
+    }
+    if (!this.CheckDescription(this.props.description))
+      return -1;
+    if (this.props.keyword != null) {
+      let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.props.item_id);
+      if (keyword != this.props.keyword) {
+        Logger.logPotentialError("Incorrect keyword - expected: " +
+          this.props.keyword + ", actual: " + keyword +
+          " for " + this.toString());
+        return -1;
+      }
+    }
+    let loadInSidebar = PlacesUtils.annotations.itemHasAnnotation(
+      this.props.item_id,
+      "bookmarkProperties/loadInSidebar");
+    if (loadInSidebar)
+      loadInSidebar = PlacesUtils.annotations.getItemAnnotation(
+        this.props.item_id,
+        "bookmarkProperties/loadInSidebar");
+    if (this.props.loadInSidebar != null &&
+        loadInSidebar != this.props.loadInSidebar) {
+      Logger.logPotentialError("Incorrect loadInSidebar setting - expected: " +
+        this.props.loadInSidebar + ", actual: " + loadInSidebar +
+        " for " + this.toString());
+      return -1;
+    }
+    if (this.props.tags != null) {
+      try {
+        let URI = Services.io.newURI(this.props.uri, null, null);
+        let tags = PlacesUtils.tagging.getTagsForURI(URI, {});
+        tags.sort();
+        this.props.tags.sort();
+        if (JSON.stringify(tags) != JSON.stringify(this.props.tags)) {
+          Logger.logPotentialError("Wrong tags - expected: " +
+            JSON.stringify(this.props.tags) + ", actual: " +
+            JSON.stringify(tags) + " for " + this.toString());
+          return -1;
+        }
+      }
+      catch (e) {
+        Logger.logPotentialError("error processing tags " + e);
+        return -1;
+      }
+    }
+    if (!this.CheckPosition(this.props.before,
+                            this.props.after,
+                            this.props.last_item_pos))
+      return -1;
+    return this.props.item_id;
+  },
+
+  /**
+   * Remove
+   *
+   * Removes this bookmark.  The bookmark should have been located previously
+   * by a call to Find.
+   *
+   * @return nothing
+   */
+  Remove: function() {
+    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+      "Invalid item_id during Remove");
+    PlacesUtils.bookmarks.removeItem(this.props.item_id);
+  },
+};
+
+extend(Bookmark, PlacesItem);
+
+/**
+ * BookmarkFolder class constructor. Initializes instance properties.
+ */
+function BookmarkFolder(props) {
+  PlacesItem.call(this, props);
+  this.props.type = "folder";
+}
+
+/**
+ * BookmarkFolder instance methods
+ */
+BookmarkFolder.prototype = {
+  /**
+   * Create
+   *
+   * Creates the bookmark folder described by this object's properties.
+   *
+   * @return the id of the created bookmark folder
+   */
+  Create: function() {
+    this.props.folder_id = this.GetOrCreateFolder(this.props.location);
+    Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
+      "folder, error creating parent folder " + this.props.location);
+    this.props.item_id = PlacesUtils.bookmarks.createFolder(this.props.folder_id,
+                                                      this.props.folder,
+                                                      -1);
+    this.SetDescription(this.props.description);
+    return this.props.folder_id;
+  },
+
+  /**
+   * Find
+   *
+   * Locates the bookmark folder which corresponds to this object's
+   * properties.
+   *
+   * @return the folder id if the folder was found, otherwise -1
+   */
+  Find: function() {
+    this.props.folder_id = this.GetFolder(this.props.location);
+    if (this.props.folder_id == -1) {
+      Logger.logError("Unable to find folder " + this.props.location);
+      return -1;
+    }
+    this.props.item_id = this.GetPlacesNodeId(
+                              this.props.folder_id,
+                              Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+                              this.props.folder);
+    if (!this.CheckDescription(this.props.description))
+      return -1;
+    if (!this.CheckPosition(this.props.before,
+                            this.props.after,
+                            this.props.last_item_pos))
+      return -1;
+    return this.props.item_id;
+  },
+
+  /**
+   * Remove
+   *
+   * Removes this folder.  The folder should have been located previously
+   * by a call to Find.
+   *
+   * @return nothing
+   */
+  Remove: function() {
+    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+      "Invalid item_id during Remove");
+    PlacesUtils.bookmarks.removeFolderChildren(this.props.item_id);
+    PlacesUtils.bookmarks.removeItem(this.props.item_id);
+  },
+
+  /**
+   * Update
+   *
+   * Updates this bookmark's properties according the properties on this
+   * object's 'updateProps' property.
+   *
+   * @return nothing
+   */
+  Update: function() {
+    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+      "Invalid item_id during Update");
+    this.SetLocation(this.updateProps.location);
+    this.SetPosition(this.updateProps.position);
+    this.SetTitle(this.updateProps.folder);
+    this.SetDescription(this.updateProps.description);
+  },
+};
+
+extend(BookmarkFolder, PlacesItem);
+
+/**
+ * Livemark class constructor. Initialzes instance properties.
+ */
+function Livemark(props) {
+  PlacesItem.call(this, props);
+  this.props.type = "livemark";
+}
+
+/**
+ * Livemark instance methods
+ */
+Livemark.prototype = {
+  /**
+   * Create
+   *
+   * Creates the livemark described by this object's properties.
+   *
+   * @return the id of the created livemark
+   */
+  Create: function() {
+    this.props.folder_id = this.GetOrCreateFolder(this.props.location);
+    Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
+      "folder, error creating parent folder " + this.props.location);
+    let siteURI = null;
+    if (this.props.siteUri != null)
+      siteURI = Services.io.newURI(this.props.siteUri, null, null);
+    let livemarkObj = {parentId: this.props.folder_id,
+                       title: this.props.livemark,
+                       siteURI: siteURI,
+                       feedURI: Services.io.newURI(this.props.feedUri, null, null),
+                       index: PlacesUtils.bookmarks.DEFAULT_INDEX};
+
+    // Until this can handle asynchronous creation, we need to spin.
+    let spinningCb = Async.makeSpinningCallback();
+
+    PlacesUtils.livemarks.addLivemark(livemarkObj).then(
+      aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
+      () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
+    );
+
+    let [status, livemark] = spinningCb.wait();
+    if (!Components.isSuccessCode(status)) {
+      throw status;
+    }
+
+    this.props.item_id = livemark.id;
+    return this.props.item_id;
+  },
+
+  /**
+   * Find
+   *
+   * Locates the livemark which corresponds to this object's
+   * properties.
+   *
+   * @return the item id if the livemark was found, otherwise -1
+   */
+  Find: function() {
+    this.props.folder_id = this.GetFolder(this.props.location);
+    if (this.props.folder_id == -1) {
+      Logger.logError("Unable to find folder " + this.props.location);
+      return -1;
+    }
+    this.props.item_id = this.GetPlacesNodeId(
+                              this.props.folder_id,
+                              Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+                              this.props.livemark);
+    if (!PlacesUtils.annotations
+                    .itemHasAnnotation(this.props.item_id, PlacesUtils.LMANNO_FEEDURI)) {
+      Logger.logPotentialError("livemark folder found, but it's just a regular folder, for " +
+        this.toString());
+      this.props.item_id = -1;
+      return -1;
+    }
+    let feedURI = Services.io.newURI(this.props.feedUri, null, null);
+    let lmFeedURISpec =
+      PlacesUtils.annotations.getItemAnnotation(this.props.item_id,
+                                                PlacesUtils.LMANNO_FEEDURI);
+    if (feedURI.spec != lmFeedURISpec) {
+      Logger.logPotentialError("livemark feed uri not correct, expected: " +
+        this.props.feedUri + ", actual: " + lmFeedURISpec +
+        " for " + this.toString());
+      return -1;
+    }
+    if (this.props.siteUri != null) {
+      let siteURI = Services.io.newURI(this.props.siteUri, null, null);
+      let lmSiteURISpec =
+        PlacesUtils.annotations.getItemAnnotation(this.props.item_id,
+                                                  PlacesUtils.LMANNO_SITEURI);
+      if (siteURI.spec != lmSiteURISpec) {
+        Logger.logPotentialError("livemark site uri not correct, expected: " +
+        this.props.siteUri + ", actual: " + lmSiteURISpec + " for " +
+        this.toString());
+        return -1;
+      }
+    }
+    if (!this.CheckPosition(this.props.before,
+                            this.props.after,
+                            this.props.last_item_pos))
+      return -1;
+    return this.props.item_id;
+  },
+
+  /**
+   * Update
+   *
+   * Updates this livemark's properties according the properties on this
+   * object's 'updateProps' property.
+   *
+   * @return nothing
+   */
+  Update: function() {
+    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+      "Invalid item_id during Update");
+    this.SetLocation(this.updateProps.location);
+    this.SetPosition(this.updateProps.position);
+    this.SetTitle(this.updateProps.livemark);
+    return true;
+  },
+
+  /**
+   * Remove
+   *
+   * Removes this livemark.  The livemark should have been located previously
+   * by a call to Find.
+   *
+   * @return nothing
+   */
+  Remove: function() {
+    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+      "Invalid item_id during Remove");
+    PlacesUtils.bookmarks.removeItem(this.props.item_id);
+  },
+};
+
+extend(Livemark, PlacesItem);
+
+/**
+ * Separator class constructor. Initializes instance properties.
+ */
+function Separator(props) {
+  PlacesItem.call(this, props);
+  this.props.type = "separator";
+}
+
+/**
+ * Separator instance methods.
+ */
+Separator.prototype = {
+  /**
+   * Create
+   *
+   * Creates the bookmark separator described by this object's properties.
+   *
+   * @return the id of the created separator
+   */
+  Create: function () {
+    this.props.folder_id = this.GetOrCreateFolder(this.props.location);
+    Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
+      "folder, error creating parent folder " + this.props.location);
+    this.props.item_id = PlacesUtils.bookmarks.insertSeparator(this.props.folder_id,
+                                                         -1);
+    return this.props.item_id;
+  },
+
+  /**
+   * Find
+   *
+   * Locates the bookmark separator which corresponds to this object's
+   * properties.
+   *
+   * @return the item id if the separator was found, otherwise -1
+   */
+  Find: function () {
+    this.props.folder_id = this.GetFolder(this.props.location);
+    if (this.props.folder_id == -1) {
+      Logger.logError("Unable to find folder " + this.props.location);
+      return -1;
+    }
+    if (this.props.before == null && this.props.last_item_pos == null) {
+      Logger.logPotentialError("Separator requires 'before' attribute if it's the" +
+        "first item in the list");
+      return -1;
+    }
+    let expected_pos = -1;
+    if (this.props.before) {
+      other_id = this.GetPlacesNodeId(this.props.folder_id,
+                                      null,
+                                      this.props.before);
+      if (other_id == -1) {
+        Logger.logPotentialError("Can't find places item " + this.props.before +
+          " for locating separator");
+        return -1;
+      }
+      expected_pos = PlacesUtils.bookmarks.getItemIndex(other_id) - 1;
+    }
+    else {
+      expected_pos = this.props.last_item_pos + 1;
+    }
+    this.props.item_id = PlacesUtils.bookmarks.getIdForItemAt(this.props.folder_id,
+                                                        expected_pos);
+    if (this.props.item_id == -1) {
+      Logger.logPotentialError("No separator found at position " + expected_pos);
+    }
+    else {
+      if (PlacesUtils.bookmarks.getItemType(this.props.item_id) !=
+          PlacesUtils.bookmarks.TYPE_SEPARATOR) {
+        Logger.logPotentialError("Places item at position " + expected_pos +
+          " is not a separator");
+        return -1;
+      }
+    }
+    return this.props.item_id;
+  },
+
+  /**
+   * Update
+   *
+   * Updates this separator's properties according the properties on this
+   * object's 'updateProps' property.
+   *
+   * @return nothing
+   */
+  Update: function() {
+    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+      "Invalid item_id during Update");
+    this.SetLocation(this.updateProps.location);
+    this.SetPosition(this.updateProps.position);
+    return true;
+  },
+
+  /**
+   * Remove
+   *
+   * Removes this separator.  The separator should have been located
+   * previously by a call to Find.
+   *
+   * @return nothing
+   */
+  Remove: function() {
+    Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+      "Invalid item_id during Update");
+    PlacesUtils.bookmarks.removeItem(this.props.item_id);
+  },
+};
+
+extend(Separator, PlacesItem);
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/forms.jsm
@@ -0,0 +1,261 @@
+/* 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 is a JavaScript module (JSM) to be imported via
+   Components.utils.import() and acts as a singleton. Only the following
+   listed symbols will exposed on import, and only when and where imported.
+  */
+
+var EXPORTED_SYMBOLS = ["FormData"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://tps/logger.jsm");
+
+let formService = Cc["@mozilla.org/satchel/form-history;1"]
+                  .getService(Ci.nsIFormHistory2);
+
+/**
+ * FormDB
+ *
+ * Helper object containing methods to interact with the moz_formhistory
+ * SQLite table.
+ */
+let FormDB = {
+  /**
+   * makeGUID
+   *
+   * Generates a brand-new globally unique identifier (GUID).  Borrowed
+   * from Weave's utils.js.
+   *
+   * @return the new guid
+   */
+  makeGUID: function makeGUID() {
+    // 70 characters that are not-escaped URL-friendly
+    const code =
+      "!()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~";
+
+    let guid = "";
+    let num = 0;
+    let val;
+
+    // Generate ten 70-value characters for a 70^10 (~61.29-bit) GUID
+    for (let i = 0; i < 10; i++) {
+      // Refresh the number source after using it a few times
+      if (i == 0 || i == 5)
+        num = Math.random();
+
+      // Figure out which code to use for the next GUID character
+      num *= 70;
+      val = Math.floor(num);
+      guid += code[val];
+      num -= val;
+    }
+
+    return guid;
+  },
+
+  /**
+   * insertValue
+   *
+   * Inserts the specified value for the specified fieldname into the
+   * moz_formhistory table.
+   *
+   * @param fieldname The form fieldname to insert
+   * @param value The form value to insert
+   * @param us The time, in microseconds, to use for the lastUsed
+   *        and firstUsed columns
+   * @return nothing
+   */
+  insertValue: function (fieldname, value, us) {
+    let query = this.createStatement(
+      "INSERT INTO moz_formhistory " +
+      "(fieldname, value, timesUsed, firstUsed, lastUsed, guid) VALUES " +
+      "(:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)");
+    query.params.fieldname = fieldname;
+    query.params.value = value;
+    query.params.timesUsed = 1;
+    query.params.firstUsed = us;
+    query.params.lastUsed = us;
+    query.params.guid = this.makeGUID();
+    query.execute();
+    query.reset();
+  },
+
+  /**
+   * updateValue
+   *
+   * Updates a row in the moz_formhistory table with a new value.
+   *
+   * @param id The id of the row to update
+   * @param newvalue The new value to set
+   * @return nothing
+   */
+  updateValue: function (id, newvalue) {
+    let query = this.createStatement(
+      "UPDATE moz_formhistory SET value = :value WHERE id = :id");
+    query.params.id = id;
+    query.params.value = newvalue;
+    query.execute();
+    query.reset();
+  },
+
+  /**
+   * getDataForValue
+   *
+   * Retrieves a set of values for a row in the database that
+   * corresponds to the given fieldname and value.
+   *
+   * @param fieldname The fieldname of the row to query
+   * @param value The value of the row to query
+   * @return null if no row is found with the specified fieldname and value,
+   *         or an object containing the row's id, lastUsed, and firstUsed
+   *         values
+   */
+  getDataForValue: function (fieldname, value) {
+    let query = this.createStatement(
+      "SELECT id, lastUsed, firstUsed FROM moz_formhistory WHERE " +
+      "fieldname = :fieldname AND value = :value");
+    query.params.fieldname = fieldname;
+    query.params.value = value;
+    if (!query.executeStep())
+      return null;
+
+    return {
+      id: query.row.id,
+      lastUsed: query.row.lastUsed,
+      firstUsed: query.row.firstUsed
+    };
+  },
+
+  /**
+   * createStatement
+   *
+   * Creates a statement from a SQL string.  This function is borrowed
+   * from Weave's forms.js.
+   *
+   * @param query The SQL query string
+   * @return the mozIStorageStatement created from the specified SQL
+   */
+  createStatement: function createStatement(query) {
+    try {
+      // Just return the statement right away if it's okay
+      return formService.DBConnection.createStatement(query);
+    }
+    catch(ex) {
+      // Assume guid column must not exist yet, so add it with an index
+      formService.DBConnection.executeSimpleSQL(
+        "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT");
+      formService.DBConnection.executeSimpleSQL(
+        "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index " +
+        "ON moz_formhistory (guid)");
+    }
+
+    // Try creating the query now that the column exists
+    return formService.DBConnection.createStatement(query);
+  }
+};
+
+/**
+ * FormData class constructor
+ *
+ * Initializes instance properties.
+ */
+function FormData(props, usSinceEpoch) {
+  this.fieldname = null;
+  this.value = null;
+  this.date = 0;
+  this.newvalue = null;
+  this.usSinceEpoch = usSinceEpoch;
+
+  for (var prop in props) {
+    if (prop in this)
+      this[prop] = props[prop];
+  }
+}
+
+/**
+ * FormData instance methods
+ */
+FormData.prototype = {
+  /**
+   * hours_to_us
+   *
+   * Converts hours since present to microseconds since epoch.
+   *
+   * @param hours The number of hours since the present time (e.g., 0 is
+   *        'now', and -1 is 1 hour ago)
+   * @return the corresponding number of microseconds since the epoch
+   */
+  hours_to_us: function(hours) {
+    return this.usSinceEpoch + (hours * 60 * 60 * 1000 * 1000);
+  },
+
+  /**
+   * Create
+   *
+   * If this FormData object doesn't exist in the moz_formhistory database,
+   * add it.  Throws on error.
+   *
+   * @return nothing
+   */
+  Create: function() {
+    Logger.AssertTrue(this.fieldname != null && this.value != null,
+      "Must specify both fieldname and value");
+
+    let formdata = FormDB.getDataForValue(this.fieldname, this.value);
+    if (!formdata) {
+      // this item doesn't exist yet in the db, so we need to insert it
+      FormDB.insertValue(this.fieldname, this.value,
+                         this.hours_to_us(this.date));
+    }
+    else {
+      /* Right now, we ignore this case.  If bug 552531 is ever fixed,
+         we might need to add code here to update the firstUsed or
+         lastUsed fields, as appropriate.
+       */
+    }
+  },
+
+  /**
+   * Find
+   *
+   * Attempts to locate an entry in the moz_formhistory database that
+   * matches the fieldname and value for this FormData object.
+   *
+   * @return true if this entry exists in the database, otherwise false
+   */
+  Find: function() {
+    let formdata = FormDB.getDataForValue(this.fieldname, this.value);
+    let status = formdata != null;
+    if (status) {
+      /*
+      //form history dates currently not synced!  bug 552531
+      let us = this.hours_to_us(this.date);
+      status = Logger.AssertTrue(
+        us >= formdata.firstUsed && us <= formdata.lastUsed,
+        "No match for with that date value");
+
+      if (status)
+      */
+        this.id = formdata.id;
+    }
+    return status;
+  },
+
+  /**
+   * Remove
+   *
+   * Removes the row represented by this FormData instance from the
+   * moz_formhistory database.
+   *
+   * @return nothing
+   */
+  Remove: function() {
+    /* Right now Weave doesn't handle this correctly, see bug 568363.
+     */
+    formService.removeEntry(this.fieldname, this.value);
+    return true;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/history.jsm
@@ -0,0 +1,201 @@
+/* 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 is a JavaScript module (JSM) to be imported via
+  * Components.utils.import() and acts as a singleton. Only the following
+  * listed symbols will exposed on import, and only when and where imported.
+  */
+
+var EXPORTED_SYMBOLS = ["HistoryEntry", "DumpHistory"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://tps/logger.jsm");
+Cu.import("resource://services-common/async.js");
+
+var DumpHistory = function TPS_History__DumpHistory() {
+  let writer = {
+    value: "",
+    write: function PlacesItem__dump__write(aStr, aLen) {
+      this.value += aStr;
+    }
+  };
+
+  let query = PlacesUtils.history.getNewQuery();
+  let options = PlacesUtils.history.getNewQueryOptions();
+  let root = PlacesUtils.history.executeQuery(query, options).root;
+  root.containerOpen = true;
+  Logger.logInfo("\n\ndumping history\n", true);
+  for (var i = 0; i < root.childCount; i++) {
+    let node = root.getChild(i);
+    let uri = node.uri;
+    let curvisits = HistoryEntry._getVisits(uri);
+    for each (var visit in curvisits) {
+      Logger.logInfo("URI: " + uri + ", type=" + visit.type + ", date=" + visit.date, true);
+    }
+  }
+  root.containerOpen = false;
+  Logger.logInfo("\nend history dump\n", true);
+};
+
+/**
+ * HistoryEntry object
+ *
+ * Contains methods for manipulating browser history entries.
+ */
+var HistoryEntry = {
+  /**
+   * _db
+   *
+   * Returns the DBConnection object for the history service.
+   */
+  get _db() {
+    return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+  },
+
+  /**
+   * _visitStm
+   *
+   * Return the SQL statement for getting history visit information
+   * from the moz_historyvisits table.  Borrowed from Weave's
+   * history.js.
+   */
+  get _visitStm() {
+    let stm = this._db.createStatement(
+      "SELECT visit_type type, visit_date date " +
+      "FROM moz_historyvisits " +
+      "WHERE place_id = (" +
+        "SELECT id " +
+        "FROM moz_places " +
+        "WHERE url = :url) " +
+      "ORDER BY date DESC LIMIT 10");
+    this.__defineGetter__("_visitStm", function() stm);
+    return stm;
+  },
+
+  /**
+   * _getVisits
+   *
+   * Gets history information about visits to a given uri.
+   *
+   * @param uri The uri to get visits for
+   * @return an array of objects with 'date' and 'type' properties,
+   * corresponding to the visits in the history database for the
+   * given uri
+   */
+  _getVisits: function HistStore__getVisits(uri) {
+    this._visitStm.params.url = uri;
+    return Async.querySpinningly(this._visitStm, ["date", "type"]);
+  },
+
+  /**
+   * Add
+   *
+   * Adds visits for a uri to the history database.  Throws on error.
+   *
+   * @param item An object representing one or more visits to a specific uri
+   * @param usSinceEpoch The number of microseconds from Epoch to
+   *        the time the current Crossweave run was started
+   * @return nothing
+   */
+  Add: function(item, usSinceEpoch) {
+    Logger.AssertTrue("visits" in item && "uri" in item,
+      "History entry in test file must have both 'visits' " +
+      "and 'uri' properties");
+    let uri = Services.io.newURI(item.uri, null, null);
+    let place = {
+      uri: uri,
+      visits: []
+    };
+    for each (visit in item.visits) {
+      place.visits.push({
+        visitDate: usSinceEpoch + (visit.date * 60 * 60 * 1000 * 1000),
+        transitionType: visit.type
+      });
+    }
+    if ("title" in item) {
+      place.title = item.title;
+    }
+    let cb = Async.makeSpinningCallback();
+    PlacesUtils.asyncHistory.updatePlaces(place, {
+        handleError: function Add_handleError() {
+          cb(new Error("Error adding history entry"));
+        },
+        handleResult: function Add_handleResult() {
+          cb();
+        },
+        handleCompletion: function Add_handleCompletion() {
+          // Nothing to do
+        }
+    });
+    // Spin the event loop to embed this async call in a sync API
+    cb.wait();
+  },
+
+  /**
+   * Find
+   *
+   * Finds visits for a uri to the history database.  Throws on error.
+   *
+   * @param item An object representing one or more visits to a specific uri
+   * @param usSinceEpoch The number of microseconds from Epoch to
+   *        the time the current Crossweave run was started
+   * @return true if all the visits for the uri are found, otherwise false
+   */
+  Find: function(item, usSinceEpoch) {
+    Logger.AssertTrue("visits" in item && "uri" in item,
+      "History entry in test file must have both 'visits' " +
+      "and 'uri' properties");
+    let curvisits = this._getVisits(item.uri);
+    for each (visit in curvisits) {
+      for each (itemvisit in item.visits) {
+        let expectedDate = itemvisit.date * 60 * 60 * 1000 * 1000
+            + usSinceEpoch;
+        if (visit.type == itemvisit.type && visit.date == expectedDate) {
+          itemvisit.found = true;
+        }
+      }
+    }
+
+    let all_items_found = true;
+    for each (itemvisit in item.visits) {
+      all_items_found = all_items_found && "found" in itemvisit;
+      Logger.logInfo("History entry for " + item.uri + ", type:" +
+              itemvisit.type + ", date:" + itemvisit.date +
+              ("found" in itemvisit ? " is present" : " is not present"));
+    }
+    return all_items_found;
+  },
+
+  /**
+   * Delete
+   *
+   * Removes visits from the history database. Throws on error.
+   *
+   * @param item An object representing items to delete
+   * @param usSinceEpoch The number of microseconds from Epoch to
+   *        the time the current Crossweave run was started
+   * @return nothing
+   */
+  Delete: function(item, usSinceEpoch) {
+    if ("uri" in item) {
+      let uri = Services.io.newURI(item.uri, null, null);
+      PlacesUtils.history.removePage(uri);
+    }
+    else if ("host" in item) {
+      PlacesUtils.history.removePagesFromHost(item.host, false);
+    }
+    else if ("begin" in item && "end" in item) {
+      PlacesUtils.history.removeVisitsByTimeframe(
+          usSinceEpoch + (item.begin * 60 * 60 * 1000 * 1000),
+          usSinceEpoch + (item.end * 60 * 60 * 1000 * 1000));
+    }
+    else {
+      Logger.AssertTrue(false, "invalid entry in delete history");
+    }
+  },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/passwords.jsm
@@ -0,0 +1,163 @@
+/* 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 is a JavaScript module (JSM) to be imported via
+  * Components.utils.import() and acts as a singleton. Only the following
+  * listed symbols will exposed on import, and only when and where imported.
+  */
+
+var EXPORTED_SYMBOLS = ["Password", "DumpPasswords"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://tps/logger.jsm");
+
+let nsLoginInfo = new Components.Constructor(
+                      "@mozilla.org/login-manager/loginInfo;1",
+                      Ci.nsILoginInfo,
+                      "init");
+
+var DumpPasswords = function TPS__Passwords__DumpPasswords() {
+  let logins = Services.logins.getAllLogins();
+  Logger.logInfo("\ndumping password list\n", true);
+  for (var i = 0; i < logins.length; i++) {
+    Logger.logInfo("* host=" + logins[i].hostname + ", submitURL=" + logins[i].formSubmitURL +
+                   ", realm=" + logins[i].httpRealm + ", password=" + logins[i].password +
+                   ", passwordField=" + logins[i].passwordField + ", username=" +
+                   logins[i].username + ", usernameField=" + logins[i].usernameField, true);
+  }
+  Logger.logInfo("\n\nend password list\n", true);
+};
+
+/**
+ * PasswordProps object; holds password properties.
+ */
+function PasswordProps(props) {
+  this.hostname = null;
+  this.submitURL = null;
+  this.realm = null;
+  this.username = "";
+  this.password = "";
+  this.usernameField = "";
+  this.passwordField = "";
+  this.delete = false;
+
+  for (var prop in props) {
+    if (prop in this)
+      this[prop] = props[prop];
+  }
+}
+
+/**
+ * Password class constructor. Initializes instance properties.
+ */
+function Password(props) {
+  this.props = new PasswordProps(props);
+  if ("changes" in props) {
+    this.updateProps = new PasswordProps(props);
+    for (var prop in props.changes)
+      if (prop in this.updateProps)
+        this.updateProps[prop] = props.changes[prop];
+  }
+  else {
+    this.updateProps = null;
+  }
+}
+
+/**
+ * Password instance methods.
+ */
+Password.prototype = {
+  /**
+   * Create
+   *
+   * Adds a password entry to the login manager for the password
+   * represented by this object's properties. Throws on error.
+   *
+   * @return the new login guid
+   */
+  Create: function() {
+    let login = new nsLoginInfo(this.props.hostname, this.props.submitURL,
+                                this.props.realm, this.props.username,
+                                this.props.password,
+                                this.props.usernameField,
+                                this.props.passwordField);
+    Services.logins.addLogin(login);
+    login.QueryInterface(Ci.nsILoginMetaInfo);
+    return login.guid;
+  },
+
+  /**
+   * Find
+   *
+   * Finds a password entry in the login manager, for the password
+   * represented by this object's properties.
+   *
+   * @return the guid of the password if found, otherwise -1
+   */
+  Find: function() {
+    let logins = Services.logins.findLogins({},
+                                            this.props.hostname,
+                                            this.props.submitURL,
+                                            this.props.realm);
+    for (var i = 0; i < logins.length; i++) {
+      if (logins[i].username == this.props.username &&
+          logins[i].password == this.props.password &&
+          logins[i].usernameField == this.props.usernameField &&
+          logins[i].passwordField == this.props.passwordField) {
+        logins[i].QueryInterface(Ci.nsILoginMetaInfo);
+        return logins[i].guid;
+      }
+    }
+    return -1;
+  },
+
+  /**
+   * Update
+   *
+   * Updates an existing password entry in the login manager with
+   * new properties. Throws on error.  The 'old' properties are this
+   * object's properties, the 'new' properties are the properties in
+   * this object's 'updateProps' object.
+   *
+   * @return nothing
+   */
+  Update: function() {
+    let oldlogin = new nsLoginInfo(this.props.hostname,
+                                   this.props.submitURL,
+                                   this.props.realm,
+                                   this.props.username,
+                                   this.props.password,
+                                   this.props.usernameField,
+                                   this.props.passwordField);
+    let newlogin = new nsLoginInfo(this.updateProps.hostname,
+                                   this.updateProps.submitURL,
+                                   this.updateProps.realm,
+                                   this.updateProps.username,
+                                   this.updateProps.password,
+                                   this.updateProps.usernameField,
+                                   this.updateProps.passwordField);
+    Services.logins.modifyLogin(oldlogin, newlogin);
+  },
+
+  /**
+   * Remove
+   *
+   * Removes an entry from the login manager for a password which
+   * matches this object's properties. Throws on error.
+   *
+   * @return nothing
+   */
+  Remove: function() {
+    let login = new nsLoginInfo(this.props.hostname,
+                                this.props.submitURL,
+                                this.props.realm,
+                                this.props.username,
+                                this.props.password,
+                                this.props.usernameField,
+                                this.props.passwordField);
+    Services.logins.removeLogin(login);
+  },
+};
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/prefs.jsm
@@ -0,0 +1,117 @@
+/* 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 is a JavaScript module (JSM) to be imported via
+   Components.utils.import() and acts as a singleton.
+   Only the following listed symbols will exposed on import, and only when
+   and where imported. */
+
+var EXPORTED_SYMBOLS = ["Preference"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+const WEAVE_PREF_PREFIX = "services.sync.prefs.sync.";
+
+let prefs = Cc["@mozilla.org/preferences-service;1"]
+            .getService(Ci.nsIPrefBranch);
+
+Cu.import("resource://tps/logger.jsm");
+
+/**
+ * Preference class constructor
+ *
+ * Initializes instance properties.
+ */
+function Preference (props) {
+  Logger.AssertTrue("name" in props && "value" in props,
+    "Preference must have both name and value");
+
+  this.name = props.name;
+  this.value = props.value;
+}
+
+/**
+ * Preference instance methods
+ */
+Preference.prototype = {
+  /**
+   * Modify
+   *
+   * Sets the value of the preference this.name to this.value.
+   * Throws on error.
+   *
+   * @return nothing
+   */
+  Modify: function() {
+    // Determine if this pref is actually something Weave even looks at.
+    let weavepref = WEAVE_PREF_PREFIX + this.name;
+    try {
+      let syncPref = prefs.getBoolPref(weavepref);
+      if (!syncPref)
+        prefs.setBoolPref(weavepref, true);
+    }
+    catch(e) {
+      Logger.AssertTrue(false, "Weave doesn't sync pref " + this.name);
+    }
+
+    // Modify the pref; throw an exception if the pref type is different
+    // than the value type specified in the test.
+    let prefType = prefs.getPrefType(this.name);
+    switch (prefType) {
+      case Ci.nsIPrefBranch.PREF_INT:
+        Logger.AssertEqual(typeof(this.value), "number",
+          "Wrong type used for preference value");
+        prefs.setIntPref(this.name, this.value);
+        break;
+      case Ci.nsIPrefBranch.PREF_STRING:
+        Logger.AssertEqual(typeof(this.value), "string",
+          "Wrong type used for preference value");
+        prefs.setCharPref(this.name, this.value);
+        break;
+      case Ci.nsIPrefBranch.PREF_BOOL:
+        Logger.AssertEqual(typeof(this.value), "boolean",
+          "Wrong type used for preference value");
+        prefs.setBoolPref(this.name, this.value);
+        break;
+    }
+  },
+
+  /**
+   * Find
+   *
+   * Verifies that the preference this.name has the value
+   * this.value. Throws on error, or if the pref's type or value
+   * doesn't match.
+   *
+   * @return nothing
+   */
+  Find: function() {
+    // Read the pref value.
+    let value;
+    try {
+      let prefType = prefs.getPrefType(this.name);
+      switch(prefType) {
+        case Ci.nsIPrefBranch.PREF_INT:
+          value = prefs.getIntPref(this.name);
+          break;
+        case Ci.nsIPrefBranch.PREF_STRING:
+          value = prefs.getCharPref(this.name);
+          break;
+        case Ci.nsIPrefBranch.PREF_BOOL:
+          value = prefs.getBoolPref(this.name);
+          break;
+      }
+    }
+    catch (e) {
+      Logger.AssertTrue(false, "Error accessing pref " + this.name);
+    }
+
+    // Throw an exception if the current and expected values aren't of
+    // the same type, or don't have the same values.
+    Logger.AssertEqual(typeof(value), typeof(this.value),
+      "Value types don't match");
+    Logger.AssertEqual(value, this.value, "Preference values don't match");
+  },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/tabs.jsm
@@ -0,0 +1,63 @@
+/* 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 is a JavaScript module (JSM) to be imported via
+   Components.utils.import() and acts as a singleton.
+   Only the following listed symbols will exposed on import, and only when
+   and where imported. */
+
+const EXPORTED_SYMBOLS = ["BrowserTabs"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://services-sync/main.js");
+
+let BrowserTabs = {
+  /**
+   * Add
+   *
+   * Opens a new tab in the current browser window for the
+   * given uri.  Throws on error.
+   *
+   * @param uri The uri to load in the new tab
+   * @return nothing
+   */
+  Add: function(uri, fn) {
+    // Open the uri in a new tab in the current browser window, and calls
+    // the callback fn from the tab's onload handler.
+    let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
+               .getService(Ci.nsIWindowMediator);
+    let mainWindow = wm.getMostRecentWindow("navigator:browser");
+    let newtab = mainWindow.getBrowser().addTab(uri);
+    mainWindow.getBrowser().selectedTab = newtab;
+    let win = mainWindow.getBrowser().getBrowserForTab(newtab);
+    win.addEventListener("load", function() { fn.call(); }, true);
+  },
+
+  /**
+   * Find
+   *
+   * Finds the specified uri and title in Weave's list of remote tabs
+   * for the specified profile.
+   *
+   * @param uri The uri of the tab to find
+   * @param title The page title of the tab to find
+   * @param profile The profile to search for tabs
+   * @return true if the specified tab could be found, otherwise false
+   */
+  Find: function(uri, title, profile) {
+    // Find the uri in Weave's list of tabs for the given profile.
+    let engine = Weave.Service.engineManager.get("tabs");
+    for (let [guid, client] in Iterator(engine.getAllClients())) {
+      for each (tab in client.tabs) {
+        let weaveTabUrl = tab.urlHistory[0];
+        if (uri == weaveTabUrl && profile == client.clientName)
+          if (title == undefined || title == tab.title)
+            return true;
+      }
+    }
+    return false;
+  },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/modules/windows.jsm
@@ -0,0 +1,36 @@
+/* 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/. */
+"use strict";
+
+ /* This is a JavaScript module (JSM) to be imported via
+   Components.utils.import() and acts as a singleton.
+   Only the following listed symbols will exposed on import, and only when
+   and where imported. */
+
+const EXPORTED_SYMBOLS = ["BrowserWindows"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://services-sync/main.js");
+
+let BrowserWindows = {
+  /**
+   * Add
+   *
+   * Opens a new window. Throws on error.
+   *
+   * @param aPrivate The private option.
+   * @return nothing
+   */
+  Add: function(aPrivate, fn) {
+    let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
+               .getService(Ci.nsIWindowMediator);
+    let mainWindow = wm.getMostRecentWindow("navigator:browser");
+    let win = mainWindow.OpenBrowserWindow({private: aPrivate});
+    win.addEventListener("load", function onLoad() {
+      win.removeEventListener("load", onLoad, false);
+      fn.call(win);
+    }, false);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/mozmill/sync.jsm
@@ -0,0 +1,115 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["TPS", "SYNC_WIPE_SERVER", "SYNC_RESET_CLIENT",
+                        "SYNC_WIPE_CLIENT"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://tps/logger.jsm");
+
+var utils = {}; Cu.import('resource://mozmill/modules/utils.js', utils);
+
+const SYNC_RESET_CLIENT = "reset-client";
+const SYNC_WIPE_CLIENT  = "wipe-client";
+const SYNC_WIPE_REMOTE  = "wipe-remote";
+const SYNC_WIPE_SERVER  = "wipe-server";
+
+var prefs = Cc["@mozilla.org/preferences-service;1"]
+            .getService(CI.nsIPrefBranch);
+
+var syncFinishedCallback = function() {
+  Logger.logInfo('syncFinishedCallback returned ' + !TPS._waitingForSync);
+  return !TPS._waitingForSync;
+};
+
+var TPS = {
+  _waitingForSync: false,
+  _syncErrors: 0,
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+
+  observe: function TPS__observe(subject, topic, data) {
+    Logger.logInfo('Mozmill observed: ' + topic);
+    switch(topic) {
+      case "weave:service:sync:error":
+        if (this._waitingForSync && this._syncErrors == 0) {
+          Logger.logInfo("sync error; retrying...");
+          this._syncErrors++;
+          Utils.namedTimer(function() {
+            Weave.service.sync();
+          }, 1000, this, "resync");
+        }
+        else if (this._waitingForSync) {
+          this._syncErrors = "sync error, see log";
+          this._waitingForSync = false;
+        }
+        break;
+      case "weave:service:sync:finish":
+        if (this._waitingForSync) {
+          this._syncErrors = 0;
+          this._waitingForSync = false;
+        }
+        break;
+    }
+  },
+
+  SetupSyncAccount: function TPS__SetupSyncAccount() {
+    try {
+      let serverURL = prefs.getCharPref('tps.serverURL');
+      if (serverURL) {
+        Weave.Service.serverURL = serverURL;
+      }
+    }
+    catch(e) {}
+
+    // Needs to be updated if this Mozmill sanity test is needed for Firefox Accounts
+    Weave.Service.identity.account       = prefs.getCharPref('tps.account.username');
+    Weave.Service.Identity.basicPassword = prefs.getCharPref('tps.account.password');
+    Weave.Service.identity.syncKey       = prefs.getCharPref('tps.account.passphrase');
+    Weave.Svc.Obs.notify("weave:service:setup-complete");
+  },
+
+  Sync: function TPS__Sync(options) {
+    Logger.logInfo('Mozmill starting sync operation: ' + options);
+    switch(options) {
+      case SYNC_WIPE_REMOTE:
+        Weave.Svc.Prefs.set("firstSync", "wipeRemote");
+        break;
+      case SYNC_WIPE_CLIENT:
+        Weave.Svc.Prefs.set("firstSync", "wipeClient");
+        break;
+      case SYNC_RESET_CLIENT:
+        Weave.Svc.Prefs.set("firstSync", "resetClient");
+        break;
+      default:
+        Weave.Svc.Prefs.reset("firstSync");
+    }
+
+    if (Weave.Status.service != Weave.STATUS_OK) {
+      return "Sync status not ok: " + Weave.Status.service;
+    }
+
+    this._syncErrors = 0;
+
+    if (options == SYNC_WIPE_SERVER) {
+      Weave.Service.wipeServer();
+    } else {
+      this._waitingForSync = true;
+      Weave.Service.sync();
+      utils.waitFor(syncFinishedCallback, null, 20000, 500, TPS);
+    }
+    return this._syncErrors;
+  },
+};
+
+Services.obs.addObserver(TPS, "weave:service:sync:finish", true);
+Services.obs.addObserver(TPS, "weave:service:sync:error", true);
+Logger.init();
+
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/quit.js
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; -*- */
+/* 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/. */
+
+/*
+  From mozilla/toolkit/content
+  These files did not have a license
+*/
+var EXPORTED_SYMBOLS = ["goQuitApplication"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function canQuitApplication() {
+  try {
+    var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
+                     .createInstance(Components.interfaces.nsISupportsPRBool);
+    Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+    // Something aborted the quit process.
+    if (cancelQuit.data) {
+      return false;
+    }
+  }
+  catch (ex) {}
+
+  return true;
+}
+
+function goQuitApplication() {
+  if (!canQuitApplication()) {
+    return false;
+  }
+
+  const kAppStartup = '@mozilla.org/toolkit/app-startup;1';
+  const kAppShell   = '@mozilla.org/appshell/appShellService;1';
+  var   appService;
+  var   forceQuit;
+
+  if (kAppStartup in Components.classes) {
+    appService = Components.classes[kAppStartup]
+                 .getService(Components.interfaces.nsIAppStartup);
+    forceQuit  = Components.interfaces.nsIAppStartup.eForceQuit;
+  }
+  else if (kAppShell in Components.classes) {
+    appService = Components.classes[kAppShell].
+      getService(Components.interfaces.nsIAppShellService);
+    forceQuit = Components.interfaces.nsIAppShellService.eForceQuit;
+  }
+  else {
+    throw 'goQuitApplication: no AppStartup/appShell';
+  }
+
+  try {
+    appService.quit(forceQuit);
+  }
+  catch(ex) {
+    throw('goQuitApplication: ' + ex);
+  }
+
+  return true;
+}
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -0,0 +1,1026 @@
+/* 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 is a JavaScript module (JSM) to be imported via
+  * Components.utils.import() and acts as a singleton. Only the following
+  * listed symbols will exposed on import, and only when and where imported.
+  */
+
+let EXPORTED_SYMBOLS = ["TPS"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+// Global modules
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.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");
+
+// TPS modules
+Cu.import("resource://tps/fxaccounts.jsm");
+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");
+Cu.import("resource://tps/modules/passwords.jsm");
+Cu.import("resource://tps/modules/prefs.jsm");
+Cu.import("resource://tps/modules/tabs.jsm");
+Cu.import("resource://tps/modules/windows.jsm");
+
+var hh = Cc["@mozilla.org/network/protocol;1?name=http"]
+         .getService(Ci.nsIHttpProtocolHandler);
+var prefs = Cc["@mozilla.org/preferences-service;1"]
+            .getService(Ci.nsIPrefBranch);
+
+var mozmillInit = {};
+Cu.import('resource://mozmill/modules/init.js', mozmillInit);
+
+const ACTION_ADD              = "add";
+const ACTION_VERIFY           = "verify";
+const ACTION_VERIFY_NOT       = "verify-not";
+const ACTION_MODIFY           = "modify";
+const ACTION_SYNC             = "sync";
+const ACTION_DELETE           = "delete";
+const ACTION_PRIVATE_BROWSING = "private-browsing";
+const ACTION_WIPE_REMOTE      = "wipe-remote";
+const ACTION_WIPE_SERVER      = "wipe-server";
+const ACTION_SET_ENABLED      = "set-enabled";
+
+const ACTIONS = [ACTION_ADD, ACTION_VERIFY, ACTION_VERIFY_NOT,
+                 ACTION_MODIFY, ACTION_SYNC, ACTION_DELETE,
+                 ACTION_PRIVATE_BROWSING, ACTION_WIPE_REMOTE,
+                 ACTION_WIPE_SERVER, ACTION_SET_ENABLED];
+
+const SYNC_WIPE_CLIENT  = "wipe-client";
+const SYNC_WIPE_REMOTE  = "wipe-remote";
+const SYNC_WIPE_SERVER  = "wipe-server";
+const SYNC_RESET_CLIENT = "reset-client";
+const SYNC_START_OVER   = "start-over";
+
+const OBSERVER_TOPICS = ["fxaccounts:onlogin",
+                         "fxaccounts:onlogout",
+                         "private-browsing",
+                         "sessionstore-windows-restored",
+                         "weave:engine:start-tracking",
+                         "weave:engine:stop-tracking",
+                         "weave:service:setup-complete",
+                         "weave:service:sync:finish",
+                         "weave:service:sync:error",
+                        ];
+
+let TPS = {
+  _waitingForSync: false,
+  _isTracking: false,
+  _test: null,
+  _currentAction: -1,
+  _currentPhase: -1,
+  _errors: 0,
+  _setupComplete: false,
+  _syncErrors: 0,
+  _usSinceEpoch: 0,
+  _tabsAdded: 0,
+  _tabsFinished: 0,
+  _phaselist: {},
+  _operations_pending: 0,
+  _loggedIn: false,
+  _enabledEngines: null,
+
+  /**
+   * Check if the Firefox Accounts feature is enabled
+   */
+  get fxaccounts_enabled() {
+    try {
+      return Services.prefs.getBoolPref("services.sync.fxaccounts.enabled");
+    } catch (e) {
+      return false;
+    }
+  },
+
+  DumpError: function (msg) {
+    this._errors++;
+    Logger.logError("[phase" + this._currentPhase + "] " + msg);
+    this.quit();
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference]),
+
+  observe: function TPS__observe(subject, topic, data) {
+    try {
+      Logger.logInfo("----------event observed: " + topic);
+
+      switch(topic) {
+        case "private-browsing":
+          Logger.logInfo("private browsing " + data);
+          break;
+
+        case "fxaccounts:onlogin":
+          this._loggedIn = true;
+          break;
+
+        case "fxaccounts:onlogout":
+          this._loggedIn = false;
+          break;
+
+        case "weave:service:sync:error":
+          if (this._waitingForSync && this._syncErrors == 0) {
+            // if this is the first sync error, retry...
+            Logger.logInfo("sync error; retrying...");
+            this._syncErrors++;
+            this._waitingForSync = false;
+            Utils.nextTick(this.RunNextTestAction, this);
+          }
+          else if (this._waitingForSync) {
+            // ...otherwise abort the test
+            this.DumpError("sync error; aborting test");
+            return;
+          }
+          break;
+
+        case "weave:service:setup-complete":
+          this._setupComplete = true;
+          break;
+
+        case "weave:service:sync:finish":
+          if (this._waitingForSync) {
+            this._syncErrors = 0;
+            this._waitingForSync = false;
+            // Wait a second before continuing, otherwise we can get
+            // 'sync not complete' errors.
+            Utils.namedTimer(function() {
+              this.FinishAsyncOperation();
+            }, 1000, this, "postsync");
+          }
+          break;
+
+        case "weave:engine:start-tracking":
+          this._isTracking = true;
+          break;
+
+        case "weave:engine:stop-tracking":
+          this._isTracking = false;
+          break;
+
+        case "sessionstore-windows-restored":
+          Utils.nextTick(this.RunNextTestAction, this);
+          break;
+      }
+    }
+    catch (e) {
+      this.DumpError("Exception caught: " + Utils.exceptionStr(e));
+      return;
+    }
+  },
+
+  StartAsyncOperation: function TPS__StartAsyncOperation() {
+    this._operations_pending++;
+  },
+
+  FinishAsyncOperation: function TPS__FinishAsyncOperation() {
+    this._operations_pending--;
+    if (!this.operations_pending) {
+      this._currentAction++;
+      Utils.nextTick(function() {
+        this.RunNextTestAction();
+      }, this);
+    }
+  },
+
+  quit: function TPS__quit() {
+    OBSERVER_TOPICS.forEach(function(topic) {
+      Services.obs.removeObserver(this, topic);
+    }, this);
+    Logger.close();
+    this.goQuitApplication();
+  },
+
+  HandleWindows: function (aWindow, action) {
+    Logger.logInfo("executing action " + action.toUpperCase() +
+                   " on window " + JSON.stringify(aWindow));
+    switch(action) {
+      case ACTION_ADD:
+        BrowserWindows.Add(aWindow.private, function(win) {
+          Logger.logInfo("window finished loading");
+          this.FinishAsyncOperation();
+        }.bind(this));
+        break;
+    }
+    Logger.logPass("executing action " + action.toUpperCase() + " on windows");
+  },
+
+  HandleTabs: function (tabs, action) {
+    this._tabsAdded = tabs.length;
+    this._tabsFinished = 0;
+    for each (let tab in tabs) {
+      Logger.logInfo("executing action " + action.toUpperCase() +
+                     " on tab " + JSON.stringify(tab));
+      switch(action) {
+        case ACTION_ADD:
+          // When adding tabs, we keep track of how many tabs we're adding,
+          // and wait until we've received that many onload events from our
+          // new tabs before continuing
+          let that = this;
+          let taburi = tab.uri;
+          BrowserTabs.Add(tab.uri, function() {
+            that._tabsFinished++;
+            Logger.logInfo("tab for " + taburi + " finished loading");
+            if (that._tabsFinished == that._tabsAdded) {
+              Logger.logInfo("all tabs loaded, continuing...");
+              that.FinishAsyncOperation();
+            }
+          });
+          break;
+        case ACTION_VERIFY:
+          Logger.AssertTrue(typeof(tab.profile) != "undefined",
+            "profile must be defined when verifying tabs");
+          Logger.AssertTrue(
+            BrowserTabs.Find(tab.uri, tab.title, tab.profile), "error locating tab");
+          break;
+        case ACTION_VERIFY_NOT:
+          Logger.AssertTrue(typeof(tab.profile) != "undefined",
+            "profile must be defined when verifying tabs");
+          Logger.AssertTrue(
+            !BrowserTabs.Find(tab.uri, tab.title, tab.profile),
+            "tab found which was expected to be absent");
+          break;
+        default:
+          Logger.AssertTrue(false, "invalid action: " + action);
+      }
+    }
+    Logger.logPass("executing action " + action.toUpperCase() + " on tabs");
+  },
+
+  HandlePrefs: function (prefs, action) {
+    for each (pref in prefs) {
+      Logger.logInfo("executing action " + action.toUpperCase() +
+                     " on pref " + JSON.stringify(pref));
+      let preference = new Preference(pref);
+      switch(action) {
+        case ACTION_MODIFY:
+          preference.Modify();
+          break;
+        case ACTION_VERIFY:
+          preference.Find();
+          break;
+        default:
+          Logger.AssertTrue(false, "invalid action: " + action);
+      }
+    }
+    Logger.logPass("executing action " + action.toUpperCase() + " on pref");
+  },
+
+  HandleForms: function (data, action) {
+    for each (datum in 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;
+        case ACTION_DELETE:
+          formdata.Remove();
+          break;
+        case ACTION_VERIFY:
+          Logger.AssertTrue(formdata.Find(), "form data not found");
+          break;
+        case ACTION_VERIFY_NOT:
+          Logger.AssertTrue(!formdata.Find(),
+            "form data found, but it shouldn't be present");
+          break;
+        default:
+          Logger.AssertTrue(false, "invalid action: " + action);
+      }
+    }
+    Logger.logPass("executing action " + action.toUpperCase() +
+                   " on formdata");
+  },
+
+  HandleHistory: function (entries, action) {
+    try {
+      for each (entry in entries) {
+        Logger.logInfo("executing action " + action.toUpperCase() +
+                       " on history entry " + JSON.stringify(entry));
+        switch(action) {
+          case ACTION_ADD:
+            HistoryEntry.Add(entry, this._usSinceEpoch);
+            break;
+          case ACTION_DELETE:
+            HistoryEntry.Delete(entry, this._usSinceEpoch);
+            break;
+          case ACTION_VERIFY:
+            Logger.AssertTrue(HistoryEntry.Find(entry, this._usSinceEpoch),
+              "Uri visits not found in history database");
+            break;
+          case ACTION_VERIFY_NOT:
+            Logger.AssertTrue(!HistoryEntry.Find(entry, this._usSinceEpoch),
+              "Uri visits found in history database, but they shouldn't be");
+            break;
+          default:
+            Logger.AssertTrue(false, "invalid action: " + action);
+        }
+      }
+      Logger.logPass("executing action " + action.toUpperCase() +
+                     " on history");
+    }
+    catch(e) {
+      DumpHistory();
+      throw(e);
+    }
+  },
+
+  HandlePasswords: function (passwords, action) {
+    try {
+      for each (password in passwords) {
+        let password_id = -1;
+        Logger.logInfo("executing action " + action.toUpperCase() +
+                      " on password " + JSON.stringify(password));
+        var password = new Password(password);
+        switch (action) {
+          case ACTION_ADD:
+            Logger.AssertTrue(password.Create() > -1, "error adding password");
+            break;
+          case ACTION_VERIFY:
+            Logger.AssertTrue(password.Find() != -1, "password not found");
+            break;
+          case ACTION_VERIFY_NOT:
+            Logger.AssertTrue(password.Find() == -1,
+              "password found, but it shouldn't exist");
+            break;
+          case ACTION_DELETE:
+            Logger.AssertTrue(password.Find() != -1, "password not found");
+            password.Remove();
+            break;
+          case ACTION_MODIFY:
+            if (password.updateProps != null) {
+              Logger.AssertTrue(password.Find() != -1, "password not found");
+              password.Update();
+            }
+            break;
+          default:
+            Logger.AssertTrue(false, "invalid action: " + action);
+        }
+      }
+      Logger.logPass("executing action " + action.toUpperCase() +
+                     " on passwords");
+    }
+    catch(e) {
+      DumpPasswords();
+      throw(e);
+    }
+  },
+
+  HandleAddons: function (addons, action, state) {
+    for each (let entry in addons) {
+      Logger.logInfo("executing action " + action.toUpperCase() +
+                     " on addon " + JSON.stringify(entry));
+      let addon = new Addon(this, entry);
+      switch(action) {
+        case ACTION_ADD:
+          addon.install();
+          break;
+        case ACTION_DELETE:
+          addon.uninstall();
+          break;
+        case ACTION_VERIFY:
+          Logger.AssertTrue(addon.find(state), 'addon ' + addon.id + ' not found');
+          break;
+        case ACTION_VERIFY_NOT:
+          Logger.AssertFalse(addon.find(state), 'addon ' + addon.id + " is present, but it shouldn't be");
+          break;
+        case ACTION_SET_ENABLED:
+          Logger.AssertTrue(addon.setEnabled(state), 'addon ' + addon.id + ' not found');
+          break;
+        default:
+          throw new Error("Unknown action for add-on: " + action);
+      }
+    }
+    Logger.logPass("executing action " + action.toUpperCase() +
+                   " on addons");
+  },
+
+  HandleBookmarks: function (bookmarks, action) {
+    try {
+      let items = [];
+      for (folder in bookmarks) {
+        let last_item_pos = -1;
+        for each (bookmark in bookmarks[folder]) {
+          Logger.clearPotentialError();
+          let placesItem;
+          bookmark['location'] = folder;
+
+          if (last_item_pos != -1)
+            bookmark['last_item_pos'] = last_item_pos;
+          let item_id = -1;
+
+          if (action != ACTION_MODIFY && action != ACTION_DELETE)
+            Logger.logInfo("executing action " + action.toUpperCase() +
+                           " on bookmark " + JSON.stringify(bookmark));
+
+          if ("uri" in bookmark)
+            placesItem = new Bookmark(bookmark);
+          else if ("folder" in bookmark)
+            placesItem = new BookmarkFolder(bookmark);
+          else if ("livemark" in bookmark)
+            placesItem = new Livemark(bookmark);
+          else if ("separator" in bookmark)
+            placesItem = new Separator(bookmark);
+
+          if (action == ACTION_ADD) {
+            item_id = placesItem.Create();
+          }
+          else {
+            item_id = placesItem.Find();
+            if (action == ACTION_VERIFY_NOT) {
+              Logger.AssertTrue(item_id == -1,
+                "places item exists but it shouldn't: " +
+                JSON.stringify(bookmark));
+            }
+            else
+              Logger.AssertTrue(item_id != -1, "places item not found", true);
+          }
+
+          last_item_pos = placesItem.GetItemIndex();
+          items.push(placesItem);
+        }
+      }
+
+      if (action == ACTION_DELETE || action == ACTION_MODIFY) {
+        for each (item in items) {
+          Logger.logInfo("executing action " + action.toUpperCase() +
+                         " on bookmark " + JSON.stringify(item));
+          switch(action) {
+            case ACTION_DELETE:
+              item.Remove();
+              break;
+            case ACTION_MODIFY:
+              if (item.updateProps != null)
+                item.Update();
+              break;
+          }
+        }
+      }
+
+      Logger.logPass("executing action " + action.toUpperCase() +
+        " on bookmarks");
+    }
+    catch (e) {
+      DumpBookmarks();
+      throw(e);
+    }
+  },
+
+  MozmillEndTestListener: function TPS__MozmillEndTestListener(obj) {
+    Logger.logInfo("mozmill endTest: " + JSON.stringify(obj));
+    if (obj.failed > 0) {
+      this.DumpError('mozmill test failed, name: ' + obj.name + ', reason: ' + JSON.stringify(obj.fails));
+      return;
+    }
+    else if ('skipped' in obj && obj.skipped) {
+      this.DumpError('mozmill test failed, name: ' + obj.name + ', reason: ' + obj.skipped_reason);
+      return;
+    }
+    else {
+      Utils.namedTimer(function() {
+        this.FinishAsyncOperation();
+      }, 2000, this, "postmozmilltest");
+    }
+  },
+
+  MozmillSetTestListener: function TPS__MozmillSetTestListener(obj) {
+    Logger.logInfo("mozmill setTest: " + obj.name);
+  },
+
+  RunNextTestAction: function() {
+    try {
+      if (this._currentAction >=
+          this._phaselist["phase" + this._currentPhase].length) {
+        // we're all done
+        Logger.logInfo("test phase " + this._currentPhase + ": " +
+          (this._errors ? "FAIL" : "PASS"));
+        this.quit();
+        return;
+      }
+
+      if (this.seconds_since_epoch)
+        this._usSinceEpoch = this.seconds_since_epoch * 1000 * 1000;
+      else {
+        this.DumpError("seconds-since-epoch not set");
+        return;
+      }
+
+      let phase = this._phaselist["phase" + this._currentPhase];
+      let action = phase[this._currentAction];
+      Logger.logInfo("starting action: " + action[0].name);
+      action[0].apply(this, action.slice(1));
+
+      // if we're in an async operation, don't continue on to the next action
+      if (this._operations_pending)
+        return;
+
+      this._currentAction++;
+    }
+    catch(e) {
+      this.DumpError("Exception caught: " + Utils.exceptionStr(e));
+      return;
+    }
+    this.RunNextTestAction();
+  },
+
+  /**
+   * Runs a single test phase.
+   *
+   * This is the main entry point for each phase of a test. The TPS command
+   * line driver loads this module and calls into the function with the
+   * arguments from the command line.
+   *
+   * When a phase is executed, the file is loaded as JavaScript into the
+   * current object.
+   *
+   * The following keys in the options argument have meaning:
+   *
+   *   - ignoreUnusedEngines  If true, unused engines will be unloaded from
+   *                          Sync. This makes output easier to parse and is
+   *                          useful for debugging test failures.
+   *
+   * @param  file
+   *         String URI of the file to open.
+   * @param  phase
+   *         String name of the phase to run.
+   * @param  logpath
+   *         String path of the log file to write to.
+   * @param  options
+   *         Object defining addition run-time options.
+   */
+  RunTestPhase: function (file, phase, logpath, options) {
+    try {
+      let settings = options || {};
+
+      Logger.init(logpath);
+      Logger.logInfo("Sync version: " + WEAVE_VERSION);
+      Logger.logInfo("Firefox builddate: " + Services.appinfo.appBuildID);
+      Logger.logInfo("Firefox version: " + Services.appinfo.version);
+
+      // do some sync housekeeping
+      if (Weave.Service.isLoggedIn) {
+        this.DumpError("Sync logged in on startup...profile may be dirty");
+        return;
+      }
+
+      // Wait for Sync service to become ready.
+      if (!Weave.Status.ready) {
+        this.waitForEvent("weave:service:ready");
+      }
+
+      // Always give Sync an extra tick to initialize. If we waited for the
+      // service:ready event, this is required to ensure all handlers have
+      // executed.
+      Utils.nextTick(this._executeTestPhase.bind(this, file, phase, settings));
+    } catch(e) {
+      this.DumpError("Exception caught: " + Utils.exceptionStr(e));
+      return;
+    }
+  },
+
+  /**
+   * Executes a single test phase.
+   *
+   * This is called by RunTestPhase() after the environment is validated.
+   */
+  _executeTestPhase: function _executeTestPhase(file, phase, settings) {
+    try {
+      OBSERVER_TOPICS.forEach(function(topic) {
+        Services.obs.addObserver(this, topic, true);
+      }, this);
+
+      // parse the test file
+      Services.scriptloader.loadSubScript(file, this);
+      this._currentPhase = phase;
+      let this_phase = this._phaselist["phase" + this._currentPhase];
+
+      if (this_phase == undefined) {
+        this.DumpError("invalid phase " + this._currentPhase);
+        return;
+      }
+
+      if (this.phases["phase" + this._currentPhase] == undefined) {
+        this.DumpError("no profile defined for phase " + this._currentPhase);
+        return;
+      }
+
+      // If we have restricted the active engines, unregister engines we don't
+      // care about.
+      if (settings.ignoreUnusedEngines && Array.isArray(this._enabledEngines)) {
+        let names = {};
+        for each (let name in this._enabledEngines) {
+          names[name] = true;
+        }
+
+        for (let engine of Weave.Service.engineManager.getEnabled()) {
+          if (!(engine.name in names)) {
+            Logger.logInfo("Unregistering unused engine: " + engine.name);
+            Weave.Service.engineManager.unregister(engine);
+          }
+        }
+      }
+
+      Logger.logInfo("Starting phase " + parseInt(phase, 10) + "/" +
+                     Object.keys(this._phaselist).length);
+
+      Logger.logInfo("setting client.name to " + this.phases["phase" + this._currentPhase]);
+      Weave.Svc.Prefs.set("client.name", this.phases["phase" + this._currentPhase]);
+
+      // TODO Phases should be defined in a data type that has strong
+      // ordering, not by lexical sorting.
+      let currentPhase = parseInt(this._currentPhase, 10);
+      // Reset everything at the beginning of the test.
+      if (currentPhase <= 1) {
+        this_phase.unshift([this.ResetData]);
+      }
+
+      // Wipe the server at the end of the final test phase.
+      if (currentPhase >= Object.keys(this.phases).length) {
+        this_phase.push([this.WipeServer]);
+      }
+
+      // Store account details as prefs so they're accessible to the Mozmill
+      // framework.
+      if (this.fxaccounts_enabled) {
+        prefs.setCharPref('tps.account.username', this.config.fx_account.username);
+        prefs.setCharPref('tps.account.password', this.config.fx_account.password);
+      }
+      else {
+        prefs.setCharPref('tps.account.username', this.config.sync_account.username);
+        prefs.setCharPref('tps.account.password', this.config.sync_account.password);
+        prefs.setCharPref('tps.account.passphrase', this.config.sync_account.passphrase);
+      }
+
+      if (this.config["serverURL"]) {
+        prefs.setCharPref('tps.serverURL', this.config.serverURL);
+      }
+
+      // start processing the test actions
+      this._currentAction = 0;
+    }
+    catch(e) {
+      this.DumpError("Exception caught: " + Utils.exceptionStr(e));
+      return;
+    }
+  },
+
+  /**
+   * Register a single phase with the test harness.
+   *
+   * This is called when loading individual test files.
+   *
+   * @param  phasename
+   *         String name of the phase being loaded.
+   * @param  fnlist
+   *         Array of functions/actions to perform.
+   */
+  Phase: function Test__Phase(phasename, fnlist) {
+    this._phaselist[phasename] = fnlist;
+  },
+
+  /**
+   * Restrict enabled Sync engines to a specified set.
+   *
+   * This can be called by a test to limit what engines are enabled. It is
+   * recommended to call it to reduce the overhead and log clutter for the
+   * test.
+   *
+   * The "clients" engine is special and is always enabled, so there is no
+   * need to specify it.
+   *
+   * @param  names
+   *         Array of Strings for engines to make active during the test.
+   */
+  EnableEngines: function EnableEngines(names) {
+    if (!Array.isArray(names)) {
+      throw new Error("Argument to RestrictEngines() is not an array: "
+                      + typeof(names));
+    }
+
+    this._enabledEngines = names;
+  },
+
+  RunMozmillTest: function TPS__RunMozmillTest(testfile) {
+    var mozmillfile = Cc["@mozilla.org/file/local;1"]
+                      .createInstance(Ci.nsILocalFile);
+    if (hh.oscpu.toLowerCase().indexOf('windows') > -1) {
+      let re = /\/(\w)\/(.*)/;
+      this.config.testdir = this.config.testdir.replace(re, "$1://$2").replace(/\//g, "\\");
+    }
+    mozmillfile.initWithPath(this.config.testdir);
+    mozmillfile.appendRelativePath(testfile);
+    Logger.logInfo("Running mozmill test " + mozmillfile.path);
+
+    var frame = {};
+    Cu.import('resource://mozmill/modules/frame.js', frame);
+    frame.events.addListener('setTest', this.MozmillSetTestListener.bind(this));
+    frame.events.addListener('endTest', this.MozmillEndTestListener.bind(this));
+    this.StartAsyncOperation();
+    frame.runTestFile(mozmillfile.path, false);
+  },
+
+  /**
+   * Synchronously wait for the named event to be observed.
+   *
+   * When the event is observed, the function will wait an extra tick before
+   * returning.
+   *
+   * @param aEventName
+   *        String event to wait for.
+   */
+  waitForEvent: function waitForEvent(aEventName) {
+    Logger.logInfo("Waiting for " + aEventName + "...");
+    let cb = Async.makeSpinningCallback();
+    Svc.Obs.add(aEventName, cb);
+    cb.wait();
+    Svc.Obs.remove(aEventName, cb);
+    Logger.logInfo(aEventName + " observed!");
+
+    let cb = Async.makeSpinningCallback();
+    Utils.nextTick(cb);
+    cb.wait();
+  },
+
+
+  /**
+   * Waits for Sync to logged in before returning
+   */
+  waitForLoggedIn: function waitForLoggedIn() {
+    if (!this._loggedIn) {
+      this.waitForEvent("fxaccount:onlogin");
+    }
+
+    let cb = Async.makeSyncCallback();
+    Utils.nextTick(cb);
+    Async.waitForSyncCallback(cb);
+  },
+
+  /**
+   * Waits for Sync to logged in before returning
+   */
+  waitForSetupComplete: function waitForSetup() {
+    if (!this._setupComplete) {
+      this.waitForEvent("weave:service:setup-complete");
+    }
+
+    let cb = Async.makeSyncCallback();
+    Utils.nextTick(cb);
+    Async.waitForSyncCallback(cb);
+  },
+
+  /**
+   * Waits for Sync to start tracking before returning.
+   */
+  waitForTracking: function waitForTracking() {
+    if (!this._isTracking) {
+      this.waitForEvent("weave:engine:start-tracking");
+    }
+
+    let cb = Async.makeSyncCallback();
+    Utils.nextTick(cb);
+    Async.waitForSyncCallback(cb);
+  },
+
+  /**
+   * Reset the client and server to an empty/pure state.
+   *
+   * All data on the server is wiped and replaced with new keys and local
+   * client data. The local client is configured such that it is in sync
+   * with the server and ready to handle changes.
+   *
+   * This is typically called at the beginning of every test to set up a clean
+   * slate.
+   *
+   * This executes synchronously and doesn't return until things are in a good
+   * state.
+   */
+  ResetData: function ResetData() {
+    this.Login(true);
+
+    Weave.Service.login();
+    Weave.Service.wipeServer();
+    Weave.Service.resetClient();
+    Weave.Service.login();
+
+    this.waitForTracking();
+  },
+
+  /**
+   * Login on the server
+   */
+  Login: function Login(force) {
+    if (this._loggedIn && !force) {
+      return;
+    }
+
+    let account = this.fxaccounts_enabled ? this.config.fx_account
+                                          : this.config.sync_account;
+    if (!account) {
+      this.DumperError("No account information found! Did you use a valid " +
+                       "config file?");
+      return;
+    }
+
+    // If there has been specified a custom server, set it now
+    if (this.config["serverURL"]) {
+      Weave.Service.serverURL = this.config.serverURL;
+    }
+
+    Logger.logInfo("Setting client credentials and login.");
+
+    if (this.fxaccounts_enabled) {
+      if (account["username"] && account["password"]) {
+        Logger.logInfo("Login via Firefox Accounts.");
+        FxAccountsHelper.signIn(account["username"], account["password"]);
+      }
+      else {
+        this.DumpError("Must specify username/password in the config file");
+        return;
+      }
+    }
+    else {
+      if (account["username"] && account["password"] && account["passphrase"]) {
+        Logger.logInfo("Login via the old Sync authentication.");
+        Weave.Service.identity.account = account["username"];
+        Weave.Service.identity.basicPassword = account["password"];
+        Weave.Service.identity.syncKey = account["passphrase"];
+
+        // Fake the login
+        Weave.Service.login();
+        this._loggedIn = true;
+        Weave.Svc.Obs.notify("weave:service:setup-complete");
+      }
+      else {
+        this.DumpError("Must specify username/password/passphrase in the config file");
+        return;
+      }
+    }
+
+    this.waitForSetupComplete();
+    Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status OK");
+    this.waitForTracking();
+  },
+
+  Sync: function TPS__Sync(options) {
+    Logger.logInfo("executing Sync " + (options ? options : ""));
+
+    if (options == SYNC_WIPE_REMOTE) {
+      Weave.Svc.Prefs.set("firstSync", "wipeRemote");
+    }
+    else if (options == SYNC_WIPE_CLIENT) {
+      Weave.Svc.Prefs.set("firstSync", "wipeClient");
+    }
+    else if (options == SYNC_RESET_CLIENT) {
+      Weave.Svc.Prefs.set("firstSync", "resetClient");
+    }
+    else if (options) {
+      throw new Error("Unhandled options to Sync(): " + options);
+    } else {
+      Weave.Svc.Prefs.reset("firstSync");
+    }
+
+    this.Login(false);
+
+    this._waitingForSync = true;
+    this.StartAsyncOperation();
+
+    Weave.Service.sync();
+  },
+
+  WipeServer: function TPS__WipeServer() {
+    Logger.logInfo("WipeServer()");
+    this.Login();
+    Weave.Service.wipeServer();
+  },
+
+  /**
+   * Action which ensures changes are being tracked before returning.
+   */
+  EnsureTracking: function EnsureTracking() {
+    this.Login(false);
+    this.waitForTracking();
+  }
+};
+
+var Addons = {
+  install: function Addons__install(addons) {
+    TPS.HandleAddons(addons, ACTION_ADD);
+  },
+  setEnabled: function Addons__setEnabled(addons, state) {
+    TPS.HandleAddons(addons, ACTION_SET_ENABLED, state);
+  },
+  uninstall: function Addons__uninstall(addons) {
+    TPS.HandleAddons(addons, ACTION_DELETE);
+  },
+  verify: function Addons__verify(addons, state) {
+    TPS.HandleAddons(addons, ACTION_VERIFY, state);
+  },
+  verifyNot: function Addons__verifyNot(addons) {
+    TPS.HandleAddons(addons, ACTION_VERIFY_NOT);
+  },
+};
+
+var Bookmarks = {
+  add: function Bookmarks__add(bookmarks) {
+    TPS.HandleBookmarks(bookmarks, ACTION_ADD);
+  },
+  modify: function Bookmarks__modify(bookmarks) {
+    TPS.HandleBookmarks(bookmarks, ACTION_MODIFY);
+  },
+  delete: function Bookmarks__delete(bookmarks) {
+    TPS.HandleBookmarks(bookmarks, ACTION_DELETE);
+  },
+  verify: function Bookmarks__verify(bookmarks) {
+    TPS.HandleBookmarks(bookmarks, ACTION_VERIFY);
+  },
+  verifyNot: function Bookmarks__verifyNot(bookmarks) {
+    TPS.HandleBookmarks(bookmarks, ACTION_VERIFY_NOT);
+  }
+};
+
+var Formdata = {
+  add: function Formdata__add(formdata) {
+    this.HandleForms(formdata, ACTION_ADD);
+  },
+  delete: function Formdata__delete(formdata) {
+    this.HandleForms(formdata, ACTION_DELETE);
+  },
+  verify: function Formdata__verify(formdata) {
+    this.HandleForms(formdata, ACTION_VERIFY);
+  },
+  verifyNot: function Formdata__verifyNot(formdata) {
+    this.HandleForms(formdata, ACTION_VERIFY_NOT);
+  }
+};
+
+var History = {
+  add: function History__add(history) {
+    this.HandleHistory(history, ACTION_ADD);
+  },
+  delete: function History__delete(history) {
+    this.HandleHistory(history, ACTION_DELETE);
+  },
+  verify: function History__verify(history) {
+    this.HandleHistory(history, ACTION_VERIFY);
+  },
+  verifyNot: function History__verifyNot(history) {
+    this.HandleHistory(history, ACTION_VERIFY_NOT);
+  }
+};
+
+var Passwords = {
+  add: function Passwords__add(passwords) {
+    this.HandlePasswords(passwords, ACTION_ADD);
+  },
+  modify: function Passwords__modify(passwords) {
+    this.HandlePasswords(passwords, ACTION_MODIFY);
+  },
+  delete: function Passwords__delete(passwords) {
+    this.HandlePasswords(passwords, ACTION_DELETE);
+  },
+  verify: function Passwords__verify(passwords) {
+    this.HandlePasswords(passwords, ACTION_VERIFY);
+  },
+  verifyNot: function Passwords__verifyNot(passwords) {
+    this.HandlePasswords(passwords, ACTION_VERIFY_NOT);
+  }
+};
+
+var Prefs = {
+  modify: function Prefs__modify(prefs) {
+    TPS.HandlePrefs(prefs, ACTION_MODIFY);
+  },
+  verify: function Prefs__verify(prefs) {
+    TPS.HandlePrefs(prefs, ACTION_VERIFY);
+  }
+};
+
+var Tabs = {
+  add: function Tabs__add(tabs) {
+    TPS.StartAsyncOperation();
+    TPS.HandleTabs(tabs, ACTION_ADD);
+  },
+  verify: function Tabs__verify(tabs) {
+    TPS.HandleTabs(tabs, ACTION_VERIFY);
+  },
+  verifyNot: function Tabs__verifyNot(tabs) {
+    TPS.HandleTabs(tabs, ACTION_VERIFY_NOT);
+  }
+};
+
+var Windows = {
+  add: function Window__add(aWindow) {
+    TPS.StartAsyncOperation();
+    TPS.HandleWindows(aWindow, ACTION_ADD);
+  },
+};
--- a/testing/tps/README
+++ b/testing/tps/README
@@ -1,14 +1,42 @@
-TPS is a test automation framework for Firefox Sync.  See
+TPS is a test automation framework for Firefox Sync. See
 https://developer.mozilla.org/en/TPS for documentation.
 
-INSTALLATION:
+Installation
+============
 
 TPS requires several packages to operate properly. To install TPS and
 required packages, use the INSTALL.sh script, provided:
 
   ./INSTALL.sh /path/to/create/virtualenv
 
 This script will create a virtalenv and install TPS into it. TPS can then
 be run by activating the virtualenv and executing:
 
   runtps --binary=/path/to/firefox
+
+
+Configuration
+=============
+To edit the TPS configuration, do not edit config/config.json.in in the tree.
+Instead, edit config.json inside your virtualenv; it will be located at
+something like:
+
+  (linux): /path/to/virtualenv/lib/python2.6/site-packages/tps-0.2.40-py2.6.egg/tps/config.json
+  (win): /path/to/virtualenv/Lib/site-packages/tps-0.2.40-py2.6.egg/tps/config.json
+
+
+Setting Up Test Accounts
+========================
+
+Firefox Accounts
+----------------
+To create a test account for using the Firefox Account authentication perform the
+following steps:
+
+1. Go to a URL like http://restmail.net/mail/%account_prefix%@restmail.net
+2. Go to https://accounts.firefox.com/signup?service=sync&context=fx_desktop_v1
+3. Sign in with the previous chosen email address and a password
+4. Go back to the Restmail URL, reload the page
+5. Search for the verification link and open that page
+
+Now you will be able to use your setup Firefox Account for Sync.
deleted file mode 100644
--- a/testing/tps/config/README.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-To edit the TPS configuration, do not edit config.json.in in the tree. 
-Instead, edit config.json inside your virtualenv; it will be located at
-something like:
-
-  (linux): /path/to/virtualenv/lib/python2.6/site-packages/tps-0.2.40-py2.6.egg/tps/config.json
-  (win): /path/to/virtualenv/Lib/site-packages/tps-0.2.40-py2.6.egg/tps/config.json
-
--- a/testing/tps/config/config.json.in
+++ b/testing/tps/config/config.json.in
@@ -1,23 +1,24 @@
 {
   "sync_account": {
-    "username": "crossweaveservices@mozilla.com",
-    "password": "crossweaveservicescrossweaveservices",
-    "passphrase": "r-jwcbc-zgf42-fjn72-p5vpp-iypmi"
+    "username": "",
+    "password": "",
+    "passphrase": ""
   },
   "fx_account": {
-    "username": "crossweaveservices@restmail.net",
-    "password": "crossweaveservicescrossweaveservices"
+    "username": "",
+    "password": ""
   },
   "email": {
     "username": "crossweave@mozilla.com",
     "password": "",
     "passednotificationlist": ["crossweave@mozilla.com"],
     "notificationlist": ["crossweave@mozilla.com"]
   },
-  "platform": "win32",
-  "os": "win7",
+  "auth_type": "fx_account",
   "es": "localhost:9200",
+  "os": "Ubuntu",
+  "platform": "linux64",
   "serverURL": null,
-  "testdir": "__TESTDIR__",
-  "extensiondir": "__EXTENSIONDIR__"
+  "extensiondir": "__EXTENSIONDIR__",
+  "testdir": "__TESTDIR__"
 }
--- a/testing/tps/setup.py
+++ b/testing/tps/setup.py
@@ -1,36 +1,42 @@
 # 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/.
 
+from setuptools import setup, find_packages
 import sys
-from setuptools import setup, find_packages
+
+version = '0.5'
 
-version = '0.4'
-
-deps = ['mozinfo >= 0.3.3', 'mozprofile >= 0.4',
-        'mozprocess >= 0.4', 'mozrunner >= 5.8', 'mozinstall >= 1.4',
-        'httplib2 >= 0.7.3']
+deps = ['httplib2 >= 0.7.3',
+        'mozfile >= 1.1',
+        'mozhttpd >= 0.7',
+        'mozinfo >= 0.7',
+        'mozinstall >= 1.9',
+        'mozprocess >= 0.18',
+        'mozprofile >= 0.21',
+        'mozrunner >= 5.35',
+       ]
 
 # we only support python 2.6+ right now
 assert sys.version_info[0] == 2
 assert sys.version_info[1] >= 6
 
 setup(name='tps',
       version=version,
       description='run automated multi-profile sync tests',
       long_description="""\
 """,
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='',
-      author='Jonathan Griffin',
-      author_email='jgriffin@mozilla.com',
-      url='http://hg.mozilla.org/services/services-central',
-      license='MPL',
+      author='Mozilla Automation and Tools team',
+      author_email='tools@lists.mozilla.org',
+      url='https://developer.mozilla.org/en-US/docs/TPS',
+      license='MPL 2.0',
       packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       runtps = tps.cli:main
--- a/testing/tps/tps/__init__.py
+++ b/testing/tps/tps/__init__.py
@@ -1,8 +1,6 @@
-# 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/.
-
-from firefoxrunner import TPSFirefoxRunner
-from testrunner import TPSTestRunner
-from mozhttpd import MozHttpd
-
+# 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/.
+
+from . firefoxrunner import TPSFirefoxRunner
+from . testrunner import TPSTestRunner
--- a/testing/tps/tps/cli.py
+++ b/testing/tps/tps/cli.py
@@ -1,99 +1,111 @@
 # 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/.
 
 import json
 import optparse
 import os
+import re
 import sys
-
 from threading import RLock
 
 from tps import TPSTestRunner
 
+
 def main():
     parser = optparse.OptionParser()
-    parser.add_option("--mobile",
-                      action = "store_true", dest = "mobile",
-                      default = False,
-                      help = "run with mobile settings")
-    parser.add_option("--testfile",
-                      action = "store", type = "string", dest = "testfile",
-                      default = '../../services/sync/tests/tps/test_sync.js',
-                      help = "path to the test file to run "
-                             "[default: %default]")
-    parser.add_option("--logfile",
-                      action = "store", type = "string", dest = "logfile",
-                      default = 'tps.log',
-                      help = "path to the log file [default: %default]")
-    parser.add_option("--resultfile",
-                      action = "store", type = "string", dest = "resultfile",
-                      default = 'tps_result.json',
-                      help = "path to the result file [default: %default]")
-    parser.add_option("--binary",
-                      action = "store", type = "string", dest = "binary",
-                      default = None,
-                      help = "path to the Firefox binary, specified either as "
-                             "a local file or a url; if omitted, the PATH "
-                             "will be searched;")
-    parser.add_option("--configfile",
-                      action = "store", type = "string", dest = "configfile",
-                      default = None,
-                      help = "path to the config file to use "
-                             "[default: %default]")
-    parser.add_option("--pulsefile",
-                      action = "store", type = "string", dest = "pulsefile",
-                      default = None,
-                      help = "path to file containing a pulse message in "
-                             "json format that you want to inject into the monitor")
-    parser.add_option("--ignore-unused-engines",
+    parser.add_option('--mobile',
+                      action='store_true',
+                      dest='mobile',
+                      default=False,
+                      help='run with mobile settings')
+    parser.add_option('--testfile',
+                      action='store',
+                      type='string',
+                      dest='testfile',
+                      default='../../services/sync/tests/tps/all_tests.json',
+                      help='path to the test file to run [default: %default]')
+    parser.add_option('--logfile',
+                      action='store',
+                      type='string',
+                      dest='logfile',
+                      default='tps.log',
+                      help='path to the log file [default: %default]')
+    parser.add_option('--resultfile',
+                      action='store',
+                      type='string',
+                      dest='resultfile',
+                      default='tps_result.json',
+                      help='path to the result file [default: %default]')
+    parser.add_option('--binary',
+                      action='store',
+                      type='string',
+                      dest='binary',
+                      default=None,
+                      help='path to the Firefox binary, specified either as '
+                           'a local file or a url; if omitted, the PATH '
+                           'will be searched;')
+    parser.add_option('--configfile',
+                      action='store',
+                      type='string',
+                      dest='configfile',
+                      default=None,
+                      help='path to the config file to use default: %default]')
+    parser.add_option('--pulsefile',
+                      action='store',
+                      type='string',
+                      dest='pulsefile',
+                      default=None,
+                      help='path to file containing a pulse message in '
+                           'json format that you want to inject into the monitor')
+    parser.add_option('--ignore-unused-engines',
                        default=False,
-                       action="store_true",
-                       dest="ignore_unused_engines",
-                       help="If defined, don't load unused engines in individual tests."
-                             " Has no effect for pulse monitor.")
+                       action='store_true',
+                       dest='ignore_unused_engines',
+                       help='If defined, do not load unused engines in individual tests.'
+                            ' Has no effect for pulse monitor.')
     (options, args) = parser.parse_args()
 
     configfile = options.configfile
     if configfile is None:
         if os.environ.get('VIRTUAL_ENV'):
             configfile = os.path.join(os.path.dirname(__file__), 'config.json')
         if configfile is None or not os.access(configfile, os.F_OK):
-            raise Exception("Unable to find config.json in a VIRTUAL_ENV; you must "
-                            "specify a config file using the --configfile option")
+            raise Exception('Unable to find config.json in a VIRTUAL_ENV; you must '
+                            'specify a config file using the --configfile option')
 
     # load the config file
     f = open(configfile, 'r')
     configcontent = f.read()
     f.close()
     config = json.loads(configcontent)
 
     rlock = RLock()
 
     print 'using result file', options.resultfile
 
-    extensionDir = config.get("extensiondir")
+    extensionDir = config.get('extensiondir')
     if not extensionDir or extensionDir == '__EXTENSIONDIR__':
-        extensionDir = os.path.join(os.getcwd(), "..", "..", "services", "sync", "tps", "extensions")
+        extensionDir = os.path.join(os.getcwd(), '..', '..',
+                                    'services', 'sync', 'tps', 'extensions')
     else:
         if sys.platform == 'win32':
             # replace msys-style paths with proper Windows paths
-            import re
             m = re.match('^\/\w\/', extensionDir)
             if m:
-                extensionDir = "%s:/%s" % (m.group(0)[1:2], extensionDir[3:])
-                extensionDir = extensionDir.replace("/", "\\")
+                extensionDir = '%s:/%s' % (m.group(0)[1:2], extensionDir[3:])
+                extensionDir = extensionDir.replace('/', '\\')
 
     TPS = TPSTestRunner(extensionDir,
                         testfile=options.testfile,
                         logfile=options.logfile,
                         binary=options.binary,
                         config=config,
                         rlock=rlock,
                         mobile=options.mobile,
                         resultfile=options.resultfile,
                         ignore_unused_engines=options.ignore_unused_engines)
     TPS.run_tests()
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     main()
--- a/testing/tps/tps/firefoxrunner.py
+++ b/testing/tps/tps/firefoxrunner.py
@@ -1,94 +1,95 @@
-# 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/.
-
-import copy
-import httplib2
-import os
-import shutil
-
-import mozinstall
-
-from mozprofile import Profile
-from mozrunner import FirefoxRunner
-
-class TPSFirefoxRunner(object):
-
-    PROCESS_TIMEOUT = 240
-  
-    def __init__(self, binary):
-        if binary is not None and ('http://' in binary or 'ftp://' in binary):
-            self.url = binary
-            self.binary = None
-        else:
-            self.url = None
-            self.binary = binary
-        self.runner = None
-        self.installdir = None
-    
-    def __del__(self):
-        if self.installdir:
-            shutil.rmtree(self.installdir, True)
-      
-    def download_url(self, url, dest=None):
-        h = httplib2.Http()
-        resp, content = h.request(url, "GET")
-        if dest == None:
-            dest = os.path.basename(url)
-    
-        local = open(dest, 'wb')
-        local.write(content)
-        local.close()
-        return dest
-    
-    def download_build(self, installdir='downloadedbuild', appname='firefox'):
-        self.installdir = os.path.abspath(installdir)
-        buildName = os.path.basename(self.url)
-        pathToBuild = os.path.join(os.path.dirname(os.path.abspath(__file__)),
-                                   buildName)
-    
-        # delete the build if it already exists
-        if os.access(pathToBuild, os.F_OK):
-            os.remove(pathToBuild)
-      
-        # download the build
-        print "downloading build"
-        self.download_url(self.url, pathToBuild)
-    
-        # install the build
-        print "installing %s" % pathToBuild
-        shutil.rmtree(self.installdir, True)
-        binary = mozinstall.install(src=pathToBuild, dest=self.installdir)
-    
-        # remove the downloaded archive
-        os.remove(pathToBuild)
-    
-        return binary
-    
-    def run(self, profile=None, timeout=PROCESS_TIMEOUT, env=None, args=None):
-        """Runs the given FirefoxRunner with the given Profile, waits
-           for completion, then returns the process exit code
-        """
-        if profile is None:
-            profile = Profile()
-        self.profile = profile
-    
-        if self.binary is None and self.url:
-            self.binary = self.download_build()
-      
-        if self.runner is None:
-            self.runner = FirefoxRunner(self.profile, binary=self.binary)
-      
-        self.runner.profile = self.profile
-    
-        if env is not None:
-            self.runner.env.update(env)
-      
-        if args is not None:
-            self.runner.cmdargs = copy.copy(args)
-      
-        self.runner.start()
-    
-        status = self.runner.process_handler.waitForFinish(timeout=timeout)
-    
-        return status
+# 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/.
+
+
+import copy
+import httplib2
+import os
+
+import mozfile
+import mozinstall
+from mozprofile import Profile
+from mozrunner import FirefoxRunner
+
+
+class TPSFirefoxRunner(object):
+
+    PROCESS_TIMEOUT = 240
+
+    def __init__(self, binary):
+        if binary is not None and ('http://' in binary or 'ftp://' in binary):
+            self.url = binary
+            self.binary = None
+        else:
+            self.url = None
+            self.binary = binary
+
+        self.runner = None
+        self.installdir = None
+
+    def __del__(self):
+        if self.installdir:
+            mozfile.remove(self.installdir, True)
+
+    def download_url(self, url, dest=None):
+        h = httplib2.Http()
+        resp, content = h.request(url, 'GET')
+        if dest == None:
+            dest = os.path.basename(url)
+
+        local = open(dest, 'wb')
+        local.write(content)
+        local.close()
+        return dest
+
+    def download_build(self, installdir='downloadedbuild', appname='firefox'):
+        self.installdir = os.path.abspath(installdir)
+        buildName = os.path.basename(self.url)
+        pathToBuild = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                                   buildName)
+
+        # delete the build if it already exists
+        if os.access(pathToBuild, os.F_OK):
+            os.remove(pathToBuild)
+
+        # download the build
+        print 'downloading build'
+        self.download_url(self.url, pathToBuild)
+
+        # install the build
+        print 'installing %s' % pathToBuild
+        mozfile.remove(self.installdir, True)
+        binary = mozinstall.install(src=pathToBuild, dest=self.installdir)
+
+        # remove the downloaded archive
+        os.remove(pathToBuild)
+
+        return binary
+
+    def run(self, profile=None, timeout=PROCESS_TIMEOUT, env=None, args=None):
+        """Runs the given FirefoxRunner with the given Profile, waits
+           for completion, then returns the process exit code
+        """
+        if profile is None:
+            profile = Profile()
+        self.profile = profile
+
+        if self.binary is None and self.url:
+            self.binary = self.download_build()
+
+        if self.runner is None:
+            self.runner = FirefoxRunner(self.profile, binary=self.binary)
+
+        self.runner.profile = self.profile
+
+        if env is not None:
+            self.runner.env.update(env)
+
+        if args is not None:
+            self.runner.cmdargs = copy.copy(args)
+
+        self.runner.start()
+        returncode = self.runner.wait(timeout)
+
+        return returncode
deleted file mode 100644
--- a/testing/tps/tps/mozhttpd.py
+++ /dev/null
@@ -1,104 +0,0 @@
-#!/usr/bin/python
-#
-# 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/.
-
-import BaseHTTPServer
-import SimpleHTTPServer
-import threading
-import sys
-import os
-import urllib
-import re
-from urlparse import urlparse
-from SocketServer import ThreadingMixIn
-
-DOCROOT = '.'
-
-class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
-    allow_reuse_address = True
-
-class MozRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
-    def translate_path(self, path):
-        # It appears that the default path is '/' and os.path.join makes the '/'
-        o = urlparse(path)
-
-        sep = '/'
-        if sys.platform == 'win32':
-            sep = ''
-
-        ret = '%s%s' % ( sep, DOCROOT.strip('/') )
-
-        # Stub out addons.mozilla.org search API, which is used when installing
-        # add-ons. The version is hard-coded because we want tests to fail when
-        # the API updates so we can update our stubbed files with the changes.
-        if o.path.find('/en-US/firefox/api/1.5/search/guid:') == 0:
-            ids = urllib.unquote(o.path[len('/en-US/firefox/api/1.5/search/guid:'):])
-
-            if ids.count(',') > 0:
-                raise Exception('Searching for multiple IDs is not supported.')
-
-            base = ids
-            at_loc = ids.find('@')
-            if at_loc > 0:
-                base = ids[0:at_loc]
-
-            ret += '/%s.xml' % base
-
-        else:
-            ret += '/%s' % o.path.strip('/')
-
-        return ret
-
-    # I found on my local network that calls to this were timing out
-    # I believe all of these calls are from log_message
-    def address_string(self):
-        return "a.b.c.d"
-
-    # This produces a LOT of noise
-    def log_message(self, format, *args):
-        pass
-
-class MozHttpd(object):
-    def __init__(self, host="127.0.0.1", port=8888, docroot='.'):
-        global DOCROOT
-        self.host = host
-        self.port = int(port)
-        DOCROOT = docroot
-
-    def start(self):
-        self.httpd = EasyServer((self.host, self.port), MozRequestHandler)
-        self.server = threading.Thread(target=self.httpd.serve_forever)
-        self.server.setDaemon(True) # don't hang on exit
-        self.server.start()
-        #self.testServer()
-
-    #TODO: figure this out
-    def testServer(self):
-        fileList = os.listdir(DOCROOT)
-        filehandle = urllib.urlopen('http://%s:%s' % (self.host, self.port))
-        data = filehandle.readlines();
-        filehandle.close()
-
-        for line in data:
-            found = False
-            # '@' denotes a symlink and we need to ignore it.
-            webline = re.sub('\<[a-zA-Z0-9\-\_\.\=\"\'\/\\\%\!\@\#\$\^\&\*\(\) ]*\>', '', line.strip('\n')).strip('/').strip().strip('@')
-            if webline != "":
-                if webline == "Directory listing for":
-                    found = True
-                else:
-                    for fileName in fileList:
-                        if fileName == webline:
-                            found = True
-
-                if (found == False):
-                    print "NOT FOUND: " + webline.strip()
-
-    def stop(self):
-        if self.httpd:
-            self.httpd.shutdown()
-            self.httpd.server_close()
-
-    __del__ = stop
--- a/testing/tps/tps/phase.py
+++ b/testing/tps/tps/phase.py
@@ -2,17 +2,17 @@
 # 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/.
 
 import re
 
 class TPSTestPhase(object):
 
     lineRe = re.compile(
-        r"^(.*?)test phase (?P<matchphase>\d+): (?P<matchstatus>.*)$")
+        r'^(.*?)test phase (?P<matchphase>\d+): (?P<matchstatus>.*)$')
 
     def __init__(self, phase, profile, testname, testpath, logfile, env,
                  firefoxRunner, logfn, ignore_unused_engines=False):
         self.phase = phase
         self.profile = profile
         self.testname = str(testname) # this might be passed in as unicode
         self.testpath = testpath
         self.logfile = logfile
@@ -37,39 +37,39 @@ class TPSTestPhase(object):
         # launch Firefox
         args = [ '-tps', self.testpath,
                  '-tpsphase', self.phasenum,
                  '-tpslogfile', self.logfile ]
 
         if self.ignore_unused_engines:
             args.append('--ignore-unused-engines')
 
-        self.log("\nlaunching Firefox for phase %s with args %s\n" %
+        self.log('\nLaunching Firefox for phase %s with args %s\n' %
                  (self.phase, str(args)))
         self.firefoxRunner.run(env=self.env,
                                args=args,
                                profile=self.profile)
 
         # parse the logfile and look for results from the current test phase
         found_test = False
         f = open(self.logfile, 'r')
         for line in f:
 
             # skip to the part of the log file that deals with the test we're running
             if not found_test:
-                if line.find("Running test %s" % self.testname) > -1:
+                if line.find('Running test %s' % self.testname) > -1:
                     found_test = True
                 else:
                     continue
 
             # look for the status of the current phase
             match = self.lineRe.match(line)
             if match:
-                if match.group("matchphase") == self.phasenum:
-                    self._status = match.group("matchstatus")
+                if match.group('matchphase') == self.phasenum:
+                    self._status = match.group('matchstatus')
                     break
 
             # set the status to FAIL if there is TPS error
-            if line.find("CROSSWEAVE ERROR: ") > -1 and not self._status:
-                self._status = "FAIL"
-                self.errline = line[line.find("CROSSWEAVE ERROR: ") + len("CROSSWEAVE ERROR: "):]
+            if line.find('CROSSWEAVE ERROR: ') > -1 and not self._status:
+                self._status = 'FAIL'
+                self.errline = line[line.find('CROSSWEAVE ERROR: ') + len('CROSSWEAVE ERROR: '):]
 
         f.close()
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -6,21 +6,23 @@ import json
 import os
 import platform
 import random
 import re
 import tempfile
 import time
 import traceback
 
+from mozhttpd import MozHttpd
+import mozinfo
 from mozprofile import Profile
 
-from tps.firefoxrunner import TPSFirefoxRunner
-from tps.phase import TPSTestPhase
-from tps.mozhttpd import MozHttpd
+from .firefoxrunner import TPSFirefoxRunner
+from .phase import TPSTestPhase
+
 
 class TempFile(object):
     """Class for temporary files that delete themselves when garbage-collected.
     """
 
     def __init__(self, prefix=None):
         self.fd, self.filename = self.tmpfile = tempfile.mkstemp(prefix=prefix)
 
@@ -36,59 +38,60 @@ class TempFile(object):
     def cleanup(self):
         if self.fd:
             self.close()
         if os.access(self.filename, os.F_OK):
             os.remove(self.filename)
 
     __del__ = cleanup
 
+
 class TPSTestRunner(object):
 
     default_env = { 'MOZ_CRASHREPORTER_DISABLE': '1',
                     'GNOME_DISABLE_CRASH_DIALOG': '1',
                     'XRE_NO_WINDOWS_CRASH_DIALOG': '1',
                     'MOZ_NO_REMOTE': '1',
                     'XPCOM_DEBUG_BREAK': 'warn',
                   }
     default_preferences = { 'app.update.enabled' : False,
-                            'extensions.getAddons.get.url': 'http://127.0.0.1:4567/en-US/firefox/api/%API_VERSION%/search/guid:%IDS%',
-                            'extensions.update.enabled'    : False,
-                            'extensions.update.notifyUser' : False,
+                            'browser.dom.window.dump.enabled': True,
+                            'browser.sessionstore.resume_from_crash': False,
                             'browser.shell.checkDefaultBrowser' : False,
                             'browser.tabs.warnOnClose' : False,
                             'browser.warnOnQuit': False,
-                            'browser.sessionstore.resume_from_crash': False,
+                            # Allow installing extensions dropped into the profile folder
+                            'extensions.autoDisableScopes': 10,
+                            'extensions.getAddons.get.url': 'http://127.0.0.1:4567/en-US/firefox/api/%API_VERSION%/search/guid:%IDS%',
+                            'extensions.update.enabled'    : False,
+                            # Don't open a dialog to show available add-on updates
+                            'extensions.update.notifyUser' : False,
                             'services.sync.addons.ignoreRepositoryChecking': True,
                             'services.sync.firstSync': 'notReady',
                             'services.sync.lastversion': '1.0',
                             'services.sync.log.rootLogger': 'Trace',
                             'services.sync.log.logger.engine.addons': 'Trace',
                             'services.sync.log.logger.service.main': 'Trace',
                             'services.sync.log.logger.engine.bookmarks': 'Trace',
                             'services.sync.log.appender.console': 'Trace',
                             'services.sync.log.appender.debugLog.enabled': True,
                             'toolkit.startup.max_resumed_crashes': -1,
-                            'browser.dom.window.dump.enabled': True,
-                            # Allow installing extensions dropped into the profile folder
-                            'extensions.autoDisableScopes': 10,
-                            # Don't open a dialog to show available add-on updates
-                            'extensions.update.notifyUser' : False,
                           }
+
     syncVerRe = re.compile(
-        r"Sync version: (?P<syncversion>.*)\n")
+        r'Sync version: (?P<syncversion>.*)\n')
     ffVerRe = re.compile(
-        r"Firefox version: (?P<ffver>.*)\n")
+        r'Firefox version: (?P<ffver>.*)\n')
     ffDateRe = re.compile(
-        r"Firefox builddate: (?P<ffdate>.*)\n")
+        r'Firefox builddate: (?P<ffdate>.*)\n')
 
     def __init__(self, extensionDir,
-                 testfile="sync.test",
+                 testfile='sync.test',
                  binary=None, config=None, rlock=None, mobile=False,
-                 logfile="tps.log", resultfile="tps_result.json",
+                 logfile='tps.log', resultfile='tps_result.json',
                  ignore_unused_engines=False):
         self.extensions = []
         self.testfile = testfile
         self.logfile = os.path.abspath(logfile)
         self.resultfile = resultfile
         self.binary = binary
         self.ignore_unused_engines = ignore_unused_engines
         self.config = config if config else {}
@@ -162,33 +165,27 @@ class TPSTestRunner(object):
             # on some OS's, adding directory entries doesn't seem to work
             pass
         for root, dirs, files in os.walk(os.path.join(rootDir, dir)):
             for f in files:
                 zip.write(os.path.join(root, f), os.path.join(dir, f))
 
     def run_single_test(self, testdir, testname):
         testpath = os.path.join(testdir, testname)
-        self.log("Running test %s\n" % testname)
-
-        # Create a random account suffix that is used when creating test
-        # accounts on a staging server.
-        #account_suffix = {"account-suffix": ''.join([str(random.randint(0,9))
-        #                                             for i in range(1,6)])}
-        #self.config['sync_account'].update(account_suffix)
+        self.log("Running test %s\n" % testname, True)
 
         # Read and parse the test file, merge it with the contents of the config
         # file, and write the combined output to a temporary file.
         f = open(testpath, 'r')
         testcontent = f.read()
         f.close()
         try:
             test = json.loads(testcontent)
         except:
-            test = json.loads(testcontent[testcontent.find("{"):testcontent.find("}") + 1])
+            test = json.loads(testcontent[testcontent.find('{'):testcontent.find('}') + 1])
 
         testcontent += 'var config = %s;\n' % json.dumps(self.config, indent=2)
         testcontent += 'var seconds_since_epoch = %d;\n' % int(time.time())
 
         tmpfile = TempFile(prefix='tps_test_')
         tmpfile.write(testcontent)
         tmpfile.close()
 
@@ -218,19 +215,19 @@ class TPSTestRunner(object):
         # sort the phase list by name
         phaselist = sorted(phaselist, key=lambda phase: phase.phase)
 
         # run each phase in sequence, aborting at the first failure
         for phase in phaselist:
             phase.run()
 
             # if a failure occurred, dump the entire sync log into the test log
-            if phase.status != "PASS":
+            if phase.status != 'PASS':
                 for profile in profiles:
-                    self.log("\nDumping sync log for profile %s\n" %  profiles[profile].profile)
+                    self.log('\nDumping sync log for profile %s\n' %  profiles[profile].profile)
                     for root, dirs, files in os.walk(os.path.join(profiles[profile].profile, 'weave', 'logs')):
                         for f in files:
                             weavelog = os.path.join(profiles[profile].profile, 'weave', 'logs', f)
                             if os.access(weavelog, os.F_OK):
                                 with open(weavelog, 'r') as fh:
                                     for line in fh:
                                         possible_time = line[0:13]
                                         if len(possible_time) == 13 and possible_time.isdigit():
@@ -242,21 +239,21 @@ class TPSTestRunner(object):
                                         else:
                                             self.log(line)
                 break;
 
         # grep the log for FF and sync versions
         f = open(self.logfile)
         logdata = f.read()
         match = self.syncVerRe.search(logdata)
-        sync_version = match.group("syncversion") if match else 'unknown'
+        sync_version = match.group('syncversion') if match else 'unknown'
         match = self.ffVerRe.search(logdata)
-        firefox_version = match.group("ffver") if match else 'unknown'
+        firefox_version = match.group('ffver') if match else 'unknown'
         match = self.ffDateRe.search(logdata)
-        firefox_builddate = match.group("ffdate") if match else 'unknown'
+        firefox_builddate = match.group('ffdate') if match else 'unknown'
         f.close()
         if phase.status == 'PASS':
             logdata = ''
         else:
             # we only care about the log data for this specific test
             logdata = logdata[logdata.find('Running test %s' % (str(testname))):]
 
         result = {
@@ -276,29 +273,29 @@ class TPSTestRunner(object):
         # save logdata to a temporary file for posting to the db
         tmplogfile = None
         if logdata:
             tmplogfile = TempFile(prefix='tps_log_')
             tmplogfile.write(logdata)
             tmplogfile.close()
             self.errorlogs[testname] = tmplogfile
 
-        resultdata = ({ "productversion": { "version": firefox_version,
-                                            "buildid": firefox_builddate,
-                                            "builddate": firefox_builddate[0:8],
-                                            "product": "Firefox",
-                                            "repository": apprepo,
-                                            "changeset": appchangeset,
+        resultdata = ({ 'productversion': { 'version': firefox_version,
+                                            'buildid': firefox_builddate,
+                                            'builddate': firefox_builddate[0:8],
+                                            'product': 'Firefox',
+                                            'repository': apprepo,
+                                            'changeset': appchangeset,
                                           },
-                        "addonversion": { "version": sync_version,
-                                          "product": "Firefox Sync" },
-                        "name": testname,
-                        "message": result[1],
-                        "state": result[0],
-                        "logdata": logdata
+                        'addonversion': { 'version': sync_version,
+                                          'product': 'Firefox Sync' },
+                        'name': testname,
+                        'message': result[1],
+                        'state': result[0],
+                        'logdata': logdata
                       })
 
         self.log(logstr, True)
         for phase in phaselist:
             print "\t%s: %s" % (phase.phase, phase.status)
             if phase.status == 'FAIL':
                 break
 
@@ -311,16 +308,20 @@ class TPSTestRunner(object):
 
         # Make a copy of the default env variables and preferences, and update
         # them for mobile settings if needed.
         self.env = self.default_env.copy()
         self.preferences = self.default_preferences.copy()
         if self.mobile:
             self.preferences.update({'services.sync.client.type' : 'mobile'})
 
+        # If sync accounts have been chosen, disable Firefox Accounts
+        if self.config.get('auth_type', 'fx_account') != 'fx_account':
+            self.preferences.update({'services.sync.fxaccounts.enabled' : False})
+
         # Acquire a lock to make sure no other threads are running tests
         # at the same time.
         if self.rlock:
             self.rlock.acquire()
 
         try:
             # Create the Firefox runner, which will download and install the
             # build, as needed.
@@ -362,32 +363,23 @@ class TPSTestRunner(object):
 
         # dump out a summary of test results
         print 'Test Summary\n'
         for test in self.postdata.get('tests', {}):
             print '%s | %s | %s' % (test['state'], test['name'], test['message'])
 
     def run_test_group(self):
         self.results = []
-        self.extensions = []
-
-        # set the OS we're running on
-        os_string = platform.uname()[2] + " " + platform.uname()[3]
-        if os_string.find("Darwin") > -1:
-            os_string = "Mac OS X " + platform.mac_ver()[0]
-        if platform.uname()[0].find("Linux") > -1:
-            os_string = "Linux " + platform.uname()[5]
-        if platform.uname()[0].find("Win") > -1:
-            os_string = "Windows " + platform.uname()[3]
 
         # reset number of passed/failed tests
         self.numpassed = 0
         self.numfailed = 0
 
         # build our tps.xpi extension
+        self.extensions = []
         self.extensions.append(os.path.join(self.extensionDir, 'tps'))
         self.extensions.append(os.path.join(self.extensionDir, "mozmill"))
 
         # build the test list
         try:
             f = open(self.testfile)
             jsondata = f.read()
             f.close()
@@ -417,14 +409,14 @@ class TPSTestRunner(object):
                 self.numpassed += 1
             else:
                 self.numfailed += 1
 
         self.mozhttpd.stop()
 
         # generate the postdata we'll use to post the results to the db
         self.postdata = { 'tests': self.results,
-                          'os':os_string,
+                          'os': '%s %sbit' % (mozinfo.version, mozinfo.bits),
                           'testtype': 'crossweave',
                           'productversion': self.productversion,
                           'addonversion': self.addonversion,
                           'synctype': self.synctype,
                         }
deleted file mode 100644
--- a/testing/tps/tps/thread.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# 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/.
-
-from threading import Thread
-
-from testrunner import TPSTestRunner
-
-class TPSTestThread(Thread):
-
-    def __init__(self, extensionDir, builddata=None,
-                 testfile=None, logfile=None, rlock=None, config=None):
-        assert(builddata)
-        assert(config)
-        self.extensionDir = extensionDir
-        self.builddata = builddata
-        self.testfile = testfile
-        self.logfile = logfile
-        self.rlock = rlock
-        self.config = config
-        Thread.__init__(self)
-
-    def run(self):
-        # run the tests in normal mode ...
-        TPS = TPSTestRunner(self.extensionDir,
-                            testfile=self.testfile,
-                            logfile=self.logfile,
-                            binary=self.builddata['buildurl'],
-                            config=self.config,
-                            rlock=self.rlock,
-                            mobile=False)
-        TPS.run_tests()
-
-        # Get the binary used by this TPS instance, and use it in subsequent
-        # ones, so it doesn't have to be re-downloaded each time.
-        binary = TPS.firefoxRunner.binary
-
-        # ... and then again in mobile mode
-        TPS_mobile = TPSTestRunner(self.extensionDir,
-                                   testfile=self.testfile,
-                                   logfile=self.logfile,
-                                   binary=binary,
-                                   config=self.config,
-                                   rlock=self.rlock,
-                                   mobile=True)
-        TPS_mobile.run_tests()
-
-        # ... and again via the staging server, if credentials are present
-        stageaccount = self.config.get('stageaccount')
-        if stageaccount:
-            username = stageaccount.get('username')
-            password = stageaccount.get('password')
-            passphrase = stageaccount.get('passphrase')
-            if username and password and passphrase:
-                stageconfig = self.config.copy()
-                stageconfig['account'] = stageaccount.copy()
-                TPS_stage = TPSTestRunner(self.extensionDir,
-                                          testfile=self.testfile,
-                                          logfile=self.logfile,
-                                          binary=binary,
-                                          config=stageconfig,
-                                          rlock=self.rlock,
-                                          mobile=False)#, autolog=self.autolog)
-                TPS_stage.run_tests()