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 295050 6ab2285938201fecc0c06d083e71dc0ec17229e0
parent 295049 d63e7bf319f4c544272479cddd9c7092361040b0
child 295051 5d7f3436f9eceb1eb3b44c113ce31fa43159acc5
push id8824
push userraliiev@mozilla.com
push dateMon, 14 Dec 2015 20:18:56 +0000
treeherdermozilla-aurora@e2031358e2a6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjgriffin
bugs1225903
milestone45.0a1
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"""