Bug 865349 - Refactor B2G mochitests off automation.py and onto mozbase, r=jgriffin,ted
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Fri, 26 Jul 2013 14:40:04 -0400
changeset 140203 a04093b3aaa4bcffa8efae07d9f854bc6c74c103
parent 140202 97ad9f8fd485505e77d0413a987ab7207d924373
child 140204 ddbd70f50d73736d5bfa5845600af86c290171a2
push id1945
push userryanvm@gmail.com
push dateSat, 27 Jul 2013 02:27:26 +0000
treeherderfx-team@4874fa438b1c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjgriffin, ted
bugs865349
milestone25.0a1
Bug 865349 - Refactor B2G mochitests off automation.py and onto mozbase, r=jgriffin,ted
testing/mochitest/Makefile.in
testing/mochitest/b2g_start_script.js
testing/mochitest/mochitest_options.py
testing/mochitest/runtests.py
testing/mochitest/runtestsb2g.py
testing/mochitest/runtestsremote.py
--- a/testing/mochitest/Makefile.in
+++ b/testing/mochitest/Makefile.in
@@ -31,16 +31,17 @@ include $(topsrcdir)/build/automation-bu
 
 # files that get copied into $objdir/_tests/
 _SERV_FILES = 	\
 		runtests.py \
 		automation.py \
 		runtestsb2g.py \
 		runtestsremote.py \
 		runtestsvmware.py \
+		mochitest_options.py \
 		manifest.webapp \
 		$(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanager.py \
 		$(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py \
 		$(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py \
 		$(topsrcdir)/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py \
 		$(topsrcdir)/testing/mozbase/mozdevice/mozdevice/droid.py \
 		$(topsrcdir)/testing/mozbase/mozdevice/mozdevice/Zeroconf.py \
 		$(topsrcdir)/build/automationutils.py \
@@ -57,16 +58,17 @@ include $(topsrcdir)/build/automation-bu
 		browser-harness.xul \
 		redirect.html \
 		$(topsrcdir)/build/pgo/server-locations.txt \
 		$(topsrcdir)/netwerk/test/httpserver/httpd.js \
 		pywebsocket_wrapper.py \
 		android.json \
 		androidx86.json \
 		b2g.json \
+		b2g_start_script.js \
 		root-ev-tester.crl \
 		intermediate-ev-tester.crl \
 		$(NULL)	
 
 ifeq ($(MOZ_BUILD_APP),mobile/android)
 _SERV_FILES += \
   $(topsrcdir)/mobile/android/base/tests/robocop.ini \
   $(topsrcdir)/mobile/android/base/tests/robocop_autophone.ini \
@@ -197,12 +199,13 @@ stage-package: stage-chromejar
 endif
 
 $(_DEST_DIR):
 	$(NSINSTALL) -D $@
 
 stage-package:
 	$(NSINSTALL) -D $(PKG_STAGE)/mochitest && $(NSINSTALL) -D $(PKG_STAGE)/bin/plugins && $(NSINSTALL) -D $(DIST)/plugins
 	(cd $(DEPTH)/_tests/testing/mochitest/ && tar $(TAR_CREATE_FLAGS) - *) | (cd $(PKG_STAGE)/mochitest && tar -xf -)
+	@cp $(DEPTH)/mozinfo.json $(PKG_STAGE)/mochitest
 	@(cd $(DIST_BIN) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_BINS)) | (cd $(PKG_STAGE)/bin && tar -xf -)
 	@(cd $(DIST_BIN)/components && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_COMPONENTS)) | (cd $(PKG_STAGE)/bin/components && tar -xf -)
 	(cd $(topsrcdir)/build/pgo/certs && tar $(TAR_CREATE_FLAGS) - *) | (cd $(PKG_STAGE)/certs && tar -xf -)
 	@(cd $(DIST)/plugins && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_PLUGINS)) | (cd $(PKG_STAGE)/bin/plugins && tar -xf -)
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/b2g_start_script.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+let outOfProcess = __marionetteParams[0]
+let mochitestUrl = __marionetteParams[1]
+
+const CHILD_SCRIPT = "chrome://specialpowers/content/specialpowers.js";
+const CHILD_SCRIPT_API = "chrome://specialpowers/content/specialpowersAPI.js";
+const CHILD_LOGGER_SCRIPT = "chrome://specialpowers/content/MozillaLogger.js";
+
+let homescreen = document.getElementById('homescreen');
+let container = homescreen.contentWindow.document.getElementById('test-container');
+
+function openWindow(aEvent) {
+  var popupIframe = aEvent.detail.frameElement;
+  popupIframe.setAttribute('style', 'position: absolute; left: 0; top: 300px; background: white; ');
+
+  popupIframe.addEventListener('mozbrowserclose', function(e) {
+    container.parentNode.removeChild(popupIframe);
+    container.focus();
+  });
+
+  // yes, the popup can call window.open too!
+  popupIframe.addEventListener('mozbrowseropenwindow', openWindow);
+
+  popupIframe.addEventListener('mozbrowserloadstart', function(e) {
+    popupIframe.focus();
+  });
+
+  container.parentNode.appendChild(popupIframe);
+}
+container.addEventListener('mozbrowseropenwindow', openWindow);
+
+let specialpowers = {};
+let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
+loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", specialpowers);
+let specialPowersObserver = new specialpowers.SpecialPowersObserver();
+specialPowersObserver.init();
+
+let mm = container.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
+mm.addMessageListener("SPPrefService", specialPowersObserver);
+mm.addMessageListener("SPProcessCrashService", specialPowersObserver);
+mm.addMessageListener("SPPingService", specialPowersObserver);
+mm.addMessageListener("SpecialPowers.Quit", specialPowersObserver);
+mm.addMessageListener("SpecialPowers.Focus", specialPowersObserver);
+mm.addMessageListener("SPPermissionManager", specialPowersObserver);
+
+mm.loadFrameScript(CHILD_LOGGER_SCRIPT, true);
+mm.loadFrameScript(CHILD_SCRIPT_API, true);
+mm.loadFrameScript(CHILD_SCRIPT, true);
+specialPowersObserver._isFrameScriptLoaded = true;
+
+container.src = mochitestUrl;
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/mochitest_options.py
@@ -0,0 +1,663 @@
+import optparse
+import os
+import sys
+import tempfile
+
+from automation import Automation
+from automationutils import addCommonOptions, isURL
+from mozprofile import DEFAULT_PORTS
+import moznetwork
+
+try:
+    from mozbuild.base import MozbuildObject
+    build_obj = MozbuildObject.from_environment()
+except ImportError:
+    build_obj = None
+
+here = os.path.abspath(os.path.dirname(sys.argv[0]))
+
+__all__ = ["MochitestOptions", "B2GOptions"]
+
+VMWARE_RECORDING_HELPER_BASENAME = "vmwarerecordinghelper"
+
+class MochitestOptions(optparse.OptionParser):
+    """Usage instructions for runtests.py.
+    All arguments are optional.
+    If --chrome is specified, chrome tests will be run instead of web content tests.
+    If --browser-chrome is specified, browser-chrome tests will be run instead of web content tests.
+    See <http://mochikit.com/doc/html/MochiKit/Logging.html> for details on the logging levels.
+    """
+
+    LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "FATAL")
+    LEVEL_STRING = ", ".join(LOG_LEVELS)
+    mochitest_options = [
+        [["--close-when-done"],
+        { "action": "store_true",
+          "dest": "closeWhenDone",
+          "default": False,
+          "help": "close the application when tests are done running",
+        }],
+        [["--appname"],
+        { "action": "store",
+          "type": "string",
+          "dest": "app",
+          "default": build_obj.get_binary_path() if build_obj is not None else None,
+          "help": "absolute path to application, overriding default",
+        }],
+        [["--utility-path"],
+        { "action": "store",
+          "type": "string",
+          "dest": "utilityPath",
+          "default": build_obj.bindir if build_obj is not None else None,
+          "help": "absolute path to directory containing utility programs (xpcshell, ssltunnel, certutil)",
+        }],
+        [["--certificate-path"],
+        { "action": "store",
+          "type": "string",
+          "dest": "certPath",
+          "help": "absolute path to directory containing certificate store to use testing profile",
+          "default": os.path.join(build_obj.topsrcdir, 'build', 'pgo', 'certs') if build_obj is not None else None,
+        }],
+        [["--log-file"],
+        { "action": "store",
+          "type": "string",
+          "dest": "logFile",
+          "metavar": "FILE",
+          "help": "file to which logging occurs",
+          "default": "",
+        }],
+        [["--autorun"],
+        { "action": "store_true",
+          "dest": "autorun",
+          "help": "start running tests when the application starts",
+          "default": False,
+        }],
+        [["--timeout"],
+        { "type": "int",
+          "dest": "timeout",
+          "help": "per-test timeout in seconds",
+          "default": None,
+        }],
+        [["--total-chunks"],
+        { "type": "int",
+          "dest": "totalChunks",
+          "help": "how many chunks to split the tests up into",
+          "default": None,
+        }],
+        [["--this-chunk"],
+        { "type": "int",
+          "dest": "thisChunk",
+          "help": "which chunk to run",
+          "default": None,
+        }],
+        [["--chunk-by-dir"],
+        { "type": "int",
+          "dest": "chunkByDir",
+          "help": "group tests together in the same chunk that are in the same top chunkByDir directories",
+          "default": 0,
+        }],
+        [["--shuffle"],
+        { "dest": "shuffle",
+          "action": "store_true",
+          "help": "randomize test order",
+          "default": False,
+        }],
+        [["--console-level"],
+        { "action": "store",
+          "type": "choice",
+          "dest": "consoleLevel",
+          "choices": LOG_LEVELS,
+          "metavar": "LEVEL",
+          "help": "one of %s to determine the level of console "
+                  "logging" % LEVEL_STRING,
+          "default": None,
+        }],
+        [["--file-level"],
+        { "action": "store",
+          "type": "choice",
+          "dest": "fileLevel",
+          "choices": LOG_LEVELS,
+          "metavar": "LEVEL",
+          "help": "one of %s to determine the level of file "
+                 "logging if a file has been specified, defaulting "
+                 "to INFO" % LEVEL_STRING,
+          "default": "INFO",
+        }],
+        [["--chrome"],
+        { "action": "store_true",
+          "dest": "chrome",
+          "help": "run chrome Mochitests",
+          "default": False,
+        }],
+        [["--ipcplugins"],
+        { "action": "store_true",
+          "dest": "ipcplugins",
+          "help": "run ipcplugins Mochitests",
+          "default": False,
+        }],
+        [["--test-path"],
+        { "action": "store",
+          "type": "string",
+          "dest": "testPath",
+          "help": "start in the given directory's tests",
+          "default": "",
+        }],
+        [["--browser-chrome"],
+        { "action": "store_true",
+          "dest": "browserChrome",
+          "help": "run browser chrome Mochitests",
+          "default": False,
+        }],
+        [["--webapprt-content"],
+        { "action": "store_true",
+          "dest": "webapprtContent",
+          "help": "run WebappRT content tests",
+          "default": False,
+        }],
+        [["--webapprt-chrome"],
+        { "action": "store_true",
+          "dest": "webapprtChrome",
+          "help": "run WebappRT chrome tests",
+          "default": False,
+        }],
+        [["--a11y"],
+        { "action": "store_true",
+          "dest": "a11y",
+          "help": "run accessibility Mochitests",
+          "default": False,
+        }],
+        [["--setenv"],
+        { "action": "append",
+          "type": "string",
+          "dest": "environment",
+          "metavar": "NAME=VALUE",
+          "help": "sets the given variable in the application's "
+                 "environment",
+          "default": [],
+        }],
+        [["--exclude-extension"],
+        { "action": "append",
+          "type": "string",
+          "dest": "extensionsToExclude",
+          "help": "excludes the given extension from being installed "
+                 "in the test profile",
+          "default": [],
+        }],
+        [["--browser-arg"],
+        { "action": "append",
+          "type": "string",
+          "dest": "browserArgs",
+          "metavar": "ARG",
+          "help": "provides an argument to the test application",
+          "default": [],
+        }],
+        [["--leak-threshold"],
+        { "action": "store",
+          "type": "int",
+          "dest": "leakThreshold",
+          "metavar": "THRESHOLD",
+          "help": "fail if the number of bytes leaked through "
+                 "refcounted objects (or bytes in classes with "
+                 "MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) is greater "
+                 "than the given number",
+          "default": 0,
+        }],
+        [["--fatal-assertions"],
+        { "action": "store_true",
+          "dest": "fatalAssertions",
+          "help": "abort testing whenever an assertion is hit "
+                 "(requires a debug build to be effective)",
+          "default": False,
+        }],
+        [["--extra-profile-file"],
+        { "action": "append",
+          "dest": "extraProfileFiles",
+          "help": "copy specified files/dirs to testing profile",
+          "default": [],
+        }],
+        [["--install-extension"],
+        { "action": "append",
+          "dest": "extensionsToInstall",
+          "help": "install the specified extension in the testing profile."
+                 "The extension file's name should be <id>.xpi where <id> is"
+                 "the extension's id as indicated in its install.rdf."
+                 "An optional path can be specified too.",
+          "default": [],
+        }],
+        [["--profile-path"],
+        { "action": "store",
+          "type": "string",
+          "dest": "profilePath",
+          "help": "Directory where the profile will be stored."
+                 "This directory will be deleted after the tests are finished",
+          "default": tempfile.mkdtemp(),
+        }],
+        [["--testing-modules-dir"],
+        { "action": "store",
+          "type": "string",
+          "dest": "testingModulesDir",
+          "help": "Directory where testing-only JS modules are located.",
+          "default": None,
+        }],
+        [["--use-vmware-recording"],
+        { "action": "store_true",
+          "dest": "vmwareRecording",
+          "help": "enables recording while the application is running "
+                 "inside a VMware Workstation 7.0 or later VM",
+          "default": False,
+        }],
+        [["--repeat"],
+        { "action": "store",
+          "type": "int",
+          "dest": "repeat",
+          "metavar": "REPEAT",
+          "help": "repeats the test or set of tests the given number of times, ie: repeat: 1 will run the test twice.",
+          "default": 0,
+        }],
+        [["--run-until-failure"],
+        { "action": "store_true",
+          "dest": "runUntilFailure",
+          "help": "Run a test repeatedly and stops on the first time the test fails. "
+                "Only available when running a single test. Default cap is 30 runs, "
+                "which can be overwritten with the --repeat parameter.",
+          "default": False,
+        }],
+        [["--run-only-tests"],
+        { "action": "store",
+          "type": "string",
+          "dest": "runOnlyTests",
+          "help": "JSON list of tests that we only want to run, cannot be specified with --exclude-tests. [DEPRECATED- please use --test-manifest]",
+          "default": None,
+        }],
+        [["--exclude-tests"],
+        { "action": "store",
+          "type": "string",
+          "dest": "excludeTests",
+          "help": "JSON list of tests that we want to not run, cannot be specified with --run-only-tests. [DEPRECATED- please use --test-manifest]",
+          "default": None,
+        }],
+        [["--test-manifest"],
+        { "action": "store",
+          "type": "string",
+          "dest": "testManifest",
+          "help": "JSON list of tests to specify 'runtests' and 'excludetests'.",
+          "default": None,
+        }],
+        [["--failure-file"],
+        { "action": "store",
+          "type": "string",
+          "dest": "failureFile",
+          "help": "Filename of the output file where we can store a .json list of failures to be run in the future with --run-only-tests.",
+          "default": None,
+        }],
+        [["--run-slower"],
+        { "action": "store_true",
+          "dest": "runSlower",
+          "help": "Delay execution between test files.",
+          "default": False,
+        }],
+        [["--metro-immersive"],
+        { "action": "store_true",
+          "dest": "immersiveMode",
+          "help": "launches tests in immersive browser",
+          "default": False,
+        }],
+        [["--httpd-path"],
+        { "action": "store",
+          "type": "string",
+          "dest": "httpdPath",
+          "default": None,
+          "help": "path to the httpd.js file",
+        }],
+        [["--setpref"],
+        { "action": "append",
+          "type": "string",
+          "default": [],
+          "dest": "extraPrefs",
+          "metavar": "PREF=VALUE",
+          "help": "defines an extra user preference",
+        }],
+    ]
+
+    def __init__(self, automation=None, **kwargs):
+        self._automation = automation or Automation()
+        optparse.OptionParser.__init__(self, **kwargs)
+        defaults = {}
+
+        # we want to pass down everything from self._automation.__all__
+        addCommonOptions(self, defaults=dict(zip(self._automation.__all__,
+                 [getattr(self._automation, x) for x in self._automation.__all__])))
+
+        for option in self.mochitest_options:
+            self.add_option(*option[0], **option[1])
+
+        self.set_defaults(**defaults)
+        self.set_usage(self.__doc__)
+
+    def verifyOptions(self, options, mochitest):
+        """ verify correct options and cleanup paths """
+
+        if options.totalChunks is not None and options.thisChunk is None:
+            self.error("thisChunk must be specified when totalChunks is specified")
+
+        if options.totalChunks:
+            if not 1 <= options.thisChunk <= options.totalChunks:
+                self.error("thisChunk must be between 1 and totalChunks")
+
+        if options.xrePath is None:
+            # default xrePath to the app path if not provided
+            # but only if an app path was explicitly provided
+            if options.app != self.defaults['app']:
+                options.xrePath = os.path.dirname(options.app)
+            elif build_obj is not None:
+                # otherwise default to dist/bin
+                options.xrePath = build_obj.bindir
+            else:
+                self.error("could not find xre directory, --xre-path must be specified")
+
+        # allow relative paths
+        options.xrePath = mochitest.getFullPath(options.xrePath)
+        options.profilePath = mochitest.getFullPath(options.profilePath)
+        options.app = mochitest.getFullPath(options.app)
+
+        if not os.path.exists(options.app):
+            msg = """\
+            Error: Path %(app)s doesn't exist.
+            Are you executing $objdir/_tests/testing/mochitest/runtests.py?"""
+            self.error(msg % {"app": options.app})
+            return None
+
+        if options.utilityPath:
+            options.utilityPath = mochitest.getFullPath(options.utilityPath)
+
+        if options.certPath:
+            options.certPath = mochitest.getFullPath(options.certPath)
+
+        if options.symbolsPath and not isURL(options.symbolsPath):
+            options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
+
+        options.webServer = self._automation.DEFAULT_WEB_SERVER
+        options.httpPort = self._automation.DEFAULT_HTTP_PORT
+        options.sslPort = self._automation.DEFAULT_SSL_PORT
+        options.webSocketPort = self._automation.DEFAULT_WEBSOCKET_PORT
+
+        if options.vmwareRecording:
+            if not self._automation.IS_WIN32:
+                self.error("use-vmware-recording is only supported on Windows.")
+            mochitest.vmwareHelperPath = os.path.join(
+                options.utilityPath, VMWARE_RECORDING_HELPER_BASENAME + ".dll")
+            if not os.path.exists(mochitest.vmwareHelperPath):
+                self.error("%s not found, cannot automate VMware recording." %
+                           mochitest.vmwareHelperPath)
+
+        if options.runOnlyTests != None and options.excludeTests != None:
+            self.error("We can only support --run-only-tests OR --exclude-tests, not both.  Please consider using --test-manifest instead.")
+
+        if options.testManifest != None and (options.runOnlyTests != None or options.excludeTests != None):
+            self.error("Please use --test-manifest only and not --run-only-tests or --exclude-tests.")
+
+        if options.runOnlyTests:
+            if not os.path.exists(os.path.abspath(options.runOnlyTests)):
+                self.error("unable to find --run-only-tests file '%s'" % options.runOnlyTests);
+            options.testManifest = options.runOnlyTests
+            options.runOnly = True
+
+        if options.excludeTests:
+            if not os.path.exists(os.path.abspath(options.excludeTests)):
+                self.error("unable to find --exclude-tests file '%s'" % options.excludeTests);
+            options.testManifest = options.excludeTests
+            options.runOnly = False
+
+        if options.webapprtContent and options.webapprtChrome:
+            self.error("Only one of --webapprt-content and --webapprt-chrome may be given.")
+
+        # Try to guess the testing modules directory.
+        # This somewhat grotesque hack allows the buildbot machines to find the
+        # modules directory without having to configure the buildbot hosts. This
+        # code should never be executed in local runs because the build system
+        # should always set the flag that populates this variable. If buildbot ever
+        # passes this argument, this code can be deleted.
+        if options.testingModulesDir is None:
+            possible = os.path.join(os.getcwd(), os.path.pardir, 'modules')
+
+            if os.path.isdir(possible):
+                options.testingModulesDir = possible
+
+        # Even if buildbot is updated, we still want this, as the path we pass in
+        # to the app must be absolute and have proper slashes.
+        if options.testingModulesDir is not None:
+            options.testingModulesDir = os.path.normpath(options.testingModulesDir)
+
+            if not os.path.isabs(options.testingModulesDir):
+                options.testingModulesDir = os.path.abspath(options.testingModulesDir)
+
+            if not os.path.isdir(options.testingModulesDir):
+                self.error('--testing-modules-dir not a directory: %s' %
+                    options.testingModulesDir)
+
+            options.testingModulesDir = options.testingModulesDir.replace('\\', '/')
+            if options.testingModulesDir[-1] != '/':
+                options.testingModulesDir += '/'
+
+        if options.immersiveMode:
+            if not self._automation.IS_WIN32:
+                self.error("immersive is only supported on Windows 8 and up.")
+            mochitest.immersiveHelperPath = os.path.join(
+                options.utilityPath, "metrotestharness.exe")
+            if not os.path.exists(mochitest.immersiveHelperPath):
+                self.error("%s not found, cannot launch immersive tests." %
+                           mochitest.immersiveHelperPath)
+
+        if options.runUntilFailure:
+            if not os.path.isfile(os.path.join(mochitest.oldcwd, os.path.dirname(__file__), mochitest.getTestRoot(options), options.testPath)):
+                self.error("--run-until-failure can only be used together with --test-path specifying a single test.")
+            if not options.repeat:
+                options.repeat = 29
+        return options
+
+
+class B2GOptions(MochitestOptions):
+    b2g_options = [
+        [["--b2gpath"],
+        { "action": "store",
+          "type": "string",
+          "dest": "b2gPath",
+          "help": "path to B2G repo or qemu dir",
+          "default": None,
+        }],
+        [["--desktop"],
+        { "action": "store_true",
+          "dest": "desktop",
+          "help": "Run the tests on a B2G desktop build",
+          "default": False,
+        }],
+        [["--marionette"],
+        { "action": "store",
+          "type": "string",
+          "dest": "marionette",
+          "help": "host:port to use when connecting to Marionette",
+          "default": None,
+        }],
+        [["--emulator"],
+        { "action": "store",
+          "type": "string",
+          "dest": "emulator",
+          "help": "Architecture of emulator to use: x86 or arm",
+          "default": None,
+        }],
+        [["--sdcard"],
+        { "action": "store",
+          "type": "string",
+          "dest": "sdcard",
+          "help": "Define size of sdcard: 1MB, 50MB...etc",
+          "default": "10MB",
+        }],
+        [["--no-window"],
+        { "action": "store_true",
+          "dest": "noWindow",
+          "help": "Pass --no-window to the emulator",
+          "default": False,
+        }],
+        [["--adbpath"],
+        { "action": "store",
+          "type": "string",
+          "dest": "adbPath",
+          "help": "path to adb",
+          "default": "adb",
+        }],
+        [["--deviceIP"],
+        { "action": "store",
+          "type": "string",
+          "dest": "deviceIP",
+          "help": "ip address of remote device to test",
+          "default": None,
+        }],
+        [["--devicePort"],
+        { "action": "store",
+          "type": "string",
+          "dest": "devicePort",
+          "help": "port of remote device to test",
+          "default": 20701,
+        }],
+        [["--remote-logfile"],
+        { "action": "store",
+          "type": "string",
+          "dest": "remoteLogFile",
+          "help": "Name of log file on the device relative to the device root. \
+                  PLEASE ONLY USE A FILENAME.",
+          "default" : None,
+        }],
+        [["--remote-webserver"],
+        { "action": "store",
+          "type": "string",
+          "dest": "remoteWebServer",
+          "help": "ip address where the remote web server is hosted at",
+          "default": None,
+        }],
+        [["--http-port"],
+        { "action": "store",
+          "type": "string",
+          "dest": "httpPort",
+          "help": "ip address where the remote web server is hosted at",
+          "default": None,
+        }],
+        [["--ssl-port"],
+        { "action": "store",
+          "type": "string",
+          "dest": "sslPort",
+          "help": "ip address where the remote web server is hosted at",
+          "default": None,
+        }],
+        [["--pidfile"],
+        { "action": "store",
+          "type": "string",
+          "dest": "pidFile",
+          "help": "name of the pidfile to generate",
+          "default": "",
+        }],
+        [["--gecko-path"],
+        { "action": "store",
+          "type": "string",
+          "dest": "geckoPath",
+          "help": "the path to a gecko distribution that should \
+                   be installed on the emulator prior to test",
+          "default": None,
+        }],
+        [["--profile"],
+        { "action": "store",
+          "type": "string",
+          "dest": "profile",
+          "help": "for desktop testing, the path to the \
+                   gaia profile to use",
+          "default": None,
+        }],
+        [["--logcat-dir"],
+        { "action": "store",
+          "type": "string",
+          "dest": "logcat_dir",
+          "help": "directory to store logcat dump files",
+          "default": None,
+        }],
+        [['--busybox'],
+        { "action": 'store',
+          "type": 'string',
+          "dest": 'busybox',
+          "help": "Path to busybox binary to install on device",
+          "default": None,
+        }],
+        [['--profile-data-dir'],
+        { "action": 'store',
+          "type": 'string',
+          "dest": 'profile_data_dir',
+          "help": "Path to a directory containing preference and other \
+                   data to be installed into the profile",
+          "default": os.path.join(here, 'profile_data'),
+        }],
+    ]
+
+    def __init__(self):
+        MochitestOptions.__init__(self)
+
+        for option in self.b2g_options:
+            self.add_option(*option[0], **option[1])
+
+        defaults = {}
+        defaults["httpPort"] = DEFAULT_PORTS['http']
+        defaults["sslPort"] = DEFAULT_PORTS['https']
+        defaults["remoteTestRoot"] = "/data/local/tests"
+        defaults["logFile"] = "mochitest.log"
+        defaults["autorun"] = True
+        defaults["closeWhenDone"] = True
+        defaults["testPath"] = ""
+        defaults["extensionsToExclude"] = ["specialpowers"]
+        self.set_defaults(**defaults)
+
+    def verifyRemoteOptions(self, options):
+        if options.remoteWebServer == None:
+            if os.name != "nt":
+                options.remoteWebServer = moznetwork.get_ip()
+            else:
+                self.error("You must specify a --remote-webserver=<ip address>")
+        options.webServer = options.remoteWebServer
+
+        if options.geckoPath and not options.emulator:
+            self.error("You must specify --emulator if you specify --gecko-path")
+
+        if options.logcat_dir and not options.emulator:
+            self.error("You must specify --emulator if you specify --logcat-dir")
+
+        if not os.path.isdir(options.xrePath):
+            self.error("--xre-path '%s' is not a directory" % options.xrePath)
+        xpcshell = os.path.join(options.xrePath, 'xpcshell')
+        if not os.access(xpcshell, os.F_OK):
+            self.error('xpcshell not found at %s' % xpcshell)
+        if self.elf_arm(xpcshell):
+            self.error('--xre-path points to an ARM version of xpcshell; it '
+                       'should instead point to a version that can run on '
+                       'your desktop')
+
+        if options.pidFile != "":
+            f = open(options.pidFile, 'w')
+            f.write("%s" % os.getpid())
+            f.close()
+
+        return options
+
+    def verifyOptions(self, options, mochitest):
+        # since we are reusing verifyOptions, it will exit if App is not found
+        temp = options.app
+        options.app = sys.argv[0]
+        tempPort = options.httpPort
+        tempSSL = options.sslPort
+        tempIP = options.webServer
+        options = MochitestOptions.verifyOptions(self, options, mochitest)
+        options.webServer = tempIP
+        options.app = temp
+        options.sslPort = tempSSL
+        options.httpPort = tempPort
+
+        return options
+
+    def elf_arm(self, filename):
+        data = open(filename, 'rb').read(20)
+        return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -3,474 +3,110 @@
 # 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/.
 
 """
 Runs the Mochitest test harness.
 """
 
 from __future__ import with_statement
-from datetime import datetime
 import optparse
 import os
 import os.path
 import sys
 import time
+import traceback
 
-SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
+SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
 sys.path.insert(0, SCRIPT_DIR);
 
 import shutil
 from urllib import quote_plus as encodeURIComponent
 import urllib2
-import commands
 from automation import Automation
-from automationutils import *
-import tempfile
-
-VMWARE_RECORDING_HELPER_BASENAME = "vmwarerecordinghelper"
-
-#######################
-# COMMANDLINE OPTIONS #
-#######################
-
-class MochitestOptions(optparse.OptionParser):
-  """Parses Mochitest commandline options."""
-  def __init__(self, automation, scriptdir, **kwargs):
-    self._automation = automation
-    optparse.OptionParser.__init__(self, **kwargs)
-    defaults = {}
-
-    # we want to pass down everything from self._automation.__all__
-    addCommonOptions(self, defaults=dict(zip(self._automation.__all__, 
-             [getattr(self._automation, x) for x in self._automation.__all__])))
-    self._automation.addCommonOptions(self)
-
-    self.add_option("--close-when-done",
-                    action = "store_true", dest = "closeWhenDone",
-                    help = "close the application when tests are done running")
-    defaults["closeWhenDone"] = False
-
-    self.add_option("--appname",
-                    action = "store", type = "string", dest = "app",
-                    help = "absolute path to application, overriding default")
-    defaults["app"] = os.path.join(scriptdir, self._automation.DEFAULT_APP)
-
-    self.add_option("--utility-path",
-                    action = "store", type = "string", dest = "utilityPath",
-                    help = "absolute path to directory containing utility programs (xpcshell, ssltunnel, certutil)")
-    defaults["utilityPath"] = self._automation.DIST_BIN
-
-    self.add_option("--certificate-path",
-                    action = "store", type = "string", dest = "certPath",
-                    help = "absolute path to directory containing certificate store to use testing profile")
-    defaults["certPath"] = self._automation.CERTS_SRC_DIR
-
-    self.add_option("--log-file",
-                    action = "store", type = "string",
-                    dest = "logFile", metavar = "FILE",
-                    help = "file to which logging occurs")
-    defaults["logFile"] = ""
-
-    self.add_option("--autorun",
-                    action = "store_true", dest = "autorun",
-                    help = "start running tests when the application starts")
-    defaults["autorun"] = False
-    
-    self.add_option("--timeout",
-                    type = "int", dest = "timeout",
-                    help = "per-test timeout in seconds")
-    defaults["timeout"] = None
-
-    self.add_option("--total-chunks",
-                    type = "int", dest = "totalChunks",
-                    help = "how many chunks to split the tests up into")
-    defaults["totalChunks"] = None
-
-    self.add_option("--this-chunk",
-                    type = "int", dest = "thisChunk",
-                    help = "which chunk to run")
-    defaults["thisChunk"] = None
-
-    self.add_option("--chunk-by-dir",
-                    type = "int", dest = "chunkByDir",
-                    help = "group tests together in the same chunk that are in the same top chunkByDir directories")
-    defaults["chunkByDir"] = 0
-
-    self.add_option("--shuffle",
-                    dest = "shuffle",
-                    action = "store_true",
-                    help = "randomize test order")
-    defaults["shuffle"] = False
-
-    LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "FATAL")
-    LEVEL_STRING = ", ".join(LOG_LEVELS)
-
-    self.add_option("--console-level",
-                    action = "store", type = "choice", dest = "consoleLevel",
-                    choices = LOG_LEVELS, metavar = "LEVEL",
-                    help = "one of %s to determine the level of console "
-                           "logging" % LEVEL_STRING)
-    defaults["consoleLevel"] = None
-
-    self.add_option("--file-level", 
-                    action = "store", type = "choice", dest = "fileLevel",
-                    choices = LOG_LEVELS, metavar = "LEVEL",
-                    help = "one of %s to determine the level of file "
-                           "logging if a file has been specified, defaulting "
-                           "to INFO" % LEVEL_STRING)
-    defaults["fileLevel"] = "INFO"
-
-    self.add_option("--chrome",
-                    action = "store_true", dest = "chrome",
-                    help = "run chrome Mochitests")
-    defaults["chrome"] = False
-
-    self.add_option("--ipcplugins",
-                    action = "store_true", dest = "ipcplugins",
-                    help = "run ipcplugins Mochitests")
-    defaults["ipcplugins"] = False
-
-    self.add_option("--test-path",
-                    action = "store", type = "string", dest = "testPath",
-                    help = "start in the given directory's tests")
-    defaults["testPath"] = ""
-
-    self.add_option("--browser-chrome",
-                    action = "store_true", dest = "browserChrome",
-                    help = "run browser chrome Mochitests")
-    defaults["browserChrome"] = False
-
-    self.add_option("--webapprt-content",
-                    action = "store_true", dest = "webapprtContent",
-                    help = "run WebappRT content tests")
-    defaults["webapprtContent"] = False
-
-    self.add_option("--webapprt-chrome",
-                    action = "store_true", dest = "webapprtChrome",
-                    help = "run WebappRT chrome tests")
-    defaults["webapprtChrome"] = False
-
-    self.add_option("--a11y",
-                    action = "store_true", dest = "a11y",
-                    help = "run accessibility Mochitests");
-    defaults["a11y"] = False
-
-    self.add_option("--setenv",
-                    action = "append", type = "string",
-                    dest = "environment", metavar = "NAME=VALUE",
-                    help = "sets the given variable in the application's "
-                           "environment")
-    defaults["environment"] = []
-
-    self.add_option("--exclude-extension",
-                    action = "append", type = "string",
-                    dest = "extensionsToExclude",
-                    help = "excludes the given extension from being installed "
-                           "in the test profile")
-    defaults["extensionsToExclude"] = []
-
-    self.add_option("--browser-arg",
-                    action = "append", type = "string",
-                    dest = "browserArgs", metavar = "ARG",
-                    help = "provides an argument to the test application")
-    defaults["browserArgs"] = []
-
-    self.add_option("--leak-threshold",
-                    action = "store", type = "int",
-                    dest = "leakThreshold", metavar = "THRESHOLD",
-                    help = "fail if the number of bytes leaked through "
-                           "refcounted objects (or bytes in classes with "
-                           "MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) is greater "
-                           "than the given number")
-    defaults["leakThreshold"] = 0
-
-    self.add_option("--fatal-assertions",
-                    action = "store_true", dest = "fatalAssertions",
-                    help = "abort testing whenever an assertion is hit "
-                           "(requires a debug build to be effective)")
-    defaults["fatalAssertions"] = False
-
-    self.add_option("--extra-profile-file",
-                    action = "append", dest = "extraProfileFiles",
-                    help = "copy specified files/dirs to testing profile")
-    defaults["extraProfileFiles"] = []
-
-    self.add_option("--install-extension",
-                    action = "append", dest = "extensionsToInstall",
-                    help = "install the specified extension in the testing profile."
-                           "The extension file's name should be <id>.xpi where <id> is"
-                           "the extension's id as indicated in its install.rdf."
-                           "An optional path can be specified too.")
-    defaults["extensionsToInstall"] = []
-
-    self.add_option("--profile-path", action = "store",
-                    type = "string", dest = "profilePath",
-                    help = "Directory where the profile will be stored."
-                           "This directory will be deleted after the tests are finished")
-    defaults["profilePath"] = tempfile.mkdtemp()
+from automationutils import getDebuggerInfo, isURL, processLeakLog
+from mochitest_options import MochitestOptions
 
-    self.add_option("--testing-modules-dir", action = "store",
-                    type = "string", dest = "testingModulesDir",
-                    help = "Directory where testing-only JS modules are "
-                           "located.")
-    defaults["testingModulesDir"] = None
-
-    self.add_option("--use-vmware-recording",
-                    action = "store_true", dest = "vmwareRecording",
-                    help = "enables recording while the application is running "
-                           "inside a VMware Workstation 7.0 or later VM")
-    defaults["vmwareRecording"] = False
-
-    self.add_option("--repeat",
-                    action = "store", type = "int",
-                    dest = "repeat", metavar = "REPEAT",
-                    help = "repeats the test or set of tests the given number of times, ie: repeat=1 will run the test twice.")                   
-    defaults["repeat"] = 0
-
-    self.add_option("--run-until-failure",
-                    action = "store_true", dest="runUntilFailure",
-                    help = "Run a test repeatedly and stops on the first time the test fails. "
-                           "Only available when running a single test. Default cap is 30 runs, "
-                           "which can be overwritten with the --repeat parameter.")
-    defaults["runUntilFailure"] = False
-
-    self.add_option("--run-only-tests",
-                    action = "store", type="string", dest = "runOnlyTests",
-                    help = "JSON list of tests that we only want to run, cannot be specified with --exclude-tests. [DEPRECATED- please use --test-manifest]")
-    defaults["runOnlyTests"] = None
-
-    self.add_option("--exclude-tests",
-                    action = "store", type="string", dest = "excludeTests",
-                    help = "JSON list of tests that we want to not run, cannot be specified with --run-only-tests. [DEPRECATED- please use --test-manifest]")
-    defaults["excludeTests"] = None
-
-    self.add_option("--test-manifest",
-                    action = "store", type="string", dest = "testManifest",
-                    help = "JSON list of tests to specify 'runtests' and 'excludetests'.")
-    defaults["testManifest"] = None
-
-    self.add_option("--failure-file",
-                    action = "store", type="string", dest = "failureFile",
-                    help = "Filename of the output file where we can store a .json list of failures to be run in the future with --run-only-tests.")
-    defaults["failureFile"] = None
-
-    self.add_option("--run-slower",
-                    action = "store_true", dest = "runSlower",
-                    help = "Delay execution between test files.")
-    defaults["runSlower"] = False
-
-    self.add_option("--metro-immersive",
-                    action = "store_true", dest = "immersiveMode",
-                    help = "launches tests in immersive browser")
-    defaults["immersiveMode"] = False
-
-    self.add_option("--httpd-path", action = "store",
-                    type = "string", dest = "httpdPath",
-                    help = "path to the httpd.js file")
-    defaults["httpdPath"] = None
-
-    # -h, --help are automatically handled by OptionParser
-
-    self.set_defaults(**defaults)
-
-    usage = """\
-Usage instructions for runtests.py.
-All arguments are optional.
-If --chrome is specified, chrome tests will be run instead of web content tests.
-If --browser-chrome is specified, browser-chrome tests will be run instead of web content tests.
-See <http://mochikit.com/doc/html/MochiKit/Logging.html> for details on the logging levels."""
-    self.set_usage(usage)
-
-  def verifyOptions(self, options, mochitest):
-    """ verify correct options and cleanup paths """
-
-    if options.totalChunks is not None and options.thisChunk is None:
-      self.error("thisChunk must be specified when totalChunks is specified")
-
-    if options.totalChunks:
-      if not 1 <= options.thisChunk <= options.totalChunks:
-        self.error("thisChunk must be between 1 and totalChunks")
-
-    if options.xrePath is None:
-      # default xrePath to the app path if not provided
-      # but only if an app path was explicitly provided
-      if options.app != self.defaults['app']:
-        options.xrePath = os.path.dirname(options.app)
-      else:
-        # otherwise default to dist/bin
-        options.xrePath = self._automation.DIST_BIN
-
-    # allow relative paths
-    options.xrePath = mochitest.getFullPath(options.xrePath)
+import mozinfo
+import mozlog
 
-    options.profilePath = mochitest.getFullPath(options.profilePath)
-
-    options.app = mochitest.getFullPath(options.app)
-    if not os.path.exists(options.app):
-      msg = """\
-      Error: Path %(app)s doesn't exist.
-      Are you executing $objdir/_tests/testing/mochitest/runtests.py?"""
-      print msg % {"app": options.app}
-      return None
-
-    options.utilityPath = mochitest.getFullPath(options.utilityPath)
-    options.certPath = mochitest.getFullPath(options.certPath)
-    if options.symbolsPath and not isURL(options.symbolsPath):
-      options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
-
-    options.webServer = self._automation.DEFAULT_WEB_SERVER
-    options.httpPort = self._automation.DEFAULT_HTTP_PORT
-    options.sslPort = self._automation.DEFAULT_SSL_PORT
-    options.webSocketPort = self._automation.DEFAULT_WEBSOCKET_PORT
-
-    if options.vmwareRecording:
-      if not self._automation.IS_WIN32:
-        self.error("use-vmware-recording is only supported on Windows.")
-      mochitest.vmwareHelperPath = os.path.join(
-        options.utilityPath, VMWARE_RECORDING_HELPER_BASENAME + ".dll")
-      if not os.path.exists(mochitest.vmwareHelperPath):
-        self.error("%s not found, cannot automate VMware recording." %
-                   mochitest.vmwareHelperPath)
-
-    if options.runOnlyTests != None and options.excludeTests != None:
-      self.error("We can only support --run-only-tests OR --exclude-tests, not both.  Please consider using --test-manifest instead.")
-
-    if options.testManifest != None and (options.runOnlyTests != None or options.excludeTests != None):
-      self.error("Please use --test-manifest only and not --run-only-tests or --exclude-tests.")
-      
-    if options.runOnlyTests:
-      if not os.path.exists(os.path.abspath(options.runOnlyTests)):
-        self.error("unable to find --run-only-tests file '%s'" % options.runOnlyTests);
-      options.testManifest = options.runOnlyTests
-      options.runOnly = True
-        
-    if options.excludeTests:
-      if not os.path.exists(os.path.abspath(options.excludeTests)):
-        self.error("unable to find --exclude-tests file '%s'" % options.excludeTests);
-      options.testManifest = options.excludeTests
-      options.runOnly = False
-
-    if options.webapprtContent and options.webapprtChrome:
-      self.error("Only one of --webapprt-content and --webapprt-chrome may be given.")
-
-    # Try to guess the testing modules directory.
-    # This somewhat grotesque hack allows the buildbot machines to find the
-    # modules directory without having to configure the buildbot hosts. This
-    # code should never be executed in local runs because the build system
-    # should always set the flag that populates this variable. If buildbot ever
-    # passes this argument, this code can be deleted.
-    if options.testingModulesDir is None:
-      possible = os.path.join(os.getcwd(), os.path.pardir, 'modules')
-
-      if os.path.isdir(possible):
-        options.testingModulesDir = possible
-
-    # Even if buildbot is updated, we still want this, as the path we pass in
-    # to the app must be absolute and have proper slashes.
-    if options.testingModulesDir is not None:
-      options.testingModulesDir = os.path.normpath(options.testingModulesDir)
-
-      if not os.path.isabs(options.testingModulesDir):
-        options.testingModulesDir = os.path.abspath(options.testingModulesDir)
-
-      if not os.path.isdir(options.testingModulesDir):
-        self.error('--testing-modules-dir not a directory: %s' %
-          options.testingModulesDir)
-
-      options.testingModulesDir = options.testingModulesDir.replace('\\', '/')
-      if options.testingModulesDir[-1] != '/':
-        options.testingModulesDir += '/'
-
-    if options.immersiveMode:
-      if not self._automation.IS_WIN32:
-        self.error("immersive is only supported on Windows 8 and up.")
-      mochitest.immersiveHelperPath = os.path.join(
-        options.utilityPath, "metrotestharness.exe")
-      if not os.path.exists(mochitest.immersiveHelperPath):
-        self.error("%s not found, cannot launch immersive tests." %
-                   mochitest.immersiveHelperPath)
-
-    if options.runUntilFailure:
-      if not os.path.isfile(os.path.join(mochitest.oldcwd, os.path.dirname(__file__), mochitest.getTestRoot(options), options.testPath)):
-        self.error("--run-until-failure can only be used together with --test-path specifying a single test.")
-      if not options.repeat:
-        options.repeat = 29
-
-    return options
+log = mozlog.getLogger('Mochitest')
 
 
 #######################
 # HTTP SERVER SUPPORT #
 #######################
 
 class MochitestServer:
   "Web server used to serve Mochitests, for closer fidelity to the real web."
 
   def __init__(self, automation, options):
-    self._automation = automation
-    self._closeWhenDone = options.closeWhenDone
-    self._utilityPath = options.utilityPath
-    self._xrePath = options.xrePath
-    self._profileDir = options.profilePath
-    self.webServer = options.webServer
-    self.httpPort = options.httpPort
+    if isinstance(options, optparse.Values):
+      options = vars(options)
+    self._automation = automation or Automation()
+    self._closeWhenDone = options['closeWhenDone']
+    self._utilityPath = options['utilityPath']
+    self._xrePath = options['xrePath']
+    self._profileDir = options['profilePath']
+    self.webServer = options['webServer']
+    self.httpPort = options['httpPort']
     self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server" : self.webServer, "port" : self.httpPort }
-    self.testPrefix = "'webapprt_'" if options.webapprtContent else "undefined"
-    if options.httpdPath:
-        self._httpdPath = options.httpdPath
+    self.testPrefix = "'webapprt_'" if options.get('webapprtContent') else "undefined"
+
+    if options.get('httpdPath'):
+        self._httpdPath = options['httpdPath']
     else:
         self._httpdPath = '.'
     self._httpdPath = os.path.abspath(self._httpdPath)
 
   def start(self):
     "Run the Mochitest server, returning the process ID of the server."
-    
+
     env = self._automation.environment(xrePath = self._xrePath)
     env["XPCOM_DEBUG_BREAK"] = "warn"
 
     # When running with an ASan build, our xpcshell server will also be ASan-enabled,
     # thus consuming too much resources when running together with the browser on
     # the test slaves. Try to limit the amount of resources by disabling certain
     # features.
     env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32"
 
-    if self._automation.IS_WIN32:
+    if mozinfo.isWin:
       env["PATH"] = env["PATH"] + ";" + self._xrePath
 
     args = ["-g", self._xrePath,
             "-v", "170",
             "-f", self._httpdPath + "/httpd.js",
             "-e", """const _PROFILE_PATH = '%(profile)s';const _SERVER_PORT = '%(port)s'; const _SERVER_ADDR = '%(server)s';
                      const _TEST_PREFIX = %(testPrefix)s; const _DISPLAY_RESULTS = %(displayResults)s;""" %
                    {"profile" : self._profileDir.replace('\\', '\\\\'), "port" : self.httpPort, "server" : self.webServer,
                     "testPrefix" : self.testPrefix, "displayResults" : str(not self._closeWhenDone).lower() },
             "-f", "./" + "server.js"]
 
     xpcshell = os.path.join(self._utilityPath,
-                            "xpcshell" + self._automation.BIN_SUFFIX)
+                            "xpcshell" + mozinfo.info['bin_suffix'])
     self._process = self._automation.Process([xpcshell] + args, env = env)
     pid = self._process.pid
     if pid < 0:
-      print "Error starting server."
+      log.error("Error starting server.")
       sys.exit(2)
-    self._automation.log.info("INFO | runtests.py | Server pid: %d", pid)
+    log.info("runtests.py | Server pid: %d", pid)
 
   def ensureReady(self, timeout):
     assert timeout >= 0
 
     aliveFile = os.path.join(self._profileDir, "server_alive.txt")
     i = 0
     while i < timeout:
       if os.path.exists(aliveFile):
         break
       time.sleep(1)
       i += 1
     else:
-      print "Timed out while waiting for server startup."
+      log.error("Timed out while waiting for server startup.")
       self.stop()
       sys.exit(1)
 
   def stop(self):
     try:
       with urllib2.urlopen(self.shutdownURL) as c:
         c.read()
 
@@ -506,160 +142,71 @@ class WebSocketServer(object):
         cmd += ['--interactive']
     cmd += ['-p', str(self.port), '-w', self._scriptdir, '-l',      \
            os.path.join(self._scriptdir, "websock.log"),            \
            '--log-level=debug', '--allow-handlers-outside-root-dir']
 
     self._process = self._automation.Process(cmd)
     pid = self._process.pid
     if pid < 0:
-      print "Error starting websocket server."
+      log.error("Error starting websocket server.")
       sys.exit(2)
-    self._automation.log.info("INFO | runtests.py | Websocket server pid: %d", pid)
+    log.info("runtests.py | Websocket server pid: %d", pid)
 
   def stop(self):
     self._process.kill()
 
-class Mochitest(object):
+class MochitestUtilsMixin(object):
+  """
+  Class containing some utility functions common to both local and remote
+  mochitest runners
+  """
+
+  # TODO Utility classes are a code smell. This class is temporary
+  #      and should be removed when desktop mochitests are refactored
+  #      on top of mozbase. Each of the functions in here should
+  #      probably live somewhere in mozbase
+
+  oldcwd = os.getcwd()
+  jarDir = 'mochijar'
+
   # Path to the test script on the server
   TEST_PATH = "tests"
   CHROME_PATH = "redirect.html"
   urlOpts = []
-  runSSLTunnel = True
-  vmwareHelper = None
 
-  oldcwd = os.getcwd()
-
-  def __init__(self, automation):
-    self.automation = automation
-
-    # Max time in seconds to wait for server startup before tests will fail -- if
-    # this seems big, it's mostly for debug machines where cold startup
-    # (particularly after a build) takes forever.
-    if self.automation.IS_DEBUG_BUILD:
-      self.SERVER_STARTUP_TIMEOUT = 180
-    else:
-      self.SERVER_STARTUP_TIMEOUT = 90
-
-    self.SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
-    os.chdir(self.SCRIPT_DIRECTORY)
+  def __init__(self):
+    os.chdir(SCRIPT_DIR)
+    mozinfo.find_and_update_from_json(SCRIPT_DIR)
 
   def getFullPath(self, path):
     " Get an absolute path relative to self.oldcwd."
     return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
 
-  def buildTestPath(self, options):
-    """ Build the url path to the specific test harness and test file or directory """
-    testHost = "http://mochi.test:8888"
-    testURL = ("/").join([testHost, self.TEST_PATH, options.testPath])
-    if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, options.testPath)) and options.repeat > 0:
-       testURL = ("/").join([testHost, self.TEST_PATH, os.path.dirname(options.testPath)])
-    if options.chrome or options.a11y:
-       testURL = ("/").join([testHost, self.CHROME_PATH])
-    elif options.browserChrome:
-      testURL = "about:blank"
-    elif options.ipcplugins:
-      testURL = ("/").join([testHost, self.TEST_PATH, "dom/plugins/test"])
-    return testURL
-
-  def startWebSocketServer(self, options, debuggerInfo):
-    """ Launch the websocket server """
-    if options.webServer != '127.0.0.1':
-      return
-
-    self.wsserver = WebSocketServer(self.automation, options,
-                                    self.SCRIPT_DIRECTORY, debuggerInfo)
-    self.wsserver.start()
-
-  def stopWebSocketServer(self, options):
-    if options.webServer != '127.0.0.1':
-      return
-
-    self.wsserver.stop()
-
-  def startWebServer(self, options):
-    if options.webServer != '127.0.0.1':
-      return
-
-    """ Create the webserver and start it up """
-    self.server = MochitestServer(self.automation, options)
-    self.server.start()
-
-    # If we're lucky, the server has fully started by now, and all paths are
-    # ready, etc.  However, xpcshell cold start times suck, at least for debug
-    # builds.  We'll try to connect to the server for awhile, and if we fail,
-    # we'll try to kill the server and exit with an error.
-    self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
-
-  def stopWebServer(self, options):
-    """ Server's no longer needed, and perhaps more importantly, anything it might
-        spew to console shouldn't disrupt the leak information table we print next.
-    """
-    if options.webServer != '127.0.0.1':
-      return
-
-    self.server.stop()
-
   def getLogFilePath(self, logFile):
-    """ return the log file path relative to the device we are testing on, in most cases 
+    """ return the log file path relative to the device we are testing on, in most cases
         it will be the full path on the local system
     """
     return self.getFullPath(logFile)
 
-  def buildProfile(self, options):
-    """ create the profile and add optional chrome bits and files if requested """
-    if options.browserChrome and options.timeout:
-      options.extraPrefs.append("testing.browserTestHarness.timeout=%d" % options.timeout)
-    self.automation.initializeProfile(options.profilePath,
-                                      options.extraPrefs,
-                                      useServerLocations=True,
-                                      prefsPath=os.path.join(self.SCRIPT_DIRECTORY,
-                                                        'profile_data', 'prefs_general.js'))
-    manifest = self.addChromeToProfile(options)
-    self.copyExtraFilesToProfile(options)
-    self.installExtensionsToProfile(options)
-    return manifest
-
-  def buildBrowserEnv(self, options):
-    """ build the environment variables for the specific test and operating system """
-    browserEnv = self.automation.environment(xrePath = options.xrePath)
-
-    # These variables are necessary for correct application startup; change
-    # via the commandline at your own risk.
-    browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
-
-    for v in options.environment:
-      ix = v.find("=")
-      if ix <= 0:
-        print "Error: syntax error in --setenv=" + v
-        return None
-      browserEnv[v[:ix]] = v[ix + 1:]
-
-    browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
-
-    if options.fatalAssertions:
-      browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
-
-    return browserEnv
-
   def buildURLOptions(self, options, env):
-    """ Add test control options from the command line to the url 
+    """ Add test control options from the command line to the url
 
         URL parameters to test URL:
 
         autorun -- kick off tests automatically
         closeWhenDone -- closes the browser after the tests
         hideResultsTable -- hides the table of individual test results
         logFile -- logs test run to an absolute path
         totalChunks -- how many chunks to split tests into
         thisChunk -- which chunk to run
         timeout -- per-test timeout in seconds
         repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice.
     """
-  
+
     # allow relative paths for logFile
     if options.logFile:
       options.logFile = self.getLogFilePath(options.logFile)
     if options.browserChrome or options.chrome or options.a11y or options.webapprtChrome:
       self.makeTestConfig(options)
     else:
       if options.autorun:
         self.urlOpts.append("autorun=1")
@@ -693,49 +240,267 @@ class Mochitest(object):
           self.urlOpts.append("runOnly=true")
         else:
           self.urlOpts.append("runOnly=false")
       if options.failureFile:
         self.urlOpts.append("failureFile=%s" % self.getFullPath(options.failureFile))
       if options.runSlower:
         self.urlOpts.append("runSlower=true")
 
+  def buildTestPath(self, options):
+    """ Build the url path to the specific test harness and test file or directory """
+    testHost = "http://mochi.test:8888"
+    testURL = ("/").join([testHost, self.TEST_PATH, options.testPath])
+    if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, options.testPath)) and options.repeat > 0:
+       testURL = ("/").join([testHost, self.PLAIN_LOOP_PATH])
+    if options.chrome or options.a11y:
+       testURL = ("/").join([testHost, self.CHROME_PATH])
+    elif options.browserChrome:
+      testURL = "about:blank"
+    elif options.ipcplugins:
+      testURL = ("/").join([testHost, self.TEST_PATH, "dom/plugins/test"])
+    return testURL
+
+  def startWebSocketServer(self, options, debuggerInfo):
+    """ Launch the websocket server """
+    if options.webServer != '127.0.0.1':
+      return
+
+    self.wsserver = WebSocketServer(self.automation, options,
+                                    SCRIPT_DIR, debuggerInfo)
+    self.wsserver.start()
+
+  def stopWebSocketServer(self, options):
+    if options.webServer != '127.0.0.1':
+      return
+
+    self.wsserver.stop()
+
+  def startWebServer(self, options):
+    if options.webServer != '127.0.0.1':
+      return
+
+    """ Create the webserver and start it up """
+    self.server = MochitestServer(self.automation, options)
+    self.server.start()
+
+    # If we're lucky, the server has fully started by now, and all paths are
+    # ready, etc.  However, xpcshell cold start times suck, at least for debug
+    # builds.  We'll try to connect to the server for awhile, and if we fail,
+    # we'll try to kill the server and exit with an error.
+    self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
+
+  def stopWebServer(self, options):
+    """ Server's no longer needed, and perhaps more importantly, anything it might
+        spew to console shouldn't disrupt the leak information table we print next.
+    """
+    if options.webServer != '127.0.0.1':
+      return
+
+    self.server.stop()
+
+
+  def copyExtraFilesToProfile(self, options):
+    "Copy extra files or dirs specified on the command line to the testing profile."
+    for f in options.extraProfileFiles:
+      abspath = self.getFullPath(f)
+      if os.path.isfile(abspath):
+        shutil.copy2(abspath, options.profilePath)
+      elif os.path.isdir(abspath):
+        dest = os.path.join(options.profilePath, os.path.basename(abspath))
+        shutil.copytree(abspath, dest)
+      else:
+        log.warning("runtests.py | Failed to copy %s to profile", abspath)
+        continue
+
+  def copyTestsJarToProfile(self, options):
+    """ copy tests.jar to the profile directory so we can auto register it in the .xul harness """
+    testsJarFile = os.path.join(SCRIPT_DIR, "tests.jar")
+    if not os.path.isfile(testsJarFile):
+      return False
+
+    shutil.copy2(testsJarFile, options.profilePath)
+    return True
+
+  def installChromeJar(self, chrome, options):
+    """
+      copy mochijar directory to profile as an extension so we have chrome://mochikit for all harness code
+    """
+    # Write chrome.manifest.
+    with open(os.path.join(options.profilePath, "extensions", "staged", "mochikit@mozilla.org", "chrome.manifest"), "a") as mfile:
+      mfile.write(chrome)
+
+  def addChromeToProfile(self, options):
+    "Adds MochiKit chrome tests to the profile."
+
+    # Create (empty) chrome directory.
+    chromedir = os.path.join(options.profilePath, "chrome")
+    os.mkdir(chromedir)
+
+    # Write userChrome.css.
+    chrome = """
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */
+toolbar,
+toolbarpalette {
+  background-color: rgb(235, 235, 235) !important;
+}
+toolbar#nav-bar {
+  background-image: none !important;
+}
+"""
+    with open(os.path.join(options.profilePath, "userChrome.css"), "a") as chromeFile:
+      chromeFile.write(chrome)
+
+    # Call copyTestsJarToProfile(), Write tests.manifest.
+    manifest = os.path.join(options.profilePath, "tests.manifest")
+    with open(manifest, "w") as manifestFile:
+      if self.copyTestsJarToProfile(options):
+        # Register tests.jar.
+        manifestFile.write("content mochitests jar:tests.jar!/content/\n");
+      else:
+        # Register chrome directory.
+        chrometestDir = os.path.abspath(".") + "/"
+        if self.automation.IS_WIN32:
+          chrometestDir = "file:///" + chrometestDir.replace("\\", "/")
+        manifestFile.write("content mochitests %s contentaccessible=yes\n" % chrometestDir)
+
+      if options.testingModulesDir is not None:
+        manifestFile.write("resource testing-common file:///%s\n" %
+          options.testingModulesDir)
+
+    # Call installChromeJar().
+    if not os.path.isdir(os.path.join(SCRIPT_DIR, self.jarDir)):
+      log.testFail("invalid setup: missing mochikit extension")
+      return None
+
+    # Support Firefox (browser), B2G (shell), SeaMonkey (navigator), and Webapp
+    # Runtime (webapp).
+    chrome = ""
+    if options.browserChrome or options.chrome or options.a11y or options.webapprtChrome:
+      chrome += """
+overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul
+overlay chrome://browser/content/shell.xul chrome://mochikit/content/browser-test-overlay.xul
+overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul
+overlay chrome://webapprt/content/webapp.xul chrome://mochikit/content/browser-test-overlay.xul
+"""
+
+    self.installChromeJar(chrome, options)
+    return manifest
+
+
+  def getExtensionsToInstall(self, options):
+    "Return a list of extensions to install in the profile"
+    extensions = options.extensionsToInstall or []
+    appDir = options.app[:options.app.rfind(os.sep)] if options.app else options.utilityPath
+
+    extensionDirs = [
+      # Extensions distributed with the test harness.
+      os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")),
+    ]
+    if appDir:
+      # Extensions distributed with the application.
+      extensionDirs.append(os.path.join(appDir, "distribution", "extensions"))
+
+    for extensionDir in extensionDirs:
+      if os.path.isdir(extensionDir):
+        for dirEntry in os.listdir(extensionDir):
+          if dirEntry not in options.extensionsToExclude:
+            path = os.path.join(extensionDir, dirEntry)
+            if os.path.isdir(path) or (os.path.isfile(path) and path.endswith(".xpi")):
+              extensions.append(path)
+
+    # append mochikit
+    extensions.append(os.path.join(SCRIPT_DIR, self.jarDir))
+    return extensions
+
+
+class Mochitest(MochitestUtilsMixin):
+  runSSLTunnel = True
+  vmwareHelper = None
+
+  def __init__(self, automation=None):
+    super(Mochitest, self).__init__()
+    self.automation = automation or Automation()
+
+    # Max time in seconds to wait for server startup before tests will fail -- if
+    # this seems big, it's mostly for debug machines where cold startup
+    # (particularly after a build) takes forever.
+    if self.automation.IS_DEBUG_BUILD:
+      self.SERVER_STARTUP_TIMEOUT = 180
+    else:
+      self.SERVER_STARTUP_TIMEOUT = 90
+
+  def buildProfile(self, options):
+    """ create the profile and add optional chrome bits and files if requested """
+    if options.browserChrome and options.timeout:
+      options.extraPrefs.append("testing.browserTestHarness.timeout=%d" % options.timeout)
+    self.automation.initializeProfile(options.profilePath,
+                                      options.extraPrefs,
+                                      useServerLocations=True,
+                                      prefsPath=os.path.join(SCRIPT_DIR,
+                                                        'profile_data', 'prefs_general.js'))
+    self.copyExtraFilesToProfile(options)
+    self.installExtensionsToProfile(options)
+    return self.addChromeToProfile(options)
+
+  def buildBrowserEnv(self, options):
+    """ build the environment variables for the specific test and operating system """
+    browserEnv = self.automation.environment(xrePath = options.xrePath)
+
+    # These variables are necessary for correct application startup; change
+    # via the commandline at your own risk.
+    browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
+
+    for v in options.environment:
+      ix = v.find("=")
+      if ix <= 0:
+        log.error("Error: syntax error in --setenv=" + v)
+        return None
+      browserEnv[v[:ix]] = v[ix + 1:]
+
+    browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
+
+    if options.fatalAssertions:
+      browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
+
+    return browserEnv
+
   def cleanup(self, manifest, options):
     """ remove temporary files and profile """
     os.remove(manifest)
     shutil.rmtree(options.profilePath)
 
   def startVMwareRecording(self, options):
     """ starts recording inside VMware VM using the recording helper dll """
     assert(self.automation.IS_WIN32)
     from ctypes import cdll
     self.vmwareHelper = cdll.LoadLibrary(self.vmwareHelperPath)
     if self.vmwareHelper is None:
-      self.automation.log.warning("WARNING | runtests.py | Failed to load "
-                                  "VMware recording helper")
+      log.warning("runtests.py | Failed to load "
+                   "VMware recording helper")
       return
-    self.automation.log.info("INFO | runtests.py | Starting VMware recording.")
+    log.info("runtests.py | Starting VMware recording.")
     try:
       self.vmwareHelper.StartRecording()
     except Exception, e:
-      self.automation.log.warning("WARNING | runtests.py | Failed to start "
-                                  "VMware recording: (%s)" % str(e))
+      log.warning("runtests.py | Failed to start "
+                  "VMware recording: (%s)" % str(e))
       self.vmwareHelper = None
 
   def stopVMwareRecording(self):
     """ stops recording inside VMware VM using the recording helper dll """
     assert(self.automation.IS_WIN32)
     if self.vmwareHelper is not None:
-      self.automation.log.info("INFO | runtests.py | Stopping VMware "
-                               "recording.")
+      log.info("runtests.py | Stopping VMware "
+               "recording.")
       try:
         self.vmwareHelper.StopRecording()
       except Exception, e:
-        self.automation.log.warning("WARNING | runtests.py | Failed to stop "
-                                    "VMware recording: (%s)" % str(e))
+        log.warning("runtests.py | Failed to stop "
+                    "VMware recording: (%s)" % str(e))
       self.vmwareHelper = None
 
   def runTests(self, options, onLaunch=None):
     """ Prepare, configure, run tests and cleanup """
     debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs,
                       options.debuggerInteractive);
 
     self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
@@ -775,43 +540,44 @@ class Mochitest(object):
     elif options.debugger or not options.autorun:
       timeout = None
     else:
       timeout = 330.0 # default JS harness timeout is 300 seconds
 
     if options.vmwareRecording:
       self.startVMwareRecording(options);
 
-    self.automation.log.info("INFO | runtests.py | Running tests: start.\n")
+    log.info("runtests.py | Running tests: start.\n")
     try:
       status = self.automation.runApp(testURL, browserEnv, options.app,
                                   options.profilePath, options.browserArgs,
                                   runSSLTunnel=self.runSSLTunnel,
                                   utilityPath=options.utilityPath,
                                   xrePath=options.xrePath,
                                   certPath=options.certPath,
                                   debuggerInfo=debuggerInfo,
                                   symbolsPath=options.symbolsPath,
                                   timeout=timeout,
                                   onLaunch=onLaunch)
     except KeyboardInterrupt:
-      self.automation.log.info("INFO | runtests.py | Received keyboard interrupt.\n");
+      log.info("runtests.py | Received keyboard interrupt.\n");
       status = -1
     except:
-      self.automation.log.exception("INFO | runtests.py | Received unexpected exception while running application\n")
+      traceback.print_exc()
+      log.error("runtests.py | Received unexpected exception while running application\n")
       status = 1
 
     if options.vmwareRecording:
       self.stopVMwareRecording();
 
     self.stopWebServer(options)
     self.stopWebSocketServer(options)
     processLeakLog(self.leak_report_file, options.leakThreshold)
 
-    self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
+    log.info("runtests.py | Running tests: end.")
 
     if manifest is not None:
       self.cleanup(manifest, options)
     return status
 
   def makeTestConfig(self, options):
     "Creates a test configuration file for customizing test execution."
     def jsonString(val):
@@ -844,17 +610,17 @@ class Mochitest(object):
     options.testPath = options.testPath.replace("\\", "\\\\")
     testRoot = self.getTestRoot(options)
 
     if "MOZ_HIDE_RESULTS_TABLE" in os.environ and os.environ["MOZ_HIDE_RESULTS_TABLE"] == "1":
       options.hideResultsTable = True
 
     #TODO: when we upgrade to python 2.6, just use json.dumps(options.__dict__)
     content = "{"
-    content += '"testRoot": "%s", ' % (testRoot) 
+    content += '"testRoot": "%s", ' % (testRoot)
     first = True
     for opt in options.__dict__.keys():
       val = options.__dict__[opt]
       if first:
         first = False
       else:
         content += ", "
 
@@ -873,154 +639,44 @@ class Mochitest(object):
     elif (options.a11y):
       return 'a11y'
     elif (options.webapprtChrome):
       return 'webapprtChrome'
     elif (options.chrome):
       return 'chrome'
     return self.TEST_PATH
 
-  def addChromeToProfile(self, options):
-    "Adds MochiKit chrome tests to the profile."
-
-    # Create (empty) chrome directory.
-    chromedir = os.path.join(options.profilePath, "chrome")
-    os.mkdir(chromedir)
-
-    # Write userChrome.css.
-    chrome = """
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */
-toolbar,
-toolbarpalette {
-  background-color: rgb(235, 235, 235) !important;
-}
-toolbar#nav-bar {
-  background-image: none !important;
-}
-"""
-    with open(os.path.join(options.profilePath, "userChrome.css"), "a") as chromeFile:
-      chromeFile.write(chrome)
-
-    # Call copyTestsJarToProfile(), Write tests.manifest.
-    manifest = os.path.join(options.profilePath, "tests.manifest")
-    with open(manifest, "w") as manifestFile:
-      if self.copyTestsJarToProfile(options):
-        # Register tests.jar.
-        manifestFile.write("content mochitests jar:tests.jar!/content/\n");
-      else:
-        # Register chrome directory.
-        chrometestDir = os.path.abspath(".") + "/"
-        if self.automation.IS_WIN32:
-          chrometestDir = "file:///" + chrometestDir.replace("\\", "/")
-        manifestFile.write("content mochitests %s contentaccessible=yes\n" % chrometestDir)
-
-      if options.testingModulesDir is not None:
-        manifestFile.write("resource testing-common file:///%s\n" %
-          options.testingModulesDir)
-
-    # Call installChromeJar().
-    jarDir = "mochijar"
-    if not os.path.isdir(os.path.join(self.SCRIPT_DIRECTORY, jarDir)):
-      self.automation.log.warning("TEST-UNEXPECTED-FAIL | invalid setup: missing mochikit extension")
-      return None
-
-    # Support Firefox (browser), B2G (shell), SeaMonkey (navigator), and Webapp
-    # Runtime (webapp).
-    chrome = ""
-    if options.browserChrome or options.chrome or options.a11y or options.webapprtChrome:
-      chrome += """
-overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul
-overlay chrome://browser/content/shell.xul chrome://mochikit/content/browser-test-overlay.xul
-overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul
-overlay chrome://webapprt/content/webapp.xul chrome://mochikit/content/browser-test-overlay.xul
-"""
-
-    self.installChromeJar(jarDir, chrome, options)
-    return manifest
-
-  def installChromeJar(self, jarDirName, chrome, options):
-    """
-      copy mochijar directory to profile as an extension so we have chrome://mochikit for all harness code
-    """
-    self.automation.installExtension(os.path.join(self.SCRIPT_DIRECTORY, jarDirName), \
-                                     options.profilePath, "mochikit@mozilla.org")
-
-    # Write chrome.manifest.
-    with open(os.path.join(options.profilePath, "extensions", "staged", "mochikit@mozilla.org", "chrome.manifest"), "a") as mfile:
-      mfile.write(chrome)
-
-  def copyTestsJarToProfile(self, options):
-    """ copy tests.jar to the profile directory so we can auto register it in the .xul harness """
-    testsJarFile = os.path.join(self.SCRIPT_DIRECTORY, "tests.jar")
-    if not os.path.isfile(testsJarFile):
-      return False
-
-    shutil.copy2(testsJarFile, options.profilePath)
-    return True
-
-  def copyExtraFilesToProfile(self, options):
-    "Copy extra files or dirs specified on the command line to the testing profile."
-    for f in options.extraProfileFiles:
-      abspath = self.getFullPath(f)
-      if os.path.isfile(abspath):
-        shutil.copy2(abspath, options.profilePath)
-      elif os.path.isdir(abspath):
-        dest = os.path.join(options.profilePath, os.path.basename(abspath))
-        shutil.copytree(abspath, dest)
-      else:
-        self.automation.log.warning("WARNING | runtests.py | Failed to copy %s to profile", abspath)
-        continue
-
-  def getExtensionsToInstall(self, options):
-    "Return a list of extensions to install in the profile"
-    extensions = options.extensionsToInstall or []
-    extensionDirs = [
-      # Extensions distributed with the test harness.
-      os.path.normpath(os.path.join(self.SCRIPT_DIRECTORY, "extensions")),
-      # Extensions distributed with the application.
-      os.path.join(options.app[ : options.app.rfind(os.sep)], "distribution", "extensions")
-    ]
-
-    for extensionDir in extensionDirs:
-      if os.path.isdir(extensionDir):
-        for dirEntry in os.listdir(extensionDir):
-          if dirEntry not in options.extensionsToExclude:
-            path = os.path.join(extensionDir, dirEntry)
-            if os.path.isdir(path) or (os.path.isfile(path) and path.endswith(".xpi")):
-              extensions.append(path)
-    return extensions
-
   def installExtensionFromPath(self, options, path, extensionID = None):
     extensionPath = self.getFullPath(path)
 
-    self.automation.log.info("INFO | runtests.py | Installing extension at %s to %s." %
-                            (extensionPath, options.profilePath))
+    log.info("runtests.py | Installing extension at %s to %s." %
+                (extensionPath, options.profilePath))
     self.automation.installExtension(extensionPath, options.profilePath,
                                      extensionID)
 
   def installExtensionsToProfile(self, options):
     "Install special testing extensions, application distributed extensions, and specified on the command line ones to testing profile."
     for path in self.getExtensionsToInstall(options):
       self.installExtensionFromPath(options, path)
 
 def main():
   automation = Automation()
   mochitest = Mochitest(automation)
-  parser = MochitestOptions(automation, mochitest.SCRIPT_DIRECTORY)
+  parser = MochitestOptions(automation)
   options, args = parser.parse_args()
 
   options = parser.verifyOptions(options, mochitest)
   if options == None:
     sys.exit(1)
 
   options.utilityPath = mochitest.getFullPath(options.utilityPath)
   options.certPath = mochitest.getFullPath(options.certPath)
   if options.symbolsPath and not isURL(options.symbolsPath):
     options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
 
-  automation.setServerInfo(options.webServer, 
-                           options.httpPort, 
-                           options.sslPort, 
+  automation.setServerInfo(options.webServer,
+                           options.httpPort,
+                           options.sslPort,
                            options.webSocketPort)
   sys.exit(mochitest.runTests(options))
 
 if __name__ == "__main__":
   main()
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -1,136 +1,93 @@
 # 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 ConfigParser
 import os
+import posixpath
 import shutil
 import sys
 import tempfile
 import threading
 import traceback
 
 try:
     import json
 except ImportError:
     import simplejson as json
 
 here = os.path.abspath(os.path.dirname(sys.argv[0]))
 sys.path.insert(0, here)
 
-from automation import Automation
-from b2gautomation import B2GRemoteAutomation, B2GDesktopAutomation
-from runtests import Mochitest
+from runtests import MochitestUtilsMixin
 from runtests import MochitestOptions
 from runtests import MochitestServer
+from mochitest_options import B2GOptions, MochitestOptions
 
 from marionette import Marionette
 
-from mozdevice import DeviceManagerADB, DMError
-from mozprofile import Profile, Preferences
+from mozdevice import DeviceManagerADB
+from mozprofile import Profile, Preferences, DEFAULT_PORTS
+from mozrunner import B2GRunner
+import mozlog
+import mozinfo
+import moznetwork
+
+log = mozlog.getLogger('Mochitest')
 
-class B2GMochitest(Mochitest):
-    def __init__(self, automation, OOP=True, profile_data_dir=None,
-                    locations=os.path.join(here, 'server-locations.txt')):
-        Mochitest.__init__(self, automation)
-        self.OOP = OOP
+class B2GMochitest(MochitestUtilsMixin):
+    def __init__(self, marionette,
+                       out_of_process=True,
+                       profile_data_dir=None,
+                       locations=os.path.join(here, 'server-locations.txt')):
+        super(B2GMochitest, self).__init__()
+        self.marionette = marionette
+        self.out_of_process = out_of_process
         self.locations = locations
         self.preferences = []
         self.webapps = None
+        self.test_script = os.path.join(here, 'b2g_start_script.js')
+        self.test_script_args = [self.out_of_process]
+        self.product = 'b2g'
 
         if profile_data_dir:
             self.preferences = [os.path.join(profile_data_dir, f)
                                  for f in os.listdir(profile_data_dir) if f.startswith('pref')]
             self.webapps = [os.path.join(profile_data_dir, f)
                              for f in os.listdir(profile_data_dir) if f.startswith('webapp')]
 
-    def setupCommonOptions(self, options):
-        # set the testURL
-        testURL = self.buildTestPath(options)
-        if len(self.urlOpts) > 0:
-            testURL += "?" + "&".join(self.urlOpts)
-        self.automation.testURL = testURL
-
-        if self.OOP:
-            OOP_script = """
-let specialpowers = {};
-let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
-loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", specialpowers);
-let specialPowersObserver = new specialpowers.SpecialPowersObserver();
-specialPowersObserver.init();
-
-let mm = container.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
-mm.addMessageListener("SPPrefService", specialPowersObserver);
-mm.addMessageListener("SPProcessCrashService", specialPowersObserver);
-mm.addMessageListener("SPPingService", specialPowersObserver);
-mm.addMessageListener("SpecialPowers.Quit", specialPowersObserver);
-mm.addMessageListener("SpecialPowers.Focus", specialPowersObserver);
-mm.addMessageListener("SPPermissionManager", specialPowersObserver);
-
-mm.loadFrameScript(CHILD_LOGGER_SCRIPT, true);
-mm.loadFrameScript(CHILD_SCRIPT_API, true);
-mm.loadFrameScript(CHILD_SCRIPT, true);
-specialPowersObserver._isFrameScriptLoaded = true;
-"""
+        # mozinfo is populated by the parent class
+        if mozinfo.info['debug']:
+            self.SERVER_STARTUP_TIMEOUT = 180
         else:
-            OOP_script = ""
+            self.SERVER_STARTUP_TIMEOUT = 90
 
-        # Execute this script on start up: loads special powers and sets
-        # the test-container apps's iframe to the mochitest URL.
-        self.automation.test_script = """
-const CHILD_SCRIPT = "chrome://specialpowers/content/specialpowers.js";
-const CHILD_SCRIPT_API = "chrome://specialpowers/content/specialpowersAPI.js";
-const CHILD_LOGGER_SCRIPT = "chrome://specialpowers/content/MozillaLogger.js";
-
-let homescreen = document.getElementById('homescreen');
-let container = homescreen.contentWindow.document.getElementById('test-container');
-
-function openWindow(aEvent) {
-  var popupIframe = aEvent.detail.frameElement;
-  popupIframe.setAttribute('style', 'position: absolute; left: 0; top: 300px; background: white; ');
+    def setup_common_options(self, options):
+        test_url = self.buildTestPath(options)
+        if len(self.urlOpts) > 0:
+            test_url += "?" + "&".join(self.urlOpts)
+        self.test_script_args.append(test_url)
 
-  popupIframe.addEventListener('mozbrowserclose', function(e) {
-    container.parentNode.removeChild(popupIframe);
-    container.focus();
-  });
-
-  // yes, the popup can call window.open too!
-  popupIframe.addEventListener('mozbrowseropenwindow', openWindow);
-
-  popupIframe.addEventListener('mozbrowserloadstart', function(e) {
-    popupIframe.focus();
-  });
-
-  container.parentNode.appendChild(popupIframe);
-}
-
-container.addEventListener('mozbrowseropenwindow', openWindow);
-%s
-
-container.src = '%s';
-""" % (OOP_script, testURL)
-
-    def buildProfile(self, options):
+    def build_profile(self, options):
         # preferences
         prefs = {}
         for path in self.preferences:
             prefs.update(Preferences.read_prefs(path))
 
         for v in options.extraPrefs:
             thispref = v.split("=", 1)
             if len(thispref) < 2:
                 print "Error: syntax error in --setpref=" + v
                 sys.exit(1)
             prefs[thispref[0]] = thispref[1]
 
         # interpolate the preferences
         interpolation = { "server": "%s:%s" % (options.webServer, options.httpPort),
-                          "OOP": "true" if self.OOP else "false" }
+                          "OOP": "true" if self.out_of_process else "false" }
         prefs = json.loads(json.dumps(prefs) % interpolation)
         for pref in prefs:
             prefs[pref] = Preferences.cast(prefs[pref])
 
         kwargs = {
             'addons': self.getExtensionsToInstall(options),
             'apps': self.webapps,
             'locations': self.locations,
@@ -144,475 +101,174 @@ container.src = '%s';
             self.profile = Profile(**kwargs)
 
         options.profilePath = self.profile.profile
         # TODO bug 839108 - mozprofile should probably handle this
         manifest = self.addChromeToProfile(options)
         self.copyExtraFilesToProfile(options)
         return manifest
 
-
-class B2GOptions(MochitestOptions):
-
-    def __init__(self, automation, scriptdir, **kwargs):
-        defaults = {}
-        MochitestOptions.__init__(self, automation, scriptdir)
-
-        self.add_option("--b2gpath", action="store",
-                        type="string", dest="b2gPath",
-                        help="path to B2G repo or qemu dir")
-        defaults["b2gPath"] = None
-
-        self.add_option("--desktop", action="store_true",
-                        dest="desktop",
-                        help="Run the tests on a B2G desktop build")
-        defaults["desktop"] = False
-
-        self.add_option("--marionette", action="store",
-                        type="string", dest="marionette",
-                        help="host:port to use when connecting to Marionette")
-        defaults["marionette"] = None
+    def run_tests(self, options):
+        """ Prepare, configure, run tests and cleanup """
 
-        self.add_option("--emulator", action="store",
-                        type="string", dest="emulator",
-                        help="Architecture of emulator to use: x86 or arm")
-        defaults["emulator"] = None
-
-        self.add_option("--sdcard", action="store",
-                        type="string", dest="sdcard",
-                        help="Define size of sdcard: 1MB, 50MB...etc")
-        defaults["sdcard"] = "10MB"
-
-        self.add_option("--no-window", action="store_true",
-                        dest="noWindow",
-                        help="Pass --no-window to the emulator")
-        defaults["noWindow"] = False
-
-        self.add_option("--adbpath", action="store",
-                        type="string", dest="adbPath",
-                        help="path to adb")
-        defaults["adbPath"] = "adb"
-
-        self.add_option("--deviceIP", action="store",
-                        type="string", dest="deviceIP",
-                        help="ip address of remote device to test")
-        defaults["deviceIP"] = None
+        self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
+        manifest = self.build_profile(options)
 
-        self.add_option("--devicePort", action="store",
-                        type="string", dest="devicePort",
-                        help="port of remote device to test")
-        defaults["devicePort"] = 20701
-
-        self.add_option("--remote-logfile", action="store",
-                        type="string", dest="remoteLogFile",
-                        help="Name of log file on the device relative to the device root.  PLEASE ONLY USE A FILENAME.")
-        defaults["remoteLogFile"] = None
-
-        self.add_option("--remote-webserver", action="store",
-                        type="string", dest="remoteWebServer",
-                        help="ip address where the remote web server is hosted at")
-        defaults["remoteWebServer"] = None
-
-        self.add_option("--http-port", action="store",
-                        type="string", dest="httpPort",
-                        help="ip address where the remote web server is hosted at")
-        defaults["httpPort"] = automation.DEFAULT_HTTP_PORT
+        self.startWebServer(options)
+        self.startWebSocketServer(options, None)
+        self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'})
 
-        self.add_option("--ssl-port", action="store",
-                        type="string", dest="sslPort",
-                        help="ip address where the remote web server is hosted at")
-        defaults["sslPort"] = automation.DEFAULT_SSL_PORT
-
-        self.add_option("--pidfile", action="store",
-                        type="string", dest="pidFile",
-                        help="name of the pidfile to generate")
-        defaults["pidFile"] = ""
-
-        self.add_option("--gecko-path", action="store",
-                        type="string", dest="geckoPath",
-                        help="the path to a gecko distribution that should "
-                        "be installed on the emulator prior to test")
-        defaults["geckoPath"] = None
-
-        self.add_option("--profile", action="store",
-                        type="string", dest="profile",
-                        help="for desktop testing, the path to the "
-                        "gaia profile to use")
-        defaults["profile"] = None
-
-        self.add_option("--logcat-dir", action="store",
-                        type="string", dest="logcat_dir",
-                        help="directory to store logcat dump files")
-        defaults["logcat_dir"] = None
+        if options.timeout:
+            timeout = options.timeout + 30
+        elif options.debugger or not options.autorun:
+            timeout = None
+        else:
+            timeout = 330.0 # default JS harness timeout is 300 seconds
 
-        self.add_option('--busybox', action='store',
-                        type='string', dest='busybox',
-                        help="Path to busybox binary to install on device")
-        defaults['busybox'] = None
-        self.add_option('--profile-data-dir', action='store',
-                        type='string', dest='profile_data_dir',
-                        help="Path to a directory containing preference and other "
-                        "data to be installed into the profile")
-        defaults['profile_data_dir'] = os.path.join(here, 'profile_data')
-
-        defaults["remoteTestRoot"] = "/data/local/tests"
-        defaults["logFile"] = "mochitest.log"
-        defaults["autorun"] = True
-        defaults["closeWhenDone"] = True
-        defaults["testPath"] = ""
-        defaults["extensionsToExclude"] = ["specialpowers"]
-
-        self.set_defaults(**defaults)
-
-    def verifyRemoteOptions(self, options, automation):
-        if not options.remoteTestRoot:
-            options.remoteTestRoot = automation._devicemanager.getDeviceRoot()
-        productRoot = options.remoteTestRoot + "/" + automation._product
-
-        if options.utilityPath == self._automation.DIST_BIN:
-            options.utilityPath = productRoot + "/bin"
-
-        if options.remoteWebServer == None:
-            if os.name != "nt":
-                options.remoteWebServer = automation.getLanIp()
-            else:
-                self.error("You must specify a --remote-webserver=<ip address>")
-        options.webServer = options.remoteWebServer
-
-        if options.geckoPath and not options.emulator:
-            self.error("You must specify --emulator if you specify --gecko-path")
-
-        if options.logcat_dir and not options.emulator:
-            self.error("You must specify --emulator if you specify --logcat-dir")
-
-        #if not options.emulator and not options.deviceIP:
-        #    print "ERROR: you must provide a device IP"
-        #    return None
-
-        if options.remoteLogFile == None:
-            options.remoteLogFile = options.remoteTestRoot + '/logs/mochitest.log'
-
-        if options.remoteLogFile.count('/') < 1:
-            options.remoteLogFile = options.remoteTestRoot + '/' + options.remoteLogFile
-
-        # Only reset the xrePath if it wasn't provided
-        if options.xrePath == None:
-            options.xrePath = options.utilityPath
+        log.info("runtestsb2g.py | Running tests: start.")
+        status = 0
+        try:
+            runner_args = { 'profile': self.profile,
+                            'devicemanager': self._dm,
+                            'marionette': self.marionette,
+                            'remote_test_root': self.remote_test_root,
+                            'test_script': self.test_script,
+                            'test_script_args': self.test_script_args }
+            self.runner = B2GRunner(**runner_args)
+            self.runner.start(outputTimeout=timeout)
+            self.runner.wait()
+        except KeyboardInterrupt:
+            log.info("runtests.py | Received keyboard interrupt.\n");
+            status = -1
+        except:
+            traceback.print_exc()
+            log.error("runtests.py | Received unexpected exception while running application\n")
+            status = 1
 
-        if not os.path.isdir(options.xrePath):
-            self.error("--xre-path '%s' is not a directory" % options.xrePath)
-        xpcshell = os.path.join(options.xrePath, 'xpcshell')
-        if not os.access(xpcshell, os.F_OK):
-            self.error('xpcshell not found at %s' % xpcshell)
-        if automation.elf_arm(xpcshell):
-            self.error('--xre-path points to an ARM version of xpcshell; it '
-                       'should instead point to a version that can run on '
-                       'your desktop')
-
-        if options.pidFile != "":
-            f = open(options.pidFile, 'w')
-            f.write("%s" % os.getpid())
-            f.close()
-
-        return options
+        self.stopWebServer(options)
+        self.stopWebSocketServer(options)
 
-    def verifyOptions(self, options, mochitest):
-        # since we are reusing verifyOptions, it will exit if App is not found
-        temp = options.app
-        options.app = sys.argv[0]
-        tempPort = options.httpPort
-        tempSSL = options.sslPort
-        tempIP = options.webServer
-        options = MochitestOptions.verifyOptions(self, options, mochitest)
-        options.webServer = tempIP
-        options.app = temp
-        options.sslPort = tempSSL
-        options.httpPort = tempPort
-
-        return options
-
+        log.info("runtestsb2g.py | Running tests: end.")
 
-class ProfileConfigParser(ConfigParser.RawConfigParser):
-    """Subclass of RawConfigParser that outputs .ini files in the exact
-       format expected for profiles.ini, which is slightly different
-       than the default format.
-    """
-
-    def optionxform(self, optionstr):
-        return optionstr
-
-    def write(self, fp):
-        if self._defaults:
-            fp.write("[%s]\n" % ConfigParser.DEFAULTSECT)
-            for (key, value) in self._defaults.items():
-                fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t')))
-            fp.write("\n")
-        for section in self._sections:
-            fp.write("[%s]\n" % section)
-            for (key, value) in self._sections[section].items():
-                if key == "__name__":
-                    continue
-                if (value is not None) or (self._optcre == self.OPTCRE):
-                    key = "=".join((key, str(value).replace('\n', '\n\t')))
-                fp.write("%s\n" % (key))
-            fp.write("\n")
+        if manifest is not None:
+            self.cleanup(manifest, options)
+        return status
 
 
 class B2GDeviceMochitest(B2GMochitest):
 
-    _automation = None
     _dm = None
 
-    def __init__(self, automation, devmgr, options):
-        self._automation = automation
-        B2GMochitest.__init__(self, automation, OOP=True, profile_data_dir=options.profile_data_dir)
-        self._dm = devmgr
-        self.runSSLTunnel = False
-        self.remoteProfile = options.remoteTestRoot + '/profile'
-        self._automation.setRemoteProfile(self.remoteProfile)
-        self.remoteLog = options.remoteLogFile
-        self.localLog = None
-        self.userJS = '/data/local/user.js'
-        self.remoteMozillaPath = '/data/b2g/mozilla'
-        self.bundlesDir = '/system/b2g/distribution/bundles'
-        self.remoteProfilesIniPath = os.path.join(self.remoteMozillaPath, 'profiles.ini')
-        self.originalProfilesIni = None
+    def __init__(self, marionette, devicemanager, profile_data_dir,
+                 local_binary_dir, remote_test_root=None, remote_log_file=None):
+        B2GMochitest.__init__(self, marionette, out_of_process=True, profile_data_dir=profile_data_dir)
+        self._dm = devicemanager
+        self.remote_test_root = remote_test_root or self._dm.getDeviceRoot()
+        self.remote_profile = posixpath.join(self.remote_test_root, 'profile')
+        self.remote_log = remote_log_file or posixpath.join(self.remote_test_root, 'log', 'mochitest.log')
+        self.local_log = None
+        self.local_binary_dir = local_binary_dir
 
-    def copyRemoteFile(self, src, dest):
-        self._dm._checkCmdAs(['shell', 'dd', 'if=%s' % src, 'of=%s' % dest])
-
-    def origUserJSExists(self):
-        return self._dm.fileExists('/data/local/user.js.orig')
+        if not self._dm.dirExists(posixpath.dirname(self.remote_log)):
+            self._dm.mkDirs(self.remote_log)
 
     def cleanup(self, manifest, options):
-        if self.localLog:
-            self._dm.getFile(self.remoteLog, self.localLog)
-            self._dm.removeFile(self.remoteLog)
-
-        # Delete any bundled extensions
-        extensionDir = os.path.join(options.profilePath, 'extensions', 'staged')
-        if os.access(extensionDir, os.F_OK):
-            for filename in os.listdir(extensionDir):
-                try:
-                    self._dm._checkCmdAs(['shell', 'rm', '-rf',
-                                          os.path.join(self.bundlesDir, filename)])
-                except DMError:
-                    pass
-
-        if not options.emulator:
-            # Remove the test profile
-            self._dm._checkCmdAs(['shell', 'rm', '-r', self.remoteProfile])
-
-            if self.origUserJSExists():
-                # Restore the original user.js
-                self._dm.removeFile(self.userJS)
-                self.copyRemoteFile('%s.orig' % self.userJS, self.userJS)
-                self._dm.removeFile("%s.orig" % self.userJS)
-
-            if self._dm.fileExists('%s.orig' % self.remoteProfilesIniPath):
-                # Restore the original profiles.ini
-                self._dm.removeFile(self.remoteProfilesIniPath)
-                self.copyRemoteFile('%s.orig' % self.remoteProfilesIniPath,
-                                    self.remoteProfilesIniPath)
-                self._dm.removeFile("%s.orig" % self.remoteProfilesIniPath)
-
-            # We've restored the original profile, so reboot the device so that
-            # it gets picked up.
-            self._automation.rebootDevice()
+        if self.local_log:
+            self._dm.getFile(self.remote_log, self.local_log)
+            self._dm.removeFile(self.remote_log)
 
         if options.pidFile != "":
             try:
                 os.remove(options.pidFile)
                 os.remove(options.pidFile + ".xpcshell.pid")
             except:
                 print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % options.pidFile
 
-    def findPath(self, paths, filename=None):
-        for path in paths:
-            p = path
-            if filename:
-                p = os.path.join(p, filename)
-            if os.path.exists(self.getFullPath(p)):
-                return path
-        return None
+        # stop and clean up the runner
+        if getattr(self, 'runner', False):
+            self.runner.cleanup()
+            self.runner = None
 
     def startWebServer(self, options):
         """ Create the webserver on the host and start it up """
-        remoteXrePath = options.xrePath
-        remoteProfilePath = options.profilePath
-        remoteUtilityPath = options.utilityPath
-        localAutomation = Automation()
-        localAutomation.IS_WIN32 = False
-        localAutomation.IS_LINUX = False
-        localAutomation.IS_MAC = False
-        localAutomation.UNIXISH = False
-        hostos = sys.platform
-        if hostos in ['mac', 'darwin']:
-            localAutomation.IS_MAC = True
-        elif hostos in ['linux', 'linux2']:
-            localAutomation.IS_LINUX = True
-            localAutomation.UNIXISH = True
-        elif hostos in ['win32', 'win64']:
-            localAutomation.BIN_SUFFIX = ".exe"
-            localAutomation.IS_WIN32 = True
-
-        paths = [options.xrePath,
-                 localAutomation.DIST_BIN,
-                 self._automation._product,
-                 os.path.join('..', self._automation._product)]
-        options.xrePath = self.findPath(paths)
-        if options.xrePath == None:
-            print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name)
-            sys.exit(1)
-        paths.append("bin")
-        paths.append(os.path.join("..", "bin"))
-
-        xpcshell = "xpcshell"
-        if (os.name == "nt"):
-            xpcshell += ".exe"
-
-        if (options.utilityPath):
-            paths.insert(0, options.utilityPath)
-        options.utilityPath = self.findPath(paths, xpcshell)
-        if options.utilityPath == None:
-            print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name)
-            sys.exit(1)
-        # httpd-path is specified by standard makefile targets and may be specified
-        # on the command line to select a particular version of httpd.js. If not
-        # specified, try to select the one from xre.zip, as required in bug 882932.
-        if not options.httpdPath:
-            options.httpdPath = os.path.join(options.utilityPath, "components")
-
-        options.profilePath = tempfile.mkdtemp()
-        self.server = MochitestServer(localAutomation, options)
+        d = vars(options).copy()
+        d['xrePath'] = self.local_binary_dir
+        d['utilityPath'] = self.local_binary_dir
+        d['profilePath'] = tempfile.mkdtemp()
+        if d.get('httpdPath') is None:
+            d['httpdPath'] = os.path.abspath(os.path.join(self.local_binary_dir, 'components'))
+        self.server = MochitestServer(None, d)
         self.server.start()
 
         if (options.pidFile != ""):
             f = open(options.pidFile + ".xpcshell.pid", 'w')
             f.write("%s" % self.server._process.pid)
             f.close()
-        self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
-
-        options.xrePath = remoteXrePath
-        options.utilityPath = remoteUtilityPath
-        options.profilePath = remoteProfilePath
+        self.server.ensureReady(90)
 
     def stopWebServer(self, options):
         if hasattr(self, 'server'):
             self.server.stop()
 
-    def updateProfilesIni(self, profilePath):
-        # update profiles.ini on the device to point to the test profile
-        self.originalProfilesIni = tempfile.mktemp()
-        self._dm.getFile(self.remoteProfilesIniPath, self.originalProfilesIni)
-
-        config = ProfileConfigParser()
-        config.read(self.originalProfilesIni)
-        for section in config.sections():
-            if 'Profile' in section:
-                config.set(section, 'IsRelative', 0)
-                config.set(section, 'Path', profilePath)
-
-        newProfilesIni = tempfile.mktemp()
-        with open(newProfilesIni, 'wb') as configfile:
-            config.write(configfile)
-
-        self._dm.pushFile(newProfilesIni, self.remoteProfilesIniPath)
-        self._dm.pushFile(self.originalProfilesIni, '%s.orig' % self.remoteProfilesIniPath)
-
-        try:
-            os.remove(newProfilesIni)
-            os.remove(self.originalProfilesIni)
-        except:
-            pass
-
     def buildURLOptions(self, options, env):
-        self.localLog = options.logFile
-        options.logFile = self.remoteLog
+        self.local_log = options.logFile
+        options.logFile = self.remote_log
         options.profilePath = self.profile.profile
-        retVal = Mochitest.buildURLOptions(self, options, env)
+        retVal = super(B2GDeviceMochitest, self).buildURLOptions(options, env)
 
-        self.setupCommonOptions(options)
-
-        # Copy the profile to the device.
-        self._dm._checkCmdAs(['shell', 'rm', '-r', self.remoteProfile])
-        try:
-            self._dm.pushDir(options.profilePath, self.remoteProfile)
-        except DMError:
-            print "Automation Error: Unable to copy profile to device."
-            raise
+        self.setup_common_options(options)
 
-        # Copy the extensions to the B2G bundles dir.
-        extensionDir = os.path.join(options.profilePath, 'extensions', 'staged')
-        # need to write to read-only dir
-        self._dm._checkCmdAs(['remount'])
-        for filename in os.listdir(extensionDir):
-            self._dm._checkCmdAs(['shell', 'rm', '-rf',
-                                  os.path.join(self.bundlesDir, filename)])
-        try:
-            self._dm.pushDir(extensionDir, self.bundlesDir)
-        except DMError:
-            print "Automation Error: Unable to copy extensions to device."
-            raise
-
-        # In B2G, user.js is always read from /data/local, not the profile
-        # directory.  Backup the original user.js first so we can restore it.
-        if not self._dm.fileExists('%s.orig' % self.userJS):
-            self.copyRemoteFile(self.userJS, '%s.orig' % self.userJS)
-        self._dm.pushFile(os.path.join(options.profilePath, "user.js"), self.userJS)
-        self.updateProfilesIni(self.remoteProfile)
-        options.profilePath = self.remoteProfile
-        options.logFile = self.localLog
+        options.profilePath = self.remote_profile
+        options.logFile = self.local_log
         return retVal
 
 
 class B2GDesktopMochitest(B2GMochitest):
 
-    def __init__(self, automation, options):
-        B2GMochitest.__init__(self, automation, OOP=False, profile_data_dir=options.profile_data_dir)
+    def __init__(self, marionette, profile_data_dir):
+        B2GMochitest.__init__(self, out_of_process=False, profile_data_dir=profile_data_dir)
 
     def runMarionetteScript(self, marionette, test_script):
         assert(marionette.wait_for_port())
         marionette.start_session()
         marionette.set_context(marionette.CONTEXT_CHROME)
         marionette.execute_script(test_script)
 
     def startTests(self):
         # This is run in a separate thread because otherwise, the app's
         # stdout buffer gets filled (which gets drained only after this
         # function returns, by waitForFinish), which causes the app to hang.
         thread = threading.Thread(target=self.runMarionetteScript,
-                                  args=(self.automation.marionette,
-                                        self.automation.test_script))
+                                  args=(self.marionette,
+                                        self.test_script))
         thread.start()
 
     def buildURLOptions(self, options, env):
-        retVal = Mochitest.buildURLOptions(self, options, env)
+        retVal = super(B2GDesktopMochitest, self).buildURLOptions(options, env)
 
-        self.setupCommonOptions(options)
+        self.setup_common_options(options)
 
         # Copy the extensions to the B2G bundles dir.
         extensionDir = os.path.join(options.profilePath, 'extensions', 'staged')
         bundlesDir = os.path.join(os.path.dirname(options.app),
                                   'distribution', 'bundles')
 
         for filename in os.listdir(extensionDir):
             shutil.rmtree(os.path.join(bundlesDir, filename), True)
             shutil.copytree(os.path.join(extensionDir, filename),
                             os.path.join(bundlesDir, filename))
 
         return retVal
 
 
-def run_remote_mochitests(automation, parser, options):
+def run_remote_mochitests(parser, options):
     # create our Marionette instance
     kwargs = {}
     if options.emulator:
         kwargs['emulator'] = options.emulator
-        automation.setEmulator(True)
         if options.noWindow:
             kwargs['noWindow'] = True
         if options.geckoPath:
             kwargs['gecko_path'] = options.geckoPath
         if options.logcat_dir:
             kwargs['logcat_dir'] = options.logcat_dir
         if options.busybox:
             kwargs['busybox'] = options.busybox
@@ -625,99 +281,80 @@ def run_remote_mochitests(automation, pa
         kwargs['homedir'] = options.b2gPath
     if options.marionette:
         host, port = options.marionette.split(':')
         kwargs['host'] = host
         kwargs['port'] = int(port)
 
     marionette = Marionette.getMarionetteOrExit(**kwargs)
 
-    automation.marionette = marionette
-
     # create the DeviceManager
     kwargs = {'adbPath': options.adbPath,
               'deviceRoot': options.remoteTestRoot}
     if options.deviceIP:
         kwargs.update({'host': options.deviceIP,
                        'port': options.devicePort})
     dm = DeviceManagerADB(**kwargs)
-    automation.setDeviceManager(dm)
-    options = parser.verifyRemoteOptions(options, automation)
+    options = parser.verifyRemoteOptions(options)
     if (options == None):
         print "ERROR: Invalid options specified, use --help for a list of valid options"
         sys.exit(1)
 
-    automation.setProduct("b2g")
-
-    mochitest = B2GDeviceMochitest(automation, dm, options)
+    mochitest = B2GDeviceMochitest(marionette, dm, options.profile_data_dir, options.xrePath,
+                                   remote_test_root=options.remoteTestRoot,
+                                   remote_log_file=options.remoteLogFile)
 
     options = parser.verifyOptions(options, mochitest)
     if (options == None):
         sys.exit(1)
 
-    logParent = os.path.dirname(options.remoteLogFile)
-    dm.mkDir(logParent)
-    automation.setRemoteLog(options.remoteLogFile)
-    automation.setServerInfo(options.webServer, options.httpPort, options.sslPort)
     retVal = 1
     try:
         mochitest.cleanup(None, options)
-        retVal = mochitest.runTests(options)
+        retVal = mochitest.run_tests(options)
     except:
         print "Automation Error: Exception caught while running tests"
         traceback.print_exc()
         mochitest.stopWebServer(options)
         mochitest.stopWebSocketServer(options)
         try:
             mochitest.cleanup(None, options)
         except:
             pass
         retVal = 1
 
     sys.exit(retVal)
 
-
 def run_desktop_mochitests(parser, options):
-    automation = B2GDesktopAutomation()
-
     # create our Marionette instance
     kwargs = {}
     if options.marionette:
         host, port = options.marionette.split(':')
         kwargs['host'] = host
         kwargs['port'] = int(port)
     marionette = Marionette.getMarionetteOrExit(**kwargs)
-    automation.marionette = marionette
 
-    mochitest = B2GDesktopMochitest(automation, options)
+    mochitest = B2GDesktopMochitest(marionette, options.profile_data_dir)
 
     # b2g desktop builds don't always have a b2g-bin file
     if options.app[-4:] == '-bin':
         options.app = options.app[:-4]
 
     options = MochitestOptions.verifyOptions(parser, options, mochitest)
     if options == None:
         sys.exit(1)
 
     if options.desktop and not options.profile:
         raise Exception("must specify --profile when specifying --desktop")
 
-    automation.setServerInfo(options.webServer,
-                             options.httpPort,
-                             options.sslPort,
-                             options.webSocketPort)
-    sys.exit(mochitest.runTests(options,
-                                onLaunch=mochitest.startTests))
-
+    sys.exit(mochitest.runTests(options, onLaunch=mochitest.startTests))
 
 def main():
-    scriptdir = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
-    automation = B2GRemoteAutomation(None, "fennec")
-    parser = B2GOptions(automation, scriptdir)
+    parser = B2GOptions()
     options, args = parser.parse_args()
 
     if options.desktop:
         run_desktop_mochitests(parser, options)
     else:
-        run_remote_mochitests(automation, parser, options)
+        run_remote_mochitests(parser, options)
 
 if __name__ == "__main__":
     main()
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -12,28 +12,28 @@ import shutil
 import math
 import base64
 
 sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))))
 
 from automation import Automation
 from remoteautomation import RemoteAutomation, fennecLogcatFilters
 from runtests import Mochitest
-from runtests import MochitestOptions
 from runtests import MochitestServer
+from mochitest_options import MochitestOptions
 
 import devicemanager
 import droid
 import manifestparser
 
 class RemoteOptions(MochitestOptions):
 
-    def __init__(self, automation, scriptdir, **kwargs):
+    def __init__(self, automation, **kwargs):
         defaults = {}
-        MochitestOptions.__init__(self, automation, scriptdir)
+        MochitestOptions.__init__(self, automation)
 
         self.add_option("--remote-app-path", action="store",
                     type = "string", dest = "remoteAppPath",
                     help = "Path to remote executable relative to device root using only forward slashes. Either this or app must be specified but not both")
         defaults["remoteAppPath"] = None
 
         self.add_option("--deviceIP", action="store",
                     type = "string", dest = "deviceIP",
@@ -514,19 +514,18 @@ class MochiRemote(Mochitest):
 
     def buildBrowserEnv(self, options):
         browserEnv = Mochitest.buildBrowserEnv(self, options)
         self.buildRobotiumConfig(options, browserEnv)
         return browserEnv
 
         
 def main():
-    scriptdir = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
     auto = RemoteAutomation(None, "fennec")
-    parser = RemoteOptions(auto, scriptdir)
+    parser = RemoteOptions(auto)
     options, args = parser.parse_args()
 
     if (options.dm_trans == "adb"):
         if (options.deviceIP):
             dm = droid.DroidADB(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot)
         else:
             dm = droid.DroidADB(deviceRoot=options.remoteTestRoot)
     else: