Bug 1225903 - Drop support for b2g desktop in mochitest, r=jgriffin
☠☠ backed out by d69c0607d8b1 ☠ ☠
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Wed, 18 Nov 2015 13:35:38 -0500
changeset 273309 6ab2285938201fecc0c06d083e71dc0ec17229e0
parent 273308 d63e7bf319f4c544272479cddd9c7092361040b0
child 273310 5d7f3436f9eceb1eb3b44c113ce31fa43159acc5
push id68251
push userahalberstadt@mozilla.com
push dateThu, 19 Nov 2015 19:13:43 +0000
treeherdermozilla-inbound@6ab228593820 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjgriffin
bugs1225903
milestone45.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1225903 - Drop support for b2g desktop in mochitest, r=jgriffin Mochitests on b2g desktop are no longer being run on any trunk branches, including b2g-inbound. Dropping support for it significantly reduces complexity in the mochitest harness.
testing/mochitest/mach_commands.py
testing/mochitest/mochitest_options.py
testing/mochitest/runtests.py
testing/mochitest/runtestsb2g.py
testing/mochitest/runtestsremote.py
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -25,40 +25,16 @@ from mach.decorators import (
     CommandProvider,
     Command,
 )
 import mozpack.path as mozpath
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
-GAIA_PROFILE_NOT_FOUND = '''
-The mochitest command requires a non-debug gaia profile. Either
-pass in --profile, or set the GAIA_PROFILE environment variable.
-
-If you do not have a non-debug gaia profile, you can build one:
-    $ git clone https://github.com/mozilla-b2g/gaia
-    $ cd gaia
-    $ make
-
-The profile should be generated in a directory called 'profile'.
-'''.lstrip()
-
-GAIA_PROFILE_IS_DEBUG = '''
-The mochitest command requires a non-debug gaia profile. The
-specified profile, {}, is a debug profile.
-
-If you do not have a non-debug gaia profile, you can build one:
-    $ git clone https://github.com/mozilla-b2g/gaia
-    $ cd gaia
-    $ make
-
-The profile should be generated in a directory called 'profile'.
-'''.lstrip()
-
 ENG_BUILD_REQUIRED = '''
 The mochitest command requires an engineering build. It may be the case that
 VARIANT=user or PRODUCTION=1 were set. Try re-building with VARIANT=eng:
 
     $ VARIANT=eng ./build.sh
 
 There should be an app called 'test-container.gaiamobile.org' located in
 {}.
@@ -94,17 +70,17 @@ NOW_RUNNING = '''
 '''
 
 
 # Maps test flavors to data needed to run them
 ALL_FLAVORS = {
     'mochitest': {
         'suite': 'plain',
         'aliases': ('plain', 'mochitest'),
-        'enabled_apps': ('firefox', 'b2g', 'android', 'mulet', 'b2g_desktop'),
+        'enabled_apps': ('firefox', 'b2g', 'android', 'mulet'),
     },
     'chrome': {
         'suite': 'chrome',
         'aliases': ('chrome', 'mochitest-chrome'),
         'enabled_apps': ('firefox', 'mulet', 'b2g', 'android'),
         'extra_args': {
             'chrome': True,
         }
@@ -154,17 +130,17 @@ ALL_FLAVORS = {
         'aliases': ('webapprt-content', 'mochitest-webapprt-content'),
         'enabled_apps': ('firefox',),
         'extra_args': {
             'webapprtContent': True,
         }
     },
 }
 
-SUPPORTED_APPS = ['firefox', 'b2g', 'android', 'mulet', 'b2g_desktop']
+SUPPORTED_APPS = ['firefox', 'b2g', 'android', 'mulet']
 SUPPORTED_FLAVORS = list(chain.from_iterable([f['aliases'] for f in ALL_FLAVORS.values()]))
 CANONICAL_FLAVORS = sorted([f['aliases'][0] for f in ALL_FLAVORS.values()])
 
 
 class MochitestRunner(MozbuildObject):
 
     """Easily run mochitests.
 
@@ -231,27 +207,17 @@ class MochitestRunner(MozbuildObject):
 
         from mozbuild.testing import TestResolver
         resolver = self._spawn(TestResolver)
         tests = list(resolver.resolve_tests(paths=test_paths, cwd=cwd))
         return tests
 
     def run_b2g_test(self, context, tests=None, suite='mochitest', **kwargs):
         """Runs a b2g mochitest."""
-        if kwargs.get('desktop'):
-            kwargs['profile'] = kwargs.get('profile') or os.environ.get('GAIA_PROFILE')
-            if not kwargs['profile'] or not os.path.isdir(kwargs['profile']):
-                print(GAIA_PROFILE_NOT_FOUND)
-                sys.exit(1)
-
-            if os.path.isfile(os.path.join(kwargs['profile'], 'extensions',
-                                           'httpd@gaiamobile.org')):
-                print(GAIA_PROFILE_IS_DEBUG.format(kwargs['profile']))
-                sys.exit(1)
-        elif context.target_out:
+        if context.target_out:
             host_webapps_dir = os.path.join(context.target_out, 'data', 'local', 'webapps')
             if not os.path.isdir(os.path.join(
                     host_webapps_dir, 'test-container.gaiamobile.org')):
                 print(ENG_BUILD_REQUIRED.format(host_webapps_dir))
                 sys.exit(1)
 
         # TODO without os.chdir, chained imports fail below
         os.chdir(self.mochitest_dir)
@@ -272,20 +238,17 @@ class MochitestRunner(MozbuildObject):
         options = Namespace(**kwargs)
 
         from manifestparser import TestManifest
         if tests:
             manifest = TestManifest()
             manifest.tests.extend(tests)
             options.manifestFile = manifest
 
-        if options.desktop:
-            return mochitest.run_desktop_mochitests(options)
-
-        return mochitest.run_remote_mochitests(options)
+        return mochitest.run_test_harness(options)
 
     def run_desktop_test(self, context, tests=None, suite=None, **kwargs):
         """Runs a mochitest.
 
         suite is the type of mochitest to run. It can be one of ('plain',
         'chrome', 'browser', 'a11y', 'jetpack-package', 'jetpack-addon',
         'webapprt-chrome', 'webapprt-content').
         """
@@ -557,17 +520,17 @@ class MachCommands(MachCommandBase):
                     reason = 'requires {}'.format(' or '.join(apps))
                 else:
                     reason = 'excluded by the command line'
                 msg.append('    mochitest -f {} ({})'.format(name, reason))
             print(SUPPORTED_TESTS_NOT_FOUND.format(
                 buildapp, '\n'.join(sorted(msg))))
             return 1
 
-        if buildapp in ('b2g', 'b2g_desktop'):
+        if buildapp in ('b2g',):
             run_mochitest = mochitest.run_b2g_test
         elif buildapp == 'android':
             run_mochitest = mochitest.run_android_test
         else:
             run_mochitest = mochitest.run_desktop_test
 
         overall = None
         for (flavor, subsuite), tests in sorted(suites.items()):
--- a/testing/mochitest/mochitest_options.py
+++ b/testing/mochitest/mochitest_options.py
@@ -591,17 +591,17 @@ class MochitestArguments(ArgumentContain
                 os.path.join(build_obj.bindir, *p) for p in gmp_modules)
 
         if options.totalChunks is not None and options.thisChunk is None:
             parser.error(
                 "thisChunk must be specified when totalChunks is specified")
 
         if options.extra_mozinfo_json:
             if not os.path.isfile(options.extra_mozinfo_json):
-               parser.error("Error: couldn't find mozinfo.json at '%s'."\
+                parser.error("Error: couldn't find mozinfo.json at '%s'."
                              % options.extra_mozinfo_json)
 
             options.extra_mozinfo_json = json.load(open(options.extra_mozinfo_json))
 
         if options.totalChunks:
             if not 1 <= options.thisChunk <= options.totalChunks:
                 parser.error("thisChunk must be between 1 and totalChunks")
 
@@ -774,37 +774,31 @@ class MochitestArguments(ArgumentContain
 
         # XXX We can't normalize test_paths in the non build_obj case here,
         # because testRoot depends on the flavor, which is determined by the
         # mach command and therefore not finalized yet. Conversely, test paths
         # need to be normalized here for the mach case.
         if options.test_paths and build_obj:
             # Normalize test paths so they are relative to test root
             options.test_paths = [build_obj._wrap_path_argument(p).relpath()
-                for p in options.test_paths]
+                                  for p in options.test_paths]
 
         return options
 
 
 class B2GArguments(ArgumentContainer):
     """B2G specific arguments."""
 
     args = [
         [["--b2gpath"],
          {"dest": "b2gPath",
           "default": None,
           "help": "Path to B2G repo or QEMU directory.",
           "suppress": True,
           }],
-        [["--desktop"],
-         {"action": "store_true",
-          "default": False,
-          "help": "Run the tests on a B2G desktop build.",
-          "suppress": True,
-          }],
         [["--marionette"],
          {"default": None,
           "help": "host:port to use when connecting to Marionette",
           }],
         [["--emulator"],
          {"default": None,
           "help": "Architecture of emulator to use, x86 or arm",
           "suppress": True,
@@ -868,21 +862,16 @@ class B2GArguments(ArgumentContainer):
           }],
         [["--gecko-path"],
          {"dest": "geckoPath",
           "default": None,
           "help": "The path to a gecko distribution that should be installed on the emulator "
                   "prior to test.",
           "suppress": True,
           }],
-        [["--profile"],
-         {"dest": "profile",
-          "default": None,
-          "help": "For desktop testing, the path to the gaia profile to use.",
-          }],
         [["--logdir"],
          {"dest": "logdir",
           "default": None,
           "help": "Directory to store log files.",
           }],
         [['--busybox'],
          {"dest": 'busybox',
           "default": None,
@@ -904,30 +893,16 @@ class B2GArguments(ArgumentContainer):
         'extensionsToExclude': ['specialpowers'],
         # See dependencies of bug 1038943.
         'defaultLeakThreshold': 5536,
     }
 
     def validate(self, parser, options, context):
         """Validate b2g options."""
 
-        if options.desktop and not options.app:
-            if not (build_obj and conditions.is_b2g_desktop(build_obj)):
-                parser.error(
-                    "--desktop specified, but no b2g desktop build detected! Either "
-                    "build for b2g desktop, or point --appname to a b2g desktop binary.")
-        elif build_obj and conditions.is_b2g_desktop(build_obj):
-            options.desktop = True
-            if not options.app:
-                options.app = build_obj.get_binary_path()
-                if not options.app.endswith('-bin'):
-                    options.app = '%s-bin' % options.app
-                if not os.path.isfile(options.app):
-                    options.app = options.app[:-len('-bin')]
-
         if options.remoteWebServer is None:
             if os.name != "nt":
                 options.remoteWebServer = moznetwork.get_ip()
             else:
                 parser.error(
                     "You must specify a --remote-webserver=<ip address>")
         options.webServer = options.remoteWebServer
 
@@ -1188,17 +1163,17 @@ class MochitestArgumentParser(ArgumentPa
     def __init__(self, app=None, **kwargs):
         ArgumentParser.__init__(self, usage=self.__doc__, conflict_handler='resolve', **kwargs)
 
         self.oldcwd = os.getcwd()
         self.app = app
         if not self.app and build_obj:
             if conditions.is_android(build_obj):
                 self.app = 'android'
-            elif conditions.is_b2g(build_obj) or conditions.is_b2g_desktop(build_obj):
+            elif conditions.is_b2g(build_obj):
                 self.app = 'b2g'
         if not self.app:
             # platform can't be determined and app wasn't specified explicitly,
             # so just use generic arguments and hope for the best
             self.app = 'generic'
 
         if self.app not in container_map:
             self.error("Unrecognized app '{}'! Must be one of: {}".format(
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -487,55 +487,49 @@ class WebSocketServer(object):
         self._process.run()
         pid = self._process.pid
         self._log.info("runtests.py | Websocket server pid: %d" % pid)
 
     def stop(self):
         self._process.kill()
 
 
-class MochitestUtilsMixin(object):
-
-    """
-    Class containing some utility functions common to both local and remote
-    mochitest runners
+class MochitestBase(object):
     """
-
-    # TODO Utility classes are a code smell. This class is temporary
-    #      and should be removed when desktop mochitests are refactored
-    #      on top of mozbase. Each of the functions in here should
-    #      probably live somewhere in mozbase
+    Base mochitest class for both desktop and b2g.
+    """
 
     oldcwd = os.getcwd()
     jarDir = 'mochijar'
 
     # Path to the test script on the server
     TEST_PATH = "tests"
     NESTED_OOP_TEST_PATH = "nested_oop"
     CHROME_PATH = "redirect.html"
     urlOpts = []
     log = None
 
     def __init__(self, logger_options):
         self.update_mozinfo()
         self.server = None
         self.wsserver = None
         self.sslTunnel = None
+        self._active_tests = None
         self._locations = None
 
         if self.log is None:
             commandline.log_formatters["tbpl"] = (
                 MochitestFormatter,
                 "Mochitest specific tbpl formatter")
             self.log = commandline.setup_logging("mochitest",
                                                  logger_options,
                                                  {
                                                      "tbpl": sys.stdout
                                                  })
-            MochitestUtilsMixin.log = self.log
+            MochitestBase.log = self.log
 
         self.message_logger = MessageLogger(logger=self.log)
 
     def update_mozinfo(self):
         """walk up directories to find mozinfo.json update the info"""
         # TODO: This should go in a more generic place, e.g. mozinfo
 
         path = SCRIPT_DIR
@@ -543,16 +537,20 @@ class MochitestUtilsMixin(object):
         while path != os.path.expanduser('~'):
             if path in dirs:
                 break
             dirs.add(path)
             path = os.path.split(path)[0]
 
         mozinfo.find_and_update_from_json(*dirs)
 
+    def environment(self, **kwargs):
+        kwargs['log'] = self.log
+        return test_environment(**kwargs)
+
     def getFullPath(self, path):
         " Get an absolute path relative to self.oldcwd."
         return os.path.normpath(
             os.path.join(
                 self.oldcwd,
                 os.path.expanduser(path)))
 
     def getLogFilePath(self, logFile):
@@ -989,16 +987,283 @@ overlay chrome://browser/content/browser
                         if os.path.isdir(path) or (
                                 os.path.isfile(path) and path.endswith(".xpi")):
                             extensions.append(path)
 
         # append mochikit
         extensions.append(os.path.join(SCRIPT_DIR, self.jarDir))
         return extensions
 
+    def logPreamble(self, tests):
+        """Logs a suite_start message and test_start/test_end at the beginning of a run.
+        """
+        self.log.suite_start([t['path'] for t in tests])
+        for test in tests:
+            if 'disabled' in test:
+                self.log.test_start(test['path'])
+                self.log.test_end(
+                    test['path'],
+                    'SKIP',
+                    message=test['disabled'])
+
+    def getActiveTests(self, options, disabled=True):
+        """
+          This method is used to parse the manifest and return active filtered tests.
+        """
+        if self._active_tests:
+            return self._active_tests
+
+        manifest = self.getTestManifest(options)
+        if manifest:
+            if options.extra_mozinfo_json:
+                mozinfo.update(options.extra_mozinfo_json)
+            info = mozinfo.info
+
+            # Bug 1089034 - imptest failure expectations are encoded as
+            # test manifests, even though they aren't tests. This gross
+            # hack causes several problems in automation including
+            # throwing off the chunking numbers. Remove them manually
+            # until bug 1089034 is fixed.
+            def remove_imptest_failure_expectations(tests, values):
+                return (t for t in tests
+                        if 'imptests/failures' not in t['path'])
+
+            filters = [
+                remove_imptest_failure_expectations,
+                subsuite(options.subsuite),
+            ]
+
+            if options.test_tags:
+                filters.append(tags(options.test_tags))
+
+            if options.test_paths:
+                options.test_paths = self.normalize_paths(options.test_paths)
+                filters.append(pathprefix(options.test_paths))
+
+            # Add chunking filters if specified
+            if options.totalChunks:
+                if options.chunkByRuntime:
+                    runtime_file = self.resolve_runtime_file(options)
+                    if not os.path.exists(runtime_file):
+                        self.log.warning("runtime file %s not found; defaulting to chunk-by-dir" %
+                                         runtime_file)
+                        options.chunkByRuntime = None
+                        flavor = self.getTestFlavor(options)
+                        if flavor in ('browser-chrome', 'devtools-chrome'):
+                            # these values match current mozharness configs
+                            options.chunkbyDir = 5
+                        else:
+                            options.chunkByDir = 4
+
+                if options.chunkByDir:
+                    filters.append(chunk_by_dir(options.thisChunk,
+                                                options.totalChunks,
+                                                options.chunkByDir))
+                elif options.chunkByRuntime:
+                    with open(runtime_file, 'r') as f:
+                        runtime_data = json.loads(f.read())
+                    runtimes = runtime_data['runtimes']
+                    default = runtime_data['excluded_test_average']
+                    filters.append(
+                        chunk_by_runtime(options.thisChunk,
+                                         options.totalChunks,
+                                         runtimes,
+                                         default_runtime=default))
+                else:
+                    filters.append(chunk_by_slice(options.thisChunk,
+                                                  options.totalChunks))
+
+            tests = manifest.active_tests(
+                exists=False, disabled=disabled, filters=filters, **info)
+
+            if len(tests) == 0:
+                self.log.error("no tests to run using specified "
+                               "combination of filters: {}".format(
+                                   manifest.fmt_filters()))
+
+        paths = []
+        for test in tests:
+            if len(tests) == 1 and 'disabled' in test:
+                del test['disabled']
+
+            pathAbs = os.path.abspath(test['path'])
+            assert pathAbs.startswith(self.testRootAbs)
+            tp = pathAbs[len(self.testRootAbs):].replace('\\', '/').strip('/')
+
+            if not self.isTest(options, tp):
+                self.log.warning(
+                    'Warning: %s from manifest %s is not a valid test' %
+                    (test['name'], test['manifest']))
+                continue
+
+            testob = {'path': tp}
+            if 'disabled' in test:
+                testob['disabled'] = test['disabled']
+            if 'expected' in test:
+                testob['expected'] = test['expected']
+            paths.append(testob)
+
+        def path_sort(ob1, ob2):
+            path1 = ob1['path'].split('/')
+            path2 = ob2['path'].split('/')
+            return cmp(path1, path2)
+
+        paths.sort(path_sort)
+        self._active_tests = paths
+        if options.dump_tests:
+            options.dump_tests = os.path.expanduser(options.dump_tests)
+            assert os.path.exists(os.path.dirname(options.dump_tests))
+            with open(options.dump_tests, 'w') as dumpFile:
+                dumpFile.write(json.dumps({'active_tests': self._active_tests}))
+
+            self.log.info("Dumping active_tests to %s file." % options.dump_tests)
+            sys.exit()
+
+        return self._active_tests
+
+    def getTestManifest(self, options):
+        if isinstance(options.manifestFile, TestManifest):
+            manifest = options.manifestFile
+        elif options.manifestFile and os.path.isfile(options.manifestFile):
+            manifestFileAbs = os.path.abspath(options.manifestFile)
+            assert manifestFileAbs.startswith(SCRIPT_DIR)
+            manifest = TestManifest([options.manifestFile], strict=False)
+        elif options.manifestFile and os.path.isfile(os.path.join(SCRIPT_DIR, options.manifestFile)):
+            manifestFileAbs = os.path.abspath(
+                os.path.join(
+                    SCRIPT_DIR,
+                    options.manifestFile))
+            assert manifestFileAbs.startswith(SCRIPT_DIR)
+            manifest = TestManifest([manifestFileAbs], strict=False)
+        else:
+            masterName = self.getTestFlavor(options) + '.ini'
+            masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
+
+            if os.path.exists(masterPath):
+                manifest = TestManifest([masterPath], strict=False)
+            else:
+                self._log.warning(
+                    'TestManifest masterPath %s does not exist' %
+                    masterPath)
+
+        return manifest
+
+    def makeTestConfig(self, options):
+        "Creates a test configuration file for customizing test execution."
+        options.logFile = options.logFile.replace("\\", "\\\\")
+
+        if "MOZ_HIDE_RESULTS_TABLE" in os.environ and os.environ[
+                "MOZ_HIDE_RESULTS_TABLE"] == "1":
+            options.hideResultsTable = True
+
+        # strip certain unnecessary items to avoid serialization errors in json.dumps()
+        d = dict((k, v) for k, v in options.__dict__.items() if (v is None) or
+                 isinstance(v, (basestring, numbers.Number)))
+        d['testRoot'] = self.testRoot
+        if not options.keep_open:
+            d['closeWhenDone'] = '1'
+        content = json.dumps(d)
+
+        with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config:
+            config.write(content)
+
+    def buildBrowserEnv(self, options, debugger=False, env=None):
+        """build the environment variables for the specific test and operating system"""
+        if mozinfo.info["asan"]:
+            lsanPath = SCRIPT_DIR
+        else:
+            lsanPath = None
+
+        browserEnv = self.environment(
+            xrePath=options.xrePath,
+            env=env,
+            debugger=debugger,
+            dmdPath=options.dmdPath,
+            lsanPath=lsanPath)
+
+        # These variables are necessary for correct application startup; change
+        # via the commandline at your own risk.
+        browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
+
+        # When creating child processes on Windows pre-Vista (e.g. Windows XP) we
+        # don't normally inherit stdout/err handles, because you can only do it by
+        # inheriting all other inheritable handles as well.
+        # We need to inherit them for plain mochitests for test logging purposes, so
+        # we do so on the basis of a specific environment variable.
+        if self.getTestFlavor(options) == "mochitest":
+            browserEnv["MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA"] = "1"
+
+        # interpolate environment passed with options
+        try:
+            browserEnv.update(
+                dict(
+                    parseKeyValue(
+                        options.environment,
+                        context='--setenv')))
+        except KeyValueParseError as e:
+            self.log.error(str(e))
+            return None
+
+        browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
+
+        try:
+            gmp_path = self.getGMPPluginPath(options)
+            if gmp_path is not None:
+                browserEnv["MOZ_GMP_PATH"] = gmp_path
+        except EnvironmentError:
+            self.log.error('Could not find path to gmp-fake plugin!')
+            return None
+
+        if options.fatalAssertions:
+            browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
+
+        # Produce an NSPR log, is setup (see NSPR_LOG_MODULES global at the top of
+        # this script).
+        self.nsprLogs = NSPR_LOG_MODULES and "MOZ_UPLOAD_DIR" in os.environ
+        if self.nsprLogs:
+            browserEnv["NSPR_LOG_MODULES"] = NSPR_LOG_MODULES
+
+            browserEnv["NSPR_LOG_FILE"] = "%s/nspr.log" % tempfile.gettempdir()
+            browserEnv["GECKO_SEPARATE_NSPR_LOGS"] = "1"
+
+        if debugger and not options.slowscript:
+            browserEnv["JS_DISABLE_SLOW_SCRIPT_SIGNALS"] = "1"
+
+        # For e10s, our tests default to suppressing the "unsafe CPOW usage"
+        # warnings that can plague test logs.
+        if not options.enableCPOWWarnings:
+            browserEnv["DISABLE_UNSAFE_CPOW_WARNINGS"] = "1"
+
+        return browserEnv
+
+    def killNamedOrphans(self, pname):
+        """ Kill orphan processes matching the given command name """
+        self.log.info("Checking for orphan %s processes..." % pname)
+
+        def _psInfo(line):
+            if pname in line:
+                self.log.info(line)
+
+        process = mozprocess.ProcessHandler(['ps', '-f'],
+                                            processOutputLine=_psInfo)
+        process.run()
+        process.wait()
+
+        def _psKill(line):
+            parts = line.split()
+            if len(parts) == 3 and parts[0].isdigit():
+                pid = int(parts[0])
+                if parts[2] == pname and parts[1] == '1':
+                    self.log.info("killing %s orphan with pid %d" % (pname, pid))
+                    killPid(pid, self.log)
+        process = mozprocess.ProcessHandler(['ps', '-o', 'pid,ppid,comm'],
+                                            processOutputLine=_psKill)
+        process.run()
+        process.wait()
+
 
 class SSLTunnel:
 
     def __init__(self, options, logger, ignoreSSLTunnelExts=False):
         self.log = logger
         self.process = None
         self.utilityPath = options.utilityPath
         self.xrePath = options.xrePath
@@ -1220,29 +1485,31 @@ def parseKeyValue(strings, separator='='
     missing = [string for string in strings if separator not in string]
     if missing:
         raise KeyValueParseError(
             "Error: syntax error in %s" %
             (context, ','.join(missing)), errors=missing)
     return [string.split(separator, 1) for string in strings]
 
 
-class Mochitest(MochitestUtilsMixin):
-    _active_tests = None
+class MochitestDesktop(MochitestBase):
+    """
+    Mochitest class for desktop firefox and mulet.
+    """
     certdbNew = False
     sslTunnel = None
     DEFAULT_TIMEOUT = 60.0
     mediaDevices = None
 
     # XXX use automation.py for test name to avoid breaking legacy
     # TODO: replace this with 'runtests.py' or 'mochitest' or the like
     test_name = 'automation.py'
 
     def __init__(self, logger_options):
-        super(Mochitest, self).__init__(logger_options)
+        MochitestBase.__init__(self, logger_options)
 
         # Max time in seconds to wait for server startup before tests will fail -- if
         # this seems big, it's mostly for debug machines where cold startup
         # (particularly after a build) takes forever.
         self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get('debug') else 90
 
         # metro browser sub process id
         self.browserProcessId = None
@@ -1251,20 +1518,16 @@ class Mochitest(MochitestUtilsMixin):
         # Create variables to count the number of passes, fails, todos.
         self.countpass = 0
         self.countfail = 0
         self.counttodo = 0
 
         self.expectedError = {}
         self.result = {}
 
-    def environment(self, **kwargs):
-        kwargs['log'] = self.log
-        return test_environment(**kwargs)
-
     def extraPrefs(self, extraPrefs):
         """interpolate extra preferences from option strings"""
 
         try:
             return dict(parseKeyValue(extraPrefs, context='--setpref='))
         except KeyValueParseError as e:
             print str(e)
             sys.exit(1)
@@ -1462,85 +1725,16 @@ class Mochitest(MochitestUtilsMixin):
                      if os.path.isdir(os.path.join(parent, sub))]
 
         if not gmp_paths:
             # This is fatal for desktop environments.
             raise EnvironmentError('Could not find test gmp plugins')
 
         return os.pathsep.join(gmp_paths)
 
-    def buildBrowserEnv(self, options, debugger=False, env=None):
-        """build the environment variables for the specific test and operating system"""
-        if mozinfo.info["asan"]:
-            lsanPath = SCRIPT_DIR
-        else:
-            lsanPath = None
-
-        browserEnv = self.environment(
-            xrePath=options.xrePath,
-            env=env,
-            debugger=debugger,
-            dmdPath=options.dmdPath,
-            lsanPath=lsanPath)
-
-        # These variables are necessary for correct application startup; change
-        # via the commandline at your own risk.
-        browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
-
-        # When creating child processes on Windows pre-Vista (e.g. Windows XP) we
-        # don't normally inherit stdout/err handles, because you can only do it by
-        # inheriting all other inheritable handles as well.
-        # We need to inherit them for plain mochitests for test logging purposes, so
-        # we do so on the basis of a specific environment variable.
-        if self.getTestFlavor(options) == "mochitest":
-            browserEnv["MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA"] = "1"
-
-        # interpolate environment passed with options
-        try:
-            browserEnv.update(
-                dict(
-                    parseKeyValue(
-                        options.environment,
-                        context='--setenv')))
-        except KeyValueParseError as e:
-            self.log.error(str(e))
-            return None
-
-        browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
-
-        try:
-            gmp_path = self.getGMPPluginPath(options)
-            if gmp_path is not None:
-                browserEnv["MOZ_GMP_PATH"] = gmp_path
-        except EnvironmentError:
-            self.log.error('Could not find path to gmp-fake plugin!')
-            return None
-
-        if options.fatalAssertions:
-            browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
-
-        # Produce an NSPR log, is setup (see NSPR_LOG_MODULES global at the top of
-        # this script).
-        self.nsprLogs = NSPR_LOG_MODULES and "MOZ_UPLOAD_DIR" in os.environ
-        if self.nsprLogs:
-            browserEnv["NSPR_LOG_MODULES"] = NSPR_LOG_MODULES
-
-            browserEnv["NSPR_LOG_FILE"] = "%s/nspr.log" % tempfile.gettempdir()
-            browserEnv["GECKO_SEPARATE_NSPR_LOGS"] = "1"
-
-        if debugger and not options.slowscript:
-            browserEnv["JS_DISABLE_SLOW_SCRIPT_SIGNALS"] = "1"
-
-        # For e10s, our tests default to suppressing the "unsafe CPOW usage"
-        # warnings that can plague test logs.
-        if not options.enableCPOWWarnings:
-            browserEnv["DISABLE_UNSAFE_CPOW_WARNINGS"] = "1"
-
-        return browserEnv
-
     def cleanup(self, options):
         """ remove temporary files and profile """
         if hasattr(self, 'manifest') and self.manifest is not None:
             os.remove(self.manifest)
         if hasattr(self, 'profile'):
             del self.profile
         if options.pidFile != "":
             try:
@@ -1668,18 +1862,16 @@ class Mochitest(MochitestUtilsMixin):
             interactive = debuggerInfo.interactive
             debug_args = [debuggerInfo.path] + debuggerInfo.args
 
         # Set up Valgrind arguments.
         if valgrindPath:
             interactive = False
             valgrindArgs_split = ([] if valgrindArgs is None
                                   else valgrindArgs.split())
-            valgrindSuppFiles_split = ([] if valgrindSuppFiles is None
-                                       else valgrindSuppFiles.split(","))
 
             valgrindSuppFiles_final = []
             if valgrindSuppFiles is not None:
                 valgrindSuppFiles_final = ["--suppressions=" + path for path in valgrindSuppFiles.split(",")]
 
             debug_args = ([valgrindPath]
                           + mozdebug.get_default_valgrind_args()
                           + valgrindArgs_split
@@ -1872,143 +2064,16 @@ class Mochitest(MochitestUtilsMixin):
         for p in paths:
             abspath = os.path.abspath(os.path.join(self.oldcwd, p))
             if abspath.startswith(self.testRootAbs):
                 norm_paths.append(os.path.relpath(abspath, self.testRootAbs))
             else:
                 norm_paths.append(p)
         return norm_paths
 
-    def getActiveTests(self, options, disabled=True):
-        """
-          This method is used to parse the manifest and return active filtered tests.
-        """
-        if self._active_tests:
-            return self._active_tests
-
-        manifest = self.getTestManifest(options)
-        if manifest:
-            if options.extra_mozinfo_json:
-                mozinfo.update(options.extra_mozinfo_json)
-            info = mozinfo.info
-
-            # Bug 1089034 - imptest failure expectations are encoded as
-            # test manifests, even though they aren't tests. This gross
-            # hack causes several problems in automation including
-            # throwing off the chunking numbers. Remove them manually
-            # until bug 1089034 is fixed.
-            def remove_imptest_failure_expectations(tests, values):
-                return (t for t in tests
-                        if 'imptests/failures' not in t['path'])
-
-            filters = [
-                remove_imptest_failure_expectations,
-                subsuite(options.subsuite),
-            ]
-
-            if options.test_tags:
-                filters.append(tags(options.test_tags))
-
-            if options.test_paths:
-                options.test_paths = self.normalize_paths(options.test_paths)
-                filters.append(pathprefix(options.test_paths))
-
-            # Add chunking filters if specified
-            if options.totalChunks:
-                if options.chunkByRuntime:
-                    runtime_file = self.resolve_runtime_file(options)
-                    if not os.path.exists(runtime_file):
-                        self.log.warning("runtime file %s not found; defaulting to chunk-by-dir" %
-                                         runtime_file)
-                        options.chunkByRuntime = None
-                        flavor = self.getTestFlavor(options)
-                        if flavor in ('browser-chrome', 'devtools-chrome'):
-                            # these values match current mozharness configs
-                            options.chunkbyDir = 5
-                        else:
-                            options.chunkByDir = 4
-
-                if options.chunkByDir:
-                    filters.append(chunk_by_dir(options.thisChunk,
-                                                options.totalChunks,
-                                                options.chunkByDir))
-                elif options.chunkByRuntime:
-                    with open(runtime_file, 'r') as f:
-                        runtime_data = json.loads(f.read())
-                    runtimes = runtime_data['runtimes']
-                    default = runtime_data['excluded_test_average']
-                    filters.append(
-                        chunk_by_runtime(options.thisChunk,
-                                         options.totalChunks,
-                                         runtimes,
-                                         default_runtime=default))
-                else:
-                    filters.append(chunk_by_slice(options.thisChunk,
-                                                  options.totalChunks))
-
-            tests = manifest.active_tests(
-                exists=False, disabled=disabled, filters=filters, **info)
-
-            if len(tests) == 0:
-                self.log.error("no tests to run using specified "
-                               "combination of filters: {}".format(
-                                    manifest.fmt_filters()))
-
-        paths = []
-        for test in tests:
-            if len(tests) == 1 and 'disabled' in test:
-                del test['disabled']
-
-            pathAbs = os.path.abspath(test['path'])
-            assert pathAbs.startswith(self.testRootAbs)
-            tp = pathAbs[len(self.testRootAbs):].replace('\\', '/').strip('/')
-
-            if not self.isTest(options, tp):
-                self.log.warning(
-                    'Warning: %s from manifest %s is not a valid test' %
-                    (test['name'], test['manifest']))
-                continue
-
-            testob = {'path': tp}
-            if 'disabled' in test:
-                testob['disabled'] = test['disabled']
-            if 'expected' in test:
-                testob['expected'] = test['expected']
-            paths.append(testob)
-
-        def path_sort(ob1, ob2):
-            path1 = ob1['path'].split('/')
-            path2 = ob2['path'].split('/')
-            return cmp(path1, path2)
-
-        paths.sort(path_sort)
-        self._active_tests = paths
-        if options.dump_tests:
-            options.dump_tests = os.path.expanduser(options.dump_tests)
-            assert os.path.exists(os.path.dirname(options.dump_tests))
-            with open(options.dump_tests, 'w') as dumpFile:
-                dumpFile.write(json.dumps({'active_tests': self._active_tests}))
-
-            self.log.info("Dumping active_tests to %s file." % options.dump_tests)
-            sys.exit()
-
-        return self._active_tests
-
-    def logPreamble(self, tests):
-        """Logs a suite_start message and test_start/test_end at the beginning of a run.
-        """
-        self.log.suite_start([t['path'] for t in tests])
-        for test in tests:
-            if 'disabled' in test:
-                self.log.test_start(test['path'])
-                self.log.test_end(
-                    test['path'],
-                    'SKIP',
-                    message=test['disabled'])
-
     def getTestsToRun(self, options):
         """
           This method makes a list of tests that are to be run. Required mainly for --bisect-chunk.
         """
         tests = self.getActiveTests(options)
         self.logPreamble(tests)
 
         testsToRun = []
@@ -2051,39 +2116,16 @@ class Mochitest(MochitestUtilsMixin):
         # We need to print the summary only if options.bisectChunk has a value.
         # Also we need to make sure that we do not print the summary in between
         # running tests via --run-by-dir.
         if options.bisectChunk and options.bisectChunk in self.result:
             bisect.print_summary()
 
         return result
 
-    def killNamedOrphans(self, pname):
-        """ Kill orphan processes matching the given command name """
-        self.log.info("Checking for orphan %s processes..." % pname)
-        def _psInfo(line):
-            if pname in line:
-                self.log.info(line)
-        process = mozprocess.ProcessHandler(['ps', '-f'],
-                                            processOutputLine=_psInfo)
-        process.run()
-        process.wait()
-
-        def _psKill(line):
-            parts = line.split()
-            if len(parts) == 3 and parts[0].isdigit():
-                pid = int(parts[0])
-                if parts[2] == pname and parts[1] == '1':
-                    self.log.info("killing %s orphan with pid %d" % (pname, pid))
-                    killPid(pid, self.log)
-        process = mozprocess.ProcessHandler(['ps', '-o', 'pid,ppid,comm'],
-                                            processOutputLine=_psKill)
-        process.run()
-        process.wait()
-
     def runTests(self, options, onLaunch=None):
         """ Prepare, configure, run tests and cleanup """
 
         self.setTestRoot(options)
 
         # Despite our efforts to clean up servers started by this script, in practice
         # we still see infrequent cases where a process is orphaned and interferes
         # with future tests, typically because the old server is keeping the port in use.
@@ -2195,17 +2237,16 @@ class Mochitest(MochitestUtilsMixin):
         self.leak_report_file = os.path.join(
             options.profilePath,
             "runtests_leaks.log")
 
         self.browserEnv = self.buildBrowserEnv(
             options,
             debuggerInfo is not None)
 
-
         # If there are any Mulet-specific tests doing remote network access,
         # we will not be aware since we are explicitely allowing this, as for
         # B2G
         #
         # In addition, the push subsuite directly accesses the production
         # push service.
         if 'MOZ_DISABLE_NONLOCAL_CONNECTIONS' in self.browserEnv:
             if mozinfo.info.get('buildapp') == 'mulet' or options.subsuite == 'push':
@@ -2509,62 +2550,16 @@ class Mochitest(MochitestUtilsMixin):
                 self.lsanLeaks.log(message['message'])
             return message
 
         def trackShutdownLeaks(self, message):
             if self.shutdownLeaks:
                 self.shutdownLeaks.log(message)
             return message
 
-    def makeTestConfig(self, options):
-        "Creates a test configuration file for customizing test execution."
-        options.logFile = options.logFile.replace("\\", "\\\\")
-
-        if "MOZ_HIDE_RESULTS_TABLE" in os.environ and os.environ[
-                "MOZ_HIDE_RESULTS_TABLE"] == "1":
-            options.hideResultsTable = True
-
-        # strip certain unnecessary items to avoid serialization errors in json.dumps()
-        d = dict((k, v) for k, v in options.__dict__.items() if (v is None) or
-            isinstance(v,(basestring,numbers.Number)))
-        d['testRoot'] = self.testRoot
-        if not options.keep_open:
-            d['closeWhenDone'] = '1'
-        content = json.dumps(d)
-
-        with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config:
-            config.write(content)
-
-    def getTestManifest(self, options):
-        if isinstance(options.manifestFile, TestManifest):
-            manifest = options.manifestFile
-        elif options.manifestFile and os.path.isfile(options.manifestFile):
-            manifestFileAbs = os.path.abspath(options.manifestFile)
-            assert manifestFileAbs.startswith(SCRIPT_DIR)
-            manifest = TestManifest([options.manifestFile], strict=False)
-        elif options.manifestFile and os.path.isfile(os.path.join(SCRIPT_DIR, options.manifestFile)):
-            manifestFileAbs = os.path.abspath(
-                os.path.join(
-                    SCRIPT_DIR,
-                    options.manifestFile))
-            assert manifestFileAbs.startswith(SCRIPT_DIR)
-            manifest = TestManifest([manifestFileAbs], strict=False)
-        else:
-            masterName = self.getTestFlavor(options) + '.ini'
-            masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
-
-            if os.path.exists(masterPath):
-                manifest = TestManifest([masterPath], strict=False)
-            else:
-                self._log.warning(
-                    'TestManifest masterPath %s does not exist' %
-                    masterPath)
-
-        return manifest
-
     def getDirectories(self, options):
         """
             Make the list of directories by parsing manifests
         """
         tests = self.getActiveTests(options)
         dirlist = []
         for test in tests:
             if 'disabled' in test:
@@ -2575,18 +2570,18 @@ class Mochitest(MochitestUtilsMixin):
                 dirlist.append(rootdir)
 
         return dirlist
 
 
 def run_test_harness(options):
     logger_options = {
         key: value for key, value in vars(options).iteritems()
-        if key.startswith('log') or key == 'valgrind' }
-    runner = Mochitest(logger_options)
+        if key.startswith('log') or key == 'valgrind'}
+    runner = MochitestDesktop(logger_options)
 
     options.runByDir = False
 
     if runner.getTestFlavor(options) == 'mochitest':
         options.runByDir = True
 
     if runner.getTestFlavor(options) == 'browser-chrome':
         options.runByDir = True
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -1,84 +1,77 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import json
 import os
 import posixpath
-import shutil
 import sys
 import tempfile
-import threading
 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 MochitestBase
 from mochitest_options import MochitestArgumentParser
 from marionette import Marionette
 from mozprofile import Profile, Preferences
 from mozrunner.utils import get_stack_fixer_function
 import mozinfo
 import mozleak
 
 
-class B2GMochitest(MochitestUtilsMixin):
+class MochitestB2G(MochitestBase):
+    """
+    Mochitest class for b2g emulators and devices.
+    """
     marionette = None
+    remote_log = None
 
     def __init__(self, marionette_args,
                  logger_options,
+                 profile_data_dir,
+                 local_binary_dir,
+                 locations=os.path.join(here, 'server-locations.txt'),
                  out_of_process=True,
-                 profile_data_dir=None,
-                 locations=os.path.join(here, 'server-locations.txt')):
-        super(B2GMochitest, self).__init__(logger_options)
+                 remote_test_root=None,
+                 remote_log_file=None):
+        MochitestBase.__init__(self, logger_options)
         self.marionette_args = marionette_args
         self.out_of_process = out_of_process
         self.locations_file = locations
         self.preferences = []
         self.webapps = None
         self.test_script = os.path.join(here, 'b2g_start_script.js')
         self.test_script_args = [self.out_of_process]
         self.product = 'b2g'
         self.remote_chrome_test_dir = None
+        self.local_log = None
+        self.local_binary_dir = local_binary_dir
 
-        if profile_data_dir:
-            self.preferences = [
-                os.path.join(
-                    profile_data_dir,
-                    f) for f in os.listdir(profile_data_dir) if f.startswith('pref')]
-            self.webapps = [
-                os.path.join(
-                    profile_data_dir,
-                    f) for f in os.listdir(profile_data_dir) if f.startswith('webapp')]
+        self.preferences = [
+            os.path.join(
+                profile_data_dir,
+                f) for f in os.listdir(profile_data_dir) if f.startswith('pref')]
+        self.webapps = [
+            os.path.join(
+                profile_data_dir,
+                f) for f in os.listdir(profile_data_dir) if f.startswith('webapp')]
 
         # mozinfo is populated by the parent class
         if mozinfo.info['debug']:
             self.SERVER_STARTUP_TIMEOUT = 180
         else:
             self.SERVER_STARTUP_TIMEOUT = 90
 
-    def setup_common_options(self, options):
-        test_url = self.buildTestPath(options)
-        # For B2G emulators buildURLOptions has been called
-        # without calling buildTestPath first and that
-        # causes manifestFile not to be set
-        if not "manifestFile=tests.json" in self.urlOpts:
-            self.urlOpts.append("manifestFile=%s" % options.manifestFile)
-
-        if len(self.urlOpts) > 0:
-            test_url += "?" + "&".join(self.urlOpts)
-        self.test_script_args.append(test_url)
-
     def buildTestPath(self, options, testsToFilter=None):
         if options.manifestFile != 'tests.json':
-            super(B2GMochitest, self).buildTestPath(options, testsToFilter, disabled=False)
+            MochitestBase.buildTestPath(self, options, testsToFilter, disabled=False)
         return self.buildTestURL(options)
 
     def build_profile(self, options):
         # preferences
         prefs = {}
         for path in self.preferences:
             prefs.update(Preferences.read_prefs(path))
 
@@ -102,21 +95,17 @@ class B2GMochitest(MochitestUtilsMixin):
         kwargs = {
             'addons': self.getExtensionsToInstall(options),
             'apps': self.webapps,
             'locations': self.locations_file,
             'preferences': prefs,
             'proxy': {"remote": options.webServer}
         }
 
-        if options.profile:
-            self.profile = Profile.clone(options.profile, **kwargs)
-        else:
-            self.profile = Profile(**kwargs)
-
+        self.profile = Profile(**kwargs)
         options.profilePath = self.profile.profile
         # TODO bug 839108 - mozprofile should probably handle this
         manifest = self.addChromeToProfile(options)
         self.copyExtraFilesToProfile(options)
         return manifest
 
     def run_tests(self, options):
         """ Prepare, configure, run tests and cleanup """
@@ -239,17 +228,17 @@ class B2GMochitest(MochitestUtilsMixin):
                   testUtils.specialPowersObserver = new testUtils.SpecialPowersObserver();
                   testUtils.specialPowersObserver.init();
                 }
                 """)
 
             if options.chrome:
                 self.app_ctx.dm.removeDir(self.remote_chrome_test_dir)
                 self.app_ctx.dm.mkDir(self.remote_chrome_test_dir)
-                local = super(B2GMochitest, self).getChromeTestDir(options)
+                local = MochitestBase.getChromeTestDir(self, options)
                 local = os.path.join(local, "chrome")
                 remote = self.remote_chrome_test_dir
                 self.log.info(
                     "pushing %s to %s on device..." %
                     (local, remote))
                 self.app_ctx.dm.pushDir(local, remote)
 
             if os.path.isfile(self.test_script):
@@ -314,37 +303,16 @@ class B2GMochitest(MochitestUtilsMixin):
         # writing the dummy.
         if hasattr(self, 'app_ctx'):
             self.remote_chrome_test_dir = posixpath.join(
                 self.app_ctx.remote_test_root,
                 'chrome')
             return self.remote_chrome_test_dir
         return 'dummy-chrome-test-dir'
 
-
-class B2GDeviceMochitest(B2GMochitest, Mochitest):
-    remote_log = None
-
-    def __init__(
-            self,
-            marionette_args,
-            logger_options,
-            profile_data_dir,
-            local_binary_dir,
-            remote_test_root=None,
-            remote_log_file=None):
-        B2GMochitest.__init__(
-            self,
-            marionette_args,
-            logger_options,
-            out_of_process=True,
-            profile_data_dir=profile_data_dir)
-        self.local_log = None
-        self.local_binary_dir = local_binary_dir
-
     def cleanup(self, manifest, options):
         if self.local_log:
             self.app_ctx.dm.getFile(self.remote_log, self.local_log)
             self.app_ctx.dm.removeFile(self.remote_log)
 
         if options.pidFile != "":
             try:
                 os.remove(options.pidFile)
@@ -365,92 +333,46 @@ class B2GDeviceMochitest(B2GMochitest, M
         """ Create the servers on the host and start them up """
         savedXre = options.xrePath
         savedUtility = options.utilityPath
         savedProfie = options.profilePath
         options.xrePath = self.local_binary_dir
         options.utilityPath = self.local_binary_dir
         options.profilePath = tempfile.mkdtemp()
 
-        MochitestUtilsMixin.startServers(self, options, debuggerInfo)
+        MochitestBase.startServers(self, options, debuggerInfo)
 
         options.xrePath = savedXre
         options.utilityPath = savedUtility
         options.profilePath = savedProfie
 
     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)
+        MochitestBase.buildURLOptions(self, options, env)
+
+        test_url = self.buildTestPath(options)
 
-        self.setup_common_options(options)
+        # For B2G emulators buildURLOptions has been called
+        # without calling buildTestPath first and that
+        # causes manifestFile not to be set
+        if "manifestFile=tests.json" not in self.urlOpts:
+            self.urlOpts.append("manifestFile=%s" % options.manifestFile)
+
+        if len(self.urlOpts) > 0:
+            test_url += "?" + "&".join(self.urlOpts)
+        self.test_script_args.append(test_url)
+
 
         options.profilePath = self.app_ctx.remote_profile
         options.logFile = self.local_log
 
 
-class B2GDesktopMochitest(B2GMochitest, Mochitest):
-
-    def __init__(self, marionette_args, logger_options, profile_data_dir):
-        B2GMochitest.__init__(
-            self,
-            marionette_args,
-            logger_options,
-            out_of_process=False,
-            profile_data_dir=profile_data_dir)
-        Mochitest.__init__(self, logger_options)
-        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)
-
-        if os.path.isfile(test_script):
-            f = open(test_script, 'r')
-            test_script = f.read()
-            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)
-
-        self.setup_common_options(options)
-
-        # Copy the extensions to the B2G bundles dir.
-        extensionDir = os.path.join(
-            options.profilePath,
-            'extensions',
-            'staged')
-        bundlesDir = os.path.join(os.path.dirname(options.app),
-                                  'distribution', 'bundles')
-
-        for filename in os.listdir(extensionDir):
-            shutil.rmtree(os.path.join(bundlesDir, filename), True)
-            shutil.copytree(os.path.join(extensionDir, filename),
-                            os.path.join(bundlesDir, filename))
-
-    def buildProfile(self, options):
-        return self.build_profile(options)
-
-
-def run_remote_mochitests(options):
+def run_test_harness(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,
@@ -461,17 +383,17 @@ def run_remote_mochitests(options):
         host, port = options.marionette.split(':')
         marionette_args['host'] = host
         marionette_args['port'] = int(port)
 
     if (options is None):
         print "ERROR: Invalid options specified, use --help for a list of valid options"
         sys.exit(1)
 
-    mochitest = B2GDeviceMochitest(
+    mochitest = MochitestB2G(
         marionette_args,
         options,
         options.profile_data_dir,
         options.xrePath,
         remote_log_file=options.remoteLogFile)
 
     if (options is None):
         sys.exit(1)
@@ -490,50 +412,15 @@ def run_remote_mochitests(options):
             pass
         retVal = 1
 
     mochitest.message_logger.finish()
 
     return retVal
 
 
-def run_desktop_mochitests(options):
-    # create our Marionette instance
-    marionette_args = {}
-    if options.marionette:
-        host, port = options.marionette.split(':')
-        marionette_args['host'] = host
-        marionette_args['port'] = int(port)
-
-    # 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
-
-    mochitest = B2GDesktopMochitest(
-        marionette_args,
-        options,
-        options.profile_data_dir)
-    if options is None:
-        sys.exit(1)
-
-    if options.desktop and not options.profile:
-        raise Exception("must specify --profile when specifying --desktop")
-
-    options.browserArgs += ['-marionette']
-    options.runByDir = False
-    retVal = mochitest.runTests(options, onLaunch=mochitest.startTests)
-    mochitest.message_logger.finish()
-
-    return retVal
-
-
 def main():
     parser = MochitestArgumentParser(app='b2g')
     options = parser.parse_args()
-
-    if options.desktop:
-        return run_desktop_mochitests(options)
-    else:
-        return run_remote_mochitests(options)
+    return run_test_harness(options)
 
 if __name__ == "__main__":
     sys.exit(main())
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -8,33 +8,34 @@ import traceback
 
 sys.path.insert(
     0, os.path.abspath(
         os.path.realpath(
             os.path.dirname(__file__))))
 
 from automation import Automation
 from remoteautomation import RemoteAutomation, fennecLogcatFilters
-from runtests import Mochitest, MessageLogger
+from runtests import MochitestDesktop, MessageLogger
 from mochitest_options import MochitestArgumentParser
 
 import devicemanager
 import mozinfo
 
 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
 
 
-class MochiRemote(Mochitest):
+# TODO inherit from MochitestBase instead
+class MochiRemote(MochitestDesktop):
     _automation = None
     _dm = None
     localProfile = None
     logMessages = []
 
     def __init__(self, automation, devmgr, options):
-        Mochitest.__init__(self, options)
+        MochitestDesktop.__init__(self, options)
 
         self._automation = automation
         self._dm = devmgr
         self.environment = self._automation.environment
         self.remoteProfile = options.remoteTestRoot + "/profile"
         self._automation.setRemoteProfile(self.remoteProfile)
         self.remoteLog = options.remoteLogFile
         self.localLog = options.logFile
@@ -58,17 +59,17 @@ class MochiRemote(Mochitest):
             self.log.warning(
                 "Unable to retrieve log file (%s) from remote device" %
                 self.remoteLog)
         self._dm.removeDir(self.remoteProfile)
         self._dm.removeDir(self.remoteChromeTestDir)
         blobberUploadDir = os.environ.get('MOZ_UPLOAD_DIR', None)
         if blobberUploadDir:
             self._dm.getDirectory(self.remoteNSPR, blobberUploadDir)
-        Mochitest.cleanup(self, options)
+        MochitestDesktop.cleanup(self, options)
 
     def findPath(self, paths, filename=None):
         for path in paths:
             p = path
             if filename:
                 p = os.path.join(p, filename)
             if os.path.exists(self.getFullPath(p)):
                 return path
@@ -150,26 +151,26 @@ class MochiRemote(Mochitest):
             options.profilePath = remoteProfilePath
 
         return fixup
 
     def startServers(self, options, debuggerInfo):
         """ Create the servers on the host and start them up """
         restoreRemotePaths = self.switchToLocalPaths(options)
         # ignoreSSLTunnelExts is a workaround for bug 1109310
-        Mochitest.startServers(
+        MochitestDesktop.startServers(
             self,
             options,
             debuggerInfo,
             ignoreSSLTunnelExts=True)
         restoreRemotePaths()
 
     def buildProfile(self, options):
         restoreRemotePaths = self.switchToLocalPaths(options)
-        manifest = Mochitest.buildProfile(self, options)
+        manifest = MochitestDesktop.buildProfile(self, options)
         self.localProfile = options.profilePath
         self._dm.removeDir(self.remoteProfile)
 
         try:
             self._dm.pushDir(options.profilePath, self.remoteProfile)
         except devicemanager.DMError:
             self.log.error(
                 "Automation Error: Unable to copy profile to device.")
@@ -180,17 +181,17 @@ class MochiRemote(Mochitest):
         return manifest
 
     def buildURLOptions(self, options, env):
         self.localLog = options.logFile
         options.logFile = self.remoteLog
         options.fileLevel = 'INFO'
         options.profilePath = self.localProfile
         env["MOZ_HIDE_RESULTS_TABLE"] = "1"
-        retVal = Mochitest.buildURLOptions(self, options, env)
+        retVal = MochitestDesktop.buildURLOptions(self, options, env)
 
         # we really need testConfig.js (for browser chrome)
         try:
             self._dm.pushDir(options.profilePath, self.remoteProfile)
         except devicemanager.DMError:
             self.log.error(
                 "Automation Error: Unable to copy profile to device.")
             raise
@@ -234,26 +235,26 @@ class MochiRemote(Mochitest):
         except devicemanager.DMError:
             self.log.warning("Error getting device information")
 
     def getGMPPluginPath(self, options):
         # TODO: bug 1149374
         return None
 
     def buildBrowserEnv(self, options, debugger=False):
-        browserEnv = Mochitest.buildBrowserEnv(
+        browserEnv = MochitestDesktop.buildBrowserEnv(
             self,
             options,
             debugger=debugger)
         # remove desktop environment not used on device
         if "MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA" in browserEnv:
             del browserEnv["MOZ_WIN_INHERIT_STD_HANDLES_PRE_VISTA"]
         if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
             del browserEnv["XPCOM_MEM_BLOAT_LOG"]
-        # override nsprLogs to avoid processing in Mochitest base class
+        # override nsprLogs to avoid processing in MochitestDesktop base class
         self.nsprLogs = None
         browserEnv["NSPR_LOG_FILE"] = os.path.join(
             self.remoteNSPR,
             self.nsprLogName)
         return browserEnv
 
     def runApp(self, *args, **kwargs):
         """front-end automation.py's `runApp` functionality until FennecRunner is written"""