Bug 997244 - Move emulator.py out of marionette and into mozrunner, r=wlach,mdas,jgriffin
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Thu, 19 Jun 2014 14:17:26 -0400
changeset 210423 f5d1163268aee3ae8d4b9b54fddfc59fb8940030
parent 210422 fefe4c4ffe939468dad5203c470046c8a9839073
child 210424 93be174c58237596c5fe303c81a15de8b4520b78
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswlach, mdas, jgriffin
bugs997244
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 997244 - Move emulator.py out of marionette and into mozrunner, r=wlach,mdas,jgriffin
build/mobile/b2gautomation.py
build/valgrind/mach_commands.py
layout/tools/reftest/b2g_desktop.py
layout/tools/reftest/mach_commands.py
layout/tools/reftest/runreftestb2g.py
testing/config/mozharness/b2g_emulator_config.py
testing/marionette/client/marionette/__init__.py
testing/marionette/client/marionette/b2gbuild.py
testing/marionette/client/marionette/b2ginstance.py
testing/marionette/client/marionette/emulator.py
testing/marionette/client/marionette/emulator_battery.py
testing/marionette/client/marionette/emulator_geo.py
testing/marionette/client/marionette/emulator_screen.py
testing/marionette/client/marionette/geckoinstance.py
testing/marionette/client/marionette/marionette.py
testing/marionette/client/marionette/runner/base.py
testing/marionette/client/marionette/runner/mixins/b2g.py
testing/marionette/client/marionette/tests/unit/test_clearing.py
testing/marionette/client/marionette/tests/unit/test_element_touch.py
testing/marionette/client/marionette/tests/unit/test_emulator.py
testing/marionette/client/marionette/tests/unit/test_execute_async_script.py
testing/marionette/client/marionette/tests/unit/test_execute_isolate.py
testing/marionette/client/marionette/tests/unit/test_execute_script.py
testing/marionette/client/marionette/tests/unit/test_findelement.py
testing/marionette/client/marionette/tests/unit/test_findelement_chrome.py
testing/marionette/client/marionette/tests/unit/test_implicit_waits.py
testing/marionette/client/marionette/tests/unit/test_navigation.py
testing/marionette/client/marionette/tests/unit/test_screen_orientation.py
testing/marionette/client/marionette/tests/unit/test_simpletest_sanity.py
testing/marionette/client/marionette/tests/unit/test_single_finger.py
testing/marionette/client/marionette/tests/unit/test_single_finger_desktop.py
testing/marionette/client/marionette/tests/unit/test_specialpowers.py
testing/marionette/client/marionette/tests/unit/test_switch_frame.py
testing/marionette/client/marionette/tests/unit/test_switch_frame_chrome.py
testing/marionette/client/marionette/tests/unit/test_timeouts.py
testing/marionette/client/marionette/tests/unit/test_typing.py
testing/marionette/client/requirements.txt
testing/mochitest/mach_commands.py
testing/mochitest/mochitest_options.py
testing/mochitest/runtests.py
testing/mochitest/runtestsb2g.py
testing/mozbase/docs/mozrunner.rst
testing/mozbase/docs/setuprunning.rst
testing/mozbase/mozdevice/mozdevice/devicemanager.py
testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
testing/mozbase/mozdevice/setup.py
testing/mozbase/mozprocess/mozprocess/processhandler.py
testing/mozbase/mozrunner/mozrunner/__init__.py
testing/mozbase/mozrunner/mozrunner/application.py
testing/mozbase/mozrunner/mozrunner/base.py
testing/mozbase/mozrunner/mozrunner/base/__init__.py
testing/mozbase/mozrunner/mozrunner/base/browser.py
testing/mozbase/mozrunner/mozrunner/base/device.py
testing/mozbase/mozrunner/mozrunner/base/runner.py
testing/mozbase/mozrunner/mozrunner/cli.py
testing/mozbase/mozrunner/mozrunner/devices/__init__.py
testing/mozbase/mozrunner/mozrunner/devices/base.py
testing/mozbase/mozrunner/mozrunner/devices/emulator.py
testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py
testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py
testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py
testing/mozbase/mozrunner/mozrunner/errors.py
testing/mozbase/mozrunner/mozrunner/local.py
testing/mozbase/mozrunner/mozrunner/remote.py
testing/mozbase/mozrunner/mozrunner/runners.py
testing/mozbase/mozrunner/mozrunner/utils.py
testing/mozbase/mozrunner/setup.py
testing/profiles/prefs_b2g_unittest.js
testing/tps/tps/firefoxrunner.py
testing/xpcshell/mach_commands.py
testing/xpcshell/remotexpcshelltests.py
testing/xpcshell/runtestsb2g.py
testing/xpcshell/runxpcshelltests.py
--- a/build/mobile/b2gautomation.py
+++ b/build/mobile/b2gautomation.py
@@ -189,17 +189,17 @@ class B2GRemoteAutomation(Automation):
     def restartB2G(self):
         # TODO hangs in subprocess.Popen without this delay
         time.sleep(5)
         self._devicemanager._checkCmd(['shell', 'stop', 'b2g'])
         # Wait for a bit to make sure B2G has completely shut down.
         time.sleep(10)
         self._devicemanager._checkCmd(['shell', 'start', 'b2g'])
         if self._is_emulator:
-            self.marionette.emulator.wait_for_port()
+            self.marionette.emulator.wait_for_port(self.marionette.port)
 
     def rebootDevice(self):
         # find device's current status and serial number
         serial, status = self.getDeviceStatus()
 
         # reboot!
         self._devicemanager._runCmd(['shell', '/system/bin/reboot'])
 
@@ -257,17 +257,17 @@ class B2GRemoteAutomation(Automation):
         # Set up port forwarding again for Marionette, since any that
         # existed previously got wiped out by the reboot.
         if not self._is_emulator:
             self._devicemanager._checkCmd(['forward',
                                            'tcp:%s' % self.marionette.port,
                                            'tcp:%s' % self.marionette.port])
 
         if self._is_emulator:
-            self.marionette.emulator.wait_for_port()
+            self.marionette.emulator.wait_for_port(self.marionette.port)
         else:
             time.sleep(5)
 
         # start a marionette session
         session = self.marionette.start_session()
         if 'b2g' not in session:
             raise Exception("bad session value %s returned by start_session" % session)
 
--- a/build/valgrind/mach_commands.py
+++ b/build/valgrind/mach_commands.py
@@ -126,17 +126,17 @@ class MachCommands(MachCommandBase):
                 valgrind_args.append('--suppressions=' + supps_file2)
 
             exitcode = None
             try:
                 runner = FirefoxRunner(profile=profile,
                                        binary=self.get_binary_path(),
                                        cmdargs=firefox_args,
                                        env=env,
-                                       kp_kwargs=kp_kwargs)
+                                       process_args=kp_kwargs)
                 runner.start(debug_args=valgrind_args)
                 exitcode = runner.wait()
 
             finally:
                 errs = outputHandler.error_count
                 supps = outputHandler.suppression_count
                 if errs != supps:
                     status = 1  # turns the TBPL job orange
--- a/layout/tools/reftest/b2g_desktop.py
+++ b/layout/tools/reftest/b2g_desktop.py
@@ -17,26 +17,29 @@ from marionette import Marionette
 from mozprocess import ProcessHandler
 from mozrunner import FirefoxRunner
 import mozinfo
 import mozlog
 
 log = mozlog.getLogger('REFTEST')
 
 class B2GDesktopReftest(RefTest):
-    def __init__(self, marionette):
+    marionette = None
+
+    def __init__(self, marionette_args):
         RefTest.__init__(self)
         self.last_test = os.path.basename(__file__)
-        self.marionette = marionette
+        self.marionette_args = marionette_args
         self.profile = None
         self.runner = None
         self.test_script = os.path.join(here, 'b2g_start_script.js')
         self.timeout = None
 
     def run_marionette_script(self):
+        self.marionette = Marionette(**self.marionette_args)
         assert(self.marionette.wait_for_port())
         self.marionette.start_session()
         self.marionette.set_context(self.marionette.CONTEXT_CHROME)
 
         if os.path.isfile(self.test_script):
             f = open(self.test_script, 'r')
             self.test_script = f.read()
             f.close()
@@ -66,18 +69,18 @@ class B2GDesktopReftest(RefTest):
         cmd, args = self.build_command_line(options.app,
                             ignore_window_size=options.ignoreWindowSize,
                             browser_arg=options.browser_arg)
         self.runner = FirefoxRunner(profile=self.profile,
                                     binary=cmd,
                                     cmdargs=args,
                                     env=env,
                                     process_class=ProcessHandler,
-                                    symbols_path=options.symbolsPath,
-                                    kp_kwargs=kp_kwargs)
+                                    process_args=kp_kwargs,
+                                    symbols_path=options.symbolsPath)
 
         status = 0
         try:
             self.runner.start(outputTimeout=self.timeout)
             log.info("%s | Application pid: %d",
                      os.path.basename(__file__),
                      self.runner.process_handler.pid)
 
@@ -146,24 +149,23 @@ class B2GDesktopReftest(RefTest):
         msg = "%s | application timed out after %s seconds with no output"
         log.testFail(msg % (self.last_test, self.timeout))
 
         # kill process to get a stack
         self.runner.stop(sig=signal.SIGABRT)
 
 
 def run_desktop_reftests(parser, options, args):
-    kwargs = {}
+    marionette_args = {}
     if options.marionette:
         host, port = options.marionette.split(':')
-        kwargs['host'] = host
-        kwargs['port'] = int(port)
-    marionette = Marionette.getMarionetteOrExit(**kwargs)
+        marionette_args['host'] = host
+        marionette_args['port'] = int(port)
 
-    reftest = B2GDesktopReftest(marionette)
+    reftest = B2GDesktopReftest(marionette_args)
 
     options = ReftestOptions.verifyCommonOptions(parser, options, reftest)
     if options == None:
         sys.exit(1)
 
     # add a -bin suffix if b2g-bin exists, but just b2g was specified
     if options.app[-4:] != '-bin':
         if os.path.isfile("%s-bin" % options.app):
--- a/layout/tools/reftest/mach_commands.py
+++ b/layout/tools/reftest/mach_commands.py
@@ -196,17 +196,17 @@ class ReftestRunner(MozbuildObject):
 
         try:
             which.which('adb')
         except which.WhichError:
             # TODO Find adb automatically if it isn't on the path
             raise Exception(ADB_NOT_FOUND % ('%s-remote' % suite, b2g_home))
 
         options.b2gPath = b2g_home
-        options.logcat_dir = self.reftest_dir
+        options.logdir = self.reftest_dir
         options.httpdPath = os.path.join(self.topsrcdir, 'netwerk', 'test', 'httpserver')
         options.xrePath = xre_path
         options.ignoreWindowSize = True
 
         # Don't enable oop for crashtest until they run oop in automation
         if suite == 'reftest':
             options.oop = True
 
@@ -330,19 +330,19 @@ def ReftestCommand(func):
 
 def B2GCommand(func):
     """Decorator that adds shared command arguments to b2g mochitest commands."""
 
     busybox = CommandArgument('--busybox', default=None,
         help='Path to busybox binary to install on device')
     func = busybox(func)
 
-    logcatdir = CommandArgument('--logcat-dir', default=None,
-        help='directory to store logcat dump files')
-    func = logcatdir(func)
+    logdir = CommandArgument('--logdir', default=None,
+        help='directory to store log files')
+    func = logdir(func)
 
     sdcard = CommandArgument('--sdcard', default="10MB",
         help='Define size of sdcard: 1MB, 50MB...etc')
     func = sdcard(func)
 
     emulator_res = CommandArgument('--emulator-res', default='800x1000',
         help='Emulator resolution of the format \'<width>x<height>\'')
     func = emulator_res(func)
--- a/layout/tools/reftest/runreftestb2g.py
+++ b/layout/tools/reftest/runreftestb2g.py
@@ -53,19 +53,19 @@ class B2GOptions(ReftestOptions):
         defaults["emulator_res"] = None
 
         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",
+                    type = "string", dest = "adb_path",
                     help = "path to adb")
-        defaults["adbPath"] = "adb"
+        defaults["adb_path"] = "adb"
 
         self.add_option("--deviceIP", action="store",
                     type = "string", dest = "deviceIP",
                     help = "ip address of remote device to test")
         defaults["deviceIP"] = None
 
         self.add_option("--devicePort", action="store",
                     type = "string", dest = "devicePort",
@@ -96,20 +96,20 @@ class B2GOptions(ReftestOptions):
                     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("--logcat-dir", action="store",
-                        type="string", dest="logcat_dir",
-                        help="directory to store logcat dump files")
-        defaults["logcat_dir"] = None
+        self.add_option("--logdir", action="store",
+                        type="string", dest="logdir",
+                        help="directory to store log files")
+        defaults["logdir"] = None
         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("--httpd-path", action = "store",
                     type = "string", dest = "httpdPath",
                     help = "path to the httpd.js file")
         defaults["httpdPath"] = None
@@ -161,18 +161,18 @@ class B2GOptions(ReftestOptions):
             options.httpPort = auto.DEFAULT_HTTP_PORT
 
         if not options.sslPort:
             options.sslPort = auto.DEFAULT_SSL_PORT
 
         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 options.logdir and not options.emulator:
+            self.error("You must specify --emulator if you specify --logdir")
 
         #if not options.emulator and not options.deviceIP:
         #    print "ERROR: you must provide a device IP"
         #    return None
 
         if options.remoteLogFile == None:
             options.remoteLogFile = "reftest.log"
 
@@ -492,43 +492,45 @@ def run_remote_reftests(parser, options,
     kwargs = {}
     if options.emulator:
         kwargs['emulator'] = options.emulator
         auto.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.logdir:
+            kwargs['logdir'] = options.logdir
         if options.busybox:
             kwargs['busybox'] = options.busybox
         if options.symbolsPath:
             kwargs['symbols_path'] = options.symbolsPath
     if options.emulator_res:
         kwargs['emulator_res'] = options.emulator_res
     if options.b2gPath:
         kwargs['homedir'] = options.b2gPath
     if options.marionette:
         host,port = options.marionette.split(':')
         kwargs['host'] = host
         kwargs['port'] = int(port)
-    marionette = Marionette.getMarionetteOrExit(**kwargs)
+    if options.adb_path:
+        kwargs['adb_path'] = options.adb_path
+    marionette = Marionette(**kwargs)
     auto.marionette = marionette
 
     if options.emulator:
         dm = marionette.emulator.dm
     else:
         # create the DeviceManager
-        kwargs = {'adbPath': options.adbPath,
+        kwargs = {'adbPath': options.adb_path,
                   'deviceRoot': options.remoteTestRoot}
         if options.deviceIP:
             kwargs.update({'host': options.deviceIP,
                            'port': options.devicePort})
-        dm = DeviagerADB(**kwargs)
+        dm = DeviceManagerADB(**kwargs)
     auto.setDeviceManager(dm)
 
     options = parser.verifyRemoteOptions(options, auto)
 
     if (options == None):
         print "ERROR: Invalid options specified, use --help for a list of valid options"
         sys.exit(1)
 
--- a/testing/config/mozharness/b2g_emulator_config.py
+++ b/testing/config/mozharness/b2g_emulator_config.py
@@ -1,50 +1,50 @@
 # 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/.
 
 config = {
     "jsreftest_options": [
         "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--emulator=%(emulator)s",
-        "--emulator-res=800x1000", "--logcat-dir=%(logcat_dir)s",
+        "--emulator-res=800x1000", "--logdir=%(logcat_dir)s",
         "--remote-webserver=%(remote_webserver)s", "--ignore-window-size",
         "--xre-path=%(xre_path)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s",
         "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s",
         "--extra-profile-file=jsreftest/tests/user.js",
         "%(test_manifest)s",
     ],
 
     "mochitest_options": [
         "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--console-level=INFO",
-        "--emulator=%(emulator)s", "--logcat-dir=%(logcat_dir)s",
+        "--emulator=%(emulator)s", "--logdir=%(logcat_dir)s",
         "--remote-webserver=%(remote_webserver)s", "%(test_manifest)s",
         "--xre-path=%(xre_path)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s",
         "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s",
         "--quiet", "--certificate-path=%(certificate_path)s",
         "--test-path=%(test_path)s",
     ],
 
     "reftest_options": [
         "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--emulator=%(emulator)s",
-        "--emulator-res=800x1000", "--logcat-dir=%(logcat_dir)s",
+        "--emulator-res=800x1000", "--logdir=%(logcat_dir)s",
         "--remote-webserver=%(remote_webserver)s", "--ignore-window-size",
         "--xre-path=%(xre_path)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s",
         "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s", "--enable-oop",
         "%(test_manifest)s",
     ],
 
     "crashtest_options": [
         "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--emulator=%(emulator)s",
-        "--emulator-res=800x1000", "--logcat-dir=%(logcat_dir)s",
+        "--emulator-res=800x1000", "--logdir=%(logcat_dir)s",
         "--remote-webserver=%(remote_webserver)s", "--ignore-window-size",
         "--xre-path=%(xre_path)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s",
         "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s",
         "%(test_manifest)s",
     ],
 
     "xpcshell_options": [
         "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--emulator=%(emulator)s",
-        "--logcat-dir=%(logcat_dir)s", "--manifest=%(test_manifest)s", "--use-device-libs",
+        "--logdir=%(logcat_dir)s", "--manifest=%(test_manifest)s", "--use-device-libs",
         "--testing-modules-dir=%(modules_dir)s", "--symbols-path=%(symbols_path)s",
         "--busybox=%(busybox)s", "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s",
     ],
 }
--- a/testing/marionette/client/marionette/__init__.py
+++ b/testing/marionette/client/marionette/__init__.py
@@ -1,26 +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/.
 
 from gestures import smooth_scroll, pinch
 from by import By
 from marionette import Marionette, HTMLElement, Actions, MultiActions
 from marionette_test import MarionetteTestCase, MarionetteJSTestCase, CommonTestCase, expectedFailure, skip, SkipTest
-from emulator import Emulator
 from errors import (
-        ErrorCodes, MarionetteException, InstallGeckoError, TimeoutException, InvalidResponseException,
-        JavascriptException, NoSuchElementException, XPathLookupException, NoSuchWindowException,
-        StaleElementException, ScriptTimeoutException, ElementNotVisibleException,
-        NoSuchFrameException, InvalidElementStateException, NoAlertPresentException,
-        InvalidCookieDomainException, UnableToSetCookieException, InvalidSelectorException,
-        MoveTargetOutOfBoundsException, FrameSendNotInitializedError, FrameSendFailureError
-        )
+    ElementNotVisibleException,
+    ErrorCodes,
+    FrameSendFailureError,
+    FrameSendNotInitializedError,
+    InvalidCookieDomainException,
+    InvalidElementStateException,
+    InvalidResponseException,
+    InvalidSelectorException,
+    JavascriptException,
+    MarionetteException,
+    MoveTargetOutOfBoundsException,
+    NoAlertPresentException,
+    NoSuchElementException,
+    NoSuchFrameException,
+    NoSuchWindowException,
+    ScriptTimeoutException,
+    StaleElementException,
+    TimeoutException,
+    UnableToSetCookieException,
+    XPathLookupException,
+)
 from runner import (
-        B2GTestCaseMixin, B2GTestResultMixin, BaseMarionetteOptions, BaseMarionetteTestRunner, EnduranceOptionsMixin,
-        EnduranceTestCaseMixin, HTMLReportingOptionsMixin, HTMLReportingTestResultMixin, HTMLReportingTestRunnerMixin,
-        Marionette, MarionetteTest, MarionetteTestResult, MarionetteTextTestRunner, MemoryEnduranceTestCaseMixin,
-        MozHttpd, OptionParser, TestManifest, TestResult, TestResultCollection
-        )
+        B2GTestCaseMixin,
+        B2GTestResultMixin,
+        BaseMarionetteOptions,
+        BaseMarionetteTestRunner,
+        EnduranceOptionsMixin,
+        EnduranceTestCaseMixin,
+        HTMLReportingOptionsMixin,
+        HTMLReportingTestResultMixin,
+        HTMLReportingTestRunnerMixin,
+        Marionette,
+        MarionetteTest,
+        MarionetteTestResult,
+        MarionetteTextTestRunner,
+        MemoryEnduranceTestCaseMixin,
+        MozHttpd,
+        OptionParser,
+        TestManifest,
+        TestResult,
+        TestResultCollection
+)
 from wait import Wait
 from date_time_value import DateTimeValue
 import decorators
deleted file mode 100644
--- a/testing/marionette/client/marionette/b2gbuild.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import os
-import platform
-import subprocess
-import sys
-
-
-class B2GBuild(object):
-
-    @classmethod
-    def find_b2g_dir(cls):
-        for env_var in ('B2G_DIR', 'B2G_HOME'):
-            if env_var in os.environ:
-                env_dir = os.environ[env_var]
-                env_dir = cls.check_b2g_dir(env_dir)
-                if env_dir:
-                    return env_dir
-
-        cwd = os.getcwd()
-        cwd = cls.check_b2g_dir(cwd)
-        if cwd:
-            return cwd
-
-        return None
-
-    @classmethod
-    def check_adb(cls, homedir):
-        if 'ADB' in os.environ:
-            env_adb = os.environ['ADB']
-            if os.path.exists(env_adb):
-                return env_adb
-
-        return cls.check_host_binary(homedir, 'adb')
-
-    @classmethod
-    def check_b2g_dir(cls, dir):
-        if os.path.isfile(os.path.join(dir, 'load-config.sh')):
-            return dir
-
-        oldstyle_dir = os.path.join(dir, 'glue', 'gonk-ics')
-        if os.access(oldstyle_dir, os.F_OK):
-            return oldstyle_dir
-
-        return None
-
-    @classmethod
-    def check_fastboot(cls, homedir):
-        return cls.check_host_binary(homedir, 'fastboot')
-
-    @classmethod
-    def check_host_binary(cls, homedir, binary):
-        host_dir = "linux-x86"
-        if platform.system() == "Darwin":
-            host_dir = "darwin-x86"
-        binary_path = subprocess.Popen(['which', binary],
-                                       stdout=subprocess.PIPE,
-                                       stderr=subprocess.STDOUT)
-        if binary_path.wait() == 0:
-            return binary_path.stdout.read().strip()  # remove trailing newline
-        binary_paths = [os.path.join(homedir, 'glue', 'gonk', 'out', 'host',
-                                     host_dir, 'bin', binary),
-                        os.path.join(homedir, 'out', 'host', host_dir,
-                                     'bin', binary),
-                        os.path.join(homedir, 'bin', binary)]
-        for option in binary_paths:
-            if os.path.exists(option):
-                return option
-        raise Exception('%s not found!' % binary)
-
-    def __init__(self, homedir=None, emulator=False):
-        if not homedir:
-            homedir = self.find_b2g_dir()
-        else:
-            homedir = self.check_b2g_dir(homedir)
-
-        if not homedir:
-            raise EnvironmentError('Must define B2G_HOME or pass the homedir parameter')
-
-        self.homedir = homedir
-        self.adb_path = self.check_adb(self.homedir)
-        self.update_tools = os.path.join(self.homedir, 'tools', 'update-tools')
-        self.fastboot_path = None if emulator else self.check_fastboot(self.homedir)
-
-    def import_update_tools(self):
-        """Import the update_tools package from B2G"""
-        sys.path.append(self.update_tools)
-        import update_tools
-        sys.path.pop()
-        return update_tools
-
-    def check_file(self, filePath):
-        if not os.access(filePath, os.F_OK):
-            raise Exception(('File not found: %s; did you pass the B2G home '
-                             'directory as the homedir parameter, or set '
-                             'B2G_HOME correctly?') % filePath)
deleted file mode 100644
--- a/testing/marionette/client/marionette/b2ginstance.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-from ConfigParser import ConfigParser
-import posixpath
-import shutil
-import tempfile
-import traceback
-
-from b2gbuild import B2GBuild
-from mozdevice import DeviceManagerADB
-import mozcrash
-
-
-class B2GInstance(B2GBuild):
-
-    def __init__(self, devicemanager=None, symbols_path=None, **kwargs):
-        B2GBuild.__init__(self, **kwargs)
-
-        self._dm = devicemanager
-        self._remote_profiles = None
-        self.symbols_path = symbols_path
-
-    @property
-    def dm(self):
-        if not self._dm:
-            self._dm = DeviceManagerADB(adbPath=self.adb_path)
-        return self._dm
-
-    @property
-    def remote_profiles(self):
-        if not self._remote_profiles:
-            self.check_remote_profiles()
-        return self._remote_profiles
-
-    def check_remote_profiles(self, remote_profiles_ini='/data/b2g/mozilla/profiles.ini'):
-        if not self.dm.fileExists(remote_profiles_ini):
-            raise Exception("Remote file '%s' not found" % remote_profiles_ini)
-
-        local_profiles_ini = tempfile.NamedTemporaryFile()
-        self.dm.getFile(remote_profiles_ini, local_profiles_ini.name)
-        cfg = ConfigParser()
-        cfg.read(local_profiles_ini.name)
-
-        remote_profiles = []
-        for section in cfg.sections():
-            if cfg.has_option(section, 'Path'):
-                if cfg.has_option(section, 'IsRelative') and cfg.getint(section, 'IsRelative'):
-                    remote_profiles.append(posixpath.join(posixpath.dirname(remote_profiles_ini), cfg.get(section, 'Path')))
-                else:
-                    remote_profiles.append(cfg.get(section, 'Path'))
-        self._remote_profiles = remote_profiles
-        return remote_profiles
-
-    def check_for_crashes(self):
-        remote_dump_dirs = [posixpath.join(p, 'minidumps') for p in self.remote_profiles]
-        crashed = False
-        for remote_dump_dir in remote_dump_dirs:
-            local_dump_dir = tempfile.mkdtemp()
-            self.dm.getDirectory(remote_dump_dir, local_dump_dir)
-            try:
-                if mozcrash.check_for_crashes(local_dump_dir, self.symbols_path):
-                    crashed = True
-            except:
-                traceback.print_exc()
-            finally:
-                shutil.rmtree(local_dump_dir)
-                self.dm.removeDir(remote_dump_dir)
-        return crashed
deleted file mode 100644
--- a/testing/marionette/client/marionette/emulator.py
+++ /dev/null
@@ -1,539 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-from b2ginstance import B2GInstance
-import datetime
-from mozdevice import devicemanagerADB, DMError
-from mozprocess import ProcessHandlerMixin
-import os
-import re
-import platform
-import shutil
-import socket
-import subprocess
-from telnetlib import Telnet
-import tempfile
-import time
-import traceback
-
-from emulator_battery import EmulatorBattery
-from emulator_geo import EmulatorGeo
-from emulator_screen import EmulatorScreen
-from decorators import uses_marionette
-
-from errors import (
-    InstallGeckoError,
-    InvalidResponseException,
-    MarionetteException,
-    ScriptTimeoutException,
-    TimeoutException
-)
-
-
-class LogOutputProc(ProcessHandlerMixin):
-    """
-    Process handler for processes which save all output to a logfile.
-    If no logfile is specified, output will still be consumed to prevent
-    the output pipe's from overflowing.
-    """
-
-    def __init__(self, cmd, logfile=None,  **kwargs):
-        self.logfile = logfile
-        kwargs.setdefault('processOutputLine', []).append(self.log_output)
-        ProcessHandlerMixin.__init__(self, cmd, **kwargs)
-
-    def log_output(self, line):
-        if not self.logfile:
-            return
-
-        f = open(self.logfile, 'a')
-        f.write(line + "\n")
-        f.flush()
-
-
-class Emulator(object):
-
-    deviceRe = re.compile(r"^emulator-(\d+)(\s*)(.*)$")
-    _default_res = '320x480'
-    prefs = {'app.update.enabled': False,
-             'app.update.staging.enabled': False,
-             'app.update.service.enabled': False}
-    env = {'MOZ_CRASHREPORTER': '1',
-           'MOZ_CRASHREPORTER_NO_REPORT': '1',
-           'MOZ_CRASHREPORTER_SHUTDOWN': '1'}
-
-    def __init__(self, homedir=None, noWindow=False, logcat_dir=None,
-                 arch="x86", emulatorBinary=None, res=None, sdcard=None,
-                 symbols_path=None, userdata=None):
-        self.port = None
-        self.dm = None
-        self._emulator_launched = False
-        self.proc = None
-        self.marionette_port = None
-        self.telnet = None
-        self._tmp_sdcard = None
-        self._tmp_userdata = None
-        self._adb_started = False
-        self.logcat_dir = logcat_dir
-        self.logcat_proc = None
-        self.arch = arch
-        self.binary = emulatorBinary
-        self.res = res or self._default_res
-        self.battery = EmulatorBattery(self)
-        self.geo = EmulatorGeo(self)
-        self.screen = EmulatorScreen(self)
-        self.homedir = homedir
-        self.sdcard = sdcard
-        self.symbols_path = symbols_path
-        self.noWindow = noWindow
-        if self.homedir is not None:
-            self.homedir = os.path.expanduser(homedir)
-        self.dataImg = userdata
-        self.copy_userdata = self.dataImg is None
-
-    def _check_for_b2g(self):
-        self.b2g = B2GInstance(homedir=self.homedir, emulator=True,
-                               symbols_path=self.symbols_path)
-        self.adb = self.b2g.adb_path
-        self.homedir = self.b2g.homedir
-
-        if self.arch not in ("x86", "arm"):
-            raise Exception("Emulator architecture must be one of x86, arm, got: %s" %
-                            self.arch)
-
-        host_dir = "linux-x86"
-        if platform.system() == "Darwin":
-            host_dir = "darwin-x86"
-
-        host_bin_dir = os.path.join("out", "host", host_dir, "bin")
-
-        if self.arch == "x86":
-            binary = os.path.join(host_bin_dir, "emulator-x86")
-            kernel = "prebuilts/qemu-kernel/x86/kernel-qemu"
-            sysdir = "out/target/product/generic_x86"
-            self.tail_args = []
-        else:
-            binary = os.path.join(host_bin_dir, "emulator")
-            kernel = "prebuilts/qemu-kernel/arm/kernel-qemu-armv7"
-            sysdir = "out/target/product/generic"
-            self.tail_args = ["-cpu", "cortex-a8"]
-
-        if(self.sdcard):
-            self.mksdcard = os.path.join(self.homedir, host_bin_dir, "mksdcard")
-            self.create_sdcard(self.sdcard)
-
-        if not self.binary:
-            self.binary = os.path.join(self.homedir, binary)
-
-        self.b2g.check_file(self.binary)
-
-        self.kernelImg = os.path.join(self.homedir, kernel)
-        self.b2g.check_file(self.kernelImg)
-
-        self.sysDir = os.path.join(self.homedir, sysdir)
-        self.b2g.check_file(self.sysDir)
-
-        if not self.dataImg:
-            self.dataImg = os.path.join(self.sysDir, 'userdata.img')
-        self.b2g.check_file(self.dataImg)
-
-    def __del__(self):
-        if self.telnet:
-            self.telnet.write('exit\n')
-            self.telnet.read_all()
-
-    @property
-    def args(self):
-        qemuArgs = [self.binary,
-                    '-kernel', self.kernelImg,
-                    '-sysdir', self.sysDir,
-                    '-data', self.dataImg]
-        if self._tmp_sdcard:
-            qemuArgs.extend(['-sdcard', self._tmp_sdcard])
-        if self.noWindow:
-            qemuArgs.append('-no-window')
-        qemuArgs.extend(['-memory', '512',
-                         '-partition-size', '512',
-                         '-verbose',
-                         '-skin', self.res,
-                         '-gpu', 'on',
-                         '-qemu'] + self.tail_args)
-        return qemuArgs
-
-    @property
-    def is_running(self):
-        if self._emulator_launched:
-            return self.proc is not None and self.proc.poll() is None
-        else:
-            return self.port is not None
-
-    def check_for_crash(self):
-        """
-        Checks if the emulator has crashed or not.  Always returns False if
-        we've connected to an already-running emulator, since we can't track
-        the emulator's pid in that case.  Otherwise, returns True iff
-        self.proc is not None (meaning the emulator hasn't been explicitly
-        closed), and self.proc.poll() is also not None (meaning the emulator
-        process has terminated).
-        """
-        return self._emulator_launched and self.proc is not None \
-                                       and self.proc.poll() is not None
-
-    def check_for_minidumps(self):
-        return self.b2g.check_for_crashes()
-
-    def create_sdcard(self, sdcard):
-        self._tmp_sdcard = tempfile.mktemp(prefix='sdcard')
-        sdargs = [self.mksdcard, "-l", "mySdCard", sdcard, self._tmp_sdcard]
-        sd = subprocess.Popen(sdargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-        retcode = sd.wait()
-        if retcode:
-            raise Exception('unable to create sdcard : exit code %d: %s'
-                            % (retcode, sd.stdout.read()))
-        return None
-
-    def _run_adb(self, args):
-        args.insert(0, self.adb)
-        if self.port:
-            args.insert(1, '-s')
-            args.insert(2, 'emulator-%d' % self.port)
-        adb = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-        retcode = adb.wait()
-        if retcode:
-            raise Exception('adb terminated with exit code %d: %s'
-                            % (retcode, adb.stdout.read()))
-        return adb.stdout.read()
-
-    def _get_telnet_response(self, command=None):
-        output = []
-        assert(self.telnet)
-        if command is not None:
-            self.telnet.write('%s\n' % command)
-        while True:
-            line = self.telnet.read_until('\n')
-            output.append(line.rstrip())
-            if line.startswith('OK'):
-                return output
-            elif line.startswith('KO:'):
-                raise Exception('bad telnet response: %s' % line)
-
-    def _run_telnet(self, command):
-        if not self.telnet:
-            self.telnet = Telnet('localhost', self.port)
-            self._get_telnet_response()
-        return self._get_telnet_response(command)
-
-    def _run_shell(self, args):
-        args.insert(0, 'shell')
-        return self._run_adb(args).split('\r\n')
-
-    def close(self):
-        if self.is_running and self._emulator_launched:
-            self.proc.kill()
-        if self._adb_started:
-            self._run_adb(['kill-server'])
-            self._adb_started = False
-        if self.proc:
-            retcode = self.proc.poll()
-            self.proc = None
-            if self._tmp_userdata:
-                os.remove(self._tmp_userdata)
-                self._tmp_userdata = None
-            if self._tmp_sdcard:
-                os.remove(self._tmp_sdcard)
-                self._tmp_sdcard = None
-            return retcode
-        if self.logcat_proc and self.logcat_proc.proc.poll() is None:
-            self.logcat_proc.kill()
-        return 0
-
-    def _get_adb_devices(self):
-        offline = set()
-        online = set()
-        output = self._run_adb(['devices'])
-        for line in output.split('\n'):
-            m = self.deviceRe.match(line)
-            if m:
-                if m.group(3) == 'offline':
-                    offline.add(m.group(1))
-                else:
-                    online.add(m.group(1))
-        return (online, offline)
-
-    def start_adb(self):
-        result = self._run_adb(['start-server'])
-        # We keep track of whether we've started adb or not, so we know
-        # if we need to kill it.
-        if 'daemon started successfully' in result:
-            self._adb_started = True
-        else:
-            self._adb_started = False
-
-    @uses_marionette
-    def wait_for_system_message(self, marionette):
-        marionette.set_script_timeout(45000)
-        # Telephony API's won't be available immediately upon emulator
-        # boot; we have to wait for the syste-message-listener-ready
-        # message before we'll be able to use them successfully.  See
-        # bug 792647.
-        print 'waiting for system-message-listener-ready...'
-        try:
-            marionette.execute_async_script("""
-waitFor(
-    function() { marionetteScriptFinished(true); },
-    function() { return isSystemMessageListenerReady(); }
-);
-            """)
-        except ScriptTimeoutException:
-            print 'timed out'
-            # We silently ignore the timeout if it occurs, since
-            # isSystemMessageListenerReady() isn't available on
-            # older emulators.  45s *should* be enough of a delay
-            # to allow telephony API's to work.
-            pass
-        except (InvalidResponseException, IOError):
-            self.check_for_minidumps()
-            raise
-        print '...done'
-
-    def connect(self):
-        self.adb = B2GInstance.check_adb(self.homedir, emulator=True)
-        self.start_adb()
-
-        online, offline = self._get_adb_devices()
-        now = datetime.datetime.now()
-        while online == set([]):
-            time.sleep(1)
-            if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
-                raise Exception('timed out waiting for emulator to be available')
-            online, offline = self._get_adb_devices()
-        self.port = int(list(online)[0])
-
-        self.dm = devicemanagerADB.DeviceManagerADB(adbPath=self.adb,
-                                                    deviceSerial='emulator-%d' % self.port)
-
-    def start(self):
-        self._check_for_b2g()
-        self.start_adb()
-
-        qemu_args = self.args[:]
-        if self.copy_userdata:
-            # Make a copy of the userdata.img for this instance of the emulator to use.
-            self._tmp_userdata = tempfile.mktemp(prefix='marionette')
-            shutil.copyfile(self.dataImg, self._tmp_userdata)
-            qemu_args[qemu_args.index('-data') + 1] = self._tmp_userdata
-
-        original_online, original_offline = self._get_adb_devices()
-
-        filename = None
-        if self.logcat_dir:
-            filename = os.path.join(self.logcat_dir, 'qemu.log')
-            if os.path.isfile(filename):
-                self.rotate_log(filename)
-
-        self.proc = LogOutputProc(qemu_args, filename)
-        self.proc.run()
-
-        online, offline = self._get_adb_devices()
-        now = datetime.datetime.now()
-        while online - original_online == set([]):
-            time.sleep(1)
-            if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
-                raise TimeoutException('timed out waiting for emulator to start')
-            online, offline = self._get_adb_devices()
-        self.port = int(list(online - original_online)[0])
-        self._emulator_launched = True
-
-        self.dm = devicemanagerADB.DeviceManagerADB(adbPath=self.adb,
-                                                    deviceSerial='emulator-%d' % self.port)
-
-        # bug 802877
-        time.sleep(10)
-        self.geo.set_default_location()
-        self.screen.initialize()
-
-        if self.logcat_dir:
-            self.save_logcat()
-
-        # setup DNS fix for networking
-        self._run_adb(['shell', 'setprop', 'net.dns1', '10.0.2.3'])
-
-    @uses_marionette
-    def wait_for_homescreen(self, marionette):
-        print 'waiting for homescreen...'
-
-        marionette.set_context(marionette.CONTEXT_CONTENT)
-        marionette.execute_async_script("""
-log('waiting for mozbrowserloadend');
-window.addEventListener('mozbrowserloadend', function loaded(aEvent) {
-  log('received mozbrowserloadend for ' + aEvent.target.src);
-  if (aEvent.target.src.indexOf('ftu') != -1 || aEvent.target.src.indexOf('homescreen') != -1) {
-    window.removeEventListener('mozbrowserloadend', loaded);
-    marionetteScriptFinished();
-  }
-});""", script_timeout=120000)
-        print '...done'
-
-    def setup(self, marionette, gecko_path=None, busybox=None):
-        self.set_environment(marionette)
-        if busybox:
-            self.install_busybox(busybox)
-
-        if gecko_path:
-            self.install_gecko(gecko_path, marionette)
-
-        self.wait_for_system_message(marionette)
-        self.set_prefs(marionette)
-
-    @uses_marionette
-    def set_environment(self, marionette):
-        for k, v in self.env.iteritems():
-            marionette.execute_script("""
-            let env = Cc["@mozilla.org/process/environment;1"].
-                      getService(Ci.nsIEnvironment);
-            env.set("%s", "%s");
-            """ % (k, v))
-
-    @uses_marionette
-    def set_prefs(self, marionette):
-        for pref in self.prefs:
-            marionette.execute_script("""
-            Components.utils.import("resource://gre/modules/Services.jsm");
-            let argtype = typeof(arguments[1]);
-            switch(argtype) {
-                case 'boolean':
-                    Services.prefs.setBoolPref(arguments[0], arguments[1]);
-                    break;
-                case 'number':
-                    Services.prefs.setIntPref(arguments[0], arguments[1]);
-                    break;
-                default:
-                    Services.prefs.setCharPref(arguments[0], arguments[1]);
-            }
-            """, [pref, self.prefs[pref]])
-
-    def restart_b2g(self):
-        print 'restarting B2G'
-        self.dm.shellCheckOutput(['stop', 'b2g'])
-        time.sleep(10)
-        self.dm.shellCheckOutput(['start', 'b2g'])
-
-        if not self.wait_for_port():
-            raise TimeoutException("Timeout waiting for marionette on port '%s'" % self.marionette_port)
-
-    def install_gecko(self, gecko_path, marionette):
-        """
-        Install gecko into the emulator using adb push.  Restart b2g after the
-        installation.
-        """
-        # See bug 800102.  We use this particular method of installing
-        # gecko in order to avoid an adb bug in which adb will sometimes
-        # hang indefinitely while copying large files to the system
-        # partition.
-        print 'installing gecko binaries...'
-
-        # see bug 809437 for the path that lead to this madness
-        try:
-            # need to remount so we can write to /system/b2g
-            self._run_adb(['remount'])
-            self.dm.removeDir('/data/local/b2g')
-            self.dm.mkDir('/data/local/b2g')
-            self.dm.pushDir(gecko_path, '/data/local/b2g', retryLimit=10)
-
-            self.dm.shellCheckOutput(['stop', 'b2g'])
-
-            for root, dirs, files in os.walk(gecko_path):
-                for filename in files:
-                    rel_path = os.path.relpath(os.path.join(root, filename), gecko_path)
-                    data_local_file = os.path.join('/data/local/b2g', rel_path)
-                    system_b2g_file = os.path.join('/system/b2g', rel_path)
-
-                    print 'copying', data_local_file, 'to', system_b2g_file
-                    self.dm.shellCheckOutput(['dd',
-                                              'if=%s' % data_local_file,
-                                              'of=%s' % system_b2g_file])
-            self.restart_b2g()
-
-        except (DMError, MarionetteException):
-            # Bug 812395 - raise a single exception type for these so we can
-            # explicitly catch them elsewhere.
-
-            # print exception, but hide from mozharness error detection
-            exc = traceback.format_exc()
-            exc = exc.replace('Traceback', '_traceback')
-            print exc
-
-            raise InstallGeckoError("unable to restart B2G after installing gecko")
-
-    def install_busybox(self, busybox):
-        self._run_adb(['remount'])
-
-        remote_file = "/system/bin/busybox"
-        print 'pushing %s' % remote_file
-        self.dm.pushFile(busybox, remote_file, retryLimit=10)
-        self._run_adb(['shell', 'cd /system/bin; chmod 555 busybox; for x in `./busybox --list`; do ln -s ./busybox $x; done'])
-        self.dm._verifyZip()
-
-    def rotate_log(self, srclog, index=1):
-        """ Rotate a logfile, by recursively rotating logs further in the sequence,
-            deleting the last file if necessary.
-        """
-        basename = os.path.basename(srclog)
-        basename = basename[:-len('.log')]
-        if index > 1:
-            basename = basename[:-len('.1')]
-        basename = '%s.%d.log' % (basename, index)
-
-        destlog = os.path.join(self.logcat_dir, basename)
-        if os.path.isfile(destlog):
-            if index == 3:
-                os.remove(destlog)
-            else:
-                self.rotate_log(destlog, index+1)
-        shutil.move(srclog, destlog)
-
-    def save_logcat(self):
-        """ Save the output of logcat to a file.
-        """
-        filename = os.path.join(self.logcat_dir, "emulator-%d.log" % self.port)
-        if os.path.isfile(filename):
-            self.rotate_log(filename)
-        cmd = [self.adb, '-s', 'emulator-%d' % self.port, 'logcat', '-v', 'threadtime']
-
-        self.logcat_proc = LogOutputProc(cmd, filename)
-        self.logcat_proc.run()
-
-    def setup_port_forwarding(self, remote_port):
-        """ Set up TCP port forwarding to the specified port on the device,
-            using any availble local port, and return the local port.
-        """
-        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        s.bind(("",0))
-        local_port = s.getsockname()[1]
-        s.close()
-
-        self._run_adb(['forward',
-                       'tcp:%d' % local_port,
-                       'tcp:%d' % remote_port])
-
-        self.marionette_port = local_port
-
-        return local_port
-
-    def wait_for_port(self, timeout=300):
-        assert(self.marionette_port)
-        starttime = datetime.datetime.now()
-        while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
-            try:
-                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-                sock.connect(('localhost', self.marionette_port))
-                data = sock.recv(16)
-                sock.close()
-                if ':' in data:
-                    return True
-            except:
-                import traceback
-                print traceback.format_exc()
-            time.sleep(1)
-        return False
deleted file mode 100644
--- a/testing/marionette/client/marionette/emulator_battery.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-class EmulatorBattery(object):
-
-    def __init__(self, emulator):
-        self.emulator = emulator
-
-    def get_state(self):
-        status = {}
-        state = {}
-
-        response = self.emulator._run_telnet('power display')
-        for line in response:
-            if ':' in line:
-                field, value = line.split(':')
-                value = value.strip()
-                if value == 'true':
-                    value = True
-                elif value == 'false':
-                    value = False
-                elif field == 'capacity':
-                    value = float(value)
-                status[field] = value
-
-        state['level'] = status.get('capacity', 0.0) / 100
-        if status.get('AC') == 'online':
-            state['charging'] = True
-        else:
-            state['charging'] = False
-
-        return state
-
-    def get_charging(self):
-        return self.get_state()['charging']
-
-    def get_level(self):
-        return self.get_state()['level']
-
-    def set_level(self, level):
-        self.emulator._run_telnet('power capacity %d' % (level * 100))
-
-    def set_charging(self, charging):
-        if charging:
-            cmd = 'power ac on'
-        else:
-            cmd = 'power ac off'
-        self.emulator._run_telnet(cmd)
-
-    charging = property(get_charging, set_charging)
-    level = property(get_level, set_level)
-
deleted file mode 100644
--- a/testing/marionette/client/marionette/emulator_geo.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-class EmulatorGeo(object):
-
-    def __init__(self, emulator):
-        self.emulator = emulator
-
-    def set_default_location(self):
-        self.lon = -122.08769
-        self.lat = 37.41857
-        self.set_location(self.lon, self.lat)
-
-    def set_location(self, lon, lat):
-        self.emulator._run_telnet('geo fix %0.5f %0.5f' % (self.lon, self.lat))
-
deleted file mode 100644
--- a/testing/marionette/client/marionette/emulator_screen.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-class EmulatorScreen(object):
-    """Class for screen related emulator commands."""
-
-    SO_PORTRAIT_PRIMARY = 'portrait-primary'
-    SO_PORTRAIT_SECONDARY = 'portrait-secondary'
-    SO_LANDSCAPE_PRIMARY = 'landscape-primary'
-    SO_LANDSCAPE_SECONDARY = 'landscape-secondary'
-
-    def __init__(self, emulator):
-        self.emulator = emulator
-
-    def initialize(self):
-        self.orientation = self.SO_PORTRAIT_PRIMARY
-
-    def _get_raw_orientation(self):
-        """Get the raw value of the current device orientation."""
-        response = self.emulator._run_telnet('sensor get orientation')
-
-        return response[0].split('=')[1].strip()
-
-    def _set_raw_orientation(self, data):
-        """Set the raw value of the specified device orientation."""
-        self.emulator._run_telnet('sensor set orientation %s' % data)
-
-    def get_orientation(self):
-        """Get the current device orientation.
-
-        Returns;
-            orientation -- Orientation of the device. One of:
-                            SO_PORTRAIT_PRIMARY - system buttons at the bottom
-                            SO_PORTRIAT_SECONDARY - system buttons at the top
-                            SO_LANDSCAPE_PRIMARY - system buttons at the right
-                            SO_LANDSCAPE_SECONDARY - system buttons at the left
-
-        """
-        data = self._get_raw_orientation()
-
-        if data == '0:-90:0':
-            orientation = self.SO_PORTRAIT_PRIMARY
-        elif data == '0:90:0':
-            orientation = self.SO_PORTRAIT_SECONDARY
-        elif data == '0:0:90':
-            orientation = self.SO_LANDSCAPE_PRIMARY
-        elif data == '0:0:-90':
-            orientation = self.SO_LANDSCAPE_SECONDARY
-        else:
-            raise ValueError('Unknown orientation sensor value: %s.' % data)
-
-        return orientation
-
-    def set_orientation(self, orientation):
-        """Set the specified device orientation.
-
-        Args
-            orientation -- Orientation of the device. One of:
-                            SO_PORTRAIT_PRIMARY - system buttons at the bottom
-                            SO_PORTRIAT_SECONDARY - system buttons at the top
-                            SO_LANDSCAPE_PRIMARY - system buttons at the right
-                            SO_LANDSCAPE_SECONDARY - system buttons at the left
-        """
-        if orientation == self.SO_PORTRAIT_PRIMARY:
-            data = '0:-90:0'
-        elif orientation == self.SO_PORTRAIT_SECONDARY:
-            data = '0:90:0'
-        elif orientation == self.SO_LANDSCAPE_PRIMARY:
-            data = '0:0:90'
-        elif orientation == self.SO_LANDSCAPE_SECONDARY:
-            data = '0:0:-90'
-        else:
-            raise ValueError('Invalid orientation: %s' % orientation)
-
-        self._set_raw_orientation(data)
-
-    orientation = property(get_orientation, set_orientation)
--- a/testing/marionette/client/marionette/geckoinstance.py
+++ b/testing/marionette/client/marionette/geckoinstance.py
@@ -20,31 +20,30 @@ class GeckoInstance(object):
                       "browser.sessionstore.resume_from_crash": False,
                       "browser.warnOnQuit": False}
 
     def __init__(self, host, port, bin, profile, app_args=None, symbols_path=None,
                   gecko_log=None):
         self.marionette_host = host
         self.marionette_port = port
         self.bin = bin
-        self.profile = profile
+        self.profile_path = profile
         self.app_args = app_args or []
         self.runner = None
         self.symbols_path = symbols_path
         self.gecko_log = gecko_log
 
     def start(self):
-        profile_path = self.profile
         profile_args = {"preferences": self.required_prefs}
-        if not profile_path:
-            runner_class = Runner
+        if not self.profile_path:
             profile_args["restore"] = False
+            profile = Profile(**profile_args)
         else:
-            runner_class = CloneRunner
-            profile_args["path_from"] = profile_path
+            profile_args["path_from"] = self.profile_path
+            profile = Profile.clone(**profile_args)
 
         if self.gecko_log is None:
             self.gecko_log = 'gecko.log'
         elif os.path.isdir(self.gecko_log):
             fname = "gecko-%d.log" % time.time()
             self.gecko_log = os.path.join(self.gecko_log, fname)
 
         self.gecko_log = os.path.realpath(self.gecko_log)
@@ -52,44 +51,39 @@ class GeckoInstance(object):
             os.remove(self.gecko_log)
 
         env = os.environ.copy()
 
         # environment variables needed for crashreporting
         # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
         env.update({ 'MOZ_CRASHREPORTER': '1',
                      'MOZ_CRASHREPORTER_NO_REPORT': '1', })
-        self.runner = runner_class.create(
+        self.runner = Runner(
             binary=self.bin,
-            profile_args=profile_args,
+            profile=profile,
             cmdargs=['-no-remote', '-marionette'] + self.app_args,
             env=env,
             symbols_path=self.symbols_path,
-            kp_kwargs={
+            process_args={
                 'processOutputLine': [NullOutput()],
                 'logfile': self.gecko_log})
         self.runner.start()
 
     def check_for_crashes(self):
         return self.runner.check_for_crashes()
 
     def close(self):
-        self.runner.stop()
-        self.runner.cleanup()
+        if self.runner:
+            self.runner.stop()
+            self.runner.cleanup()
 
 
 class B2GDesktopInstance(GeckoInstance):
+    required_prefs = {"focusmanager.testmode": True}
 
-    required_prefs = {"focusmanager.testmode": True}
+
+class NullOutput(object):
+    def __call__(self, line):
+        pass
+
 
 apps = {'b2g': B2GDesktopInstance,
         'b2gdesktop': B2GDesktopInstance}
-
-
-class CloneRunner(Runner):
-
-    profile_class = Profile.clone
-
-
-class NullOutput(object):
-
-    def __call__(self, line):
-        pass
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -1,37 +1,30 @@
 # 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 datetime
 import os
 import socket
-import sys
+import StringIO
 import time
 import traceback
 import base64
 
 from application_cache import ApplicationCache
 from decorators import do_crash_check
-from emulator import Emulator
-from emulator_screen import EmulatorScreen
-from errors import (
-        ErrorCodes, MarionetteException, InstallGeckoError, TimeoutException, InvalidResponseException,
-        JavascriptException, NoSuchElementException, XPathLookupException, NoSuchWindowException,
-        StaleElementException, ScriptTimeoutException, ElementNotVisibleException,
-        NoSuchFrameException, InvalidElementStateException, NoAlertPresentException,
-        InvalidCookieDomainException, UnableToSetCookieException, InvalidSelectorException,
-        MoveTargetOutOfBoundsException, FrameSendNotInitializedError, FrameSendFailureError
-        )
 from keys import Keys
 from marionette_transport import MarionetteTransport
 
+from mozrunner import B2GEmulatorRunner
+
 import geckoinstance
+import errors
 
 class HTMLElement(object):
     """
     Represents a DOM Element.
     """
 
     CLASS = "class name"
     SELECTOR = "css selector"
@@ -446,51 +439,43 @@ class Marionette(object):
     Represents a Marionette connection to a browser or device.
     """
 
     CONTEXT_CHROME = 'chrome' # non-browser content: windows, dialogs, etc.
     CONTEXT_CONTENT = 'content' # browser content: iframes, divs, etc.
     TIMEOUT_SEARCH = 'implicit'
     TIMEOUT_SCRIPT = 'script'
     TIMEOUT_PAGE = 'page load'
-    SCREEN_ORIENTATIONS = {"portrait": EmulatorScreen.SO_PORTRAIT_PRIMARY,
-                           "landscape": EmulatorScreen.SO_LANDSCAPE_PRIMARY,
-                           "portrait-primary": EmulatorScreen.SO_PORTRAIT_PRIMARY,
-                           "landscape-primary": EmulatorScreen.SO_LANDSCAPE_PRIMARY,
-                           "portrait-secondary": EmulatorScreen.SO_PORTRAIT_SECONDARY,
-                           "landscape-secondary": EmulatorScreen.SO_LANDSCAPE_SECONDARY}
 
     def __init__(self, host='localhost', port=2828, app=None, app_args=None, bin=None,
-                 profile=None, emulator=None, sdcard=None, emulatorBinary=None,
-                 emulatorImg=None, emulator_res=None, gecko_path=None,
-                 connectToRunningEmulator=False, homedir=None, baseurl=None,
-                 noWindow=False, logcat_dir=None, busybox=None, symbols_path=None,
-                 timeout=None, device_serial=None, gecko_log=None):
+                 profile=None, emulator=None, sdcard=None, emulator_img=None,
+                 emulator_binary=None, emulator_res=None, connect_to_running_emulator=False,
+                 gecko_log=None, homedir=None, baseurl=None, no_window=False, logdir=None,
+                 busybox=None, symbols_path=None, timeout=None, device_serial=None,
+                 adb_path=None):
         self.host = host
         self.port = self.local_port = port
         self.bin = bin
         self.instance = None
-        self.profile = profile
         self.session = None
         self.window = None
+        self.runner = None
         self.emulator = None
         self.extra_emulators = []
-        self.homedir = homedir
         self.baseurl = baseurl
-        self.noWindow = noWindow
-        self.logcat_dir = logcat_dir
+        self.no_window = no_window
         self._test_name = None
         self.timeout = timeout
         self.device_serial = device_serial
 
         if bin:
             port = int(self.port)
             if not Marionette.is_port_available(port, host=self.host):
                 ex_msg = "%s:%d is unavailable." % (self.host, port)
-                raise MarionetteException(message=ex_msg)
+                raise errors.MarionetteException(message=ex_msg)
             if app:
                 # select instance class for the given app
                 try:
                     instance_class = geckoinstance.apps[app]
                 except KeyError:
                     msg = 'Application "%s" unknown (should be one of %s)'
                     raise NotImplementedError(msg % (app, geckoinstance.apps.keys()))
             else:
@@ -499,62 +484,66 @@ class Marionette(object):
                     config.read(os.path.join(os.path.dirname(bin), 'application.ini'))
                     app = config.get('App', 'Name')
                     instance_class = geckoinstance.apps[app.lower()]
                 except (ConfigParser.NoOptionError,
                         ConfigParser.NoSectionError,
                         KeyError):
                     instance_class = geckoinstance.GeckoInstance
             self.instance = instance_class(host=self.host, port=self.port,
-                                           bin=self.bin, profile=self.profile,
+                                           bin=self.bin, profile=profile,
                                            app_args=app_args, symbols_path=symbols_path,
                                            gecko_log=gecko_log)
             self.instance.start()
             assert(self.wait_for_port()), "Timed out waiting for port!"
 
         if emulator:
-            self.emulator = Emulator(homedir=homedir,
-                                     noWindow=self.noWindow,
-                                     logcat_dir=self.logcat_dir,
-                                     arch=emulator,
-                                     sdcard=sdcard,
-                                     symbols_path=symbols_path,
-                                     emulatorBinary=emulatorBinary,
-                                     userdata=emulatorImg,
-                                     res=emulator_res)
+            self.runner = B2GEmulatorRunner(b2g_home=homedir,
+                                            no_window=self.no_window,
+                                            logdir=logdir,
+                                            arch=emulator,
+                                            sdcard=sdcard,
+                                            symbols_path=symbols_path,
+                                            binary=emulator_binary,
+                                            userdata=emulator_img,
+                                            resolution=emulator_res,
+                                            profile=profile,
+                                            adb_path=adb_path)
+            self.emulator = self.runner.device
             self.emulator.start()
             self.port = self.emulator.setup_port_forwarding(self.port)
-            assert(self.emulator.wait_for_port()), "Timed out waiting for port!"
+            assert(self.emulator.wait_for_port(self.port)), "Timed out waiting for port!"
 
-        if connectToRunningEmulator:
-            self.emulator = Emulator(homedir=homedir,
-                                     logcat_dir=self.logcat_dir)
+        if connect_to_running_emulator:
+            self.runner = B2GEmulatorRunner(b2g_home=homedir,
+                                            logdir=logdir)
+            self.emulator = self.runner.device
             self.emulator.connect()
             self.port = self.emulator.setup_port_forwarding(self.port)
-            assert(self.emulator.wait_for_port()), "Timed out waiting for port!"
+            assert(self.emulator.wait_for_port(self.port)), "Timed out waiting for port!"
 
         self.client = MarionetteTransport(self.host, self.port)
 
         if emulator:
-            self.emulator.setup(self,
-                                gecko_path=gecko_path,
-                                busybox=busybox)
+            if busybox:
+                self.emulator.install_busybox(busybox=busybox)
+            self.emulator.wait_for_system_message(self)
 
     def cleanup(self):
         if self.session:
             try:
                 self.delete_session()
-            except (MarionetteException, socket.error, IOError):
+            except (errors.MarionetteException, socket.error, IOError):
                 # These exceptions get thrown if the Marionette server
                 # hit an exception/died or the connection died. We can
                 # do no further server-side cleanup in this case.
                 pass
             self.session = None
-        if self.emulator:
-            self.emulator.close()
+        if self.runner:
+            self.runner.cleanup()
         if self.instance:
             self.instance.close()
         for qemu in self.extra_emulators:
             qemu.emulator.close()
 
     def __del__(self):
         self.cleanup()
 
@@ -565,36 +554,16 @@ class Marionette(object):
         try:
             s.bind((host, port))
             return True
         except socket.error:
             return False
         finally:
             s.close()
 
-    @classmethod
-    def getMarionetteOrExit(cls, *args, **kwargs):
-        try:
-            m = cls(*args, **kwargs)
-            return m
-        except InstallGeckoError:
-            # Bug 812395 - the process of installing gecko into the emulator
-            # and then restarting B2G tickles some bug in the emulator/b2g
-            # that intermittently causes B2G to fail to restart.  To work
-            # around this in TBPL runs, we will fail gracefully from this
-            # error so that the mozharness script can try the run again.
-
-            # This string will get caught by mozharness and will cause it
-            # to retry the tests.
-            print "Error installing gecko!"
-
-            # Exit without a normal exception to prevent mozharness from
-            # flagging the error.
-            sys.exit()
-
     def wait_for_port(self, timeout=60):
         starttime = datetime.datetime.now()
         while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
             try:
                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                 sock.connect((self.host, self.port))
                 data = sock.recv(16)
                 sock.close()
@@ -604,32 +573,32 @@ class Marionette(object):
             except socket.error:
                 pass
             time.sleep(1)
         return False
 
     @do_crash_check
     def _send_message(self, command, response_key="ok", **kwargs):
         if not self.session and command != "newSession":
-            raise MarionetteException("Please start a session")
+            raise errors.MarionetteException("Please start a session")
 
         message = {"name": command}
         if self.session:
             message["sessionId"] = self.session
         if kwargs:
             message["parameters"] = kwargs
 
         try:
             response = self.client.send(message)
         except socket.timeout:
             self.session = None
             self.window = None
             self.client.close()
-            raise TimeoutException(
-                "Connection timed out", status=ErrorCodes.TIMEOUT)
+            raise errors.TimeoutException(
+                "Connection timed out", status=errors.ErrorCodes.TIMEOUT)
 
         # Process any emulator commands that are sent from a script
         # while it's executing.
         while True:
             if response.get("emulator_cmd"):
                 response = self._handle_emulator_cmd(response)
                 continue;
 
@@ -641,97 +610,97 @@ class Marionette(object):
 
         if response_key in response:
             return response[response_key]
         self._handle_error(response)
 
     def _handle_emulator_cmd(self, response):
         cmd = response.get("emulator_cmd")
         if not cmd or not self.emulator:
-            raise MarionetteException(
+            raise errors.MarionetteException(
                 "No emulator in this test to run command against")
         cmd = cmd.encode("ascii")
         result = self.emulator._run_telnet(cmd)
         return self.client.send({"name": "emulatorCmdResult",
                                  "id": response.get("id"),
                                  "result": result})
 
     def _handle_emulator_shell(self, response):
         args = response.get("emulator_shell")
         if not isinstance(args, list) or not self.emulator:
-            raise MarionetteException(
+            raise errors.MarionetteException(
                 "No emulator in this test to run shell command against")
-        result = self.emulator._run_shell(args)
+        buf = StringIO.StringIO()
+        self.emulator.dm.shell(args, buf)
+        result = str(buf.getvalue()[0:-1]).rstrip().splitlines()
+        buf.close()
         return self.client.send({"name": "emulatorCmdResult",
                                  "id": response.get("id"),
                                  "result": result})
 
     def _handle_error(self, response):
         if 'error' in response and isinstance(response['error'], dict):
             status = response['error'].get('status', 500)
             message = response['error'].get('message')
             stacktrace = response['error'].get('stacktrace')
             # status numbers come from
             # http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
-            if status == ErrorCodes.NO_SUCH_ELEMENT:
-                raise NoSuchElementException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.NO_SUCH_FRAME:
-                raise NoSuchFrameException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.STALE_ELEMENT_REFERENCE:
-                raise StaleElementException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.ELEMENT_NOT_VISIBLE:
-                raise ElementNotVisibleException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.INVALID_ELEMENT_STATE:
-                raise InvalidElementStateException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.UNKNOWN_ERROR:
-                raise MarionetteException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.ELEMENT_IS_NOT_SELECTABLE:
-                raise ElementNotSelectableException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.JAVASCRIPT_ERROR:
-                raise JavascriptException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.XPATH_LOOKUP_ERROR:
-                raise XPathLookupException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.TIMEOUT:
-                raise TimeoutException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.NO_SUCH_WINDOW:
-                raise NoSuchWindowException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.INVALID_COOKIE_DOMAIN:
-                raise InvalidCookieDomainException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.UNABLE_TO_SET_COOKIE:
-                raise UnableToSetCookieException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.NO_ALERT_OPEN:
-                raise NoAlertPresentException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.SCRIPT_TIMEOUT:
-                raise ScriptTimeoutException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.INVALID_SELECTOR \
-                 or status == ErrorCodes.INVALID_XPATH_SELECTOR \
-                 or status == ErrorCodes.INVALID_XPATH_SELECTOR_RETURN_TYPER:
-                raise InvalidSelectorException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.MOVE_TARGET_OUT_OF_BOUNDS:
-                raise MoveTargetOutOfBoundsException(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.FRAME_SEND_NOT_INITIALIZED_ERROR:
-                raise FrameSendNotInitializedError(message=message, status=status, stacktrace=stacktrace)
-            elif status == ErrorCodes.FRAME_SEND_FAILURE_ERROR:
-                raise FrameSendFailureError(message=message, status=status, stacktrace=stacktrace)
+            if status == errors.ErrorCodes.NO_SUCH_ELEMENT:
+                raise errors.NoSuchElementException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.NO_SUCH_FRAME:
+                raise errors.NoSuchFrameException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.STALE_ELEMENT_REFERENCE:
+                raise errors.StaleElementException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.ELEMENT_NOT_VISIBLE:
+                raise errors.ElementNotVisibleException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.INVALID_ELEMENT_STATE:
+                raise errors.InvalidElementStateException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.UNKNOWN_ERROR:
+                raise errors.MarionetteException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.ELEMENT_IS_NOT_SELECTABLE:
+                raise errors.ElementNotSelectableException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.JAVASCRIPT_ERROR:
+                raise errors.JavascriptException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.XPATH_LOOKUP_ERROR:
+                raise errors.XPathLookupException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.TIMEOUT:
+                raise errors.TimeoutException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.NO_SUCH_WINDOW:
+                raise errors.NoSuchWindowException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.INVALID_COOKIE_DOMAIN:
+                raise errors.InvalidCookieDomainException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.UNABLE_TO_SET_COOKIE:
+                raise errors.UnableToSetCookieException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.NO_ALERT_OPEN:
+                raise errors.NoAlertPresentException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.SCRIPT_TIMEOUT:
+                raise errors.ScriptTimeoutException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.INVALID_SELECTOR \
+                 or status == errors.ErrorCodes.INVALID_XPATH_SELECTOR \
+                 or status == errors.ErrorCodes.INVALID_XPATH_SELECTOR_RETURN_TYPER:
+                raise errors.InvalidSelectorException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.MOVE_TARGET_OUT_OF_BOUNDS:
+                raise errors.MoveTargetOutOfBoundsException(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.FRAME_SEND_NOT_INITIALIZED_ERROR:
+                raise errors.FrameSendNotInitializedError(message=message, status=status, stacktrace=stacktrace)
+            elif status == errors.ErrorCodes.FRAME_SEND_FAILURE_ERROR:
+                raise errors.FrameSendFailureError(message=message, status=status, stacktrace=stacktrace)
             else:
-                raise MarionetteException(message=message, status=status, stacktrace=stacktrace)
-        raise MarionetteException(message=response, status=500)
+                raise errors.MarionetteException(message=message, status=status, stacktrace=stacktrace)
+        raise errors.MarionetteException(message=response, status=500)
 
     def check_for_crash(self):
         returncode = None
         name = None
         crashed = False
-        if self.emulator:
-            if self.emulator.check_for_crash():
+        if self.runner:
+            if self.runner.check_for_crashes():
                 returncode = self.emulator.proc.returncode
                 name = 'emulator'
                 crashed = True
-
-            if self.emulator.check_for_minidumps():
-                crashed = True
         elif self.instance:
             if self.instance.check_for_crashes():
                 crashed = True
         if returncode is not None:
             print ('PROCESS-CRASH | %s | abnormal termination with exit code %d' %
                 (name, returncode))
         return crashed
 
@@ -1454,9 +1423,9 @@ class Marionette(object):
         respectively, and "portrait-secondary" as well as
         "landscape-secondary".
 
         :param orientation: The orientation to lock the screen in.
 
         """
         self._send_message("setScreenOrientation", "ok", orientation=orientation)
         if self.emulator:
-            self.emulator.screen.orientation = self.SCREEN_ORIENTATIONS[orientation.lower()]
+            self.emulator.screen.orientation = orientation.lower()
--- a/testing/marionette/client/marionette/runner/base.py
+++ b/testing/marionette/client/marionette/runner/base.py
@@ -362,39 +362,39 @@ class BaseMarionetteOptions(OptionParser
                         dest='emulator',
                         choices=['x86', 'arm'],
                         help='if no --address is given, then the harness will launch a B2G emulator on which to run '
                              'emulator tests. if --address is given, then the harness assumes you are running an '
                              'emulator already, and will run the emulator tests using that emulator. you need to '
                              'specify which architecture to emulate for both cases')
         self.add_option('--emulator-binary',
                         action='store',
-                        dest='emulatorBinary',
+                        dest='emulator_binary',
                         help='launch a specific emulator binary rather than launching from the B2G built emulator')
         self.add_option('--emulator-img',
                         action='store',
-                        dest='emulatorImg',
+                        dest='emulator_img',
                         help='use a specific image file instead of a fresh one')
         self.add_option('--emulator-res',
                         action='store',
                         dest='emulator_res',
                         type='str',
                         help='set a custom resolution for the emulator'
                              'Example: "480x800"')
         self.add_option('--sdcard',
                         action='store',
                         dest='sdcard',
                         help='size of sdcard to create for the emulator')
         self.add_option('--no-window',
                         action='store_true',
-                        dest='noWindow',
+                        dest='no_window',
                         default=False,
                         help='when Marionette launches an emulator, start it with the -no-window argument')
         self.add_option('--logcat-dir',
-                        dest='logcat_dir',
+                        dest='logdir',
                         action='store',
                         help='directory to store logcat dump files')
         self.add_option('--address',
                         dest='address',
                         action='store',
                         help='host:port of running Gecko instance to connect to')
         self.add_option('--device',
                         dest='device_serial',
@@ -436,20 +436,16 @@ class BaseMarionetteOptions(OptionParser
                         action='store',
                         type=int,
                         default=0,
                         help='number of times to repeat the test(s)')
         self.add_option('-x', '--xml-output',
                         action='store',
                         dest='xml_output',
                         help='xml output')
-        self.add_option('--gecko-path',
-                        dest='gecko_path',
-                        action='store',
-                        help='path to b2g gecko binaries that should be installed on the device or emulator')
         self.add_option('--testvars',
                         dest='testvars',
                         action='store',
                         help='path to a json file with any test data required')
         self.add_option('--tree',
                         dest='tree',
                         action='store',
                         default='b2g',
@@ -520,18 +516,18 @@ class BaseMarionetteOptions(OptionParser
             print 'can\'t specify both --emulator and --binary'
             sys.exit(1)
 
         if not options.es_servers:
             options.es_servers = ['elasticsearch-zlb.dev.vlan81.phx.mozilla.com:9200',
                                   'elasticsearch-zlb.webapp.scl3.mozilla.com:9200']
 
         # default to storing logcat output for emulator runs
-        if options.emulator and not options.logcat_dir:
-            options.logcat_dir = 'logcat'
+        if options.emulator and not options.logdir:
+            options.logdir = 'logcat'
 
         # check for valid resolution string, strip whitespaces
         try:
             if options.emulator_res:
                 dims = options.emulator_res.split('x')
                 assert len(dims) == 2
                 width = str(int(dims[0]))
                 height = str(int(dims[1]))
@@ -557,48 +553,47 @@ class BaseMarionetteOptions(OptionParser
 
         return (options, tests)
 
 
 class BaseMarionetteTestRunner(object):
 
     textrunnerclass = MarionetteTextTestRunner
 
-    def __init__(self, address=None, emulator=None, emulatorBinary=None,
-                 emulatorImg=None, emulator_res='480x800', homedir=None,
+    def __init__(self, address=None, emulator=None, emulator_binary=None,
+                 emulator_img=None, emulator_res='480x800', homedir=None,
                  app=None, app_args=None, bin=None, profile=None, autolog=False,
-                 revision=None, logger=None, testgroup="marionette", noWindow=False,
-                 logcat_dir=None, xml_output=None, repeat=0, gecko_path=None,
+                 revision=None, logger=None, testgroup="marionette", no_window=False,
+                 logdir=None, xml_output=None, repeat=0,
                  testvars=None, tree=None, type=None, device_serial=None,
                  symbols_path=None, timeout=None, es_servers=None, shuffle=False,
                  shuffle_seed=random.randint(0, sys.maxint), sdcard=None,
                  this_chunk=1, total_chunks=1, sources=None, server_root=None,
                  gecko_log=None,
                  **kwargs):
         self.address = address
         self.emulator = emulator
-        self.emulatorBinary = emulatorBinary
-        self.emulatorImg = emulatorImg
+        self.emulator_binary = emulator_binary
+        self.emulator_img = emulator_img
         self.emulator_res = emulator_res
         self.homedir = homedir
         self.app = app
         self.app_args = app_args or []
         self.bin = bin
         self.profile = profile
         self.autolog = autolog
         self.testgroup = testgroup
         self.revision = revision
         self.logger = logger
-        self.noWindow = noWindow
+        self.no_window = no_window
         self.httpd = None
         self.marionette = None
-        self.logcat_dir = logcat_dir
+        self.logdir = logdir
         self.xml_output = xml_output
         self.repeat = repeat
-        self.gecko_path = gecko_path
         self.testvars = {}
         self.test_kwargs = kwargs
         self.tree = tree
         self.type = type
         self.device_serial = device_serial
         self.symbols_path = symbols_path
         self.timeout = timeout
         self._device = None
@@ -635,19 +630,19 @@ class BaseMarionetteTestRunner(object):
 
         self.reset_test_stats()
 
         if self.logger is None:
             self.logger = logging.getLogger('Marionette')
             self.logger.setLevel(logging.INFO)
             self.logger.addHandler(logging.StreamHandler())
 
-        if self.logcat_dir:
-            if not os.access(self.logcat_dir, os.F_OK):
-                os.mkdir(self.logcat_dir)
+        if self.logdir:
+            if not os.access(self.logdir, os.F_OK):
+                os.mkdir(self.logdir)
 
         # for XML output
         self.testvars['xml_output'] = self.xml_output
         self.results = []
 
     @property
     def capabilities(self):
         if self._capabilities:
@@ -712,18 +707,17 @@ class BaseMarionetteTestRunner(object):
                 'bin': self.bin,
                 'profile': self.profile,
                 'gecko_log': self.gecko_log,
             })
 
         if self.emulator:
             kwargs.update({
                 'homedir': self.homedir,
-                'logcat_dir': self.logcat_dir,
-                'gecko_path': self.gecko_path,
+                'logdir': self.logdir,
             })
 
         if self.address:
             host, port = self.address.split(':')
             kwargs.update({
                 'host': host,
                 'port': int(port),
             })
@@ -736,33 +730,33 @@ class BaseMarionetteTestRunner(object):
                     connection = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
                     connection.connect((host,int(port)))
                     connection.close()
                 except Exception, e:
                     raise Exception("Connection attempt to %s:%s failed with error: %s" %(host,port,e))
         elif self.emulator:
             kwargs.update({
                 'emulator': self.emulator,
-                'emulatorBinary': self.emulatorBinary,
-                'emulatorImg': self.emulatorImg,
+                'emulator_binary': self.emulator_binary,
+                'emulator_img': self.emulator_img,
                 'emulator_res': self.emulator_res,
-                'noWindow': self.noWindow,
+                'no_window': self.no_window,
                 'sdcard': self.sdcard,
             })
         return kwargs
 
     def start_marionette(self):
         self.marionette = Marionette(**self._build_kwargs())
 
     def post_to_autolog(self, elapsedtime):
         self.logger.info('posting results to autolog')
 
         logfile = None
         if self.emulator:
-            filename = os.path.join(os.path.abspath(self.logcat_dir),
+            filename = os.path.join(os.path.abspath(self.logdir),
                                     "emulator-%d.log" % self.marionette.emulator.port)
             if os.access(filename, os.F_OK):
                 logfile = filename
 
         for es_server in self.es_servers:
 
             # This is all autolog stuff.
             # See: https://wiki.mozilla.org/Auto-tools/Projects/Autolog
--- a/testing/marionette/client/marionette/runner/mixins/b2g.py
+++ b/testing/marionette/client/marionette/runner/mixins/b2g.py
@@ -4,24 +4,20 @@
 
 import mozdevice
 import os
 import re
 
 
 def get_dm(marionette=None,**kwargs):
     dm_type = os.environ.get('DM_TRANS', 'adb')
-    if marionette and marionette.emulator:
-        adb_path = marionette.emulator.b2g.adb_path
-        return mozdevice.DeviceManagerADB(adbPath=adb_path,
-                                          deviceSerial='emulator-%d' % marionette.emulator.port,
-                                          **kwargs)
+    if marionette and hasattr(marionette.runner, 'device'):
+        return marionette.runner.app_ctx.dm
     elif marionette and marionette.device_serial and dm_type == 'adb':
-        return mozdevice.DeviceManagerADB(deviceSerial=marionette.device_serial,
-                                          **kwargs)
+        return mozdevice.DeviceManagerADB(deviceSerial=marionette.device_serial, **kwargs)
     else:
         if dm_type == 'adb':
             return mozdevice.DeviceManagerADB(**kwargs)
         elif dm_type == 'sut':
             host = os.environ.get('TEST_DEVICE')
             if not host:
                 raise Exception('Must specify host with SUT!')
             return mozdevice.DeviceManagerSUT(host=host)
--- a/testing/marionette/client/marionette/tests/unit/test_clearing.py
+++ b/testing/marionette/client/marionette/tests/unit/test_clearing.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import InvalidElementStateException
+from errors import InvalidElementStateException
 
 class TestClear(MarionetteTestCase):
     def testWriteableTextInputShouldClear(self):
         test_html = self.marionette.absolute_url("test_clearing.html")
         self.marionette.navigate(test_html)
         element = self.marionette.find_element("id", "writableTextInput")
         element.clear()
         self.assertEqual("", element.get_attribute("value"))
--- a/testing/marionette/client/marionette/tests/unit/test_element_touch.py
+++ b/testing/marionette/client/marionette/tests/unit/test_element_touch.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import MarionetteException
+from errors import MarionetteException
 
 class testElementTouch(MarionetteTestCase):
     def test_touch(self):
       testAction = self.marionette.absolute_url("testAction.html")
       self.marionette.navigate(testAction)
       button = self.marionette.find_element("id", "button1")
       button.tap()
       expected = "button1-touchstart-touchend-mousemove-mousedown-mouseup-click"
--- a/testing/marionette/client/marionette/tests/unit/test_emulator.py
+++ b/testing/marionette/client/marionette/tests/unit/test_emulator.py
@@ -1,30 +1,30 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import JavascriptException, MarionetteException
+from errors import JavascriptException, MarionetteException
 
 
 class TestEmulatorContent(MarionetteTestCase):
 
     def test_emulator_cmd(self):
         self.marionette.set_script_timeout(10000)
         expected = ["<build>",
                     "OK"]
         result = self.marionette.execute_async_script("""
         runEmulatorCmd("avd name", marionetteScriptFinished)
         """);
         self.assertEqual(result, expected)
 
     def test_emulator_shell(self):
         self.marionette.set_script_timeout(10000)
-        expected = ["Hello World!", ""]
+        expected = ["Hello World!"]
         result = self.marionette.execute_async_script("""
         runEmulatorShell(["echo", "Hello World!"], marionetteScriptFinished)
         """);
         self.assertEqual(result, expected)
 
     def test_emulator_order(self):
         self.marionette.set_script_timeout(10000)
         self.assertRaises(MarionetteException,
--- a/testing/marionette/client/marionette/tests/unit/test_execute_async_script.py
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_async_script.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import JavascriptException, MarionetteException, ScriptTimeoutException
+from errors import JavascriptException, MarionetteException, ScriptTimeoutException
 import time
 
 
 class TestExecuteAsyncContent(MarionetteTestCase):
     def setUp(self):
         super(TestExecuteAsyncContent, self).setUp()
         self.marionette.set_script_timeout(1000)
 
--- a/testing/marionette/client/marionette/tests/unit/test_execute_isolate.py
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_isolate.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase, skip_if_b2g
-from marionette import JavascriptException, MarionetteException, ScriptTimeoutException
+from errors import JavascriptException, MarionetteException, ScriptTimeoutException
 
 class TestExecuteIsolationContent(MarionetteTestCase):
     def setUp(self):
         super(TestExecuteIsolationContent, self).setUp()
         self.content = True
 
     def test_execute_async_isolate(self):
         # Results from one execute call that has timed out should not
--- a/testing/marionette/client/marionette/tests/unit/test_execute_script.py
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_script.py
@@ -1,16 +1,16 @@
 # 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 urllib
 
 from by import By
-from marionette import JavascriptException, MarionetteException
+from errors import JavascriptException, MarionetteException
 from marionette_test import MarionetteTestCase
 
 def inline(doc):
     return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
 
 elements = inline("<p>foo</p> <p>bar</p>")
 
 class TestExecuteContent(MarionetteTestCase):
--- a/testing/marionette/client/marionette/tests/unit/test_findelement.py
+++ b/testing/marionette/client/marionette/tests/unit/test_findelement.py
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
 from marionette import HTMLElement
 from by import By
-from marionette import NoSuchElementException
+from errors import NoSuchElementException
 
 
 class TestElements(MarionetteTestCase):
     def test_id(self):
         test_html = self.marionette.absolute_url("test.html")
         self.marionette.navigate(test_html)
         el = self.marionette.execute_script("return window.document.getElementById('mozLink');")
         found_el = self.marionette.find_element(By.ID, "mozLink")
--- a/testing/marionette/client/marionette/tests/unit/test_findelement_chrome.py
+++ b/testing/marionette/client/marionette/tests/unit/test_findelement_chrome.py
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
 from marionette import HTMLElement
 from by import By
-from marionette import NoSuchElementException
+from errors import NoSuchElementException
 
 
 class TestElementsChrome(MarionetteTestCase):
     def setUp(self):
         MarionetteTestCase.setUp(self)
         self.marionette.set_context("chrome")
         self.win = self.marionette.current_window_handle
         self.marionette.execute_script("window.open('chrome://marionette/content/test.xul', 'foo', 'chrome,centerscreen');")
--- a/testing/marionette/client/marionette/tests/unit/test_implicit_waits.py
+++ b/testing/marionette/client/marionette/tests/unit/test_implicit_waits.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import NoSuchElementException
+from errors import NoSuchElementException
 
 class TestImplicitWaits(MarionetteTestCase):
     def testShouldImplicitlyWaitForASingleElement(self):
         test_html = self.marionette.absolute_url("test_dynamic.html")
         self.marionette.navigate(test_html)
         add = self.marionette.find_element("id", "adder")
         self.marionette.set_search_timeout("3000")
         add.click()
--- a/testing/marionette/client/marionette/tests/unit/test_navigation.py
+++ b/testing/marionette/client/marionette/tests/unit/test_navigation.py
@@ -1,15 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import MarionetteException
-from marionette import TimeoutException
+from errors import MarionetteException, TimeoutException
 
 class TestNavigate(MarionetteTestCase):
     def test_navigate(self):
         self.assertTrue(self.marionette.execute_script("window.location.href = 'about:blank'; return true;"))
         self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
         test_html = self.marionette.absolute_url("test.html")
         self.marionette.navigate(test_html)
         self.assertNotEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
--- a/testing/marionette/client/marionette/tests/unit/test_screen_orientation.py
+++ b/testing/marionette/client/marionette/tests/unit/test_screen_orientation.py
@@ -1,17 +1,17 @@
 # -*- fill-column: 100; comment-column: 100; -*-
 
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-from emulator_screen import EmulatorScreen
-from marionette import MarionetteException
+from errors import MarionetteException
 from marionette_test import MarionetteTestCase
+from mozrunner.devices.emulator_screen import EmulatorScreen
 
 default_orientation = "portrait-primary"
 unknown_orientation = "Unknown screen orientation: %s"
 
 class TestScreenOrientation(MarionetteTestCase):
     def tearDown(self):
         self.marionette.set_orientation(default_orientation)
         self.assertEqual(self.marionette.orientation, default_orientation, "invalid state")
--- a/testing/marionette/client/marionette/tests/unit/test_simpletest_sanity.py
+++ b/testing/marionette/client/marionette/tests/unit/test_simpletest_sanity.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import JavascriptException, MarionetteException, ScriptTimeoutException
+from errors import JavascriptException, MarionetteException, ScriptTimeoutException
 
 class SimpletestSanityTest(MarionetteTestCase):
 
     callFinish = "return finish();"
 
     def test_is(self):
         def runtests():
             sentFail1 = "is(true, false, 'isTest1', TEST_UNEXPECTED_FAIL, TEST_PASS);" + self.callFinish
--- a/testing/marionette/client/marionette/tests/unit/test_single_finger.py
+++ b/testing/marionette/client/marionette/tests/unit/test_single_finger.py
@@ -1,15 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
 from marionette import Actions
-from marionette import MarionetteException
+from errors import MarionetteException
 #add this directory to the path
 import os
 import sys
 sys.path.append(os.path.dirname(__file__))
 from single_finger_functions import (
         chain, chain_flick, context_menu, double_tap,
         long_press_action, long_press_on_xy_action,
         move_element, move_element_offset, press_release, single_tap, wait,
--- a/testing/marionette/client/marionette/tests/unit/test_single_finger_desktop.py
+++ b/testing/marionette/client/marionette/tests/unit/test_single_finger_desktop.py
@@ -1,11 +1,11 @@
 from marionette_test import MarionetteTestCase
 from marionette import Actions
-from marionette import MarionetteException
+from errors import MarionetteException
 #add this directory to the path
 import os
 import sys
 sys.path.append(os.path.dirname(__file__))
 from single_finger_functions import (
         chain, chain_flick, context_menu, double_tap,
         long_press_action, long_press_on_xy_action,
         move_element, move_element_offset, press_release, single_tap, wait,
--- a/testing/marionette/client/marionette/tests/unit/test_specialpowers.py
+++ b/testing/marionette/client/marionette/tests/unit/test_specialpowers.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import JavascriptException, MarionetteException
+from errors import JavascriptException, MarionetteException
 
 class TestSpecialPowersContent(MarionetteTestCase):
 
     testpref = "testing.marionette.contentcharpref"
     testvalue = "blabla"
 
     def test_prefs(self):
         result = self.marionette.execute_script("""
--- a/testing/marionette/client/marionette/tests/unit/test_switch_frame.py
+++ b/testing/marionette/client/marionette/tests/unit/test_switch_frame.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import JavascriptException
+from errors import JavascriptException
 
 
 class TestSwitchFrame(MarionetteTestCase):
     def test_switch_simple(self):
         start_url = "test_iframe.html"
         verify_title = "Marionette IFrame Test"
         verify_url = "test.html"
         test_html = self.marionette.absolute_url(start_url)
--- a/testing/marionette/client/marionette/tests/unit/test_switch_frame_chrome.py
+++ b/testing/marionette/client/marionette/tests/unit/test_switch_frame_chrome.py
@@ -1,14 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
-from marionette import JavascriptException
+from errors import JavascriptException
 
 class TestSwitchFrameChrome(MarionetteTestCase):
     def setUp(self):
         MarionetteTestCase.setUp(self)
         self.marionette.set_context("chrome")
         self.win = self.marionette.current_window_handle
         self.marionette.execute_script("window.open('chrome://marionette/content/test.xul', 'foo', 'chrome,centerscreen');")
         self.marionette.switch_to_window('foo')
--- a/testing/marionette/client/marionette/tests/unit/test_timeouts.py
+++ b/testing/marionette/client/marionette/tests/unit/test_timeouts.py
@@ -1,16 +1,16 @@
 # 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 os
 from marionette_test import MarionetteTestCase
 from marionette import HTMLElement
-from marionette import NoSuchElementException, JavascriptException, MarionetteException, ScriptTimeoutException
+from errors import NoSuchElementException, JavascriptException, MarionetteException, ScriptTimeoutException
 
 class TestTimeouts(MarionetteTestCase):
     def test_pagetimeout_notdefinetimeout_pass(self):
         test_html = self.marionette.absolute_url("test.html")
         self.marionette.navigate(test_html)
 
     def test_pagetimeout_fail(self):
         self.marionette.timeouts("page load", 0)
--- a/testing/marionette/client/marionette/tests/unit/test_typing.py
+++ b/testing/marionette/client/marionette/tests/unit/test_typing.py
@@ -1,15 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_test import MarionetteTestCase
 from keys import Keys
-from marionette import ElementNotVisibleException
+from errors import ElementNotVisibleException
 
 
 class TestTyping(MarionetteTestCase):
 
     def testShouldFireKeyPressEvents(self):
         test_html = self.marionette.absolute_url("javascriptPage.html")
         self.marionette.navigate(test_html)
         keyReporter = self.marionette.find_element("id", "keyReporter")
--- a/testing/marionette/client/requirements.txt
+++ b/testing/marionette/client/requirements.txt
@@ -1,12 +1,12 @@
 marionette-transport == 0.2
 manifestparser
 mozhttpd >= 0.5
 mozinfo >= 0.7
 mozprocess >= 0.9
-mozrunner >= 5.15
-mozdevice >= 0.22
+mozrunner >= 6.0
+mozdevice >= 0.37
 moznetwork >= 0.21
 mozcrash >= 0.5
 mozprofile >= 0.7
 moztest >= 0.1
 mozversion >= 0.2
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -173,17 +173,17 @@ class MochitestRunner(MozbuildObject):
         try:
             which.which('adb')
         except which.WhichError:
             # TODO Find adb automatically if it isn't on the path
             print(ADB_NOT_FOUND % ('mochitest-remote', b2g_home))
             return 1
 
         options.b2gPath = b2g_home
-        options.logcat_dir = self.mochitest_dir
+        options.logdir = self.mochitest_dir
         options.httpdPath = self.mochitest_dir
         options.xrePath = xre_path
         return mochitest.run_remote_mochitests(parser, options)
 
     def run_desktop_test(self, context, suite=None, test_paths=None, debugger=None,
         debugger_args=None, slowscript=False, screenshot_on_fail = False, shuffle=False, keep_open=False,
         rerun_failures=False, no_autorun=False, repeat=0, run_until_failure=False,
         slow=False, chunk_by_dir=0, total_chunks=None, this_chunk=None,
@@ -550,19 +550,19 @@ def MochitestCommand(func):
 
 def B2GCommand(func):
     """Decorator that adds shared command arguments to b2g mochitest commands."""
 
     busybox = CommandArgument('--busybox', default=None,
         help='Path to busybox binary to install on device')
     func = busybox(func)
 
-    logcatdir = CommandArgument('--logcat-dir', default=None,
-        help='directory to store logcat dump files')
-    func = logcatdir(func)
+    logdir = CommandArgument('--logdir', default=None,
+        help='directory to store log files')
+    func = logdir(func)
 
     profile = CommandArgument('--profile', default=None,
         help='for desktop testing, the path to the \
               gaia profile to use')
     func = profile(func)
 
     geckopath = CommandArgument('--gecko-path', default=None,
         help='the path to a gecko distribution that should \
--- a/testing/mochitest/mochitest_options.py
+++ b/testing/mochitest/mochitest_options.py
@@ -700,21 +700,21 @@ class B2GOptions(MochitestOptions):
         [["--profile"],
         { "action": "store",
           "type": "string",
           "dest": "profile",
           "help": "for desktop testing, the path to the \
                    gaia profile to use",
           "default": None,
         }],
-        [["--logcat-dir"],
+        [["--logdir"],
         { "action": "store",
           "type": "string",
-          "dest": "logcat_dir",
-          "help": "directory to store logcat dump files",
+          "dest": "logdir",
+          "help": "directory to store log files",
           "default": None,
         }],
         [['--busybox'],
         { "action": 'store',
           "type": 'string',
           "dest": 'busybox',
           "help": "Path to busybox binary to install on device",
           "default": None,
@@ -733,17 +733,16 @@ class B2GOptions(MochitestOptions):
         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):
@@ -752,18 +751,18 @@ class B2GOptions(MochitestOptions):
                 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 options.logdir and not options.emulator:
+            self.error("You must specify --emulator if you specify --logdir")
 
         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 '
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -1253,33 +1253,28 @@ class Mochitest(MochitestUtilsMixin):
                    'cwd': SCRIPT_DIR,
                    'onTimeout': [timeoutHandler]}
       kp_kwargs['processOutputLine'] = [outputHandler]
 
       # create mozrunner instance and start the system under test process
       self.lastTestSeen = self.test_name
       startTime = datetime.now()
 
-      # b2g desktop requires FirefoxRunner even though appname is b2g
+      # b2g desktop requires Runner even though appname is b2g
       if mozinfo.info.get('appname') == 'b2g' and mozinfo.info.get('toolkit') != 'gonk':
-          runner_cls = mozrunner.FirefoxRunner
+          runner_cls = mozrunner.Runner
       else:
           runner_cls = mozrunner.runners.get(mozinfo.info.get('appname', 'firefox'),
                                              mozrunner.Runner)
       runner = runner_cls(profile=self.profile,
                           binary=cmd,
                           cmdargs=args,
                           env=env,
                           process_class=mozprocess.ProcessHandlerMixin,
-                          kp_kwargs=kp_kwargs,
-                          )
-
-      # XXX work around bug 898379 until mozrunner is updated for m-c; see
-      # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c49
-      runner.kp_kwargs = kp_kwargs
+                          process_args=kp_kwargs)
 
       # start the runner
       runner.start(debug_args=debug_args,
                    interactive=interactive,
                    outputTimeout=timeout)
       proc = runner.process_handler
       log.info("INFO | runtests.py | Application pid: %d", proc.pid)
 
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -4,46 +4,44 @@
 
 import json
 import os
 import posixpath
 import shutil
 import sys
 import tempfile
 import threading
-import time
 import traceback
 
 here = os.path.abspath(os.path.dirname(__file__))
 sys.path.insert(0, here)
 
 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
 from mozprofile import Profile, Preferences
-from mozrunner import B2GRunner
 import mozlog
 import mozinfo
-import moznetwork
 
 log = mozlog.getLogger('Mochitest')
 
 class B2GMochitest(MochitestUtilsMixin):
-    def __init__(self, marionette,
+    marionette = None
+
+    def __init__(self, marionette_args,
                        out_of_process=True,
                        profile_data_dir=None,
                        locations=os.path.join(here, 'server-locations.txt')):
         super(B2GMochitest, self).__init__()
-        self.marionette = marionette
+        self.marionette_args = marionette_args
         self.out_of_process = out_of_process
         self.locations_file = 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'
 
@@ -116,97 +114,117 @@ class B2GMochitest(MochitestUtilsMixin):
         return manifest
 
     def run_tests(self, options):
         """ Prepare, configure, run tests and cleanup """
 
         self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
         manifest = self.build_profile(options)
 
-        self.startServers(options, None)
-        self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'})
-        self.test_script_args.append(not options.emulator)
-        self.test_script_args.append(options.wifi)
-
         if options.debugger or not options.autorun:
             timeout = None
         else:
             if not options.timeout:
                 if mozinfo.info['debug']:
                     options.timeout = 420
                 else:
                     options.timeout = 300
             timeout = options.timeout + 30.0
 
         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,
-                            'symbols_path': options.symbolsPath,
-                            'test_script': self.test_script,
-                            'test_script_args': self.test_script_args }
-            self.runner = B2GRunner(**runner_args)
+            self.marionette_args['profile'] = self.profile
+            self.marionette = Marionette(**self.marionette_args)
+            self.runner = self.marionette.runner
+            self.app_ctx = self.runner.app_ctx
+
+            self.remote_log = posixpath.join(self.app_ctx.remote_test_root,
+                                             'log', 'mochitest.log')
+            if not self.app_ctx.dm.dirExists(posixpath.dirname(self.remote_log)):
+                self.app_ctx.dm.mkDirs(self.remote_log)
+
+            self.startServers(options, None)
+            self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'})
+            self.test_script_args.append(not options.emulator)
+            self.test_script_args.append(options.wifi)
+
+
             self.runner.start(outputTimeout=timeout)
+
+            self.marionette.wait_for_port()
+            self.marionette.start_session()
+            self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+
+            # Disable offline status management (bug 777145), otherwise the network
+            # will be 'offline' when the mochitests start.  Presumably, the network
+            # won't be offline on a real device, so we only do this for emulators.
+            self.marionette.execute_script("""
+                Components.utils.import("resource://gre/modules/Services.jsm");
+                Services.io.manageOfflineStatus = false;
+                Services.io.offline = false;
+                """)
+
+            if os.path.isfile(self.test_script):
+                with open(self.test_script, 'r') as script:
+                    self.marionette.execute_script(script.read(),
+                                                   script_args=self.test_script_args)
+            else:
+                self.marionette.execute_script(self.test_script,
+                                               script_args=self.test_script_args)
             status = self.runner.wait()
             if status is None:
                 # the runner has timed out
                 status = 124
         except KeyboardInterrupt:
             log.info("runtests.py | Received keyboard interrupt.\n");
             status = -1
         except:
             traceback.print_exc()
             log.error("Automation Error: Received unexpected exception while running application\n")
-            self.runner.check_for_crashes()
+            if hasattr(self, 'runner'):
+                self.runner.check_for_crashes()
             status = 1
 
         self.stopServers()
 
         log.info("runtestsb2g.py | Running tests: end.")
 
         if manifest is not None:
             self.cleanup(manifest, options)
         return status
 
 
 class B2GDeviceMochitest(B2GMochitest, Mochitest):
+    remote_log = None
 
-    _dm = None
-
-    def __init__(self, marionette, devicemanager, profile_data_dir,
+    def __init__(self, marionette_args, 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)
-        Mochitest.__init__(self)
-        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')
+        B2GMochitest.__init__(self, marionette_args, out_of_process=True, profile_data_dir=profile_data_dir)
         self.local_log = None
         self.local_binary_dir = local_binary_dir
 
-        if not self._dm.dirExists(posixpath.dirname(self.remote_log)):
-            self._dm.mkDirs(self.remote_log)
-
     def cleanup(self, manifest, options):
         if self.local_log:
-            self._dm.getFile(self.remote_log, self.local_log)
-            self._dm.removeFile(self.remote_log)
+            self.app_ctx.dm.getFile(self.remote_log, self.local_log)
+            self.app_ctx.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
 
         # stop and clean up the runner
         if getattr(self, 'runner', False):
+            if self.local_log:
+                self.app_ctx.dm.getFile(self.remote_log, self.local_log)
+                self.app_ctx.dm.removeFile(self.remote_log)
+
             self.runner.cleanup()
             self.runner = None
 
     def startServers(self, options, debuggerInfo):
         """ Create the servers on the host and start them up """
         savedXre = options.xrePath
         savedUtility = options.utilityPath
         savedProfie = options.profilePath
@@ -223,24 +241,24 @@ class B2GDeviceMochitest(B2GMochitest, M
     def buildURLOptions(self, options, env):
         self.local_log = options.logFile
         options.logFile = self.remote_log
         options.profilePath = self.profile.profile
         super(B2GDeviceMochitest, self).buildURLOptions(options, env)
 
         self.setup_common_options(options)
 
-        options.profilePath = self.remote_profile
+        options.profilePath = self.app_ctx.remote_profile
         options.logFile = self.local_log
 
 
 class B2GDesktopMochitest(B2GMochitest, Mochitest):
 
-    def __init__(self, marionette, profile_data_dir):
-        B2GMochitest.__init__(self, marionette, out_of_process=False, profile_data_dir=profile_data_dir)
+    def __init__(self, marionette_args, profile_data_dir):
+        B2GMochitest.__init__(self, marionette_args, out_of_process=False, profile_data_dir=profile_data_dir)
         Mochitest.__init__(self)
         self.certdbNew = True
 
     def runMarionetteScript(self, marionette, test_script, test_script_args):
         assert(marionette.wait_for_port())
         marionette.start_session()
         marionette.set_context(marionette.CONTEXT_CHROME)
 
@@ -250,16 +268,17 @@ class B2GDesktopMochitest(B2GMochitest, 
             f.close()
         self.marionette.execute_script(test_script,
                                        script_args=test_script_args)
 
     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.
+        self.marionette = Marionette(**self.marionette_args)
         thread = threading.Thread(target=self.runMarionetteScript,
                                   args=(self.marionette,
                                         self.test_script,
                                         self.test_script_args))
         thread.start()
 
     def buildURLOptions(self, options, env):
         super(B2GDesktopMochitest, self).buildURLOptions(options, env)
@@ -277,59 +296,37 @@ class B2GDesktopMochitest(B2GMochitest, 
                             os.path.join(bundlesDir, filename))
 
     def buildProfile(self, options):
         return self.build_profile(options)
 
 
 def run_remote_mochitests(parser, options):
     # create our Marionette instance
-    kwargs = {}
-    if options.emulator:
-        kwargs['emulator'] = options.emulator
-        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
-        if options.symbolsPath:
-            kwargs['symbols_path'] = options.symbolsPath
-    # needless to say sdcard is only valid if using an emulator
-    if options.sdcard:
-        kwargs['sdcard'] = options.sdcard
-    if options.b2gPath:
-        kwargs['homedir'] = options.b2gPath
+    marionette_args = {
+        'adb_path': options.adbPath,
+        'emulator': options.emulator,
+        'no_window': options.noWindow,
+        'logdir': options.logdir,
+        'busybox': options.busybox,
+        'symbols_path': options.symbolsPath,
+        'sdcard': options.sdcard,
+        'homedir': options.b2gPath,
+    }
     if options.marionette:
         host, port = options.marionette.split(':')
-        kwargs['host'] = host
-        kwargs['port'] = int(port)
-
-    marionette = Marionette.getMarionetteOrExit(**kwargs)
-
-    if options.emulator:
-        dm = marionette.emulator.dm
-    else:
-        # create the DeviceManager
-        kwargs = {'adbPath': options.adbPath,
-                  'deviceRoot': options.remoteTestRoot}
-        if options.deviceIP:
-            kwargs.update({'host': options.deviceIP,
-                           'port': options.devicePort})
-        dm = DeviceManagerADB(**kwargs)
+        marionette_args['host'] = host
+        marionette_args['port'] = int(port)
 
     options = parser.verifyRemoteOptions(options)
     if (options == None):
         print "ERROR: Invalid options specified, use --help for a list of valid options"
         sys.exit(1)
 
-    mochitest = B2GDeviceMochitest(marionette, dm, options.profile_data_dir, options.xrePath,
-                                   remote_test_root=options.remoteTestRoot,
+    mochitest = B2GDeviceMochitest(marionette_args, options.profile_data_dir, options.xrePath,
                                    remote_log_file=options.remoteLogFile)
 
     options = parser.verifyOptions(options, mochitest)
     if (options == None):
         sys.exit(1)
 
     retVal = 1
     try:
@@ -344,23 +341,22 @@ def run_remote_mochitests(parser, option
         except:
             pass
         retVal = 1
 
     sys.exit(retVal)
 
 def run_desktop_mochitests(parser, options):
     # create our Marionette instance
-    kwargs = {}
+    marionette_args = {}
     if options.marionette:
         host, port = options.marionette.split(':')
-        kwargs['host'] = host
-        kwargs['port'] = int(port)
-    marionette = Marionette.getMarionetteOrExit(**kwargs)
-    mochitest = B2GDesktopMochitest(marionette, options.profile_data_dir)
+        marionette_args['host'] = host
+        marionette_args['port'] = int(port)
+    mochitest = B2GDesktopMochitest(marionette_args, options.profile_data_dir)
 
     # add a -bin suffix if b2g-bin exists, but just b2g was specified
     if options.app[-4:] != '-bin':
         if os.path.isfile("%s-bin" % options.app):
             options.app = "%s-bin" % options.app
 
     options = MochitestOptions.verifyOptions(parser, options, mochitest)
     if options == None:
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/docs/mozrunner.rst
@@ -0,0 +1,177 @@
+:mod:`mozrunner` --- Manage remote and local gecko processes
+============================================================
+
+Mozrunner provides an API to manage a gecko-based application with an
+arbitrary configuration profile. It currently supports local desktop
+binaries such as Firefox and Thunderbird, as well as Firefox OS on
+mobile devices and emulators.
+
+
+Basic usage
+-----------
+
+The simplest way to use mozrunner, is to instantiate a runner, start it
+and then wait for it to finish:
+
+.. code-block:: python
+
+    from mozrunner import FirefoxRunner
+    binary = 'path/to/firefox/binary'
+    runner = FirefoxRunner(binary=binary)
+    runner.start()
+    runner.wait()
+
+This automatically creates and uses a default mozprofile object. If you
+wish to use a specialized or pre-existing profile, you can create a
+:doc:`mozprofile <mozprofile>` object and pass it in:
+
+.. code-block:: python
+
+    from mozprofile import Profile
+    from mozrunner import FirefoxRunner
+    import os
+
+    binary = 'path/to/firefox/binary'
+    profile_path = 'path/to/profile'
+    if os.path.exists(profile_path):
+        profile = Profile.clone(path_from=profile_path)
+    else:
+        profile = Profile(profile=profile_path)
+    runner = FirefoxRunner(binary=binary, profile=profile)
+    runner.start()
+    runner.wait()
+
+
+Handling output
+---------------
+
+By default, mozrunner dumps the output of the gecko process to standard output.
+It is possible to add arbitrary output handlers by passing them in via the
+`process_args` argument. Be careful, passing in a handler overrides the default
+behaviour. So if you want to use a handler in addition to dumping to stdout, you
+need to specify that explicitly. For example:
+
+.. code-block:: python
+
+    from mozrunner import FirefoxRunner
+
+    def handle_output_line(line):
+        do_something(line)
+
+    binary = 'path/to/firefox/binary'
+    process_args = { 'stream': sys.stdout,
+                     'processOutputLine': [handle_output_line] }
+    runner = FirefoxRunner(binary=binary, process_args=process_args)
+
+Mozrunner uses :doc:`mozprocess <mozprocess>` to manage the underlying gecko
+process and handle output. See the :doc:`mozprocess documentation <mozprocess>`
+for all available arguments accepted by `process_args`.
+
+
+Handling timeouts
+-----------------
+
+Sometimes gecko can hang, or maybe it is just taking too long. To handle this case you
+may want to set a timeout. Mozrunner has two kinds of timeouts, the
+traditional `timeout`, and the `outputTimeout`. These get passed into the
+`runner.start()` method. Setting `timeout` will cause gecko to be killed after
+the specified number of seconds, no matter what. Setting `outputTimeout` will cause
+gecko to be killed after the specified number of seconds with no output. In both
+cases the process handler's `onTimeout` callbacks will be triggered.
+
+.. code-block:: python
+
+    from mozrunner import FirefoxRunner
+
+    def on_timeout():
+        print('timed out after 10 seconds with no output!')
+
+    binary = 'path/to/firefox/binary'
+    process_args = { 'onTimeout': on_timeout }
+    runner = FirefoxRunner(binary=binary, process_args=process_args)
+    runner.start(outputTimeout=10)
+    runner.wait()
+
+The `runner.wait()` method also accepts a timeout argument. But unlike the arguments
+to `runner.start()`, this one simply returns from the wait call and does not kill the
+gecko process.
+
+.. code-block:: python
+
+    runner.start(timeout=100)
+
+    waiting = 0
+    while runner.wait(timeout=1) is None:
+        waiting += 1
+        print("Been waiting for %d seconds so far.." % waiting)
+    assert waiting <= 100
+
+
+Using a device runner
+---------------------
+
+The previous examples used a GeckoRuntimeRunner. If you want to control a
+gecko process on a remote device, you need to use a DeviceRunner. The api is
+nearly identical except you don't pass in a binary, instead you create a device
+object. For example, for B2G (Firefox OS) emulators you might do:
+
+.. code-block:: python
+
+    from mozrunner import B2GEmulatorRunner
+
+    b2g_home = 'path/to/B2G'
+    runner = B2GEmulatorRunner(arch='arm', b2g_home=b2g_home)
+    runner.start()
+    runner.wait()
+
+Device runners have a `device` object. Remember that the gecko process runs on
+the device. In the case of the emulator, it is possible to start the
+device independently of the gecko process.
+
+.. code-block:: python
+
+    runner.device.start() # launches the emulator (which also launches gecko)
+    runner.start()        # stops the gecko process, installs the profile, restarts the gecko process
+
+
+Runner API Documentation
+------------------------
+
+Application Runners
+~~~~~~~~~~~~~~~~~~~
+.. automodule:: mozrunner.runners
+   :members:
+
+BaseRunner
+~~~~~~~~~~
+.. autoclass:: mozrunner.base.BaseRunner
+   :members:
+
+GeckoRuntimeRunner
+~~~~~~~~~~~~~~~~~~
+.. autoclass:: mozrunner.base.GeckoRuntimeRunner
+   :show-inheritance:
+   :members:
+
+DeviceRunner
+~~~~~~~~~~~~
+.. autoclass:: mozrunner.base.DeviceRunner
+   :show-inheritance:
+   :members:
+
+Device API Documentation
+------------------------
+
+Generally using the device classes directly shouldn't be required, but in some
+cases it may be desirable.
+
+Device
+~~~~~~
+.. autoclass:: mozrunner.devices.Device
+   :members:
+
+Emulator
+~~~~~~~~
+.. autoclass:: mozrunner.devices.Emulator
+   :show-inheritance:
+   :members:
--- a/testing/mozbase/docs/setuprunning.rst
+++ b/testing/mozbase/docs/setuprunning.rst
@@ -7,9 +7,10 @@ controlled environment such that it can 
 correctly handling the case where the system crashes.
 
 .. toctree::
    :maxdepth: 2
 
    mozfile
    mozprofile
    mozprocess
+   mozrunner
    mozcrash
--- a/testing/mozbase/mozdevice/mozdevice/devicemanager.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
@@ -1,15 +1,14 @@
 # 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 hashlib
 import mozlog
-import socket
 import os
 import posixpath
 import re
 import struct
 import StringIO
 import zlib
 
 from Zeroconf import Zeroconf, ServiceBrowser
@@ -373,23 +372,25 @@ class DeviceManager(object):
         :param env: Environment to pass to exec command
         :param cwd: Directory to execute command from
         :param timeout: specified in seconds, defaults to 'default_timeout'
         :param root: Specifies whether command requires root privileges
         """
 
     def shellCheckOutput(self, cmd, env=None, cwd=None, timeout=None, root=False):
         """
-        Executes shell command on device and returns output as a string.
+        Executes shell command on device and returns output as a string. Raises if
+        the return code is non-zero.
 
         :param cmd: Commandline list to execute
         :param env: Environment to pass to exec command
         :param cwd: Directory to execute command from
         :param timeout: specified in seconds, defaults to 'default_timeout'
         :param root: Specifies whether command requires root privileges
+        :raises: DMError
         """
         buf = StringIO.StringIO()
         retval = self.shell(cmd, buf, env=env, cwd=cwd, timeout=timeout, root=root)
         output = str(buf.getvalue()[0:-1]).rstrip()
         buf.close()
         if retval != 0:
             raise DMError("Non-zero return code for command: %s (output: '%s', retval: '%s')" % (cmd, output, retval))
         return output
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
@@ -24,21 +24,22 @@ class DeviceManagerADB(DeviceManager):
 
     _haveRootShell = False
     _haveSu = False
     _useZip = False
     _logcatNeedsRoot = False
     _pollingInterval = 0.01
     _packageName = None
     _tempDir = None
+    connected = False
     default_timeout = 300
 
     def __init__(self, host=None, port=5555, retryLimit=5, packageName='fennec',
                  adbPath='adb', deviceSerial=None, deviceRoot=None,
-                 logLevel=mozlog.ERROR, **kwargs):
+                 logLevel=mozlog.ERROR, autoconnect=True, **kwargs):
         DeviceManager.__init__(self, logLevel)
         self.host = host
         self.port = port
         self.retryLimit = retryLimit
         self.deviceRoot = deviceRoot
 
         # the path to adb, or 'adb' to assume that it's on the PATH
         self._adbPath = adbPath
@@ -53,38 +54,43 @@ class DeviceManagerADB(DeviceManager):
             else:
                 self._packageName = 'org.mozilla.fennec_'
         elif packageName:
             self._packageName = packageName
 
         # verify that we can run the adb command. can't continue otherwise
         self._verifyADB()
 
-        # try to connect to the device over tcp/ip if we have a hostname
-        if self.host:
-            self._connectRemoteADB()
+        if autoconnect:
+            self.connect()
 
-        # verify that we can connect to the device. can't continue
-        self._verifyDevice()
+    def connect(self):
+        if not self.connected:
+            # try to connect to the device over tcp/ip if we have a hostname
+            if self.host:
+                self._connectRemoteADB()
 
-        # set up device root
-        self._setupDeviceRoot()
+            # verify that we can connect to the device. can't continue
+            self._verifyDevice()
 
-        # Some commands require root to work properly, even with ADB (e.g.
-        # grabbing APKs out of /data). For these cases, we check whether
-        # we're running as root. If that isn't true, check for the
-        # existence of an su binary
-        self._checkForRoot()
+            # set up device root
+            self._setupDeviceRoot()
 
-        # can we use zip to speed up some file operations? (currently not
-        # required)
-        try:
-            self._verifyZip()
-        except DMError:
-            pass
+            # Some commands require root to work properly, even with ADB (e.g.
+            # grabbing APKs out of /data). For these cases, we check whether
+            # we're running as root. If that isn't true, check for the
+            # existence of an su binary
+            self._checkForRoot()
+
+            # can we use zip to speed up some file operations? (currently not
+            # required)
+            try:
+                self._verifyZip()
+            except DMError:
+                pass
 
     def __del__(self):
         if self.host:
             self._disconnectRemoteADB()
 
     def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
         # FIXME: this function buffers all output of the command into memory,
         # always. :(
--- a/testing/mozbase/mozdevice/setup.py
+++ b/testing/mozbase/mozdevice/setup.py
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
 PACKAGE_NAME = 'mozdevice'
-PACKAGE_VERSION = '0.36'
+PACKAGE_VERSION = '0.37'
 
 deps = ['mozfile >= 1.0',
         'mozlog',
         'moznetwork >= 0.24'
        ]
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
--- a/testing/mozbase/mozprocess/mozprocess/processhandler.py
+++ b/testing/mozbase/mozprocess/mozprocess/processhandler.py
@@ -688,17 +688,17 @@ falling back to not using job objects fo
             self.proc.kill(sig=sig)
 
             # When we kill the the managed process we also have to wait for the
             # outThread to be finished. Otherwise consumers would have to assume
             # that it still has not completely shutdown.
             return self.wait()
         except AttributeError:
             # Try to print a relevant error message.
-            if not self.proc:
+            if not hasattr(self, 'proc'):
                 print >> sys.stderr, "Unable to kill Process because call to ProcessHandler constructor failed."
             else:
                 raise
 
     def readWithTimeout(self, f, timeout):
         """
         Try to read a line of output from the file object *f*.
 
--- a/testing/mozbase/mozrunner/mozrunner/__init__.py
+++ b/testing/mozbase/mozrunner/mozrunner/__init__.py
@@ -1,11 +1,10 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
-
+from .cli import *
 from .errors import *
-from .local import *
-from .local import LocalRunner as Runner
-from .remote import *
+from .runners import *
 
-runners = local_runners
-runners.update(remote_runners)
+import base
+import devices
+import utils
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/application.py
@@ -0,0 +1,129 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from distutils.spawn import find_executable
+import glob
+import os
+import posixpath
+import sys
+
+from mozdevice import DeviceManagerADB
+from mozprofile import (
+    Profile,
+    FirefoxProfile,
+    MetroFirefoxProfile,
+    ThunderbirdProfile
+)
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+def get_app_context(appname):
+    context_map = { 'default': DefaultContext,
+                    'b2g': B2GContext,
+                    'firefox': FirefoxContext,
+                    'thunderbird': ThunderbirdContext,
+                    'metro': MetroContext }
+    if appname not in context_map:
+        raise KeyError("Application '%s' not supported!" % appname)
+    return context_map[appname]
+
+
+class DefaultContext(object):
+    profile_class = Profile
+
+
+class B2GContext(object):
+    _bindir = None
+    _dm = None
+    _remote_profile = None
+    profile_class = Profile
+
+    def __init__(self, b2g_home=None, adb_path=None):
+        self.homedir = b2g_home or os.environ.get('B2G_HOME')
+        if not self.homedir:
+            raise EnvironmentError('Must define B2G_HOME or pass the b2g_home parameter')
+
+        if not os.path.isdir(self.homedir):
+            raise OSError('Homedir \'%s\' does not exist!' % self.homedir)
+
+        self._adb = adb_path
+        self.update_tools = os.path.join(self.homedir, 'tools', 'update-tools')
+        self.fastboot = self.which('fastboot')
+
+        self.remote_binary = '/system/bin/b2g.sh'
+        self.remote_process = '/system/b2g/b2g'
+        self.remote_bundles_dir = '/system/b2g/distribution/bundles'
+        self.remote_busybox = '/system/bin/busybox'
+        self.remote_profiles_ini = '/data/b2g/mozilla/profiles.ini'
+        self.remote_test_root = '/data/local/tests'
+
+    @property
+    def adb(self):
+        if not self._adb:
+            paths = [os.environ.get('ADB'),
+                     os.environ.get('ADB_PATH'),
+                     self.which('adb')]
+            paths = [p for p in paths if p is not None if os.path.isfile(p)]
+            if not paths:
+                raise OSError('Could not find the adb binary, make sure it is on your' \
+                              'path or set the $ADB_PATH environment variable.')
+            self._adb = paths[0]
+        return self._adb
+
+    @property
+    def bindir(self):
+        if not self._bindir:
+            # TODO get this via build configuration
+            path = os.path.join(self.homedir, 'out', 'host', '*', 'bin')
+            self._bindir = glob.glob(path)[0]
+        return self._bindir
+
+    @property
+    def dm(self):
+        if not self._dm:
+            self._dm = DeviceManagerADB(adbPath=self.adb, autoconnect=False, deviceRoot=self.remote_test_root)
+        return self._dm
+
+    @property
+    def remote_profile(self):
+        if not self._remote_profile:
+            self._remote_profile = posixpath.join(self.remote_test_root, 'profile')
+        return self._remote_profile
+
+
+    def which(self, binary):
+        if self.bindir not in sys.path:
+            sys.path.insert(0, self.bindir)
+
+        return find_executable(binary, os.pathsep.join(sys.path))
+
+    def stop_application(self):
+        self.dm.shellCheckOutput(['stop', 'b2g'])
+
+        # For some reason user.js in the profile doesn't get picked up.
+        # Manually copy it over to prefs.js. See bug 1009730 for more details.
+        self.dm.moveTree(posixpath.join(self.remote_profile, 'user.js'),
+                         posixpath.join(self.remote_profile, 'prefs.js'))
+
+
+class FirefoxContext(object):
+    profile_class = FirefoxProfile
+
+
+class ThunderbirdContext(object):
+    profile_class = ThunderbirdProfile
+
+
+class MetroContext(object):
+    profile_class = MetroFirefoxProfile
+
+    def __init__(self, binary=None):
+        self.binary = binary or os.environ.get('BROWSER_PATH', None)
+
+    def wrap_command(self, command):
+        immersive_helper_path = os.path.join(os.path.dirname(here),
+                                             'resources',
+                                             'metrotestharness.exe')
+        command[:0] = [immersive_helper_path, '-firefoxpath']
+        return command
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/base.py
+++ /dev/null
@@ -1,187 +0,0 @@
-#!/usr/bin/env python
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
-# You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import os
-import subprocess
-import traceback
-
-from mozprocess.processhandler import ProcessHandler
-import mozcrash
-import mozlog
-
-from .errors import RunnerNotStartedError
-
-
-# we can replace these methods with 'abc'
-# (http://docs.python.org/library/abc.html) when we require Python 2.6+
-def abstractmethod(method):
-    line = method.func_code.co_firstlineno
-    filename = method.func_code.co_filename
-
-    def not_implemented(*args, **kwargs):
-        raise NotImplementedError('Abstract method %s at File "%s", line %s '
-                                  'should be implemented by a concrete class' %
-                                  (repr(method), filename, line))
-    return not_implemented
-
-
-class Runner(object):
-
-    def __init__(self, profile, clean_profile=True, process_class=None,
-                 kp_kwargs=None, env=None, symbols_path=None):
-        self.clean_profile = clean_profile
-        self.env = env or {}
-        self.kp_kwargs = kp_kwargs or {}
-        self.process_class = process_class or ProcessHandler
-        self.process_handler = None
-        self.profile = profile
-        self.log = mozlog.getLogger('MozRunner')
-        self.symbols_path = symbols_path
-
-    def __del__(self):
-        self.cleanup()
-
-    # Once we can use 'abc' it should become an abstract property
-    @property
-    def command(self):
-        pass
-
-    @property
-    def returncode(self):
-        if self.process_handler:
-            return self.process_handler.poll()
-        else:
-            raise RunnerNotStartedError("returncode retrieved before process started")
-
-    def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None):
-        """Run self.command in the proper environment
-
-        returns the process id
-
-        :param debug_args: arguments for the debugger
-        :param interactive: uses subprocess.Popen directly
-        :param timeout: see process_handler.run()
-        :param outputTimeout: see process_handler.run()
-
-        """
-        # ensure the runner is stopped
-        self.stop()
-
-        # ensure the profile exists
-        if not self.profile.exists():
-            self.profile.reset()
-            assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__
-
-        cmd = self.command
-
-        # attach a debugger, if specified
-        if debug_args:
-            cmd = list(debug_args) + cmd
-
-        if interactive:
-            self.process_handler = subprocess.Popen(cmd, env=self.env)
-            # TODO: other arguments
-        else:
-            # this run uses the managed processhandler
-            self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs)
-            self.process_handler.run(timeout, outputTimeout)
-
-        return self.process_handler.pid
-
-    def wait(self, timeout=None):
-        """Wait for the process to exit
-
-        returns the process return code if the process exited,
-        returns -<signal> if the process was killed (Unix only)
-        returns None if the process is still running.
-
-        :param timeout: if not None, will return after timeout seconds.
-                        Use is_running() to determine whether or not a
-                        timeout occured. Timeout is ignored if
-                        interactive was set to True.
-
-        """
-        if self.is_running():
-            # The interactive mode uses directly a Popen process instance. It's
-            # wait() method doesn't have any parameters. So handle it separately.
-            if isinstance(self.process_handler, subprocess.Popen):
-                self.process_handler.wait()
-            else:
-                self.process_handler.wait(timeout)
-
-        elif not self.process_handler:
-            raise RunnerNotStartedError("Wait() called before process started")
-
-        return self.returncode
-
-    def is_running(self):
-        """Checks if the process is running
-
-        returns True if the process is active
-
-        """
-        return self.returncode is None
-
-    def stop(self, sig=None):
-        """Kill the process
-
-        returns -<signal> when the process got killed (Unix only)
-
-        :param sig: Signal used to kill the process, defaults to SIGKILL
-                    (has no effect on Windows).
-
-        """
-        try:
-            if not self.is_running():
-                return
-        except RunnerNotStartedError:
-            return
-
-
-        # The interactive mode uses directly a Popen process instance. It's
-        # kill() method doesn't have any parameters. So handle it separately.
-        if isinstance(self.process_handler, subprocess.Popen):
-            self.process_handler.kill()
-        else:
-            self.process_handler.kill(sig=sig)
-
-        return self.returncode
-
-    def reset(self):
-        """Reset the runner to its default state"""
-        if getattr(self, 'profile', False):
-            self.profile.reset()
-
-    def check_for_crashes(self, dump_directory=None, dump_save_path=None,
-                          test_name=None, quiet=False):
-        """Check for a possible crash and output stack trace
-
-        :param dump_directory: Directory to search for minidump files
-        :param dump_save_path: Directory to save the minidump files to
-        :param test_name: Name to use in the crash output
-        :param quiet: If `True` don't print the PROCESS-CRASH message to stdout
-
-        """
-        if not dump_directory:
-            dump_directory = os.path.join(self.profile.profile, 'minidumps')
-
-        crashed = False
-        try:
-            crashed = mozcrash.check_for_crashes(dump_directory,
-                                                 self.symbols_path,
-                                                 dump_save_path=dump_save_path,
-                                                 test_name=test_name,
-                                                 quiet=quiet)
-        except:
-            traceback.print_exc()
-
-        return crashed
-
-    def cleanup(self):
-        """Cleanup all runner state"""
-        self.stop()
-
-        if getattr(self, 'profile', False) and self.clean_profile:
-            self.profile.cleanup()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/base/__init__.py
@@ -0,0 +1,3 @@
+from .runner import BaseRunner
+from .device import DeviceRunner
+from .browser import GeckoRuntimeRunner
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/base/browser.py
@@ -0,0 +1,74 @@
+# 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 mozinfo
+import os
+import platform
+import sys
+
+from .runner import BaseRunner
+
+
+class GeckoRuntimeRunner(BaseRunner):
+    """
+    The base runner class used for local gecko runtime binaries,
+    such as Firefox and Thunderbird.
+    """
+
+    def __init__(self, binary, cmdargs=None, **runner_args):
+        BaseRunner.__init__(self, **runner_args)
+
+        self.binary = binary
+        self.cmdargs = cmdargs or []
+
+        # allows you to run an instance of Firefox separately from any other instances
+        self.env['MOZ_NO_REMOTE'] = '1'
+        # keeps Firefox attached to the terminal window after it starts
+        self.env['NO_EM_RESTART'] = '1'
+
+        # set the library path if needed on linux
+        if sys.platform == 'linux2' and self.binary.endswith('-bin'):
+            dirname = os.path.dirname(self.binary)
+            if os.environ.get('LD_LIBRARY_PATH', None):
+                self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
+            else:
+                self.env['LD_LIBRARY_PATH'] = dirname
+
+    @property
+    def command(self):
+        command = [self.binary, '-profile', self.profile.profile]
+
+        _cmdargs = [i for i in self.cmdargs
+                    if i != '-foreground']
+        if len(_cmdargs) != len(self.cmdargs):
+            # foreground should be last; see
+            # https://bugzilla.mozilla.org/show_bug.cgi?id=625614
+            self.cmdargs = _cmdargs
+            self.cmdargs.append('-foreground')
+        if mozinfo.isMac and '-foreground' not in self.cmdargs:
+            # runner should specify '-foreground' on Mac; see
+            # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
+            self.cmdargs.append('-foreground')
+
+        # Bug 775416 - Ensure that binary options are passed in first
+        command[1:1] = self.cmdargs
+
+        # If running on OS X 10.5 or older, wrap |cmd| so that it will
+        # be executed as an i386 binary, in case it's a 32-bit/64-bit universal
+        # binary.
+        if mozinfo.isMac and hasattr(platform, 'mac_ver') and \
+                platform.mac_ver()[0][:4] < '10.6':
+            command = ["arch", "-arch", "i386"] + command
+
+        if hasattr(self.app_ctx, 'wrap_command'):
+            command = self.app_ctx.wrap_command(command)
+        return command
+
+    def start(self, *args, **kwargs):
+        # ensure the profile exists
+        if not self.profile.exists():
+            self.profile.reset()
+            assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__
+
+        BaseRunner.start(self, *args, **kwargs)
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/base/device.py
@@ -0,0 +1,111 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import print_function
+
+import datetime
+import re
+import signal
+import sys
+import tempfile
+import time
+
+from .runner import BaseRunner
+
+class DeviceRunner(BaseRunner):
+    """
+    The base runner class used for running gecko on
+    remote devices (or emulators), such as B2G.
+    """
+    def __init__(self, device_class, device_args=None, **kwargs):
+        process_args = kwargs.get('process_args', {})
+        process_args.update({ 'stream': sys.stdout,
+                              'processOutputLine': self.on_output,
+                              'onTimeout': self.on_timeout })
+        kwargs['process_args'] = process_args
+        BaseRunner.__init__(self, **kwargs)
+
+        device_args = device_args or {}
+        self.device = device_class(**device_args)
+
+        process_log = tempfile.NamedTemporaryFile(suffix='pidlog')
+        self._env =  { 'MOZ_CRASHREPORTER': '1',
+                       'MOZ_CRASHREPORTER_NO_REPORT': '1',
+                       'MOZ_CRASHREPORTER_SHUTDOWN': '1',
+                       'MOZ_HIDE_RESULTS_TABLE': '1',
+                       'MOZ_PROCESS_LOG': process_log.name,
+                       'NSPR_LOG_MODULES': 'signaling:5,mtransport:3',
+                       'R_LOG_LEVEL': '5',
+                       'R_LOG_DESTINATION': 'stderr',
+                       'R_LOG_VERBOSE': '1',
+                       'NO_EM_RESTART': '1', }
+        if kwargs.get('env'):
+            self._env.update(kwargs['env'])
+
+        # In this case we need to pass in env as part of the command.
+        # Make this empty so runner doesn't pass anything into the
+        # process class.
+        self.env = None
+
+    @property
+    def command(self):
+        cmd = [self.app_ctx.adb]
+        if self.app_ctx.dm._deviceSerial:
+            cmd.extend(['-s', self.app_ctx.dm._deviceSerial])
+        cmd.append('shell')
+        for k, v in self._env.iteritems():
+            cmd.append('%s=%s' % (k, v))
+        cmd.append(self.app_ctx.remote_binary)
+        return cmd
+
+    def start(self, *args, **kwargs):
+        if not self.device.proc:
+            self.device.start()
+        self.device.setup_profile(self.profile)
+        self.app_ctx.stop_application()
+
+        BaseRunner.start(self, *args, **kwargs)
+
+        timeout = 10 # seconds
+        starttime = datetime.datetime.now()
+        while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
+            if self.app_ctx.dm.processExist(self.app_ctx.remote_process):
+                break
+            time.sleep(1)
+        else:
+            print("timed out waiting for '%s' process to start" % self.app_ctx.remote_process)
+
+    def on_output(self, line):
+        match = re.findall(r"TEST-START \| ([^\s]*)", line)
+        if match:
+            self.last_test = match[-1]
+
+    def on_timeout(self, line):
+        self.dm.killProcess(self.app_ctx.remote_process, sig=signal.SIGABRT)
+        timeout = 10 # seconds
+        starttime = datetime.datetime.now()
+        while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
+            if not self.app_ctx.dm.processExist(self.app_ctx.remote_process):
+                break
+            time.sleep(1)
+        else:
+            print("timed out waiting for '%s' process to exit" % self.app_ctx.remote_process)
+
+        msg = "%s | application timed out after %s seconds"
+        if self.timeout:
+            timeout = self.timeout
+        else:
+            timeout = self.output_timeout
+            msg = "%s with no output" % msg
+
+        self.log.testFail(msg % (self.last_test, timeout))
+        self.check_for_crashes()
+
+    def check_for_crashes(self):
+        dump_dir = self.device.pull_minidumps()
+        BaseRunner.check_for_crashes(self, dump_directory=dump_dir, test_name=self.last_test)
+
+    def cleanup(self, *args, **kwargs):
+        BaseRunner.cleanup(self, *args, **kwargs)
+        self.device.cleanup()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/base/runner.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from abc import ABCMeta, abstractproperty
+import os
+import subprocess
+import traceback
+
+from mozprocess import ProcessHandler
+import mozcrash
+
+from ..application import DefaultContext
+from ..errors import RunnerNotStartedError
+
+
+class BaseRunner(object):
+    """
+    The base runner class for all mozrunner objects, both local and remote.
+    """
+    __metaclass__ = ABCMeta
+    last_test = 'automation'
+    process_handler = None
+    timeout = None
+    output_timeout = None
+
+    def __init__(self, app_ctx=None, profile=None, clean_profile=True, env=None,
+                 process_class=None, process_args=None, symbols_path=None):
+        self.app_ctx = app_ctx or DefaultContext()
+
+        if isinstance(profile, basestring):
+            self.profile = self.app_ctx.profile_class(profile=profile)
+        else:
+            self.profile = profile or self.app_ctx.profile_class(**getattr(self.app_ctx, 'profile_args', {}))
+
+        # process environment
+        if env is None:
+            self.env = os.environ.copy()
+        else:
+            self.env = env.copy()
+
+        self.clean_profile = clean_profile
+        self.process_class = process_class or ProcessHandler
+        self.process_args = process_args or {}
+        self.symbols_path = symbols_path
+
+    def __del__(self):
+        self.cleanup()
+
+    @abstractproperty
+    def command(self):
+        """Returns the command list to run."""
+        pass
+
+    @property
+    def returncode(self):
+        """
+        The returncode of the process_handler. A value of None
+        indicates the process is still running. A negative
+        value indicates the process was killed with the
+        specified signal.
+
+        :raises: RunnerNotStartedError
+        """
+        if self.process_handler:
+            return self.process_handler.poll()
+        else:
+            raise RunnerNotStartedError("returncode accessed before runner started")
+
+    def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None):
+        """
+        Run self.command in the proper environment.
+
+        :param debug_args: arguments for a debugger
+        :param interactive: uses subprocess.Popen directly
+        :param timeout: see process_handler.run()
+        :param outputTimeout: see process_handler.run()
+        :returns: the process id
+        """
+        self.timeout = timeout
+        self.output_timeout = outputTimeout
+        cmd = self.command
+
+        # ensure the runner is stopped
+        self.stop()
+
+        # attach a debugger, if specified
+        if debug_args:
+            cmd = list(debug_args) + cmd
+
+        if interactive:
+            self.process_handler = subprocess.Popen(cmd, env=self.env)
+            # TODO: other arguments
+        else:
+            # this run uses the managed processhandler
+            self.process_handler = self.process_class(cmd, env=self.env, **self.process_args)
+            self.process_handler.run(self.timeout, self.output_timeout)
+
+        return self.process_handler.pid
+
+    def wait(self, timeout=None):
+        """
+        Wait for the process to exit.
+
+        :param timeout: if not None, will return after timeout seconds.
+                        Timeout is ignored if interactive was set to True.
+        :returns: the process return code if process exited normally,
+                  -<signal> if process was killed (Unix only),
+                  None if timeout was reached and the process is still running.
+        :raises: RunnerNotStartedError
+        """
+        if self.is_running():
+            # The interactive mode uses directly a Popen process instance. It's
+            # wait() method doesn't have any parameters. So handle it separately.
+            if isinstance(self.process_handler, subprocess.Popen):
+                self.process_handler.wait()
+            else:
+                self.process_handler.wait(timeout)
+
+        elif not self.process_handler:
+            raise RunnerNotStartedError("Wait() called before process started")
+
+        return self.returncode
+
+    def is_running(self):
+        """
+        Checks if the process is running.
+
+        :returns: True if the process is active
+        """
+        return self.returncode is None
+
+    def stop(self, sig=None):
+        """
+        Kill the process.
+
+        :param sig: Signal used to kill the process, defaults to SIGKILL
+                    (has no effect on Windows).
+        :returns: the process return code if process was already stopped,
+                  -<signal> if process was killed (Unix only)
+        :raises: RunnerNotStartedError
+        """
+        try:
+            if not self.is_running():
+                return self.returncode
+        except RunnerNotStartedError:
+            return
+
+        # The interactive mode uses directly a Popen process instance. It's
+        # kill() method doesn't have any parameters. So handle it separately.
+        if isinstance(self.process_handler, subprocess.Popen):
+            self.process_handler.kill()
+        else:
+            self.process_handler.kill(sig=sig)
+
+        return self.returncode
+
+    def reset(self):
+        """
+        Reset the runner to its default state.
+        """
+        self.stop()
+        self.process_handler = None
+
+    def check_for_crashes(self, dump_directory=None, dump_save_path=None,
+                          test_name=None, quiet=False):
+        """
+        Check for a possible crash and output stack trace.
+
+        :param dump_directory: Directory to search for minidump files
+        :param dump_save_path: Directory to save the minidump files to
+        :param test_name: Name to use in the crash output
+        :param quiet: If `True` don't print the PROCESS-CRASH message to stdout
+        :returns: True if a crash was detected, otherwise False
+        """
+        if not dump_directory:
+            dump_directory = os.path.join(self.profile.profile, 'minidumps')
+
+        crashed = False
+        try:
+            crashed = mozcrash.check_for_crashes(dump_directory,
+                                                 self.symbols_path,
+                                                 dump_save_path=dump_save_path,
+                                                 test_name=test_name,
+                                                 quiet=quiet)
+        except:
+            traceback.print_exc()
+
+        return crashed
+
+    def cleanup(self):
+        """
+        Cleanup all runner state
+        """
+        self.stop()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/cli.py
@@ -0,0 +1,193 @@
+# 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 optparse
+import os
+import sys
+
+from mozprofile import MozProfileCLI, Profile
+from .runners import (
+    FirefoxRunner,
+    MetroRunner,
+    ThunderbirdRunner,
+)
+
+from .utils import findInPath
+
+RUNNER_MAP = {
+    'firefox': FirefoxRunner,
+    'metro': MetroRunner,
+    'thunderbird': ThunderbirdRunner,
+}
+
+# Map of debugging programs to information about them
+# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59
+DEBUGGERS = {'gdb': {'interactive': True,
+                     'args': ['-q', '--args'],},
+             'valgrind': {'interactive': False,
+                          'args': ['--leak-check=full']}
+             }
+
+def debugger_arguments(debugger, arguments=None, interactive=None):
+    """Finds debugger arguments from debugger given and defaults
+
+    :param debugger: name or path to debugger
+    :param arguments: arguments for the debugger, or None to use defaults
+    :param interactive: whether the debugger should run in interactive mode
+
+    """
+    # find debugger executable if not a file
+    executable = debugger
+    if not os.path.exists(executable):
+        executable = findInPath(debugger)
+    if executable is None:
+        raise Exception("Path to '%s' not found" % debugger)
+
+    # if debugger not in dictionary of knowns return defaults
+    dirname, debugger = os.path.split(debugger)
+    if debugger not in DEBUGGERS:
+        return ([executable] + (arguments or []), bool(interactive))
+
+    # otherwise use the dictionary values for arguments unless specified
+    if arguments is None:
+        arguments = DEBUGGERS[debugger].get('args', [])
+    if interactive is None:
+        interactive = DEBUGGERS[debugger].get('interactive', False)
+    return ([executable] + arguments, interactive)
+
+
+class CLI(MozProfileCLI):
+    """Command line interface"""
+
+    module = "mozrunner"
+
+    def __init__(self, args=sys.argv[1:]):
+        self.metadata = getattr(sys.modules[self.module],
+                                'package_metadata',
+                                {})
+        version = self.metadata.get('Version')
+        parser_args = {'description': self.metadata.get('Summary')}
+        if version:
+            parser_args['version'] = "%prog " + version
+        self.parser = optparse.OptionParser(**parser_args)
+        self.add_options(self.parser)
+        (self.options, self.args) = self.parser.parse_args(args)
+
+        if getattr(self.options, 'info', None):
+            self.print_metadata()
+            sys.exit(0)
+
+        # choose appropriate runner and profile classes
+        try:
+            self.runner_class = RUNNER_MAP[self.options.app]
+        except KeyError:
+            self.parser.error('Application "%s" unknown (should be one of "%s")' %
+                              (self.options.app, ', '.join(RUNNER_MAP.keys())))
+
+    def add_options(self, parser):
+        """add options to the parser"""
+
+        # add profile options
+        MozProfileCLI.add_options(self, parser)
+
+        # add runner options
+        parser.add_option('-b', "--binary",
+                          dest="binary", help="Binary path.",
+                          metavar=None, default=None)
+        parser.add_option('--app', dest='app', default='firefox',
+                          help="Application to use [DEFAULT: %default]")
+        parser.add_option('--app-arg', dest='appArgs',
+                          default=[], action='append',
+                          help="provides an argument to the test application")
+        parser.add_option('--debugger', dest='debugger',
+                          help="run under a debugger, e.g. gdb or valgrind")
+        parser.add_option('--debugger-args', dest='debugger_args',
+                          action='store',
+                          help="arguments to the debugger")
+        parser.add_option('--interactive', dest='interactive',
+                          action='store_true',
+                          help="run the program interactively")
+        if self.metadata:
+            parser.add_option("--info", dest="info", default=False,
+                              action="store_true",
+                              help="Print module information")
+
+    ### methods for introspecting data
+
+    def get_metadata_from_egg(self):
+        import pkg_resources
+        ret = {}
+        dist = pkg_resources.get_distribution(self.module)
+        if dist.has_metadata("PKG-INFO"):
+            for line in dist.get_metadata_lines("PKG-INFO"):
+                key, value = line.split(':', 1)
+                ret[key] = value
+        if dist.has_metadata("requires.txt"):
+            ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
+        return ret
+
+    def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
+                                   "Author", "Author-email", "License", "Platform", "Dependencies")):
+        for key in data:
+            if key in self.metadata:
+                print key + ": " + self.metadata[key]
+
+    ### methods for running
+
+    def command_args(self):
+        """additional arguments for the mozilla application"""
+        return map(os.path.expanduser, self.options.appArgs)
+
+    def runner_args(self):
+        """arguments to instantiate the runner class"""
+        return dict(cmdargs=self.command_args(),
+                    binary=self.options.binary)
+
+    def create_runner(self):
+        profile = Profile(**self.profile_args())
+        return self.runner_class(profile=profile, **self.runner_args())
+
+    def run(self):
+        runner = self.create_runner()
+        self.start(runner)
+        runner.cleanup()
+
+    def debugger_arguments(self):
+        """Get the debugger arguments
+
+        returns a 2-tuple of debugger arguments:
+            (debugger_arguments, interactive)
+
+        """
+        debug_args = self.options.debugger_args
+        if debug_args is not None:
+            debug_args = debug_args.split()
+        interactive = self.options.interactive
+        if self.options.debugger:
+            debug_args, interactive = debugger_arguments(self.options.debugger, debug_args, interactive)
+        return debug_args, interactive
+
+    def start(self, runner):
+        """Starts the runner and waits for the application to exit
+
+        It can also happen via a keyboard interrupt. It should be
+        overwritten to provide custom running of the runner instance.
+
+        """
+        # attach a debugger if specified
+        debug_args, interactive = self.debugger_arguments()
+        runner.start(debug_args=debug_args, interactive=interactive)
+        print 'Starting: ' + ' '.join(runner.command)
+        try:
+            runner.wait()
+        except KeyboardInterrupt:
+            runner.stop()
+
+
+def cli(args=sys.argv[1:]):
+    CLI(args).run()
+
+
+if __name__ == '__main__':
+    cli()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/devices/__init__.py
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from emulator import Emulator
+from base import Device
+
+import emulator_battery
+import emulator_geo
+import emulator_screen
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/devices/base.py
@@ -0,0 +1,227 @@
+from ConfigParser import (
+    ConfigParser,
+    RawConfigParser
+)
+import datetime
+import os
+import posixpath
+import socket
+import subprocess
+import tempfile
+import time
+import traceback
+
+from mozdevice import DMError
+
+class Device(object):
+    def __init__(self, app_ctx, restore=True):
+        self.app_ctx = app_ctx
+        self.dm = self.app_ctx.dm
+        self.restore = restore
+        self.added_files = set()
+        self.backup_files = set()
+
+    @property
+    def remote_profiles(self):
+        """
+        A list of remote profiles on the device.
+        """
+        remote_ini = self.app_ctx.remote_profiles_ini
+        if not self.dm.fileExists(remote_ini):
+            raise Exception("Remote file '%s' not found" % remote_ini)
+
+        local_ini = tempfile.NamedTemporaryFile()
+        self.dm.getFile(remote_ini, local_ini.name)
+        cfg = ConfigParser()
+        cfg.read(local_ini.name)
+
+        profiles = []
+        for section in cfg.sections():
+            if cfg.has_option(section, 'Path'):
+                if cfg.has_option(section, 'IsRelative') and cfg.getint(section, 'IsRelative'):
+                    profiles.append(posixpath.join(posixpath.dirname(remote_ini), \
+                                    cfg.get(section, 'Path')))
+                else:
+                    profiles.append(cfg.get(section, 'Path'))
+        return profiles
+
+    def pull_minidumps(self):
+        """
+        Saves any minidumps found in the remote profile on the local filesystem.
+
+        :returns: Path to directory containing the dumps.
+        """
+        remote_dump_dir = posixpath.join(self.app_ctx.remote_profile, 'minidumps')
+        local_dump_dir = tempfile.mkdtemp()
+        self.dm.getDirectory(remote_dump_dir, local_dump_dir)
+        self.dm.removeDir(remote_dump_dir)
+        return local_dump_dir
+
+    def setup_profile(self, profile):
+        """
+        Copy profile to the device and update the remote profiles.ini
+        to point to the new profile.
+
+        :param profile: mozprofile object to copy over.
+        """
+        self.dm.remount()
+
+        if self.dm.dirExists(self.app_ctx.remote_profile):
+            self.dm.shellCheckOutput(['rm', '-r', self.app_ctx.remote_profile])
+
+        self.dm.pushDir(profile.profile, self.app_ctx.remote_profile)
+
+        extension_dir = os.path.join(profile.profile, 'extensions', 'staged')
+        if os.path.isdir(extension_dir):
+            # Copy the extensions to the B2G bundles dir.
+            # need to write to read-only dir
+            for filename in os.listdir(extension_dir):
+                path = posixpath.join(self.app_ctx.remote_bundles_dir, filename)
+                if self.dm.fileExists(path):
+                    self.dm.shellCheckOutput(['rm', '-rf', path])
+            self.dm.pushDir(extension_dir, self.app_ctx.remote_bundles_dir)
+
+        timeout = 5 # seconds
+        starttime = datetime.datetime.now()
+        while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
+            if self.dm.fileExists(self.app_ctx.remote_profiles_ini):
+                break
+            time.sleep(1)
+        else:
+            print "timed out waiting for profiles.ini"
+
+        local_profiles_ini = tempfile.NamedTemporaryFile()
+        self.dm.getFile(self.app_ctx.remote_profiles_ini, local_profiles_ini.name)
+
+        config = ProfileConfigParser()
+        config.read(local_profiles_ini.name)
+        for section in config.sections():
+            if 'Profile' in section:
+                config.set(section, 'IsRelative', 0)
+                config.set(section, 'Path', self.app_ctx.remote_profile)
+
+        new_profiles_ini = tempfile.NamedTemporaryFile()
+        config.write(open(new_profiles_ini.name, 'w'))
+
+        self.backup_file(self.app_ctx.remote_profiles_ini)
+        self.dm.pushFile(new_profiles_ini.name, self.app_ctx.remote_profiles_ini)
+
+    def install_busybox(self, busybox):
+        """
+        Installs busybox on the device.
+
+        :param busybox: Path to busybox binary to install.
+        """
+        self.dm.remount()
+        print 'pushing %s' % self.app_ctx.remote_busybox
+        self.dm.pushFile(busybox, self.app_ctx.remote_busybox, retryLimit=10)
+        # TODO for some reason using dm.shellCheckOutput doesn't work,
+        #      while calling adb shell directly does.
+        args = [self.app_ctx.adb, '-s', self.dm._deviceSerial,
+                'shell', 'cd /system/bin; chmod 555 busybox;' \
+                'for x in `./busybox --list`; do ln -s ./busybox $x; done']
+        adb = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        adb.wait()
+        self.dm._verifyZip()
+
+    def setup_port_forwarding(self, remote_port):
+        """
+        Set up TCP port forwarding to the specified port on the device,
+        using any availble local port, and return the local port.
+
+        :param remote_port: The remote port to wait on.
+        """
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        s.bind(("",0))
+        local_port = s.getsockname()[1]
+        s.close()
+
+        self.dm.forward('tcp:%d' % local_port, 'tcp:%d' % remote_port)
+        return local_port
+
+    def wait_for_port(self, port, timeout=300):
+        starttime = datetime.datetime.now()
+        while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
+            try:
+                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                sock.connect(('localhost', port))
+                data = sock.recv(16)
+                sock.close()
+                if ':' in data:
+                    return True
+            except:
+                traceback.print_exc()
+            time.sleep(1)
+        return False
+
+    def backup_file(self, remote_path):
+        if not self.restore:
+            return
+
+        if self.dm.fileExists(remote_path):
+            self.dm.copyTree(remote_path, '%s.orig' % remote_path)
+            self.backup_files.add(remote_path)
+        else:
+            self.added_files.add(remote_path)
+
+    def cleanup(self):
+        """
+        Cleanup the device.
+        """
+        if not self.restore:
+            return
+
+        try:
+            self.dm._verifyDevice()
+        except DMError:
+            return
+
+        self.dm.remount()
+        # Restore the original profile
+        for added_file in self.added_files:
+            self.dm.removeFile(added_file)
+
+        for backup_file in self.backup_files:
+            if self.dm.fileExists('%s.orig' % backup_file):
+                self.dm.moveTree('%s.orig' % backup_file, backup_file)
+
+        # Delete any bundled extensions
+        extension_dir = posixpath.join(self.app_ctx.remote_profile, 'extensions', 'staged')
+        if self.dm.dirExists(extension_dir):
+            for filename in self.dm.listFiles(extension_dir):
+                try:
+                    self.dm.removeDir(posixpath.join(self.app_ctx.remote_bundles_dir, filename))
+                except DMError:
+                    pass
+        # Remove the test profile
+        self.dm.removeDir(self.app_ctx.remote_profile)
+
+
+class ProfileConfigParser(RawConfigParser):
+    """
+    Class to create profiles.ini config files
+
+    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")
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator.py
@@ -0,0 +1,258 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telnetlib import Telnet
+import datetime
+import os
+import shutil
+import subprocess
+import tempfile
+import time
+
+from mozprocess import ProcessHandler
+
+from .base import Device
+from .emulator_battery import EmulatorBattery
+from .emulator_geo import EmulatorGeo
+from .emulator_screen import EmulatorScreen
+from ..utils import uses_marionette
+from ..errors import TimeoutException, ScriptTimeoutException
+
+class ArchContext(object):
+    def __init__(self, arch, context, binary=None):
+        kernel = os.path.join(context.homedir, 'prebuilts', 'qemu-kernel', '%s', '%s')
+        sysdir = os.path.join(context.homedir, 'out', 'target', 'product', '%s')
+        if arch == 'x86':
+            self.binary = os.path.join(context.bindir, 'emulator-x86')
+            self.kernel = kernel % ('x86', 'kernel-qemu')
+            self.sysdir = sysdir % 'generic_x86'
+            self.extra_args = []
+        else:
+            self.binary = os.path.join(context.bindir, 'emulator')
+            self.kernel = kernel % ('arm', 'kernel-qemu-armv7')
+            self.sysdir = sysdir % 'generic'
+            self.extra_args = ['-cpu', 'cortex-a8']
+
+        if binary:
+            self.binary = binary
+
+
+class Emulator(Device):
+    logcat_proc = None
+    port = None
+    proc = None
+    telnet = None
+
+    def __init__(self, app_ctx, arch, resolution=None, sdcard=None, userdata=None,
+                 logdir=None, no_window=None, binary=None):
+        Device.__init__(self, app_ctx)
+
+        self.arch = ArchContext(arch, self.app_ctx, binary=binary)
+        self.resolution = resolution or '320x480'
+        self.sdcard = None
+        if sdcard:
+            self.sdcard = self.create_sdcard(sdcard)
+        self.userdata = os.path.join(self.arch.sysdir, 'userdata.img')
+        if userdata:
+            self.userdata = tempfile.NamedTemporaryFile(prefix='qemu-userdata')
+            shutil.copyfile(userdata, self.userdata)
+        self.logdir = logdir
+        self.no_window = no_window
+
+        self.battery = EmulatorBattery(self)
+        self.geo = EmulatorGeo(self)
+        self.screen = EmulatorScreen(self)
+
+    @property
+    def args(self):
+        """
+        Arguments to pass into the emulator binary.
+        """
+        qemu_args = [self.arch.binary,
+                     '-kernel', self.arch.kernel,
+                     '-sysdir', self.arch.sysdir,
+                     '-data', self.userdata]
+        if self.no_window:
+            qemu_args.append('-no-window')
+        if self.sdcard:
+            qemu_args.extend(['-sdcard', self.sdcard])
+        qemu_args.extend(['-memory', '512',
+                          '-partition-size', '512',
+                          '-verbose',
+                          '-skin', self.resolution,
+                          '-gpu', 'on',
+                          '-qemu'] + self.arch.extra_args)
+        return qemu_args
+
+    def _get_online_devices(self):
+        return set([d[0] for d in self.dm.devices() if d[1] != 'offline'])
+
+    def start(self):
+        """
+        Starts a new emulator.
+        """
+        original_devices = self._get_online_devices()
+
+        qemu_log = None
+        qemu_proc_args = {}
+        if self.logdir:
+            # save output from qemu to logfile
+            qemu_log = os.path.join(self.logdir, 'qemu.log')
+            if os.path.isfile(qemu_log):
+                self._rotate_log(qemu_log)
+            qemu_proc_args['logfile'] = qemu_log
+        else:
+            qemu_proc_args['processOutputLine'] = lambda line: None
+        self.proc = ProcessHandler(self.args, **qemu_proc_args)
+        self.proc.run()
+
+        devices = self._get_online_devices()
+        now = datetime.datetime.now()
+        while (devices - original_devices) == set([]):
+            time.sleep(1)
+            if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
+                raise TimeoutException('timed out waiting for emulator to start')
+            devices = self._get_online_devices()
+        self.connect(devices - original_devices)
+
+    def connect(self, devices=None):
+        """
+        Connects to an already running emulator.
+        """
+        devices = list(devices or self._get_online_devices())
+        serial = [d for d in devices if d.startswith('emulator')][0]
+        self.dm._deviceSerial = serial
+        self.dm.connect()
+        self.port = int(serial[serial.rindex('-')+1:])
+
+        self.geo.set_default_location()
+        self.screen.initialize()
+
+        print self.logdir
+        if self.logdir:
+            # save logcat
+            logcat_log = os.path.join(self.logdir, '%s.log' % serial)
+            if os.path.isfile(logcat_log):
+                self._rotate_log(logcat_log)
+            logcat_args = [self.app_ctx.adb, '-s', '%s' % serial,
+                           'logcat', '-v', 'threadtime']
+            self.logcat_proc = ProcessHandler(logcat_args, logfile=logcat_log)
+            self.logcat_proc.run()
+
+        # setup DNS fix for networking
+        self.app_ctx.dm.shellCheckOutput(['setprop', 'net.dns1', '10.0.2.3'])
+
+    def create_sdcard(self, sdcard_size):
+        """
+        Creates an sdcard partition in the emulator.
+
+        :param sdcard_size: Size of partition to create, e.g '10MB'.
+        """
+        mksdcard = self.app_ctx.which('mksdcard')
+        path = tempfile.mktemp(prefix='sdcard')
+        sdargs = [mksdcard, '-l', 'mySdCard', sdcard_size, path]
+        sd = subprocess.Popen(sdargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        retcode = sd.wait()
+        if retcode:
+            raise Exception('unable to create sdcard: exit code %d: %s'
+                            % (retcode, sd.stdout.read()))
+        return path
+
+    def cleanup(self):
+        """
+        Cleans up and kills the emulator.
+        """
+        Device.cleanup(self)
+        if self.proc:
+            self.proc.kill()
+            self.proc = None
+
+        # Remove temporary sdcard
+        if self.sdcard and os.path.isfile(self.sdcard):
+            os.remove(self.sdcard)
+
+    def _rotate_log(self, srclog, index=1):
+        """
+        Rotate a logfile, by recursively rotating logs further in the sequence,
+        deleting the last file if necessary.
+        """
+        basename = os.path.basename(srclog)
+        basename = basename[:-len('.log')]
+        if index > 1:
+            basename = basename[:-len('.1')]
+        basename = '%s.%d.log' % (basename, index)
+
+        destlog = os.path.join(self.logdir, basename)
+        if os.path.isfile(destlog):
+            if index == 3:
+                os.remove(destlog)
+            else:
+                self._rotate_log(destlog, index+1)
+        shutil.move(srclog, destlog)
+
+    # TODO this function is B2G specific and shouldn't live here
+    @uses_marionette
+    def wait_for_system_message(self, marionette):
+        marionette.set_script_timeout(45000)
+        # Telephony API's won't be available immediately upon emulator
+        # boot; we have to wait for the syste-message-listener-ready
+        # message before we'll be able to use them successfully.  See
+        # bug 792647.
+        print 'waiting for system-message-listener-ready...'
+        try:
+            marionette.execute_async_script("""
+waitFor(
+    function() { marionetteScriptFinished(true); },
+    function() { return isSystemMessageListenerReady(); }
+);
+            """)
+        except ScriptTimeoutException:
+            print 'timed out'
+            # We silently ignore the timeout if it occurs, since
+            # isSystemMessageListenerReady() isn't available on
+            # older emulators.  45s *should* be enough of a delay
+            # to allow telephony API's to work.
+            pass
+        print '...done'
+
+    # TODO this function is B2G specific and shouldn't live here
+    @uses_marionette
+    def wait_for_homescreen(self, marionette):
+        print 'waiting for homescreen...'
+
+        marionette.set_context(marionette.CONTEXT_CONTENT)
+        marionette.execute_async_script("""
+log('waiting for mozbrowserloadend');
+window.addEventListener('mozbrowserloadend', function loaded(aEvent) {
+  log('received mozbrowserloadend for ' + aEvent.target.src);
+  if (aEvent.target.src.indexOf('ftu') != -1 || aEvent.target.src.indexOf('homescreen') != -1) {
+    window.removeEventListener('mozbrowserloadend', loaded);
+    marionetteScriptFinished();
+  }
+});""", script_timeout=120000)
+        print '...done'
+
+    def _get_telnet_response(self, command=None):
+        output = []
+        assert(self.telnet)
+        if command is not None:
+            self.telnet.write('%s\n' % command)
+        while True:
+            line = self.telnet.read_until('\n')
+            output.append(line.rstrip())
+            if line.startswith('OK'):
+                return output
+            elif line.startswith('KO:'):
+                raise Exception('bad telnet response: %s' % line)
+
+    def _run_telnet(self, command):
+        if not self.telnet:
+            self.telnet = Telnet('localhost', self.port)
+            self._get_telnet_response()
+        return self._get_telnet_response(command)
+
+    def __del__(self):
+        if self.telnet:
+            self.telnet.write('exit\n')
+            self.telnet.read_all()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py
@@ -0,0 +1,53 @@
+# 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/.
+
+class EmulatorBattery(object):
+
+    def __init__(self, emulator):
+        self.emulator = emulator
+
+    def get_state(self):
+        status = {}
+        state = {}
+
+        response = self.emulator._run_telnet('power display')
+        for line in response:
+            if ':' in line:
+                field, value = line.split(':')
+                value = value.strip()
+                if value == 'true':
+                    value = True
+                elif value == 'false':
+                    value = False
+                elif field == 'capacity':
+                    value = float(value)
+                status[field] = value
+
+        state['level'] = status.get('capacity', 0.0) / 100
+        if status.get('AC') == 'online':
+            state['charging'] = True
+        else:
+            state['charging'] = False
+
+        return state
+
+    def get_charging(self):
+        return self.get_state()['charging']
+
+    def get_level(self):
+        return self.get_state()['level']
+
+    def set_level(self, level):
+        self.emulator._run_telnet('power capacity %d' % (level * 100))
+
+    def set_charging(self, charging):
+        if charging:
+            cmd = 'power ac on'
+        else:
+            cmd = 'power ac off'
+        self.emulator._run_telnet(cmd)
+
+    charging = property(get_charging, set_charging)
+    level = property(get_level, set_level)
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py
@@ -0,0 +1,17 @@
+# 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/.
+
+class EmulatorGeo(object):
+
+    def __init__(self, emulator):
+        self.emulator = emulator
+
+    def set_default_location(self):
+        self.lon = -122.08769
+        self.lat = 37.41857
+        self.set_location(self.lon, self.lat)
+
+    def set_location(self, lon, lat):
+        self.emulator._run_telnet('geo fix %0.5f %0.5f' % (self.lon, self.lat))
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py
@@ -0,0 +1,89 @@
+# 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/.
+
+class EmulatorScreen(object):
+    """Class for screen related emulator commands."""
+
+    SO_PORTRAIT_PRIMARY = 'portrait-primary'
+    SO_PORTRAIT_SECONDARY = 'portrait-secondary'
+    SO_LANDSCAPE_PRIMARY = 'landscape-primary'
+    SO_LANDSCAPE_SECONDARY = 'landscape-secondary'
+
+    def __init__(self, emulator):
+        self.emulator = emulator
+
+    def initialize(self):
+        self.orientation = self.SO_PORTRAIT_PRIMARY
+
+    def _get_raw_orientation(self):
+        """Get the raw value of the current device orientation."""
+        response = self.emulator._run_telnet('sensor get orientation')
+
+        return response[0].split('=')[1].strip()
+
+    def _set_raw_orientation(self, data):
+        """Set the raw value of the specified device orientation."""
+        self.emulator._run_telnet('sensor set orientation %s' % data)
+
+    def get_orientation(self):
+        """Get the current device orientation.
+
+        Returns;
+            orientation -- Orientation of the device. One of:
+                            SO_PORTRAIT_PRIMARY - system buttons at the bottom
+                            SO_PORTRIAT_SECONDARY - system buttons at the top
+                            SO_LANDSCAPE_PRIMARY - system buttons at the right
+                            SO_LANDSCAPE_SECONDARY - system buttons at the left
+
+        """
+        data = self._get_raw_orientation()
+
+        if data == '0:-90:0':
+            orientation = self.SO_PORTRAIT_PRIMARY
+        elif data == '0:90:0':
+            orientation = self.SO_PORTRAIT_SECONDARY
+        elif data == '0:0:90':
+            orientation = self.SO_LANDSCAPE_PRIMARY
+        elif data == '0:0:-90':
+            orientation = self.SO_LANDSCAPE_SECONDARY
+        else:
+            raise ValueError('Unknown orientation sensor value: %s.' % data)
+
+        return orientation
+
+    def set_orientation(self, orientation):
+        """Set the specified device orientation.
+
+        Args
+            orientation -- Orientation of the device. One of:
+                            SO_PORTRAIT_PRIMARY - system buttons at the bottom
+                            SO_PORTRIAT_SECONDARY - system buttons at the top
+                            SO_LANDSCAPE_PRIMARY - system buttons at the right
+                            SO_LANDSCAPE_SECONDARY - system buttons at the left
+        """
+        orientation = SCREEN_ORIENTATIONS[orientation]
+
+        if orientation == self.SO_PORTRAIT_PRIMARY:
+            data = '0:-90:0'
+        elif orientation == self.SO_PORTRAIT_SECONDARY:
+            data = '0:90:0'
+        elif orientation == self.SO_LANDSCAPE_PRIMARY:
+            data = '0:0:90'
+        elif orientation == self.SO_LANDSCAPE_SECONDARY:
+            data = '0:0:-90'
+        else:
+            raise ValueError('Invalid orientation: %s' % orientation)
+
+        self._set_raw_orientation(data)
+
+    orientation = property(get_orientation, set_orientation)
+
+
+SCREEN_ORIENTATIONS = {"portrait": EmulatorScreen.SO_PORTRAIT_PRIMARY,
+                       "landscape": EmulatorScreen.SO_LANDSCAPE_PRIMARY,
+                       "portrait-primary": EmulatorScreen.SO_PORTRAIT_PRIMARY,
+                       "landscape-primary": EmulatorScreen.SO_LANDSCAPE_PRIMARY,
+                       "portrait-secondary": EmulatorScreen.SO_PORTRAIT_SECONDARY,
+                       "landscape-secondary": EmulatorScreen.SO_LANDSCAPE_SECONDARY}
+
--- a/testing/mozbase/mozrunner/mozrunner/errors.py
+++ b/testing/mozbase/mozrunner/mozrunner/errors.py
@@ -1,11 +1,16 @@
 #!/usr/bin/env python
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 class RunnerException(Exception):
     """Base exception handler for mozrunner related errors"""
 
-
 class RunnerNotStartedError(RunnerException):
     """Exception handler in case the runner hasn't been started"""
+
+class TimeoutException(RunnerException):
+    """Raised on timeout waiting for targets to start."""
+
+class ScriptTimeoutException(RunnerException):
+    """Raised on timeout waiting for execute_script to finish."""
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/local.py
+++ /dev/null
@@ -1,362 +0,0 @@
-#!/usr/bin/env python
-
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
-# You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import ConfigParser
-import mozinfo
-import optparse
-import os
-import platform
-import subprocess
-import sys
-
-if mozinfo.isMac:
-    from plistlib import readPlist
-
-from mozprofile import Profile, FirefoxProfile, MetroFirefoxProfile, ThunderbirdProfile, MozProfileCLI
-
-from .base import Runner
-from .utils import findInPath, get_metadata_from_egg
-
-
-__all__ = ['CLI',
-           'cli',
-           'LocalRunner',
-           'local_runners',
-           'package_metadata',
-           'FirefoxRunner',
-           'MetroFirefoxRunner',
-           'ThunderbirdRunner']
-
-
-package_metadata = get_metadata_from_egg('mozrunner')
-
-
-# Map of debugging programs to information about them
-# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59
-debuggers = {'gdb': {'interactive': True,
-                     'args': ['-q', '--args'],},
-             'valgrind': {'interactive': False,
-                          'args': ['--leak-check=full']}
-             }
-
-
-def debugger_arguments(debugger, arguments=None, interactive=None):
-    """Finds debugger arguments from debugger given and defaults
-
-    :param debugger: name or path to debugger
-    :param arguments: arguments for the debugger, or None to use defaults
-    :param interactive: whether the debugger should run in interactive mode
-
-    """
-    # find debugger executable if not a file
-    executable = debugger
-    if not os.path.exists(executable):
-        executable = findInPath(debugger)
-    if executable is None:
-        raise Exception("Path to '%s' not found" % debugger)
-
-    # if debugger not in dictionary of knowns return defaults
-    dirname, debugger = os.path.split(debugger)
-    if debugger not in debuggers:
-        return ([executable] + (arguments or []), bool(interactive))
-
-    # otherwise use the dictionary values for arguments unless specified
-    if arguments is None:
-        arguments = debuggers[debugger].get('args', [])
-    if interactive is None:
-        interactive = debuggers[debugger].get('interactive', False)
-    return ([executable] + arguments, interactive)
-
-
-class LocalRunner(Runner):
-    """Handles all running operations. Finds bins, runs and kills the process"""
-
-    profile_class = Profile # profile class to use by default
-
-    @classmethod
-    def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None,
-               clean_profile=True, process_class=None, **kwargs):
-        profile = cls.profile_class(**(profile_args or {}))
-        return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs,
-                                           clean_profile=clean_profile, process_class=process_class, **kwargs)
-
-    def __init__(self, profile, binary, cmdargs=None, env=None,
-                 kp_kwargs=None, clean_profile=None, process_class=None, **kwargs):
-
-        Runner.__init__(self, profile, clean_profile=clean_profile, kp_kwargs=kp_kwargs,
-                        process_class=process_class, env=env, **kwargs)
-
-        # find the binary
-        self.binary = binary
-        if not self.binary:
-            raise Exception("Binary not specified")
-        if not os.path.exists(self.binary):
-            raise OSError("Binary path does not exist: %s" % self.binary)
-
-        # To be safe the absolute path of the binary should be used
-        self.binary = os.path.abspath(self.binary)
-
-        # allow Mac binaries to be specified as an app bundle
-        plist = '%s/Contents/Info.plist' % self.binary
-        if mozinfo.isMac and os.path.exists(plist):
-            info = readPlist(plist)
-            self.binary = os.path.join(self.binary, "Contents/MacOS/",
-                                       info['CFBundleExecutable'])
-
-        self.cmdargs = cmdargs or []
-        _cmdargs = [i for i in self.cmdargs
-                    if i != '-foreground']
-        if len(_cmdargs) != len(self.cmdargs):
-            # foreground should be last; see
-            # https://bugzilla.mozilla.org/show_bug.cgi?id=625614
-            self.cmdargs = _cmdargs
-            self.cmdargs.append('-foreground')
-        if mozinfo.isMac and '-foreground' not in self.cmdargs:
-            # runner should specify '-foreground' on Mac; see
-            # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
-            self.cmdargs.append('-foreground')
-
-        # process environment
-        if env is None:
-            self.env = os.environ.copy()
-        else:
-            self.env = env.copy()
-        # allows you to run an instance of Firefox separately from any other instances
-        self.env['MOZ_NO_REMOTE'] = '1'
-        # keeps Firefox attached to the terminal window after it starts
-        self.env['NO_EM_RESTART'] = '1'
-
-        # set the library path if needed on linux
-        if sys.platform == 'linux2' and self.binary.endswith('-bin'):
-            dirname = os.path.dirname(self.binary)
-            if os.environ.get('LD_LIBRARY_PATH', None):
-                self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
-            else:
-                self.env['LD_LIBRARY_PATH'] = dirname
-
-    @property
-    def command(self):
-        """Returns the command list to run"""
-        commands = [self.binary, '-profile', self.profile.profile]
-
-        # Bug 775416 - Ensure that binary options are passed in first
-        commands[1:1] = self.cmdargs
-
-        # If running on OS X 10.5 or older, wrap |cmd| so that it will
-        # be executed as an i386 binary, in case it's a 32-bit/64-bit universal
-        # binary.
-        if mozinfo.isMac and hasattr(platform, 'mac_ver') and \
-                platform.mac_ver()[0][:4] < '10.6':
-            commands = ["arch", "-arch", "i386"] + commands
-
-        return commands
-
-    def get_repositoryInfo(self):
-        """Read repository information from application.ini and platform.ini"""
-        config = ConfigParser.RawConfigParser()
-        dirname = os.path.dirname(self.binary)
-        repository = { }
-
-        for file, section in [('application', 'App'), ('platform', 'Build')]:
-            config.read(os.path.join(dirname, '%s.ini' % file))
-
-            for key, id in [('SourceRepository', 'repository'),
-                            ('SourceStamp', 'changeset')]:
-                try:
-                    repository['%s_%s' % (file, id)] = config.get(section, key);
-                except:
-                    repository['%s_%s' % (file, id)] = None
-
-        return repository
-
-
-class FirefoxRunner(LocalRunner):
-    """Specialized LocalRunner subclass for running Firefox."""
-
-    profile_class = FirefoxProfile
-
-    def __init__(self, profile, binary=None, **kwargs):
-
-        # if no binary given take it from the BROWSER_PATH environment variable
-        binary = binary or os.environ.get('BROWSER_PATH')
-        LocalRunner.__init__(self, profile, binary, **kwargs)
-
-
-class MetroFirefoxRunner(LocalRunner):
-    """Specialized LocalRunner subclass for running Firefox Metro"""
-
-    profile_class = MetroFirefoxProfile
-
-    # helper application to launch Firefox in Metro mode
-    here = os.path.dirname(os.path.abspath(__file__))
-    immersiveHelperPath = os.path.sep.join([here,
-                                            'resources',
-                                            'metrotestharness.exe'])
-
-    def __init__(self, profile, binary=None, **kwargs):
-
-        # if no binary given take it from the BROWSER_PATH environment variable
-        binary = binary or os.environ.get('BROWSER_PATH')
-        LocalRunner.__init__(self, profile, binary, **kwargs)
-
-        if not os.path.exists(self.immersiveHelperPath):
-            raise OSError('Can not find Metro launcher: %s' % self.immersiveHelperPath)
-
-        if not mozinfo.isWin:
-            raise Exception('Firefox Metro mode is only supported on Windows 8 and onwards')
-
-    @property
-    def command(self):
-       command = LocalRunner.command.fget(self)
-       command[:0] = [self.immersiveHelperPath, '-firefoxpath']
-
-       return command
-
-
-class ThunderbirdRunner(LocalRunner):
-    """Specialized LocalRunner subclass for running Thunderbird"""
-    profile_class = ThunderbirdProfile
-
-
-local_runners = {'firefox': FirefoxRunner,
-                 'metrofirefox' : MetroFirefoxRunner,
-                 'thunderbird': ThunderbirdRunner}
-
-
-class CLI(MozProfileCLI):
-    """Command line interface"""
-
-    module = "mozrunner"
-
-    def __init__(self, args=sys.argv[1:]):
-        self.metadata = getattr(sys.modules[self.module],
-                                'package_metadata',
-                                {})
-        version = self.metadata.get('Version')
-        parser_args = {'description': self.metadata.get('Summary')}
-        if version:
-            parser_args['version'] = "%prog " + version
-        self.parser = optparse.OptionParser(**parser_args)
-        self.add_options(self.parser)
-        (self.options, self.args) = self.parser.parse_args(args)
-
-        if getattr(self.options, 'info', None):
-            self.print_metadata()
-            sys.exit(0)
-
-        # choose appropriate runner and profile classes
-        try:
-            self.runner_class = local_runners[self.options.app]
-        except KeyError:
-            self.parser.error('Application "%s" unknown (should be one of "%s")' %
-                              (self.options.app, ', '.join(local_runners.keys())))
-
-    def add_options(self, parser):
-        """add options to the parser"""
-
-        # add profile options
-        MozProfileCLI.add_options(self, parser)
-
-        # add runner options
-        parser.add_option('-b', "--binary",
-                          dest="binary", help="Binary path.",
-                          metavar=None, default=None)
-        parser.add_option('--app', dest='app', default='firefox',
-                          help="Application to use [DEFAULT: %default]")
-        parser.add_option('--app-arg', dest='appArgs',
-                          default=[], action='append',
-                          help="provides an argument to the test application")
-        parser.add_option('--debugger', dest='debugger',
-                          help="run under a debugger, e.g. gdb or valgrind")
-        parser.add_option('--debugger-args', dest='debugger_args',
-                          action='store',
-                          help="arguments to the debugger")
-        parser.add_option('--interactive', dest='interactive',
-                          action='store_true',
-                          help="run the program interactively")
-        if self.metadata:
-            parser.add_option("--info", dest="info", default=False,
-                              action="store_true",
-                              help="Print module information")
-
-    ### methods for introspecting data
-
-    def get_metadata_from_egg(self):
-        import pkg_resources
-        ret = {}
-        dist = pkg_resources.get_distribution(self.module)
-        if dist.has_metadata("PKG-INFO"):
-            for line in dist.get_metadata_lines("PKG-INFO"):
-                key, value = line.split(':', 1)
-                ret[key] = value
-        if dist.has_metadata("requires.txt"):
-            ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
-        return ret
-
-    def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
-                                   "Author", "Author-email", "License", "Platform", "Dependencies")):
-        for key in data:
-            if key in self.metadata:
-                print key + ": " + self.metadata[key]
-
-    ### methods for running
-
-    def command_args(self):
-        """additional arguments for the mozilla application"""
-        return map(os.path.expanduser, self.options.appArgs)
-
-    def runner_args(self):
-        """arguments to instantiate the runner class"""
-        return dict(cmdargs=self.command_args(),
-                    binary=self.options.binary,
-                    profile_args=self.profile_args())
-
-    def create_runner(self):
-        return self.runner_class.create(**self.runner_args())
-
-    def run(self):
-        runner = self.create_runner()
-        self.start(runner)
-        runner.cleanup()
-
-    def debugger_arguments(self):
-        """Get the debugger arguments
-
-        returns a 2-tuple of debugger arguments:
-            (debugger_arguments, interactive)
-
-        """
-        debug_args = self.options.debugger_args
-        if debug_args is not None:
-            debug_args = debug_args.split()
-        interactive = self.options.interactive
-        if self.options.debugger:
-            debug_args, interactive = debugger_arguments(self.options.debugger, debug_args, interactive)
-        return debug_args, interactive
-
-    def start(self, runner):
-        """Starts the runner and waits for the application to exit
-
-        It can also happen via a keyboard interrupt. It should be
-        overwritten to provide custom running of the runner instance.
-
-        """
-        # attach a debugger if specified
-        debug_args, interactive = self.debugger_arguments()
-        runner.start(debug_args=debug_args, interactive=interactive)
-        print 'Starting: ' + ' '.join(runner.command)
-        try:
-            runner.wait()
-        except KeyboardInterrupt:
-            runner.stop()
-
-
-def cli(args=sys.argv[1:]):
-    CLI(args).run()
-
-
-if __name__ == '__main__':
-    cli()
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/remote.py
+++ /dev/null
@@ -1,382 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
-# You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import ConfigParser
-import os
-import posixpath
-import re
-import signal
-from StringIO import StringIO
-import subprocess
-import sys
-import tempfile
-import time
-
-from mozdevice import DMError
-import mozfile
-import mozlog
-
-from .base import Runner
-
-
-__all__ = ['B2GRunner',
-           'RemoteRunner',
-           'remote_runners']
-
-
-class RemoteRunner(Runner):
-
-    def __init__(self, profile,
-                       devicemanager,
-                       clean_profile=None,
-                       process_class=None,
-                       env=None,
-                       remote_test_root=None,
-                       restore=True,
-                       **kwargs):
-
-        Runner.__init__(self, profile, clean_profile=clean_profile,
-                        process_class=process_class, env=env, **kwargs)
-        self.log = mozlog.getLogger('RemoteRunner')
-
-        self.dm = devicemanager
-        self.last_test = None
-        self.remote_test_root = remote_test_root or self.dm.getDeviceRoot()
-        self.log.info('using %s as test_root' % self.remote_test_root)
-        self.remote_profile = posixpath.join(self.remote_test_root, 'profile')
-        self.restore = restore
-        self.added_files = set()
-        self.backup_files = set()
-
-    def backup_file(self, remote_path):
-        if not self.restore:
-            return
-
-        if self.dm.fileExists(remote_path):
-            self.dm.shellCheckOutput(['dd', 'if=%s' % remote_path, 'of=%s.orig' % remote_path])
-            self.backup_files.add(remote_path)
-        else:
-            self.added_files.add(remote_path)
-
-    def check_for_crashes(self, last_test=None):
-        last_test = last_test or self.last_test
-        remote_dump_dir = posixpath.join(self.remote_profile, 'minidumps')
-        crashed = False
-
-        self.log.info("checking for crashes in '%s'" % remote_dump_dir)
-        if self.dm.dirExists(remote_dump_dir):
-            local_dump_dir = tempfile.mkdtemp()
-            self.dm.getDirectory(remote_dump_dir, local_dump_dir)
-
-            crashed = Runner.check_for_crashes(self, local_dump_dir, \
-                                               test_name=last_test)
-            mozfile.remove(local_dump_dir)
-            self.dm.removeDir(remote_dump_dir)
-
-        return crashed
-
-    def cleanup(self):
-        if not self.restore:
-            return
-
-        Runner.cleanup(self)
-
-        self.dm.remount()
-        # Restore the original profile
-        for added_file in self.added_files:
-            self.dm.removeFile(added_file)
-
-        for backup_file in self.backup_files:
-            if self.dm.fileExists('%s.orig' % backup_file):
-                self.dm.shellCheckOutput(['dd', 'if=%s.orig' % backup_file, 'of=%s' % backup_file])
-                self.dm.removeFile("%s.orig" % backup_file)
-
-        # Delete any bundled extensions
-        extension_dir = posixpath.join(self.remote_profile, 'extensions', 'staged')
-        if self.dm.dirExists(extension_dir):
-            for filename in self.dm.listFiles(extension_dir):
-                try:
-                    self.dm.removeDir(posixpath.join(self.bundles_dir, filename))
-                except DMError:
-                    pass
-        # Remove the test profile
-        self.dm.removeDir(self.remote_profile)
-
-
-class B2GRunner(RemoteRunner):
-
-    def __init__(self, profile, devicemanager, marionette=None, context_chrome=True,
-                 test_script=None, test_script_args=None,
-                 marionette_port=None, emulator=None, **kwargs):
-
-        remote_test_root = kwargs.get('remote_test_root')
-        if not remote_test_root:
-            kwargs['remote_test_root'] = '/data/local'
-        RemoteRunner.__init__(self, profile, devicemanager, **kwargs)
-        self.log = mozlog.getLogger('B2GRunner')
-
-        tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
-        os.close(tmpfd)
-        tmp_env = self.env or {}
-        self.env = { 'MOZ_CRASHREPORTER': '1',
-                     'MOZ_CRASHREPORTER_NO_REPORT': '1',
-                     'MOZ_HIDE_RESULTS_TABLE': '1',
-                     'MOZ_PROCESS_LOG': processLog,
-                     'NSPR_LOG_MODULES': 'signaling:5,mtransport:3',
-                     'R_LOG_LEVEL': '5',
-                     'R_LOG_DESTINATION': 'stderr',
-                     'R_LOG_VERBOSE': '1',
-                     'NO_EM_RESTART': '1', }
-        self.env.update(tmp_env)
-        self.last_test = "automation"
-
-        self.marionette = marionette
-        if self.marionette is not None:
-            if marionette_port is None:
-                marionette_port = self.marionette.port
-            elif self.marionette.port != marionette_port:
-                raise ValueError("Got a marionette object and a port but they don't match")
-
-            if emulator is None:
-                emulator = marionette.emulator
-            elif marionette.emulator != emulator:
-                raise ValueError("Got a marionette object and an emulator argument but they don't match")
-
-        self.marionette_port = marionette_port
-        self.emulator = emulator
-
-        self.context_chrome = context_chrome
-        self.test_script = test_script
-        self.test_script_args = test_script_args
-        self.remote_profiles_ini = '/data/b2g/mozilla/profiles.ini'
-        self.bundles_dir = '/system/b2g/distribution/bundles'
-
-    @property
-    def command(self):
-        cmd = [self.dm._adbPath]
-        if self.dm._deviceSerial:
-            cmd.extend(['-s', self.dm._deviceSerial])
-        cmd.append('shell')
-        for k, v in self.env.iteritems():
-            cmd.append("%s=%s" % (k, v))
-        cmd.append('/system/bin/b2g.sh')
-        return cmd
-
-    def start(self, timeout=None, outputTimeout=None):
-        self.timeout = timeout
-        self.outputTimeout = outputTimeout
-        self._setup_remote_profile()
-        # reboot device so it starts up with the proper profile
-        if not self.emulator:
-            self.dm.reboot(wait=True)
-            #wait for wlan to come up
-            if not self._wait_for_net():
-                raise Exception("network did not come up, please configure the network" +
-                                " prior to running before running the automation framework")
-
-        self.dm.shellCheckOutput(['stop', 'b2g'])
-
-        # For some reason user.js in the profile doesn't get picked up.
-        # Manually copy it over to prefs.js. See bug 1009730 for more details.
-        self.dm.moveTree(posixpath.join(self.remote_profile, 'user.js'),
-                         posixpath.join(self.remote_profile, 'prefs.js'))
-
-        self.kp_kwargs.update({'stream': sys.stdout,
-                               'processOutputLine': self.on_output,
-                               'onTimeout': self.on_timeout,})
-        self.process_handler = self.process_class(self.command, **self.kp_kwargs)
-        self.process_handler.run(timeout=timeout, outputTimeout=outputTimeout)
-
-        # Set up port forwarding again for Marionette, since any that
-        # existed previously got wiped out by the reboot.
-        if self.emulator is None:
-            subprocess.Popen([self.dm._adbPath,
-                              'forward',
-                              'tcp:%s' % self.marionette_port,
-                              'tcp:2828']).communicate()
-
-        if self.marionette is not None:
-            self.start_marionette()
-
-        if self.test_script is not None:
-            self.start_tests()
-
-    def start_marionette(self):
-        self.marionette.wait_for_port()
-
-        # start a marionette session
-        session = self.marionette.start_session()
-        if 'b2g' not in session:
-            raise Exception("bad session value %s returned by start_session" % session)
-
-        if self.marionette.emulator:
-            # Disable offline status management (bug 777145), otherwise the network
-            # will be 'offline' when the mochitests start.  Presumably, the network
-            # won't be offline on a real device, so we only do this for emulators.
-            self.marionette.set_context(self.marionette.CONTEXT_CHROME)
-            self.marionette.execute_script("""
-                Components.utils.import("resource://gre/modules/Services.jsm");
-                Services.io.manageOfflineStatus = false;
-                Services.io.offline = false;
-                """)
-
-        if self.context_chrome:
-            self.marionette.set_context(self.marionette.CONTEXT_CHROME)
-        else:
-            self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
-
-
-    def start_tests(self):
-        #self.marionette.execute_script("""
-        #    var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
-        #    var homeUrl = prefs.getCharPref("browser.homescreenURL");
-        #    dump(homeURL + "\n");
-        #""")
-
-        # run the script that starts the tests
-        if os.path.isfile(self.test_script):
-            script = open(self.test_script, 'r')
-            self.marionette.execute_script(script.read(), script_args=self.test_script_args)
-            script.close()
-        elif isinstance(self.test_script, basestring):
-            self.marionette.execute_script(self.test_script, script_args=self.test_script_args)
-
-    def on_output(self, line):
-        match = re.findall(r"TEST-START \| ([^\s]*)", line)
-        if match:
-            self.last_test = match[-1]
-
-    def on_timeout(self):
-        self.dm.killProcess('/system/b2g/b2g', sig=signal.SIGABRT)
-
-        msg = "%s | application timed out after %s seconds"
-
-        if self.timeout:
-            timeout = self.timeout
-        else:
-            timeout = self.outputTimeout
-            msg = "%s with no output" % msg
-
-        self.log.testFail(msg % (self.last_test, timeout))
-        self.check_for_crashes()
-
-    def _get_device_status(self, serial=None):
-        # If we know the device serial number, we look for that,
-        # otherwise we use the (presumably only) device shown in 'adb devices'.
-        serial = serial or self.dm._deviceSerial
-        status = 'unknown'
-
-        proc = subprocess.Popen([self.dm._adbPath, 'devices'], stdout=subprocess.PIPE)
-        line = proc.stdout.readline()
-        while line != '':
-            result = re.match('(.*?)\t(.*)', line)
-            if result:
-                thisSerial = result.group(1)
-                if not serial or thisSerial == serial:
-                    serial = thisSerial
-                    status = result.group(2)
-                    break
-            line = proc.stdout.readline()
-        return (serial, status)
-
-    def _wait_for_net(self):
-        active = False
-        time_out = 0
-        while not active and time_out < 40:
-            proc = subprocess.Popen([self.dm._adbPath, 'shell', '/system/bin/netcfg'], stdout=subprocess.PIPE)
-            proc.stdout.readline() # ignore first line
-            line = proc.stdout.readline()
-            while line != "":
-                if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)):
-                    active = True
-                    break
-                line = proc.stdout.readline()
-            time_out += 1
-            time.sleep(1)
-        return active
-
-    def _setup_remote_profile(self):
-        """Copy profile and update the remote profiles.ini to point to the new profile"""
-        self.dm.remount()
-
-        # copy the profile to the device.
-        if self.dm.dirExists(self.remote_profile):
-            self.dm.shellCheckOutput(['rm', '-r', self.remote_profile])
-
-        try:
-            self.dm.pushDir(self.profile.profile, self.remote_profile)
-        except DMError:
-            self.log.error("Automation Error: Unable to copy profile to device.")
-            raise
-
-        extension_dir = os.path.join(self.profile.profile, 'extensions', 'staged')
-        if os.path.isdir(extension_dir):
-            # Copy the extensions to the B2G bundles dir.
-            # need to write to read-only dir
-            for filename in os.listdir(extension_dir):
-                fpath = os.path.join(self.bundles_dir, filename)
-                if self.dm.fileExists(fpath):
-                    self.dm.shellCheckOutput(['rm', '-rf', fpath])
-            try:
-                self.dm.pushDir(extension_dir, self.bundles_dir)
-            except DMError:
-                self.log.error("Automation Error: Unable to copy extensions to device.")
-                raise
-
-        if not self.dm.fileExists(self.remote_profiles_ini):
-            raise DMError("The profiles.ini file '%s' does not exist on the device" % self.remote_profiles_ini)
-
-        local_profiles_ini = tempfile.NamedTemporaryFile()
-        self.dm.getFile(self.remote_profiles_ini, local_profiles_ini.name)
-
-        config = ProfileConfigParser()
-        config.read(local_profiles_ini.name)
-        for section in config.sections():
-            if 'Profile' in section:
-                config.set(section, 'IsRelative', 0)
-                config.set(section, 'Path', self.remote_profile)
-
-        new_profiles_ini = tempfile.NamedTemporaryFile()
-        config.write(open(new_profiles_ini.name, 'w'))
-
-        self.backup_file(self.remote_profiles_ini)
-        self.dm.pushFile(new_profiles_ini.name, self.remote_profiles_ini)
-
-    def cleanup(self):
-        RemoteRunner.cleanup(self)
-        if getattr(self.marionette, 'instance', False):
-            self.marionette.instance.close()
-        del self.marionette
-
-
-class ProfileConfigParser(ConfigParser.RawConfigParser):
-    """Class to create profiles.ini config files
-
-    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")
-
-remote_runners = {'b2g': 'B2GRunner',
-                  'fennec': 'FennecRunner'}
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/runners.py
@@ -0,0 +1,155 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+This module contains a set of shortcut methods that create runners for commonly
+used Mozilla applications, such as Firefox or B2G emulator.
+"""
+
+from .application import get_app_context
+from .base import DeviceRunner, GeckoRuntimeRunner
+from .devices import Emulator
+
+
+def Runner(*args, **kwargs):
+    """
+    Create a generic GeckoRuntime runner.
+
+    :param binary: Path to binary.
+    :param cmdargs: Arguments to pass into binary.
+    :param profile: Profile object to use.
+    :param env: Environment variables to pass into the gecko process.
+    :param clean_profile: If True, restores profile back to original state.
+    :param process_class: Class used to launch the binary.
+    :param process_args: Arguments to pass into process_class.
+    :param symbols_path: Path to symbol files used for crash analysis.
+    :returns: A generic GeckoRuntimeRunner.
+    """
+    return GeckoRuntimeRunner(*args, **kwargs)
+
+
+def FirefoxRunner(*args, **kwargs):
+    """
+    Create a desktop Firefox runner.
+
+    :param binary: Path to Firefox binary.
+    :param cmdargs: Arguments to pass into binary.
+    :param profile: Profile object to use.
+    :param env: Environment variables to pass into the gecko process.
+    :param clean_profile: If True, restores profile back to original state.
+    :param process_class: Class used to launch the binary.
+    :param process_args: Arguments to pass into process_class.
+    :param symbols_path: Path to symbol files used for crash analysis.
+    :returns: A GeckoRuntimeRunner for Firefox.
+    """
+    kwargs['app_ctx'] = get_app_context('firefox')()
+    return GeckoRuntimeRunner(*args, **kwargs)
+
+
+def ThunderbirdRunner(*args, **kwargs):
+    """
+    Create a desktop Thunderbird runner.
+
+    :param binary: Path to Thunderbird binary.
+    :param cmdargs: Arguments to pass into binary.
+    :param profile: Profile object to use.
+    :param env: Environment variables to pass into the gecko process.
+    :param clean_profile: If True, restores profile back to original state.
+    :param process_class: Class used to launch the binary.
+    :param process_args: Arguments to pass into process_class.
+    :param symbols_path: Path to symbol files used for crash analysis.
+    :returns: A GeckoRuntimeRunner for Thunderbird.
+    """
+    kwargs['app_ctx'] = get_app_context('thunderbird')()
+    return GeckoRuntimeRunner(*args, **kwargs)
+
+
+def MetroRunner(*args, **kwargs):
+    """
+    Create a Windows metro Firefox runner.
+
+    :param binary: Path to metro Firefox binary.
+    :param cmdargs: Arguments to pass into binary.
+    :param profile: Profile object to use.
+    :param env: Environment variables to pass into the gecko process.
+    :param clean_profile: If True, restores profile back to original state.
+    :param process_class: Class used to launch the binary.
+    :param process_args: Arguments to pass into process_class.
+    :param symbols_path: Path to symbol files used for crash analysis.
+    :returns: A GeckoRuntimeRunner for metro Firefox.
+    """
+    kwargs['app_ctx'] = get_app_context('metro')()
+    return GeckoRuntimeRunner(*args, **kwargs)
+
+
+def B2GDesktopRunner(*args, **kwargs):
+    """
+    Create a B2G desktop runner.
+
+    :param binary: Path to b2g desktop binary.
+    :param cmdargs: Arguments to pass into binary.
+    :param profile: Profile object to use.
+    :param env: Environment variables to pass into the gecko process.
+    :param clean_profile: If True, restores profile back to original state.
+    :param process_class: Class used to launch the binary.
+    :param process_args: Arguments to pass into process_class.
+    :param symbols_path: Path to symbol files used for crash analysis.
+    :returns: A GeckoRuntimeRunner for b2g desktop.
+    """
+    # There is no difference between a generic and b2g desktop runner,
+    # but expose a separate entry point for clarity.
+    return Runner(*args, **kwargs)
+
+
+def B2GEmulatorRunner(arch='arm',
+                      b2g_home=None,
+                      adb_path=None,
+                      logdir=None,
+                      binary=None,
+                      no_window=None,
+                      resolution=None,
+                      sdcard=None,
+                      userdata=None,
+                      **kwargs):
+    """
+    Create a B2G emulator runner.
+
+    :param arch: The architecture of the emulator, either 'arm' or 'x86'. Defaults to 'arm'.
+    :param b2g_home: Path to root B2G repository.
+    :param logdir: Path to save logfiles such as logcat and qemu output.
+    :param no_window: Run emulator without a window.
+    :param resolution: Screen resolution to set emulator to, e.g '800x1000'.
+    :param sdcard: Path to local emulated sdcard storage.
+    :param userdata: Path to custom userdata image.
+    :param profile: Profile object to use.
+    :param env: Environment variables to pass into the b2g.sh process.
+    :param clean_profile: If True, restores profile back to original state.
+    :param process_class: Class used to launch the b2g.sh process.
+    :param process_args: Arguments to pass into the b2g.sh process.
+    :param symbols_path: Path to symbol files used for crash analysis.
+    :returns: A DeviceRunner for B2G emulators.
+    """
+    kwargs['app_ctx'] = get_app_context('b2g')(b2g_home, adb_path=adb_path)
+    device_args = { 'app_ctx': kwargs['app_ctx'],
+                    'arch': arch,
+                    'binary': binary,
+                    'resolution': resolution,
+                    'sdcard': sdcard,
+                    'userdata': userdata,
+                    'no_window': no_window,
+                    'logdir': logdir }
+    return DeviceRunner(device_class=Emulator,
+                        device_args=device_args,
+                        **kwargs)
+
+
+runners = {
+ 'default': Runner,
+ 'b2g_desktop': B2GDesktopRunner,
+ 'b2g_emulator': B2GEmulatorRunner,
+ 'firefox': FirefoxRunner,
+ 'metro': MetroRunner,
+ 'thunderbird': ThunderbirdRunner,
+}
+
--- a/testing/mozbase/mozrunner/mozrunner/utils.py
+++ b/testing/mozbase/mozrunner/mozrunner/utils.py
@@ -1,19 +1,20 @@
 #!/usr/bin/env python
 
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """Utility functions for mozrunner"""
 
-__all__ = ['findInPath', 'get_metadata_from_egg']
+__all__ = ['findInPath', 'get_metadata_from_egg', 'uses_marionette']
 
 
+from functools import wraps
 import mozinfo
 import os
 import sys
 
 
 ### python package method metadata by introspection
 try:
     import pkg_resources
@@ -58,8 +59,38 @@ def findInPath(fileName, path=os.environ
             return os.path.join(dir, fileName)
         if mozinfo.isWin:
             if os.path.isfile(os.path.join(dir, fileName + ".exe")):
                 return os.path.join(dir, fileName + ".exe")
 
 if __name__ == '__main__':
     for i in sys.argv[1:]:
         print findInPath(i)
+
+
+def _find_marionette_in_args(*args, **kwargs):
+    try:
+        m = [a for a in args + tuple(kwargs.values()) if hasattr(a, 'session')][0]
+    except IndexError:
+        print("Can only apply decorator to function using a marionette object")
+        raise
+    return m
+
+def uses_marionette(func):
+    """Decorator which creates a marionette session and deletes it
+    afterwards if one doesn't already exist.
+    """
+    @wraps(func)
+    def _(*args, **kwargs):
+        m = _find_marionette_in_args(*args, **kwargs)
+        delete_session = False
+        if not m.session:
+            delete_session = True
+            m.start_session()
+
+        m.set_context(m.CONTEXT_CHROME)
+        ret = func(*args, **kwargs)
+
+        if delete_session:
+            m.delete_session()
+
+        return ret
+    return _
--- a/testing/mozbase/mozrunner/setup.py
+++ b/testing/mozbase/mozrunner/setup.py
@@ -1,22 +1,22 @@
 # 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 sys
-from setuptools import setup
+from setuptools import setup, find_packages
 
 PACKAGE_NAME = 'mozrunner'
-PACKAGE_VERSION = '5.37'
+PACKAGE_VERSION = '6.0'
 
 desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
 
 deps = ['mozcrash >= 0.11',
-        'mozdevice >= 0.30',
+        'mozdevice >= 0.37',
         'mozfile >= 1.0',
         'mozinfo >= 0.7',
         'mozlog >= 1.5',
         'mozprocess >= 0.17',
         'mozprofile >= 0.18',
        ]
 
 # we only support python 2 right now
@@ -34,17 +34,17 @@ setup(name=PACKAGE_NAME,
                    'Programming Language :: Python',
                    'Topic :: Software Development :: Libraries :: Python Modules',
                    ],
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
       url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL 2.0',
-      packages=['mozrunner'],
+      packages=find_packages(),
       package_data={'mozrunner': [
             'resources/metrotestharness.exe'
       ]},
       zip_safe=False,
       install_requires = deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
--- a/testing/profiles/prefs_b2g_unittest.js
+++ b/testing/profiles/prefs_b2g_unittest.js
@@ -1,10 +1,10 @@
 // Prefs specific to b2g mochitests
 
+user_pref("b2g.system_manifest_url","app://test-container.gaiamobile.org/manifest.webapp");
 user_pref("b2g.system_startup_url","app://test-container.gaiamobile.org/index.html");
-user_pref("b2g.system_manifest_url","app://test-container.gaiamobile.org/manifest.webapp");
-user_pref("dom.mozBrowserFramesEnabled", "%(OOP)s");
+user_pref("dom.ipc.browser_frames.oop_by_default", false);
 user_pref("dom.ipc.tabs.disabled", false);
-user_pref("dom.ipc.browser_frames.oop_by_default", false);
+user_pref("dom.mozBrowserFramesEnabled", "%(OOP)s");
 user_pref("dom.mozBrowserFramesWhitelist","app://test-container.gaiamobile.org,http://mochi.test:8888");
+user_pref("dom.testing.datastore_enabled_for_hosted_apps", true);
 user_pref("marionette.force-local", true);
-user_pref("dom.testing.datastore_enabled_for_hosted_apps", true);
--- a/testing/tps/tps/firefoxrunner.py
+++ b/testing/tps/tps/firefoxrunner.py
@@ -74,22 +74,12 @@ class TPSFirefoxRunner(object):
         if profile is None:
             profile = Profile()
         self.profile = profile
 
         if self.binary is None and self.url:
             self.binary = self.download_build()
 
         if self.runner is None:
-            self.runner = FirefoxRunner(self.profile, binary=self.binary)
-
-        self.runner.profile = self.profile
-
-        if env is not None:
-            self.runner.env.update(env)
+            self.runner = FirefoxRunner(profile=self.profile, binary=self.binary, env=env, cmdargs=args)
 
-        if args is not None:
-            self.runner.cmdargs = copy.copy(args)
-
-        self.runner.start()
-        returncode = self.runner.wait(timeout)
-
-        return returncode
+        self.runner.start(timeout=timeout)
+        return self.runner.wait()
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -351,17 +351,17 @@ class B2GXPCShellRunner(MozbuildObject):
         import runtestsb2g
         parser = runtestsb2g.B2GOptions()
         options, args = parser.parse_args([])
 
         options.b2g_path = b2g_home
         options.busybox = busybox or os.environ.get('BUSYBOX')
         options.localLib = self.bin_dir
         options.localBin = self.bin_dir
-        options.logcat_dir = self.xpcshell_dir
+        options.logdir = self.xpcshell_dir
         options.manifest = os.path.join(self.xpcshell_dir, 'xpcshell_b2g.ini')
         options.mozInfo = os.path.join(self.topobjdir, 'mozinfo.json')
         options.objdir = self.topobjdir
         options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols'),
         options.testingModulesDir = os.path.join(self.tests_dir, 'modules')
         options.testsRootDir = self.xpcshell_dir
         options.testPath = test_path
         options.use_device_libs = True
--- a/testing/xpcshell/remotexpcshelltests.py
+++ b/testing/xpcshell/remotexpcshelltests.py
@@ -121,23 +121,23 @@ class RemoteXPCShellTestThread(xpcshell.
               self.xpcsCmd]
 
     def testTimeout(self, test_file, proc):
         self.timedout = True
         if not self.retry:
             self.log.error("TEST-UNEXPECTED-FAIL | %s | Test timed out" % test_file)
         self.kill(proc)
 
-    def launchProcess(self, cmd, stdout, stderr, env, cwd):
+    def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None):
         self.timedout = False
         cmd.insert(1, self.remoteHere)
         outputFile = "xpcshelloutput"
         with open(outputFile, 'w+') as f:
             try:
-                self.shellReturnCode = self.device.shell(cmd, f)
+                self.shellReturnCode = self.device.shell(cmd, f, timeout=timeout+10)
             except devicemanager.DMError as e:
                 if self.timedout:
                     # If the test timed out, there is a good chance the SUTagent also
                     # timed out and failed to return a return code, generating a
                     # DMError. Ignore the DMError to simplify the error report.
                     self.shellReturnCode = None
                     pass
                 else:
--- a/testing/xpcshell/runtestsb2g.py
+++ b/testing/xpcshell/runtestsb2g.py
@@ -14,20 +14,20 @@ from mozdevice import devicemanagerADB, 
 
 DEVICE_TEST_ROOT = '/data/local/tests'
 
 
 from marionette import Marionette
 
 class B2GXPCShellTestThread(RemoteXPCShellTestThread):
     # Overridden
-    def launchProcess(self, cmd, stdout, stderr, env, cwd):
+    def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None):
         try:
             # This returns 1 even when tests pass - hardcode returncode to 0 (bug 773703)
-            outputFile = RemoteXPCShellTestThread.launchProcess(self, cmd, stdout, stderr, env, cwd)
+            outputFile = RemoteXPCShellTestThread.launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=timeout)
             self.shellReturnCode = 0
         except DMError:
             self.shellReturnCode = -1
             outputFile = "xpcshelloutput"
             f = open(outputFile, "a")
             f.write("\n%s" % traceback.format_exc())
             f.close()
         return outputFile
@@ -121,20 +121,20 @@ class B2GOptions(RemoteXPCShellOptions):
                         dest='use_device_libs',
                         help="Don't push .so's")
         defaults['use_device_libs'] = False
         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("--logcat-dir", action="store",
-                        type="string", dest="logcat_dir",
-                        help="directory to store logcat dump files")
-        defaults["logcat_dir"] = None
+        self.add_option("--logdir", action="store",
+                        type="string", dest="logdir",
+                        help="directory to store log files")
+        defaults["logdir"] = None
         self.add_option('--busybox', action='store',
                         type='string', dest='busybox',
                         help="Path to busybox binary to install on device")
         defaults['busybox'] = None
 
         defaults["remoteTestRoot"] = DEVICE_TEST_ROOT
         defaults['dm_trans'] = 'adb'
         defaults['debugger'] = None
@@ -144,33 +144,33 @@ class B2GOptions(RemoteXPCShellOptions):
 
     def verifyRemoteOptions(self, options):
         if options.b2g_path is None:
             self.error("Need to specify a --b2gpath")
 
         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 options.logdir and not options.emulator:
+            self.error("You must specify --emulator if you specify --logdir")
         return RemoteXPCShellOptions.verifyRemoteOptions(self, options)
 
 def run_remote_xpcshell(parser, options, args):
     options = parser.verifyRemoteOptions(options)
 
     # Create the Marionette instance
     kwargs = {}
     if options.emulator:
         kwargs['emulator'] = options.emulator
         if options.no_window:
             kwargs['noWindow'] = True
         if options.geckoPath:
             kwargs['gecko_path'] = options.geckoPath
-        if options.logcat_dir:
-            kwargs['logcat_dir'] = options.logcat_dir
+        if options.logdir:
+            kwargs['logdir'] = options.logdir
         if options.busybox:
             kwargs['busybox'] = options.busybox
         if options.symbolsPath:
             kwargs['symbols_path'] = options.symbolsPath
     if options.b2g_path:
         kwargs['homedir'] = options.emu_path or options.b2g_path
     if options.address:
         host, port = options.address.split(':')
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -227,21 +227,23 @@ class XPCShellTestThread(Thread):
                     break
                 self.process_line(line)
 
             if self.saw_proc_start and not self.saw_proc_end:
                 self.has_failure_output = True
 
         return proc.communicate()
 
-    def launchProcess(self, cmd, stdout, stderr, env, cwd):
+    def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None):
         """
           Simple wrapper to launch a process.
           On a remote system, this is more complex and we need to overload this function.
         """
+        # timeout is needed by remote and b2g xpcshell to extend the
+        # devicemanager.shell() timeout. It is not used in this function.
         if HAVE_PSUTIL:
             popen_func = psutil.Popen
         else:
             popen_func = Popen
         proc = popen_func(cmd, stdout=stdout, stderr=stderr,
                     env=env, cwd=cwd)
         return proc
 
@@ -618,17 +620,17 @@ class XPCShellTestThread(Thread):
 
         try:
             self.log.info("TEST-INFO | %s | running test ..." % name)
             if self.verbose:
                 self.logCommand(name, completeCmd, test_dir)
 
             startTime = time.time()
             proc = self.launchProcess(completeCmd,
-                stdout=self.pStdout, stderr=self.pStderr, env=self.env, cwd=test_dir)
+                stdout=self.pStdout, stderr=self.pStderr, env=self.env, cwd=test_dir, timeout=testTimeoutInterval)
 
             if self.interactive:
                 self.log.info("TEST-INFO | %s | Process ID: %d" % (name, proc.pid))
 
             stdout, stderr = self.communicate(proc)
 
             if self.interactive:
                 # Not sure what else to do here...