Backed out changeset 81a51de30a2a (bug 997244) for breaking b2g emulator mochitests on an otherwise CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Wed, 18 Jun 2014 14:15:48 -0700
changeset 210268 a7088551f8b6bd77361e8c2d1bc8b3da25181301
parent 210267 aa128021779936650fef32760c50e656b3fc96f1
child 210269 1b61cfccf82880b5a90c9a265df4d681de82349f
push id1
push usersledru@mozilla.com
push dateThu, 04 Dec 2014 17:57:20 +0000
bugs997244
milestone33.0a1
backs out81a51de30a2a5ac43d3462afd0d4d8d9b9e32dc8
Backed out changeset 81a51de30a2a (bug 997244) for breaking b2g emulator mochitests on an otherwise CLOSED TREE
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.port)
+            self.marionette.emulator.wait_for_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.port)
+            self.marionette.emulator.wait_for_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,
-                                       process_args=kp_kwargs)
+                                       kp_kwargs=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,29 +17,26 @@ from marionette import Marionette
 from mozprocess import ProcessHandler
 from mozrunner import FirefoxRunner
 import mozinfo
 import mozlog
 
 log = mozlog.getLogger('REFTEST')
 
 class B2GDesktopReftest(RefTest):
-    marionette = None
-
-    def __init__(self, marionette_args):
+    def __init__(self, marionette):
         RefTest.__init__(self)
         self.last_test = os.path.basename(__file__)
-        self.marionette_args = marionette_args
+        self.marionette = marionette
         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()
@@ -69,18 +66,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,
-                                    process_args=kp_kwargs,
-                                    symbols_path=options.symbolsPath)
+                                    symbols_path=options.symbolsPath,
+                                    kp_kwargs=kp_kwargs)
 
         status = 0
         try:
             self.runner.start(outputTimeout=self.timeout)
             log.info("%s | Application pid: %d",
                      os.path.basename(__file__),
                      self.runner.process_handler.pid)
 
@@ -149,23 +146,24 @@ 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):
-    marionette_args = {}
+    kwargs = {}
     if options.marionette:
         host, port = options.marionette.split(':')
-        marionette_args['host'] = host
-        marionette_args['port'] = int(port)
+        kwargs['host'] = host
+        kwargs['port'] = int(port)
+    marionette = Marionette.getMarionetteOrExit(**kwargs)
 
-    reftest = B2GDesktopReftest(marionette_args)
+    reftest = B2GDesktopReftest(marionette)
 
     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.logdir = self.reftest_dir
+        options.logcat_dir = 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)
 
-    logdir = CommandArgument('--logdir', default=None,
-        help='directory to store log files')
-    func = logdir(func)
+    logcatdir = CommandArgument('--logcat-dir', default=None,
+        help='directory to store logcat dump files')
+    func = logcatdir(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 = "adb_path",
+                    type = "string", dest = "adbPath",
                     help = "path to adb")
-        defaults["adb_path"] = "adb"
+        defaults["adbPath"] = "adb"
 
         self.add_option("--deviceIP", action="store",
                     type = "string", dest = "deviceIP",
                     help = "ip address of remote device to test")
         defaults["deviceIP"] = None
 
         self.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("--logdir", action="store",
-                        type="string", dest="logdir",
-                        help="directory to store log files")
-        defaults["logdir"] = 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('--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.logdir and not options.emulator:
-            self.error("You must specify --emulator if you specify --logdir")
+        if options.logcat_dir and not options.emulator:
+            self.error("You must specify --emulator if you specify --logcat-dir")
 
         #if not options.emulator and not options.deviceIP:
         #    print "ERROR: you must provide a device IP"
         #    return None
 
         if options.remoteLogFile == None:
             options.remoteLogFile = "reftest.log"
 
@@ -492,45 +492,43 @@ 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.logdir:
-            kwargs['logdir'] = options.logdir
+        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
     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)
-    if options.adb_path:
-        kwargs['adb_path'] = options.adb_path
-    marionette = Marionette(**kwargs)
+    marionette = Marionette.getMarionetteOrExit(**kwargs)
     auto.marionette = marionette
 
     if options.emulator:
         dm = marionette.emulator.dm
     else:
         # create the DeviceManager
-        kwargs = {'adbPath': options.adb_path,
+        kwargs = {'adbPath': options.adbPath,
                   'deviceRoot': options.remoteTestRoot}
         if options.deviceIP:
             kwargs.update({'host': options.deviceIP,
                            'port': options.devicePort})
-        dm = DeviceManagerADB(**kwargs)
+        dm = DeviagerADB(**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", "--logdir=%(logcat_dir)s",
+        "--emulator-res=800x1000", "--logcat-dir=%(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", "--logdir=%(logcat_dir)s",
+        "--emulator=%(emulator)s", "--logcat-dir=%(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", "--logdir=%(logcat_dir)s",
+        "--emulator-res=800x1000", "--logcat-dir=%(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", "--logdir=%(logcat_dir)s",
+        "--emulator-res=800x1000", "--logcat-dir=%(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",
-        "--logdir=%(logcat_dir)s", "--manifest=%(test_manifest)s", "--use-device-libs",
+        "--logcat-dir=%(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,54 +1,26 @@
 # 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 (
-    ElementNotVisibleException,
-    ErrorCodes,
-    FrameSendFailureError,
-    FrameSendNotInitializedError,
-    InvalidCookieDomainException,
-    InvalidElementStateException,
-    InvalidResponseException,
-    InvalidSelectorException,
-    JavascriptException,
-    MarionetteException,
-    MoveTargetOutOfBoundsException,
-    NoAlertPresentException,
-    NoSuchElementException,
-    NoSuchFrameException,
-    NoSuchWindowException,
-    ScriptTimeoutException,
-    StaleElementException,
-    TimeoutException,
-    UnableToSetCookieException,
-    XPathLookupException,
-)
+        ErrorCodes, MarionetteException, InstallGeckoError, TimeoutException, InvalidResponseException,
+        JavascriptException, NoSuchElementException, XPathLookupException, NoSuchWindowException,
+        StaleElementException, ScriptTimeoutException, ElementNotVisibleException,
+        NoSuchFrameException, InvalidElementStateException, NoAlertPresentException,
+        InvalidCookieDomainException, UnableToSetCookieException, InvalidSelectorException,
+        MoveTargetOutOfBoundsException, FrameSendNotInitializedError, FrameSendFailureError
+        )
 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
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/b2gbuild.py
@@ -0,0 +1,98 @@
+# 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)
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/b2ginstance.py
@@ -0,0 +1,70 @@
+# 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
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/emulator.py
@@ -0,0 +1,539 @@
+# 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
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/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/marionette/client/marionette/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/marionette/client/marionette/emulator_screen.py
@@ -0,0 +1,78 @@
+# 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,30 +20,31 @@ 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_path = profile
+        self.profile = 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 self.profile_path:
+        if not profile_path:
+            runner_class = Runner
             profile_args["restore"] = False
-            profile = Profile(**profile_args)
         else:
-            profile_args["path_from"] = self.profile_path
-            profile = Profile.clone(**profile_args)
+            runner_class = CloneRunner
+            profile_args["path_from"] = profile_path
 
         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)
@@ -51,39 +52,44 @@ 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(
+        self.runner = runner_class.create(
             binary=self.bin,
-            profile=profile,
+            profile_args=profile_args,
             cmdargs=['-no-remote', '-marionette'] + self.app_args,
             env=env,
             symbols_path=self.symbols_path,
-            process_args={
+            kp_kwargs={
                 'processOutputLine': [NullOutput()],
                 'logfile': self.gecko_log})
         self.runner.start()
 
     def check_for_crashes(self):
         return self.runner.check_for_crashes()
 
     def close(self):
-        if self.runner:
-            self.runner.stop()
-            self.runner.cleanup()
+        self.runner.stop()
+        self.runner.cleanup()
 
 
 class B2GDesktopInstance(GeckoInstance):
+
     required_prefs = {"focusmanager.testmode": True}
 
+apps = {'b2g': B2GDesktopInstance,
+        'b2gdesktop': B2GDesktopInstance}
+
+
+class CloneRunner(Runner):
+
+    profile_class = Profile.clone
+
 
 class NullOutput(object):
+
     def __call__(self, line):
         pass
-
-
-apps = {'b2g': B2GDesktopInstance,
-        'b2gdesktop': B2GDesktopInstance}
--- a/testing/marionette/client/marionette/marionette.py
+++ b/testing/marionette/client/marionette/marionette.py
@@ -1,30 +1,37 @@
 # 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 StringIO
+import sys
 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"
@@ -439,43 +446,51 @@ 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, 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):
+                 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):
         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.no_window = no_window
+        self.noWindow = noWindow
+        self.logcat_dir = logcat_dir
         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 errors.MarionetteException(message=ex_msg)
+                raise 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:
@@ -484,66 +499,62 @@ 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=profile,
+                                           bin=self.bin, profile=self.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.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 = 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.emulator.start()
             self.port = self.emulator.setup_port_forwarding(self.port)
-            assert(self.emulator.wait_for_port(self.port)), "Timed out waiting for port!"
+            assert(self.emulator.wait_for_port()), "Timed out waiting for port!"
 
-        if connect_to_running_emulator:
-            self.runner = B2GEmulatorRunner(b2g_home=homedir,
-                                            logdir=logdir)
-            self.emulator = self.runner.device
+        if connectToRunningEmulator:
+            self.emulator = Emulator(homedir=homedir,
+                                     logcat_dir=self.logcat_dir)
             self.emulator.connect()
             self.port = self.emulator.setup_port_forwarding(self.port)
-            assert(self.emulator.wait_for_port(self.port)), "Timed out waiting for port!"
+            assert(self.emulator.wait_for_port()), "Timed out waiting for port!"
 
         self.client = MarionetteTransport(self.host, self.port)
 
         if emulator:
-            if busybox:
-                self.emulator.install_busybox(busybox=busybox)
-            self.emulator.wait_for_system_message(self)
+            self.emulator.setup(self,
+                                gecko_path=gecko_path,
+                                busybox=busybox)
 
     def cleanup(self):
         if self.session:
             try:
                 self.delete_session()
-            except (errors.MarionetteException, socket.error, IOError):
+            except (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.runner:
-            self.runner.cleanup()
+        if self.emulator:
+            self.emulator.close()
         if self.instance:
             self.instance.close()
         for qemu in self.extra_emulators:
             qemu.emulator.close()
 
     def __del__(self):
         self.cleanup()
 
@@ -554,16 +565,36 @@ 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()
@@ -573,32 +604,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 errors.MarionetteException("Please start a session")
+            raise 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 errors.TimeoutException(
-                "Connection timed out", status=errors.ErrorCodes.TIMEOUT)
+            raise TimeoutException(
+                "Connection timed out", status=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;
 
@@ -610,97 +641,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 errors.MarionetteException(
+            raise 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 errors.MarionetteException(
+            raise MarionetteException(
                 "No emulator in this test to run shell command against")
-        buf = StringIO.StringIO()
-        self.emulator.dm.shell(args, buf)
-        result = str(buf.getvalue()[0:-1]).rstrip().splitlines()
-        buf.close()
+        result = self.emulator._run_shell(args)
         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 == 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)
+            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)
             else:
-                raise errors.MarionetteException(message=message, status=status, stacktrace=stacktrace)
-        raise errors.MarionetteException(message=response, status=500)
+                raise MarionetteException(message=message, status=status, stacktrace=stacktrace)
+        raise MarionetteException(message=response, status=500)
 
     def check_for_crash(self):
         returncode = None
         name = None
         crashed = False
-        if self.runner:
-            if self.runner.check_for_crashes():
+        if self.emulator:
+            if self.emulator.check_for_crash():
                 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
 
@@ -1423,9 +1454,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 = orientation.lower()
+            self.emulator.screen.orientation = self.SCREEN_ORIENTATIONS[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='emulator_binary',
+                        dest='emulatorBinary',
                         help='launch a specific emulator binary rather than launching from the B2G built emulator')
         self.add_option('--emulator-img',
                         action='store',
-                        dest='emulator_img',
+                        dest='emulatorImg',
                         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='no_window',
+                        dest='noWindow',
                         default=False,
                         help='when Marionette launches an emulator, start it with the -no-window argument')
         self.add_option('--logcat-dir',
-                        dest='logdir',
+                        dest='logcat_dir',
                         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,16 +436,20 @@ 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',
@@ -516,18 +520,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.logdir:
-            options.logdir = 'logcat'
+        if options.emulator and not options.logcat_dir:
+            options.logcat_dir = '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]))
@@ -553,47 +557,48 @@ class BaseMarionetteOptions(OptionParser
 
         return (options, tests)
 
 
 class BaseMarionetteTestRunner(object):
 
     textrunnerclass = MarionetteTextTestRunner
 
-    def __init__(self, address=None, emulator=None, emulator_binary=None,
-                 emulator_img=None, emulator_res='480x800', homedir=None,
+    def __init__(self, address=None, emulator=None, emulatorBinary=None,
+                 emulatorImg=None, emulator_res='480x800', homedir=None,
                  app=None, app_args=None, bin=None, profile=None, autolog=False,
-                 revision=None, logger=None, testgroup="marionette", no_window=False,
-                 logdir=None, xml_output=None, repeat=0,
+                 revision=None, logger=None, testgroup="marionette", noWindow=False,
+                 logcat_dir=None, xml_output=None, repeat=0, gecko_path=None,
                  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.emulator_binary = emulator_binary
-        self.emulator_img = emulator_img
+        self.emulatorBinary = emulatorBinary
+        self.emulatorImg = emulatorImg
         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.no_window = no_window
+        self.noWindow = noWindow
         self.httpd = None
         self.marionette = None
-        self.logdir = logdir
+        self.logcat_dir = logcat_dir
         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
@@ -630,19 +635,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.logdir:
-            if not os.access(self.logdir, os.F_OK):
-                os.mkdir(self.logdir)
+        if self.logcat_dir:
+            if not os.access(self.logcat_dir, os.F_OK):
+                os.mkdir(self.logcat_dir)
 
         # for XML output
         self.testvars['xml_output'] = self.xml_output
         self.results = []
 
     @property
     def capabilities(self):
         if self._capabilities:
@@ -707,17 +712,18 @@ class BaseMarionetteTestRunner(object):
                 'bin': self.bin,
                 'profile': self.profile,
                 'gecko_log': self.gecko_log,
             })
 
         if self.emulator:
             kwargs.update({
                 'homedir': self.homedir,
-                'logdir': self.logdir,
+                'logcat_dir': self.logcat_dir,
+                'gecko_path': self.gecko_path,
             })
 
         if self.address:
             host, port = self.address.split(':')
             kwargs.update({
                 'host': host,
                 'port': int(port),
             })
@@ -730,33 +736,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,
-                'emulator_binary': self.emulator_binary,
-                'emulator_img': self.emulator_img,
+                'emulatorBinary': self.emulatorBinary,
+                'emulatorImg': self.emulatorImg,
                 'emulator_res': self.emulator_res,
-                'no_window': self.no_window,
+                'noWindow': self.noWindow,
                 '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.logdir),
+            filename = os.path.join(os.path.abspath(self.logcat_dir),
                                     "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,20 +4,24 @@
 
 import mozdevice
 import os
 import re
 
 
 def get_dm(marionette=None,**kwargs):
     dm_type = os.environ.get('DM_TRANS', 'adb')
-    if marionette and hasattr(marionette.runner, 'device'):
-        return marionette.runner.app_ctx.dm
+    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)
     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 errors import InvalidElementStateException
+from marionette 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 errors import MarionetteException
+from marionette 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 errors import JavascriptException, MarionetteException
+from marionette 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 errors import JavascriptException, MarionetteException, ScriptTimeoutException
+from marionette 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 errors import JavascriptException, MarionetteException, ScriptTimeoutException
+from marionette 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 errors import JavascriptException, MarionetteException
+from marionette 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 errors import NoSuchElementException
+from marionette 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 errors import NoSuchElementException
+from marionette 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 errors import NoSuchElementException
+from marionette 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,14 +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 errors import MarionetteException, TimeoutException
+from marionette import MarionetteException
+from marionette import 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 errors import MarionetteException
+from emulator_screen import EmulatorScreen
+from marionette 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 errors import JavascriptException, MarionetteException, ScriptTimeoutException
+from marionette 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 errors import MarionetteException
+from marionette 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 errors import MarionetteException
+from marionette 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 errors import JavascriptException, MarionetteException
+from marionette 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 errors import JavascriptException
+from marionette 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 errors import JavascriptException
+from marionette 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 errors import NoSuchElementException, JavascriptException, MarionetteException, ScriptTimeoutException
+from marionette 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 errors import ElementNotVisibleException
+from marionette 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 >= 6.0
-mozdevice >= 0.37
+mozrunner >= 5.15
+mozdevice >= 0.22
 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.logdir = self.mochitest_dir
+        options.logcat_dir = 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)
 
-    logdir = CommandArgument('--logdir', default=None,
-        help='directory to store log files')
-    func = logdir(func)
+    logcatdir = CommandArgument('--logcat-dir', default=None,
+        help='directory to store logcat dump files')
+    func = logcatdir(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,
         }],
-        [["--logdir"],
+        [["--logcat-dir"],
         { "action": "store",
           "type": "string",
-          "dest": "logdir",
-          "help": "directory to store log files",
+          "dest": "logcat_dir",
+          "help": "directory to store logcat dump files",
           "default": None,
         }],
         [['--busybox'],
         { "action": 'store',
           "type": 'string',
           "dest": 'busybox',
           "help": "Path to busybox binary to install on device",
           "default": None,
@@ -733,16 +733,17 @@ 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):
@@ -751,18 +752,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.logdir and not options.emulator:
-            self.error("You must specify --emulator if you specify --logdir")
+        if options.logcat_dir and not options.emulator:
+            self.error("You must specify --emulator if you specify --logcat-dir")
 
         if not os.path.isdir(options.xrePath):
             self.error("--xre-path '%s' is not a directory" % options.xrePath)
         xpcshell = os.path.join(options.xrePath, 'xpcshell')
         if not os.access(xpcshell, os.F_OK):
             self.error('xpcshell not found at %s' % xpcshell)
         if self.elf_arm(xpcshell):
             self.error('--xre-path points to an ARM version of xpcshell; it '
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -1250,28 +1250,33 @@ 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 Runner even though appname is b2g
+      # b2g desktop requires FirefoxRunner even though appname is b2g
       if mozinfo.info.get('appname') == 'b2g' and mozinfo.info.get('toolkit') != 'gonk':
-          runner_cls = mozrunner.Runner
+          runner_cls = mozrunner.FirefoxRunner
       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,
-                          process_args=kp_kwargs)
+                          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
 
       # 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,44 +4,46 @@
 
 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):
-    marionette = None
-
-    def __init__(self, marionette_args,
+    def __init__(self, marionette,
                        out_of_process=True,
                        profile_data_dir=None,
                        locations=os.path.join(here, 'server-locations.txt')):
         super(B2GMochitest, self).__init__()
-        self.marionette_args = marionette_args
+        self.marionette = marionette
         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'
 
@@ -114,117 +116,97 @@ 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:
-            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)
-
-
+            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.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")
-            if hasattr(self, 'runner'):
-                self.runner.check_for_crashes()
+            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_args, profile_data_dir,
+    def __init__(self, marionette, devicemanager, profile_data_dir,
                  local_binary_dir, remote_test_root=None, remote_log_file=None):
-        B2GMochitest.__init__(self, marionette_args, out_of_process=True, profile_data_dir=profile_data_dir)
+        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')
         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)
 
         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
@@ -241,24 +223,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.app_ctx.remote_profile
+        options.profilePath = self.remote_profile
         options.logFile = self.local_log
 
 
 class B2GDesktopMochitest(B2GMochitest, Mochitest):
 
-    def __init__(self, marionette_args, profile_data_dir):
-        B2GMochitest.__init__(self, marionette_args, out_of_process=False, profile_data_dir=profile_data_dir)
+    def __init__(self, marionette, profile_data_dir):
+        B2GMochitest.__init__(self, marionette, 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)
 
@@ -268,17 +250,16 @@ 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)
@@ -296,37 +277,59 @@ 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
-    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,
-    }
+    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
     if options.marionette:
         host, port = options.marionette.split(':')
-        marionette_args['host'] = host
-        marionette_args['port'] = int(port)
+        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)
 
     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_args, options.profile_data_dir, options.xrePath,
+    mochitest = B2GDeviceMochitest(marionette, dm, options.profile_data_dir, options.xrePath,
+                                   remote_test_root=options.remoteTestRoot,
                                    remote_log_file=options.remoteLogFile)
 
     options = parser.verifyOptions(options, mochitest)
     if (options == None):
         sys.exit(1)
 
     retVal = 1
     try:
@@ -341,22 +344,23 @@ def run_remote_mochitests(parser, option
         except:
             pass
         retVal = 1
 
     sys.exit(retVal)
 
 def run_desktop_mochitests(parser, options):
     # create our Marionette instance
-    marionette_args = {}
+    kwargs = {}
     if options.marionette:
         host, port = options.marionette.split(':')
-        marionette_args['host'] = host
-        marionette_args['port'] = int(port)
-    mochitest = B2GDesktopMochitest(marionette_args, options.profile_data_dir)
+        kwargs['host'] = host
+        kwargs['port'] = int(port)
+    marionette = Marionette.getMarionetteOrExit(**kwargs)
+    mochitest = B2GDesktopMochitest(marionette, 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:
deleted file mode 100644
--- a/testing/mozbase/docs/mozrunner.rst
+++ /dev/null
@@ -1,177 +0,0 @@
-: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,10 +7,9 @@ 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,14 +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/.
 
 import hashlib
 import mozlog
+import socket
 import os
 import posixpath
 import re
 import struct
 import StringIO
 import zlib
 
 from Zeroconf import Zeroconf, ServiceBrowser
@@ -372,25 +373,23 @@ 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. Raises if
-        the return code is non-zero.
+        Executes shell command on device and returns output as a string.
 
         :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,22 +24,21 @@ 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, autoconnect=True, **kwargs):
+                 logLevel=mozlog.ERROR, **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
@@ -54,43 +53,38 @@ 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()
 
-        if autoconnect:
-            self.connect()
+        # try to connect to the device over tcp/ip if we have a hostname
+        if self.host:
+            self._connectRemoteADB()
 
-    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()
+        # verify that we can connect to the device. can't continue
+        self._verifyDevice()
 
-            # verify that we can connect to the device. can't continue
-            self._verifyDevice()
+        # set up device root
+        self._setupDeviceRoot()
 
-            # set up device root
-            self._setupDeviceRoot()
+        # 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()
 
-            # 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
+        # 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.37'
+PACKAGE_VERSION = '0.36'
 
 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 hasattr(self, 'proc'):
+            if not 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,10 +1,11 @@
 # 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 .errors import *
-from runners import *
 
-import base
-import cli
-import devices
-import utils
+from .errors import *
+from .local import *
+from .local import LocalRunner as Runner
+from .remote import *
+
+runners = local_runners
+runners.update(remote_runners)
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/application.py
+++ /dev/null
@@ -1,129 +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 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
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/base.py
@@ -0,0 +1,187 @@
+#!/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()
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/base/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .runner import BaseRunner
-from .device import DeviceRunner
-from .browser import GeckoRuntimeRunner
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/base/browser.py
+++ /dev/null
@@ -1,74 +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 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)
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/base/device.py
+++ /dev/null
@@ -1,111 +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 __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)
-
-    def cleanup(self, *args, **kwargs):
-        BaseRunner.cleanup(self, *args, **kwargs)
-        self.device.cleanup()
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/base/runner.py
+++ /dev/null
@@ -1,196 +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/.
-
-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()
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/cli.py
+++ /dev/null
@@ -1,193 +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 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()
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/devices/__init__.py
+++ /dev/null
@@ -1,10 +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 emulator import Emulator
-from base import Device
-
-import emulator_battery
-import emulator_geo
-import emulator_screen
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/devices/base.py
+++ /dev/null
@@ -1,227 +0,0 @@
-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")
-
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/devices/emulator.py
+++ /dev/null
@@ -1,258 +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 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()
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/devices/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/mozbase/mozrunner/mozrunner/devices/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/mozbase/mozrunner/mozrunner/devices/emulator_screen.py
+++ /dev/null
@@ -1,89 +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
-        """
-        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,16 +1,11 @@
 #!/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."""
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/local.py
@@ -0,0 +1,362 @@
+#!/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()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/remote.py
@@ -0,0 +1,382 @@
+# 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'}
deleted file mode 100644
--- a/testing/mozbase/mozrunner/mozrunner/runners.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
-# You can obtain one at http://mozilla.org/MPL/2.0/.
-
-"""
-This 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,20 +1,19 @@
 #!/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', 'uses_marionette']
+__all__ = ['findInPath', 'get_metadata_from_egg']
 
 
-from functools import wraps
 import mozinfo
 import os
 import sys
 
 
 ### python package method metadata by introspection
 try:
     import pkg_resources
@@ -59,38 +58,8 @@ 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, find_packages
+from setuptools import setup
 
 PACKAGE_NAME = 'mozrunner'
-PACKAGE_VERSION = '6.0'
+PACKAGE_VERSION = '5.37'
 
 desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
 
 deps = ['mozcrash >= 0.11',
-        'mozdevice >= 0.37',
+        'mozdevice >= 0.30',
         '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=find_packages(),
+      packages=['mozrunner'],
       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_startup_url","app://test-container.gaiamobile.org/index.html");
 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("dom.ipc.browser_frames.oop_by_default", false);
+user_pref("dom.mozBrowserFramesEnabled", "%(OOP)s");
 user_pref("dom.ipc.tabs.disabled", false);
-user_pref("dom.mozBrowserFramesEnabled", "%(OOP)s");
+user_pref("dom.ipc.browser_frames.oop_by_default", false);
 user_pref("dom.mozBrowserFramesWhitelist","app://test-container.gaiamobile.org,http://mochi.test:8888");
+user_pref("marionette.force-local", true);
 user_pref("dom.testing.datastore_enabled_for_hosted_apps", true);
-user_pref("marionette.force-local", true);
--- a/testing/tps/tps/firefoxrunner.py
+++ b/testing/tps/tps/firefoxrunner.py
@@ -74,12 +74,22 @@ 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(profile=self.profile, binary=self.binary, env=env, cmdargs=args)
+            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.start(timeout=timeout)
-        return self.runner.wait()
+        if args is not None:
+            self.runner.cmdargs = copy.copy(args)
+
+        self.runner.start()
+        returncode = self.runner.wait(timeout)
+
+        return returncode
--- 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.logdir = self.xpcshell_dir
+        options.logcat_dir = 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, timeout=None):
+    def launchProcess(self, cmd, stdout, stderr, env, cwd):
         self.timedout = False
         cmd.insert(1, self.remoteHere)
         outputFile = "xpcshelloutput"
         with open(outputFile, 'w+') as f:
             try:
-                self.shellReturnCode = self.device.shell(cmd, f, timeout=timeout+10)
+                self.shellReturnCode = self.device.shell(cmd, f)
             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, timeout=None):
+    def launchProcess(self, cmd, stdout, stderr, env, cwd):
         try:
             # This returns 1 even when tests pass - hardcode returncode to 0 (bug 773703)
-            outputFile = RemoteXPCShellTestThread.launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=timeout)
+            outputFile = RemoteXPCShellTestThread.launchProcess(self, cmd, stdout, stderr, env, cwd)
             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("--logdir", action="store",
-                        type="string", dest="logdir",
-                        help="directory to store log files")
-        defaults["logdir"] = 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('--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.logdir and not options.emulator:
-            self.error("You must specify --emulator if you specify --logdir")
+        if options.logcat_dir and not options.emulator:
+            self.error("You must specify --emulator if you specify --logcat-dir")
         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.logdir:
-            kwargs['logdir'] = options.logdir
+        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
     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,23 +227,21 @@ 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, timeout=None):
+    def launchProcess(self, cmd, stdout, stderr, env, cwd):
         """
           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
 
@@ -620,17 +618,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, timeout=testTimeoutInterval)
+                stdout=self.pStdout, stderr=self.pStderr, env=self.env, cwd=test_dir)
 
             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...