Bug 1155338 - Move mach command arguments into mochitest harness, r=chmanchester,gbrown
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Thu, 30 Apr 2015 13:47:01 -0400
changeset 273842 2a4e56526e674d5ea81faf0c94e917fed954174d
parent 273841 1952e37ea41541c962b7ed49acba6add708ab37b
child 273843 8a39b2b16bee5234e75c55ad37330320f9c18bf3
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerschmanchester, gbrown
bugs1155338
milestone40.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 1155338 - Move mach command arguments into mochitest harness, r=chmanchester,gbrown
testing/config/mozharness/android_arm_4_3_config.py
testing/config/mozharness/android_arm_config.py
testing/config/mozharness/android_panda_config.py
testing/config/mozharness/android_x86_config.py
testing/config/mozharness/b2g_desktop_config.py
testing/config/mozharness/b2g_emulator_config.py
testing/config/mozharness/linux_config.py
testing/config/mozharness/mac_config.py
testing/config/mozharness/taskcluster_linux_config.py
testing/config/mozharness/windows_config.py
testing/mochitest/mach_commands.py
testing/mochitest/mochitest_options.py
testing/mochitest/runtests.py
testing/mochitest/runtestsb2g.py
testing/mochitest/runtestsremote.py
testing/testsuite-targets.mk
--- a/testing/config/mozharness/android_arm_4_3_config.py
+++ b/testing/config/mozharness/android_arm_4_3_config.py
@@ -2,51 +2,48 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 config = {
     "suite_definitions": {
         "mochitest": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
-            "options": ["--autorun", "--close-when-done", "--dm_trans=adb",
-                "--console-level=INFO", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
+            "options": ["--dm_trans=adb", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s", "--utility-path=%(utility_path)s",
                 "--http-port=%(http_port)s", "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s", "--symbols-path=%(symbols_path)s",
                 "--quiet", "--log-raw=%(raw_log_file)s",
                 "--total-chunks=16",
             ],
         },
         "mochitest-gl": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
-            "options": ["--autorun", "--close-when-done", "--dm_trans=adb",
-                "--console-level=INFO", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
+            "options": ["--dm_trans=adb", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s", "--utility-path=%(utility_path)s",
                 "--http-port=%(http_port)s", "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s", "--symbols-path=%(symbols_path)s",
                 "--quiet", "--log-raw=%(raw_log_file)s",
                 "--total-chunks=4",
                 "--subsuite=webgl",
             ],
         },
         "robocop": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
-            "options": ["--autorun", "--close-when-done", "--dm_trans=adb",
-                "--console-level=INFO", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
+            "options": ["--dm_trans=adb", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s", "--utility-path=%(utility_path)s",
                 "--http-port=%(http_port)s", "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s", "--symbols-path=%(symbols_path)s",
                 "--quiet", "--log-raw=%(raw_log_file)s",
                 "--total-chunks=4",
-                "--robocop-path=../..",
+                "--robocop-apk=../../robocop.apk",
                 "--robocop-ids=fennec_ids.txt",
-                "--robocop=robocop.ini",
+                "--robocop-ini=robocop.ini",
             ],
         },
         "reftest": {
             "run_filename": "remotereftest.py",
             "testsdir": "reftest",
             "options": [ "--app=%(app)s", "--ignore-window-size",
                 "--dm_trans=adb",
                 "--bootstrap",
--- a/testing/config/mozharness/android_arm_config.py
+++ b/testing/config/mozharness/android_arm_config.py
@@ -2,54 +2,51 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 config = {
     "suite_definitions": {
         "mochitest": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
-            "options": ["--autorun", "--close-when-done", "--dm_trans=sut",
-                "--console-level=INFO", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
+            "options": ["--dm_trans=sut", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s", "--utility-path=%(utility_path)s",
                 "--deviceIP=%(device_ip)s", "--devicePort=%(device_port)s",
                 "--http-port=%(http_port)s", "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s", "--symbols-path=%(symbols_path)s",
                 "--quiet", "--log-raw=%(raw_log_file)s",
                 "--total-chunks=16",
             ],
         },
         "mochitest-gl": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
-            "options": ["--autorun", "--close-when-done", "--dm_trans=sut",
-                "--console-level=INFO", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
+            "options": ["--dm_trans=sut", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s", "--utility-path=%(utility_path)s",
                 "--deviceIP=%(device_ip)s", "--devicePort=%(device_port)s",
                 "--http-port=%(http_port)s", "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s", "--symbols-path=%(symbols_path)s",
                 "--quiet", "--log-raw=%(raw_log_file)s",
                 "--total-chunks=4",
                 "--subsuite=webgl",
             ],
         },
         "robocop": {
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest",
-            "options": ["--autorun", "--close-when-done", "--dm_trans=sut",
-                "--console-level=INFO", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
+            "options": ["--dm_trans=sut", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s", "--utility-path=%(utility_path)s",
                 "--deviceIP=%(device_ip)s", "--devicePort=%(device_port)s",
                 "--http-port=%(http_port)s", "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s", "--symbols-path=%(symbols_path)s",
                 "--quiet", "--log-raw=%(raw_log_file)s",
                 "--total-chunks=4",
-                "--robocop-path=../..",
+                "--robocop-apk=../../robocop.apk",
                 "--robocop-ids=fennec_ids.txt",
-                "--robocop=robocop.ini",
+                "--robocop-ini=robocop.ini",
             ],
         },
         "reftest": {
             "run_filename": "remotereftest.py",
             "testsdir": "reftest",
             "options": [ "--app=%(app)s", "--ignore-window-size",
                 "--bootstrap",
                 "--remote-webserver=%(remote_webserver)s", "--xre-path=%(xre_path)s",
--- a/testing/config/mozharness/android_panda_config.py
+++ b/testing/config/mozharness/android_panda_config.py
@@ -3,17 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 config = {
     "suite_definitions": {
         "cppunittest": {
             "options": [
                 "--symbols-path=%(symbols_path)s",
                 "--xre-path=tests/bin",
-                "--dm_trans=SUT",
+                "--dm_trans=sut",
                 "--deviceIP=%(device_ip)s",
                 "--localBinDir=../tests/bin",
                 "--apk=%(apk_path)s",
                 "--skip-manifest=../tests/cppunittests/android_cppunittest_manifest.txt"
             ],
             "run_filename": "remotecppunittests.py",
             "testsdir": "cppunittests"
         },
@@ -64,22 +64,22 @@ config = {
                 "--ssl-port=%(ssl_port)s",
                 "--symbols-path=%(symbols_path)s"
             ],
             "run_filename": "remotereftest.py",
             "testsdir": "reftest"
         },
         "mochitest": {
             "options": [
+                "--dm_trans=sut",
                 "--deviceIP=%(device_ip)s",
                 "--xre-path=../hostutils/xre",
                 "--utility-path=../hostutils/bin",
                 "--certificate-path=certs",
                 "--app=%(app_name)s",
-                "--console-level=INFO",
                 "--http-port=%(http_port)s",
                 "--ssl-port=%(ssl_port)s",
                 "--symbols-path=%(symbols_path)s",
                 "--quiet",
                 "--log-raw=%(raw_log_file)s"
             ],
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest"
@@ -97,26 +97,27 @@ config = {
                 "--symbols-path=%(symbols_path)s",
                 "reftest/tests/layout/reftests/reftest.list"
             ],
             "run_filename": "remotereftest.py",
             "testsdir": "reftest"
         },
         "robocop": {
             "options": [
+                "--dm_trans=sut",
                 "--deviceIP=%(device_ip)s",
                 "--xre-path=../hostutils/xre",
                 "--utility-path=../hostutils/bin",
                 "--certificate-path=certs",
                 "--app=%(app_name)s",
                 "--console-level=INFO",
                 "--http-port=%(http_port)s",
                 "--ssl-port=%(ssl_port)s",
                 "--symbols-path=%(symbols_path)s",
-                "--robocop=mochitest/robocop.ini"
+                "--robocop-ini=mochitest/robocop.ini"
             ],
             "run_filename": "runtestsremote.py",
             "testsdir": "mochitest"
         },
         "xpcshell": {
             "options": [
                 "--deviceIP=%(device_ip)s",
                 "--xre-path=../hostutils/xre",
@@ -128,9 +129,9 @@ config = {
                 "--no-logfiles",
                 "--symbols-path=%(symbols_path)s",
                 "--log-raw=%(raw_log_file)s"
             ],
             "run_filename": "remotexpcshelltests.py",
             "testsdir": "xpcshell"
         }
     }
-}
\ No newline at end of file
+}
--- a/testing/config/mozharness/android_x86_config.py
+++ b/testing/config/mozharness/android_x86_config.py
@@ -1,18 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 config = {
     "suite_definitions": {
         "mochitest": {
             "run_filename": "runtestsremote.py",
-            "options": ["--autorun", "--close-when-done", "--dm_trans=sut",
-                "--console-level=INFO", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
+            "options": ["--dm_trans=sut", "--app=%(app)s", "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s", "--utility-path=%(utility_path)s",
                 "--deviceIP=%(device_ip)s", "--devicePort=%(device_port)s",
                 "--http-port=%(http_port)s", "--ssl-port=%(ssl_port)s",
                 "--certificate-path=%(certs_path)s", "--symbols-path=%(symbols_path)s",
                 "--quiet", "--log-raw=%(raw_log_file)s",
             ],
         },
         "reftest": {
--- a/testing/config/mozharness/b2g_desktop_config.py
+++ b/testing/config/mozharness/b2g_desktop_config.py
@@ -1,17 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 config = {
     "suite_definitions": {
         "mochitest": {
             "options": [
-                "--console-level=INFO",
                 "--total-chunks=%(total_chunks)s",
                 "--this-chunk=%(this_chunk)s",
                 "--profile=%(gaia_profile)s",
                 "--app=%(application)s",
                 "--desktop",
                 "--utility-path=%(utility_path)s",
                 "--certificate-path=%(cert_path)s",
                 "--symbols-path=%(symbols_path)s",
--- a/testing/config/mozharness/b2g_emulator_config.py
+++ b/testing/config/mozharness/b2g_emulator_config.py
@@ -56,17 +56,16 @@ config = {
             ],
             "run_filename": "remotereftest.py",
             "testsdir": "reftest"
         },
         "mochitest": {
             "options": [
                 "--adbpath=%(adbpath)s",
                 "--b2gpath=%(b2gpath)s",
-                "--console-level=INFO",
                 "--emulator=%(emulator)s",
                 "--logdir=%(logcat_dir)s",
                 "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s",
                 "--symbols-path=%(symbols_path)s",
                 "--busybox=%(busybox)s",
                 "--total-chunks=%(total_chunks)s",
                 "--this-chunk=%(this_chunk)s",
@@ -77,17 +76,16 @@ config = {
             ],
             "run_filename": "runtestsb2g.py",
             "testsdir": "mochitest"
         },
         "mochitest-chrome": {
             "options": [
                 "--adbpath=%(adbpath)s",
                 "--b2gpath=%(b2gpath)s",
-                "--console-level=INFO",
                 "--emulator=%(emulator)s",
                 "--logdir=%(logcat_dir)s",
                 "--remote-webserver=%(remote_webserver)s",
                 "--xre-path=%(xre_path)s",
                 "--symbols-path=%(symbols_path)s",
                 "--busybox=%(busybox)s",
                 "--total-chunks=%(total_chunks)s",
                 "--this-chunk=%(this_chunk)s",
--- a/testing/config/mozharness/linux_config.py
+++ b/testing/config/mozharness/linux_config.py
@@ -25,19 +25,16 @@ config = {
         },
         "mochitest": {
             "options": [
                 "--appname=%(binary_path)s",
                 "--utility-path=tests/bin",
                 "--extra-profile-file=tests/bin/plugins",
                 "--symbols-path=%(symbols_path)s",
                 "--certificate-path=tests/certs",
-                "--autorun",
-                "--close-when-done",
-                "--console-level=INFO",
                 "--setpref=webgl.force-enabled=true",
                 "--quiet",
                 "--log-raw=%(raw_log_file)s",
                 "--use-test-media-devices"
             ],
             "run_filename": "runtests.py",
             "testsdir": "mochitest"
         },
--- a/testing/config/mozharness/mac_config.py
+++ b/testing/config/mozharness/mac_config.py
@@ -25,19 +25,16 @@ config = {
         },
         "mochitest": {
             "options": [
                 "--appname=%(binary_path)s",
                 "--utility-path=tests/bin",
                 "--extra-profile-file=tests/bin/plugins",
                 "--symbols-path=%(symbols_path)s",
                 "--certificate-path=tests/certs",
-                "--autorun",
-                "--close-when-done",
-                "--console-level=INFO",
                 "--quiet",
                 "--log-raw=%(raw_log_file)s"
             ],
             "run_filename": "runtests.py",
             "testsdir": "mochitest"
         },
         "mozbase": {
             "options": [
--- a/testing/config/mozharness/taskcluster_linux_config.py
+++ b/testing/config/mozharness/taskcluster_linux_config.py
@@ -5,18 +5,17 @@
 config = {
     "reftest_options": [
         "--appname=%(binary_path)s", "--utility-path=tests/bin",
         "--extra-profile-file=tests/bin/plugins", "--symbols-path=%(symbols_path)s"
     ],
     "mochitest_options": [
         "--appname=%(binary_path)s", "--utility-path=tests/bin",
         "--extra-profile-file=tests/bin/plugins", "--symbols-path=%(symbols_path)s",
-        "--certificate-path=tests/certs", "--autorun", "--close-when-done",
-        "--console-level=INFO", "--setpref=webgl.force-enabled=true",
+        "--certificate-path=tests/certs", "--setpref=webgl.force-enabled=true",
         "--quiet", "--log-raw=%(raw_log_file)s"
     ],
     "webapprt_options": [
         "--app=%(app_path)s", "--utility-path=tests/bin",
         "--extra-profile-file=tests/bin/plugins", "--symbols-path=%(symbols_path)s",
         "--certificate-path=tests/certs", "--autorun", "--close-when-done",
         "--console-level=INFO", "--testing-modules-dir=tests/modules",
         "--quiet"
--- a/testing/config/mozharness/windows_config.py
+++ b/testing/config/mozharness/windows_config.py
@@ -25,19 +25,16 @@ config = {
         },
         "mochitest": {
             "options": [
                 "--appname=%(binary_path)s",
                 "--utility-path=tests/bin",
                 "--extra-profile-file=tests/bin/plugins",
                 "--symbols-path=%(symbols_path)s",
                 "--certificate-path=tests/certs",
-                "--autorun",
-                "--close-when-done",
-                "--console-level=INFO",
                 "--quiet",
                 "--log-raw=%(raw_log_file)s"
             ],
             "run_filename": "runtests.py",
             "testsdir": "mochitest"
         },
         "mozbase": {
             "options": [
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -1,15 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import unicode_literals
 
-import argparse
+from argparse import Namespace
 import logging
 import mozpack.path as mozpath
 import os
 import sys
 import warnings
 import which
 
 from mozbuild.base import (
@@ -19,18 +19,18 @@ from mozbuild.base import (
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
+here = os.path.abspath(os.path.dirname(__file__))
 
-from mozlog import structured
 
 ADB_NOT_FOUND = '''
 The %s command requires the adb binary to be on your path.
 
 If you have a B2G build, this can be found in
 '%s/out/host/<platform>/bin'.
 '''.lstrip()
 
@@ -112,28 +112,17 @@ class MochitestRunner(MozbuildObject):
 
         self.tests_dir = os.path.join(self.topobjdir, '_tests')
         self.mochitest_dir = os.path.join(
             self.tests_dir,
             'testing',
             'mochitest')
         self.bin_dir = os.path.join(self.topobjdir, 'dist', 'bin')
 
-    def run_b2g_test(
-            self,
-            test_paths=None,
-            b2g_home=None,
-            xre_path=None,
-            total_chunks=None,
-            this_chunk=None,
-            no_window=None,
-            repeat=0,
-            run_until_failure=False,
-            chrome=False,
-            **kwargs):
+    def run_b2g_test(self, test_paths=None, **kwargs):
         """Runs a b2g mochitest.
 
         test_paths is an enumerable of paths to tests. It can be a relative path
         from the top source directory, an absolute filename, or a directory
         containing test files.
         """
         # Need to call relpath before os.chdir() below.
         test_path = ''
@@ -152,158 +141,64 @@ class MochitestRunner(MozbuildObject):
 
             import imp
             path = os.path.join(self.mochitest_dir, 'runtestsb2g.py')
             with open(path, 'r') as fh:
                 imp.load_module('mochitest', fh, path,
                                 ('.py', 'r', imp.PY_SOURCE))
 
             import mochitest
-            from mochitest_options import B2GOptions
 
-        parser = B2GOptions()
-        options = parser.parse_args([])
+        options = Namespace(**kwargs)
 
         if test_path:
-            if chrome:
+            if options.chrome:
                 test_root_file = mozpath.join(
                     self.mochitest_dir,
                     'chrome',
                     test_path)
             else:
                 test_root_file = mozpath.join(
                     self.mochitest_dir,
                     'tests',
                     test_path)
             if not os.path.exists(test_root_file):
                 print(
                     'Specified test path does not exist: %s' %
                     test_root_file)
                 return 1
             options.testPath = test_path
 
-        for k, v in kwargs.iteritems():
-            setattr(options, k, v)
-        options.noWindow = no_window
-        options.totalChunks = total_chunks
-        options.thisChunk = this_chunk
-        options.repeat = repeat
-        options.runUntilFailure = run_until_failure
-
-        options.symbolsPath = os.path.join(
-            self.distdir,
-            'crashreporter-symbols')
-
-        options.consoleLevel = 'INFO'
-        if conditions.is_b2g_desktop(self):
-            options.desktop = True
-            options.app = self.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')]
-
-            return mochitest.run_desktop_mochitests(parser, options)
+        if options.desktop:
+            return mochitest.run_desktop_mochitests(options)
 
         try:
             which.which('adb')
         except which.WhichError:
             # TODO Find adb automatically if it isn't on the path
-            print(ADB_NOT_FOUND % ('mochitest-remote', b2g_home))
+            print(ADB_NOT_FOUND % ('mochitest-remote', options.b2gPath))
             return 1
 
-        options.b2gPath = b2g_home
-        options.logdir = self.mochitest_dir
-        options.httpdPath = self.mochitest_dir
-        options.xrePath = xre_path
-        options.chrome = chrome
-        return mochitest.run_remote_mochitests(parser, options)
+        return mochitest.run_remote_mochitests(options)
 
-    def run_desktop_test(
-            self,
-            context,
-            suite=None,
-            test_paths=None,
-            debugger=None,
-            debugger_args=None,
-            slowscript=False,
-            screenshot_on_fail=False,
-            shuffle=False,
-            closure_behaviour='auto',
-            rerun_failures=False,
-            no_autorun=False,
-            repeat=0,
-            run_until_failure=False,
-            slow=False,
-            chunk_by_dir=0,
-            chunk_by_runtime=False,
-            total_chunks=None,
-            this_chunk=None,
-            extraPrefs=[],
-            jsdebugger=False,
-            debug_on_failure=False,
-            start_at=None,
-            end_at=None,
-            e10s=False,
-            enable_cpow_warnings=False,
-            strict_content_sandbox=False,
-            nested_oop=False,
-            dmd=False,
-            dump_output_directory=None,
-            dump_about_memory_after_test=False,
-            dump_dmd_after_test=False,
-            install_extension=None,
-            quiet=False,
-            environment=[],
-            app_override=None,
-            bisectChunk=None,
-            runByDir=False,
-            useTestMediaDevices=False,
-            timeout=None,
-            max_timeouts=None,
-            **kwargs):
+    def run_desktop_test(self, context, suite=None, test_paths=None, **kwargs):
         """Runs a mochitest.
 
+        suite is the type of mochitest to run. It can be one of ('plain',
+        'chrome', 'browser', 'metro', 'a11y', 'jetpack-package', 'jetpack-addon').
+
         test_paths are path to tests. They can be a relative path from the
         top source directory, an absolute filename, or a directory containing
         test files.
-
-        suite is the type of mochitest to run. It can be one of ('plain',
-        'chrome', 'browser', 'metro', 'a11y', 'jetpack-package', 'jetpack-addon').
-
-        debugger is a program name or path to a binary (presumably a debugger)
-        to run the test in. e.g. 'gdb'
-
-        debugger_args are the arguments passed to the debugger.
-
-        slowscript is true if the user has requested the SIGSEGV mechanism of
-        invoking the slow script dialog.
-
-        shuffle is whether test order should be shuffled (defaults to false).
-
-        closure_behaviour denotes whether to keep the browser open after tests
-        complete.
         """
-        if rerun_failures and test_paths:
-            print('Cannot specify both --rerun-failures and a test path.')
-            return 1
-
         # Make absolute paths relative before calling os.chdir() below.
         if test_paths:
             test_paths = [self._wrap_path_argument(
                 p).relpath() if os.path.isabs(p) else p for p in test_paths]
 
-        failure_file_path = os.path.join(
-            self.statedir,
-            'mochitest_failures.json')
-
-        if rerun_failures and not os.path.exists(failure_file_path):
-            print('No failure file present. Did you run mochitests before?')
-            return 1
-
         # runtests.py is ambiguous, so we load the file/module manually.
         if 'mochitest' not in sys.modules:
             import imp
             path = os.path.join(self.mochitest_dir, 'runtests.py')
             with open(path, 'r') as fh:
                 imp.load_module('mochitest', fh, path,
                                 ('.py', 'r', imp.PY_SOURCE))
 
@@ -316,108 +211,52 @@ class MochitestRunner(MozbuildObject):
 
         # Automation installs its own stream handler to stdout. Since we want
         # all logging to go through us, we just remove their handler.
         remove_handlers = [l for l in logging.getLogger().handlers
                            if isinstance(l, logging.StreamHandler)]
         for handler in remove_handlers:
             logging.getLogger().removeHandler(handler)
 
-        opts = mochitest.MochitestOptions()
-        options = opts.parse_args([])
+        options = Namespace(**kwargs)
 
-        options.subsuite = ''
         flavor = suite
 
-        # Need to set the suite options before verifyOptions below.
         if suite == 'plain':
             # Don't need additional options for plain.
             flavor = 'mochitest'
         elif suite == 'chrome':
             options.chrome = True
         elif suite == 'browser':
             options.browserChrome = True
             flavor = 'browser-chrome'
         elif suite == 'devtools':
             options.browserChrome = True
+            options.subsuite = 'devtools'
         elif suite == 'jetpack-package':
             options.jetpackPackage = True
         elif suite == 'jetpack-addon':
             options.jetpackAddon = True
         elif suite == 'metro':
             options.immersiveMode = True
             options.browserChrome = True
         elif suite == 'a11y':
             options.a11y = True
         elif suite == 'webapprt-content':
             options.webapprtContent = True
-            options.app = self.get_webapp_runtime_path()
+            if not options.app or options.app == self.get_binary_path():
+                options.app = self.get_webapp_runtime_path()
         elif suite == 'webapprt-chrome':
             options.webapprtChrome = True
-            options.app = self.get_webapp_runtime_path()
             options.browserArgs.append("-test-mode")
+            if not options.app or options.app == self.get_binary_path():
+                options.app = self.get_webapp_runtime_path()
         else:
             raise Exception('None or unrecognized mochitest suite type.')
 
-        if dmd:
-            options.dmdPath = self.bin_dir
-
-        options.autorun = not no_autorun
-        options.closeWhenDone = closure_behaviour != 'open'
-        options.slowscript = slowscript
-        options.screenshotOnFail = screenshot_on_fail
-        options.shuffle = shuffle
-        options.consoleLevel = 'INFO'
-        options.repeat = repeat
-        options.runUntilFailure = run_until_failure
-        options.runSlower = slow
-        options.testingModulesDir = os.path.join(self.tests_dir, 'modules')
-        options.extraProfileFiles.append(os.path.join(self.distdir, 'plugins'))
-        options.symbolsPath = os.path.join(
-            self.distdir,
-            'crashreporter-symbols')
-        options.chunkByDir = chunk_by_dir
-        options.chunkByRuntime = chunk_by_runtime
-        options.totalChunks = total_chunks
-        options.thisChunk = this_chunk
-        options.jsdebugger = jsdebugger
-        options.debugOnFailure = debug_on_failure
-        options.startAt = start_at
-        options.endAt = end_at
-        options.e10s = e10s
-        options.enableCPOWWarnings = enable_cpow_warnings
-        options.strictContentSandbox = strict_content_sandbox
-        options.nested_oop = nested_oop
-        options.dumpAboutMemoryAfterTest = dump_about_memory_after_test
-        options.dumpDMDAfterTest = dump_dmd_after_test
-        options.dumpOutputDirectory = dump_output_directory
-        options.quiet = quiet
-        options.environment = environment
-        options.extraPrefs = extraPrefs
-        options.bisectChunk = bisectChunk
-        options.runByDir = runByDir
-        options.useTestMediaDevices = useTestMediaDevices
-        if timeout:
-            options.timeout = int(timeout)
-        if max_timeouts:
-            options.maxTimeouts = int(max_timeouts)
-
-        options.failureFile = failure_file_path
-        if install_extension is not None:
-            options.extensionsToInstall = [
-                os.path.join(
-                    self.topsrcdir,
-                    install_extension)]
-
-        for k, v in kwargs.iteritems():
-            setattr(options, k, v)
-
-        if suite == 'devtools':
-            options.subsuite = 'devtools'
-
         if test_paths:
             resolver = self._spawn(TestResolver)
 
             tests = list(
                 resolver.resolve_tests(
                     paths=test_paths,
                     flavor=flavor))
 
@@ -425,519 +264,230 @@ class MochitestRunner(MozbuildObject):
                 print('No tests could be found in the path specified. Please '
                       'specify a path that is a test file or is a directory '
                       'containing tests.')
                 return 1
 
             manifest = TestManifest()
             manifest.tests.extend(tests)
 
-            if len(
-                    tests) == 1 and closure_behaviour == 'auto' and suite == 'plain':
+            # XXX why is this such a special case?
+            if len(tests) == 1 and options.closeWhenDone and suite == 'plain':
                 options.closeWhenDone = False
 
             options.manifestFile = manifest
 
-        if rerun_failures:
-            options.testManifest = failure_file_path
-
-        if debugger:
-            options.debugger = debugger
-
-        if debugger_args:
-            if options.debugger is None:
-                print("--debugger-args passed, but no debugger specified.")
-                return 1
-            options.debuggerArgs = debugger_args
-
-        if app_override:
-            if app_override == "dist":
-                options.app = self.get_binary_path(where='staged-package')
-            elif app_override:
-                options.app = app_override
-            if options.gmp_path is None:
-                # Need to fix the location of gmp_fake which might not be
-                # shipped in the binary
-                bin_path = self.get_binary_path()
-                gmp_modules = (
-                    ('gmp-fake', '1.0'),
-                    ('gmp-clearkey', '0.1'),
-                    ('gmp-fakeopenh264', '1.0')
-                )
-                options.gmp_path = os.pathsep.join(
-                    os.path.join(os.path.dirname(bin_path), *p)
-                    for p in gmp_modules)
-
-        logger_options = {
-            key: value for key,
-            value in vars(options).iteritems() if key.startswith('log')}
-        runner = mochitest.Mochitest(logger_options)
-        options = opts.verifyOptions(options, runner)
-
-        if options is None:
-            raise Exception('mochitest option validator failed.')
-
         # We need this to enable colorization of output.
         self.log_manager.enable_unstructured()
-
-        result = runner.runTests(options)
-
+        result = mochitest.run_test_harness(options)
         self.log_manager.disable_unstructured()
-        if runner.message_logger.errors:
-            result = 1
-            runner.message_logger.logger.warning("The following tests failed:")
-            for error in runner.message_logger.errors:
-                runner.message_logger.logger.log_raw(error)
-
-        runner.message_logger.finish()
-
         return result
 
-    def run_android_test(self, args):
+    def run_android_test(self, test_path, **kwargs):
         self.tests_dir = os.path.join(self.topobjdir, '_tests')
         self.mochitest_dir = os.path.join(self.tests_dir, 'testing', 'mochitest')
         import imp
         path = os.path.join(self.mochitest_dir, 'runtestsremote.py')
         with open(path, 'r') as fh:
             imp.load_module('runtestsremote', fh, path,
-                ('.py', 'r', imp.PY_SOURCE))
+                            ('.py', 'r', imp.PY_SOURCE))
         import runtestsremote
 
-        sys.exit(runtestsremote.main(args))
-
-def add_mochitest_general_args(parser):
-    parser.add_argument(
-        '--debugger',
-        '-d',
-        metavar='DEBUGGER',
-        help='Debugger binary to run test in. Program name or path.')
-
-    parser.add_argument(
-        '--debugger-args',
-        metavar='DEBUGGER_ARGS',
-        help='Arguments to pass to the debugger.')
-
-    # Bug 933807 introduced JS_DISABLE_SLOW_SCRIPT_SIGNALS to avoid clever
-    # segfaults induced by the slow-script-detecting logic for Ion/Odin JITted
-    # code. If we don't pass this, the user will need to periodically type
-    # "continue" to (safely) resume execution. There are ways to implement
-    # automatic resuming; see the bug.
-    parser.add_argument(
-        '--slowscript',
-        action='store_true',
-        help='Do not set the JS_DISABLE_SLOW_SCRIPT_SIGNALS env variable; when not set, recoverable but misleading SIGSEGV instances may occur in Ion/Odin JIT code')
-
-    parser.add_argument(
-        '--screenshot-on-fail',
-        action='store_true',
-        help='Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory for storing the screenshots.')
-
-    parser.add_argument(
-        '--shuffle', action='store_true',
-        help='Shuffle execution order.')
-
-    parser.add_argument(
-        '--keep-open',
-        action='store_const',
-        dest='closure_behaviour',
-        const='open',
-        default='auto',
-        help='Always keep the browser open after tests complete.')
-
-    parser.add_argument(
-        '--auto-close',
-        action='store_const',
-        dest='closure_behaviour',
-        const='close',
-        default='auto',
-        help='Always close the browser after tests complete.')
-
-    parser.add_argument(
-        '--rerun-failures',
-        action='store_true',
-        help='Run only the tests that failed during the last test run.')
-
-    parser.add_argument(
-        '--no-autorun',
-        action='store_true',
-        help='Do not starting running tests automatically.')
-
-    parser.add_argument(
-        '--repeat', type=int, default=0,
-        help='Repeat the test the given number of times.')
-
-    parser.add_argument(
-        "--run-until-failure",
-        action='store_true',
-        help='Run tests repeatedly and stops on the first time a test fails. '
-        'Default cap is 30 runs, which can be overwritten '
-        'with the --repeat parameter.')
-
-    parser.add_argument(
-        '--slow', action='store_true',
-        help='Delay execution between tests.')
-
-    parser.add_argument(
-        '--end-at',
-        type=str,
-        help='Stop running the test sequence at this test.')
-
-    parser.add_argument(
-        '--start-at',
-        type=str,
-        help='Start running the test sequence at this test.')
-
-    parser.add_argument(
-        '--chunk-by-dir',
-        type=int,
-        help='Group tests together in chunks by this many top directories.')
-
-    parser.add_argument(
-        '--chunk-by-runtime',
-        action='store_true',
-        help="Group tests such that each chunk has roughly the same runtime.")
-
-    parser.add_argument(
-        '--total-chunks',
-        type=int,
-        help='Total number of chunks to split tests into.')
-
-    parser.add_argument(
-        '--this-chunk',
-        type=int,
-        help='If running tests by chunks, the number of the chunk to run.')
-
-    parser.add_argument(
-        '--debug-on-failure',
-        action='store_true',
-        help='Breaks execution and enters the JS debugger on a test failure. '
-        'Should be used together with --jsdebugger.')
-
-    parser.add_argument(
-        '--setpref', default=[], action='append',
-        metavar='PREF=VALUE', dest='extraPrefs',
-        help='defines an extra user preference')
+        options = Namespace(**kwargs)
+        if test_path:
+            options.testPath = test_path
 
-    parser.add_argument(
-        '--jsdebugger',
-        action='store_true',
-        help='Start the browser JS debugger before running the test. Implies --no-autorun.')
-
-    parser.add_argument(
-        '--e10s',
-        action='store_true',
-        help='Run tests with electrolysis preferences and test filtering enabled.')
-
-    parser.add_argument(
-        '--enable-cpow-warnings',
-        action='store_true',
-        help='Run tests with unsafe CPOW usage warnings enabled.')
-
-    parser.add_argument(
-        '--strict-content-sandbox',
-        action='store_true',
-        help='Run tests with a more strict content sandbox (Windows only).')
-
-    parser.add_argument(
-        '--nested_oop',
-        action='store_true',
-        help='Run tests with nested oop preferences and test filtering enabled.')
-
-    parser.add_argument(
-        '--dmd', action='store_true',
-        help='Run tests with DMD active.')
-
-    parser.add_argument(
-        '--dump-about-memory-after-test',
-        action='store_true',
-        help='Dump an about:memory log after every test.')
-
-    parser.add_argument(
-        '--dump-dmd-after-test', action='store_true',
-        help='Dump a DMD log after every test.')
-
-    parser.add_argument(
-        '--dump-output-directory',
-        action='store',
-        help='Specifies the directory in which to place dumped memory reports.')
-
-    parser.add_argument(
-        'test_paths',
-        default=None,
-        nargs='*',
-        metavar='TEST',
-        help='Test to run. Can be specified as a single file, a '
-        'directory, or omitted. If omitted, the entire test suite is '
-        'executed.')
-
-    parser.add_argument(
-        '--install-extension',
-        help='Install given extension before running selected tests. '
-        'Parameter is a path to xpi file.')
-
-    parser.add_argument(
-        '--quiet',
-        default=False,
-        action='store_true',
-        help='Do not print test log lines unless a failure occurs.')
-
-    parser.add_argument(
-        '--setenv',
-        default=[],
-        action='append',
-        metavar='NAME=VALUE',
-        dest='environment',
-        help="Sets the given variable in the application's environment")
-
-    parser.add_argument(
-        '--run-by-dir',
-        default=False,
-        action='store_true',
-        dest='runByDir',
-        help='Run each directory in a single browser instance with a fresh profile.')
-
-    parser.add_argument(
-        '--bisect-chunk',
-        type=str,
-        dest='bisectChunk',
-        help='Specify the failing test name to find the previous tests that may be causing the failure.')
-
-    parser.add_argument(
-        '--use-test-media-devices',
-        default=False,
-        action='store_true',
-        dest='useTestMediaDevices',
-        help='Use test media device drivers for media testing.')
-
-    parser.add_argument(
-        '--app-override',
-        default=None,
-        action='store',
-        help="Override the default binary used to run tests with the path you provide, e.g. "
-        " --app-override /usr/bin/firefox . "
-        "If you have run ./mach package beforehand, you can specify 'dist' to "
-        "run tests against the distribution bundle's binary.")
-
-    parser.add_argument(
-        '--timeout',
-        default=None,
-        help='The per-test timeout time in seconds (default: 60 seconds)')
-
-    parser.add_argument(
-        '--max-timeouts', default=None,
-        help='The maximum number of timeouts permitted before halting testing')
-
-    parser.add_argument(
-        "--tag",
-        dest='test_tags', action='append',
-        help="Filter out tests that don't have the given tag. Can be used "
-             "multiple times in which case the test must contain at least one "
-             "of the given tags.")
-
-    parser.add_argument(
-        "--subsuite",
-        default=None,
-        help="Subsuite of tests to run. Unlike tags, subsuites also remove "
-             "tests from the default set. Only one can be specified at once.")
+        sys.exit(runtestsremote.run_test_harness(options))
 
 
-    return parser
-
-def add_mochitest_b2g_args(parser):
-    parser.add_argument(
-        '--busybox',
-        default=None,
-        help='Path to busybox binary to install on device')
-
-    parser.add_argument(
-        '--logdir', default=None,
-        help='directory to store log files')
-
-    parser.add_argument(
-        '--profile', default=None,
-        help='for desktop testing, the path to the \
-              gaia profile to use')
+# parser
 
-    parser.add_argument(
-        '--gecko-path', default=None,
-        help='the path to a gecko distribution that should \
-              be installed on the emulator prior to test')
-
-    parser.add_argument(
-        '--no-window',
-        action='store_true',
-        default=False,
-        help='Pass --no-window to the emulator')
-
-    parser.add_argument(
-        '--sdcard', default="10MB",
-        help='Define size of sdcard: 1MB, 50MB...etc')
-
-    parser.add_argument(
-        '--marionette',
-        default=None,
-        help='host:port to use when connecting to Marionette')
-
-    return parser
+def TestPathArg(func):
+    test_paths = CommandArgument('test_paths', nargs='*', metavar='TEST', default=None,
+        help='Test to run. Can be a single test file or a directory of tests to '
+             '(run recursively). If omitted, the entire suite is run.')
+    return test_paths(func)
 
 
 def setup_argument_parser():
-    parser = argparse.ArgumentParser()
+    build_obj = MozbuildObject.from_environment(cwd=here)
 
-    general_args = parser.add_argument_group('Mochitest Arguments',
-        'Arguments that apply to all versions of mochitest.')
-    general_args = add_mochitest_general_args(general_args)
+    build_path = os.path.join(build_obj.topobjdir, 'build')
+    if build_path not in sys.path:
+        sys.path.append(build_path)
+
+    mochitest_dir = os.path.join(build_obj.topobjdir, '_tests', 'testing', 'mochitest')
 
-    b2g_args = parser.add_argument_group('B2G Arguments', 'Arguments specific \
-        to running mochitest on B2G devices and emulator')
-    b2g_args = add_mochitest_b2g_args(b2g_args)
+    with warnings.catch_warnings():
+        warnings.simplefilter('ignore')
 
-    structured.commandline.add_logging_group(parser)
-    return parser
+        import imp
+        path = os.path.join(build_obj.topobjdir, mochitest_dir, 'runtests.py')
+        with open(path, 'r') as fh:
+            imp.load_module('mochitest', fh, path,
+                            ('.py', 'r', imp.PY_SOURCE))
+
+        from mochitest_options import MochitestArgumentParser
+
+    return MochitestArgumentParser()
 
 
 # condition filters
 
 def is_platform_in(*platforms):
     def is_platform_supported(cls):
         for p in platforms:
             c = getattr(conditions, 'is_{}'.format(p), None)
             if c and c(cls):
                 return True
         return False
 
     is_platform_supported.__doc__ = 'Must have a {} build.'.format(
         ' or '.join(platforms))
     return is_platform_supported
 
+
 def verify_host_bin():
     # validate MOZ_HOST_BIN environment variables for Android tests
     MOZ_HOST_BIN = os.environ.get('MOZ_HOST_BIN')
     if not MOZ_HOST_BIN:
         print('environment variable MOZ_HOST_BIN must be set to a directory containing host xpcshell')
         return 1
     elif not os.path.isdir(MOZ_HOST_BIN):
         print('$MOZ_HOST_BIN does not specify a directory')
         return 1
     elif not os.path.isfile(os.path.join(MOZ_HOST_BIN, 'xpcshell')):
         print('$MOZ_HOST_BIN/xpcshell does not exist')
         return 1
     return 0
 
+
 @CommandProvider
 class MachCommands(MachCommandBase):
 
     def __init__(self, context):
         MachCommandBase.__init__(self, context)
 
-        for attr in ('b2g_home', 'xre_path', 'device_name', 'target_out'):
+        for attr in ('device_name', 'target_out'):
             setattr(self, attr, getattr(context, attr, None))
 
     @Command(
         'mochitest-plain',
         category='testing',
         conditions=[is_platform_in('firefox', 'mulet', 'b2g', 'b2g_desktop', 'android')],
         description='Run a plain mochitest (integration test, plain web page).',
         parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_plain(self, test_paths, **kwargs):
         if is_platform_in('firefox', 'mulet')(self):
             return self.run_mochitest(test_paths, 'plain', **kwargs)
         elif conditions.is_emulator(self):
             return self.run_mochitest_remote(test_paths, **kwargs)
         elif conditions.is_b2g_desktop(self):
             return self.run_mochitest_b2g_desktop(test_paths, **kwargs)
         elif conditions.is_android(self):
             return self.run_mochitest_android(test_paths, **kwargs)
 
     @Command(
         'mochitest-chrome',
         category='testing',
         conditions=[is_platform_in('firefox', 'emulator', 'android')],
         description='Run a chrome mochitest (integration test with some XUL).',
         parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_chrome(self, test_paths, **kwargs):
+        kwargs['chrome'] = True
         if conditions.is_firefox(self):
             return self.run_mochitest(test_paths, 'chrome', **kwargs)
         elif conditions.is_b2g(self) and conditions.is_emulator(self):
-            return self.run_mochitest_remote(test_paths, chrome=True, **kwargs)
+            return self.run_mochitest_remote(test_paths, **kwargs)
         elif conditions.is_android(self):
-            return self.run_mochitest_android(test_paths, chrome=True, **kwargs)
+            return self.run_mochitest_android(test_paths, **kwargs)
 
     @Command(
         'mochitest-browser',
         category='testing',
         conditions=[conditions.is_firefox],
         description='Run a mochitest with browser chrome (integration test with a standard browser).',
         parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_browser(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'browser', **kwargs)
 
     @Command(
         'mochitest-devtools',
         category='testing',
         conditions=[conditions.is_firefox],
         description='Run a devtools mochitest with browser chrome (integration test with a standard browser with the devtools frame).',
         parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_devtools(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'devtools', **kwargs)
 
     @Command('jetpack-package', category='testing',
              conditions=[conditions.is_firefox],
              description='Run a jetpack package test.',
              parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_jetpack_package(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'jetpack-package', **kwargs)
 
     @Command('jetpack-addon', category='testing',
              conditions=[conditions.is_firefox],
              description='Run a jetpack addon test.',
              parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_jetpack_addon(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'jetpack-addon', **kwargs)
 
     @Command(
         'mochitest-metro',
         category='testing',
         conditions=[conditions.is_firefox],
         description='Run a mochitest with metro browser chrome (tests for Windows touch interface).',
         parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_metro(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'metro', **kwargs)
 
     @Command('mochitest-a11y', category='testing',
              conditions=[conditions.is_firefox],
              description='Run an a11y mochitest (accessibility tests).',
              parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_a11y(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'a11y', **kwargs)
 
     @Command(
         'webapprt-test-chrome',
         category='testing',
         conditions=[conditions.is_firefox],
         description='Run a webapprt chrome mochitest (Web App Runtime with the browser chrome).',
         parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_webapprt_chrome(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'webapprt-chrome', **kwargs)
 
     @Command(
         'webapprt-test-content',
         category='testing',
         conditions=[conditions.is_firefox],
         description='Run a webapprt content mochitest (Content rendering of the Web App Runtime).',
         parser=setup_argument_parser)
+    @TestPathArg
     def run_mochitest_webapprt_content(self, test_paths, **kwargs):
         return self.run_mochitest(test_paths, 'webapprt-content', **kwargs)
 
     @Command('mochitest', category='testing',
              conditions=[conditions.is_firefox],
              description='Run any flavor of mochitest (integration test).',
              parser=setup_argument_parser)
     @CommandArgument('-f', '--flavor', choices=FLAVORS.keys(),
                      help='Only run tests of this flavor.')
+    @TestPathArg
     def run_mochitest_general(self, test_paths, flavor=None, test_objects=None,
                               **kwargs):
         self._preruntest()
 
         from mozbuild.testing import TestResolver
 
         if test_objects:
             tests = test_objects
@@ -1014,31 +564,23 @@ class MachCommands(MachCommandBase):
                     'test-container.gaiamobile.org')):
                 print(
                     ENG_BUILD_REQUIRED %
                     ('mochitest-remote', host_webapps_dir))
                 return 1
 
         from mozbuild.controller.building import BuildDriver
 
-        if self.device_name.startswith('emulator'):
-            emulator = 'arm'
-            if 'x86' in self.device_name:
-                emulator = 'x86'
-            kwargs['emulator'] = emulator
-
         self._ensure_state_subdir_exists('.')
 
         driver = self._spawn(BuildDriver)
         driver.install_tests(remove=False)
 
         mochitest = self._spawn(MochitestRunner)
         return mochitest.run_b2g_test(
-            b2g_home=self.b2g_home,
-            xre_path=self.xre_path,
             test_paths=test_paths,
             **kwargs)
 
     def run_mochitest_b2g_desktop(self, test_paths, **kwargs):
         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 % 'mochitest-b2g-desktop')
@@ -1055,82 +597,51 @@ class MachCommands(MachCommandBase):
         self._ensure_state_subdir_exists('.')
 
         driver = self._spawn(BuildDriver)
         driver.install_tests(remove=False)
 
         mochitest = self._spawn(MochitestRunner)
         return mochitest.run_b2g_test(test_paths=test_paths, **kwargs)
 
-    def run_mochitest_android(self, test_paths, chrome=False, **kwargs):
+    def run_mochitest_android(self, test_paths, **kwargs):
         host_ret = verify_host_bin()
         if host_ret != 0:
             return host_ret
 
-        args = [
-            '--xre-path=' + os.environ.get('MOZ_HOST_BIN'),
-            '--dm_trans=adb',
-            '--deviceIP=',
-            '--console-level=INFO',
-            '--app=' + self.substs['ANDROID_PACKAGE_NAME'],
-            '--log-mach=-',
-            '--autorun',
-            '--close-when-done',
-            '--testing-modules-dir=' + os.path.join(self.topobjdir, '_tests', 'modules'),
-        ]
+        test_path = None
         if test_paths:
             if len(test_paths) > 1:
                 print('Warning: Only the first test path will be used.')
             test_path = self._wrap_path_argument(test_paths[0]).relpath()
-            args.append('--test-path=%s' % test_path)
-        if chrome:
-            args.append('--chrome')
 
         mochitest = self._spawn(MochitestRunner)
-        return mochitest.run_android_test(args)
+        return mochitest.run_android_test(test_path, **kwargs)
+
 
 @CommandProvider
 class AndroidCommands(MachCommandBase):
 
     @Command('robocop', category='testing',
              conditions=[conditions.is_android],
-             description='Run a Robocop test.')
+             description='Run a Robocop test.',
+             parser=setup_argument_parser)
     @CommandArgument(
         'test_path',
         default=None,
         nargs='?',
         metavar='TEST',
         help='Test to run. Can be specified as a Robocop test name (like "testLoad"), '
         'or omitted. If omitted, the entire test suite is executed.')
-    def run_robocop(self, test_path):
+    def run_robocop(self, test_path, **kwargs):
         host_ret = verify_host_bin()
         if host_ret != 0:
             return host_ret
 
-        args = [
-            '--xre-path=' + os.environ.get('MOZ_HOST_BIN'),
-            '--dm_trans=adb',
-            '--deviceIP=',
-            '--console-level=INFO',
-            '--app=' +
-            self.substs['ANDROID_PACKAGE_NAME'],
-            '--robocop-apk=' +
-            os.path.join(
-                self.topobjdir,
-                'build',
-                'mobile',
-                'robocop',
-                'robocop-debug.apk'),
-            '--robocop-ini=' +
-            os.path.join(
-                self.topobjdir,
-                '_tests',
-                'testing',
-                'mochitest',
-                'robocop.ini'),
-            '--log-mach=-',
-        ]
+        if not kwargs.get('robocopIni'):
+            kwargs['robocopIni'] = os.path.join(self.topobjdir, '_tests', 'testing',
+                                                'mochitest', 'robocop.ini')
 
-        if test_path:
-            args.append('--test-path=%s' % test_path)
-
+        if not kwargs.get('robocopApk'):
+            kwargs['robocopApk'] = os.path.join(self.topobjdir, 'build', 'mobile',
+                                                'robocop', 'robocop-debug.apk')
         mochitest = self._spawn(MochitestRunner)
-        return mochitest.run_android_test(args)
+        return mochitest.run_android_test(test_path, **kwargs)
--- a/testing/mochitest/mochitest_options.py
+++ b/testing/mochitest/mochitest_options.py
@@ -1,845 +1,945 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-from argparse import ArgumentParser
+from abc import ABCMeta, abstractmethod, abstractproperty
+from argparse import ArgumentParser, SUPPRESS
 from urlparse import urlparse
-import logging
 import os
 import tempfile
 
+from droid import DroidADB, DroidSUT
+from mozlog import structured
 from mozprofile import DEFAULT_PORTS
 import mozinfo
 import moznetwork
 
-from automation import Automation
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 try:
-    from mozbuild.base import MozbuildObject
+    from mozbuild.base import (
+        MozbuildObject,
+        MachCommandConditions as conditions,
+    )
     build_obj = MozbuildObject.from_environment(cwd=here)
 except ImportError:
     build_obj = None
+    conditions = None
 
-__all__ = ["MochitestOptions", "B2GOptions"]
 
 VMWARE_RECORDING_HELPER_BASENAME = "vmwarerecordinghelper"
 
 
-class MochitestOptions(ArgumentParser):
-    """Usage instructions for runtests.py.
-    All arguments are optional.
-    If --chrome is specified, chrome tests will be run instead of web content tests.
-    If --browser-chrome is specified, browser-chrome tests will be run instead of web content tests.
-    See <http://mochikit.com/doc/html/MochiKit/Logging.html> for details on the logging levels.
-    """
+class ArgumentContainer():
+    __metaclass__ = ABCMeta
+
+    @abstractproperty
+    def args(self):
+        pass
+
+    @abstractproperty
+    def defaults(self):
+        pass
+
+    @abstractmethod
+    def validate(self, parser, args, context):
+        pass
+
+    def get_full_path(self, path, cwd):
+        """Get an absolute path relative to cwd."""
+        return os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
+
+
+class MochitestArguments(ArgumentContainer):
+    """General mochitest arguments."""
 
     LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "FATAL")
     LEVEL_STRING = ", ".join(LOG_LEVELS)
-    mochitest_options = [
-        [["--close-when-done"],
-         {"action": "store_true",
+
+    args = [
+        [["--keep-open"],
+         {"action": "store_false",
           "dest": "closeWhenDone",
-          "default": False,
-          "help": "close the application when tests are done running",
+          "default": True,
+          "help": "Always keep the browser open after tests complete.",
           }],
         [["--appname"],
          {"dest": "app",
           "default": None,
-          "help": "absolute path to application, overriding default",
+          "help": "Override the default binary used to run tests with the path provided, e.g "
+                  "/usr/bin/firefox. If you have run ./mach package beforehand, you can "
+                  "specify 'dist' to run tests against the distribution bundle's binary.",
+          "suppress": True,
           }],
         [["--utility-path"],
          {"dest": "utilityPath",
           "default": build_obj.bindir if build_obj is not None else None,
-          "help": "absolute path to directory containing utility programs (xpcshell, ssltunnel, certutil)",
+          "help": "absolute path to directory containing utility programs "
+                  "(xpcshell, ssltunnel, certutil)",
+          "suppress": True,
           }],
         [["--certificate-path"],
          {"dest": "certPath",
+          "default": None,
           "help": "absolute path to directory containing certificate store to use testing profile",
-          "default": os.path.join(build_obj.topsrcdir, 'build', 'pgo', 'certs') if build_obj is not None else None,
+          "suppress": True,
           }],
-        [["--autorun"],
-         {"action": "store_true",
+        [["--no-autorun"],
+         {"action": "store_false",
           "dest": "autorun",
-          "help": "start running tests when the application starts",
-          "default": False,
+          "default": True,
+          "help": "Do not start running tests automatically.",
           }],
         [["--timeout"],
          {"type": int,
-          "dest": "timeout",
-          "help": "per-test timeout in seconds",
           "default": None,
+          "help": "The per-test timeout in seconds (default: 60 seconds).",
+          }],
+        [["--max-timeouts"],
+         {"type": int,
+          "dest": "maxTimeouts",
+          "default": None,
+          "help": "The maximum number of timeouts permitted before halting testing.",
           }],
         [["--total-chunks"],
          {"type": int,
           "dest": "totalChunks",
-          "help": "how many chunks to split the tests up into",
+          "help": "Total number of chunks to split tests into.",
           "default": None,
           }],
         [["--this-chunk"],
          {"type": int,
           "dest": "thisChunk",
-          "help": "which chunk to run",
+          "help": "If running tests by chunks, the chunk number to run.",
           "default": None,
           }],
         [["--chunk-by-runtime"],
          {"action": "store_true",
           "dest": "chunkByRuntime",
-          "help": "group tests such that each chunk has roughly the same runtime",
+          "help": "Group tests such that each chunk has roughly the same runtime.",
           "default": False,
           }],
         [["--chunk-by-dir"],
          {"type": int,
           "dest": "chunkByDir",
-          "help": "group tests together in the same chunk that are in the same top chunkByDir directories",
+          "help": "Group tests together in the same chunk that are in the same top "
+                  "chunkByDir directories.",
           "default": 0,
           }],
         [["--run-by-dir"],
          {"action": "store_true",
           "dest": "runByDir",
-          "help": "Run each directory in a single browser instance with a fresh profile",
+          "help": "Run each directory in a single browser instance with a fresh profile.",
           "default": False,
           }],
         [["--shuffle"],
-         {"dest": "shuffle",
-          "action": "store_true",
-          "help": "randomize test order",
+         {"action": "store_true",
+          "help": "Shuffle execution order of tests.",
           "default": False,
           }],
         [["--console-level"],
          {"dest": "consoleLevel",
           "choices": LOG_LEVELS,
-          "metavar": "LEVEL",
-          "help": "one of %s to determine the level of console "
-                  "logging" % LEVEL_STRING,
-          "default": None,
+          "default": "INFO",
+          "help": "One of %s to determine the level of console logging." % LEVEL_STRING,
+          "suppress": True,
           }],
         [["--chrome"],
          {"action": "store_true",
-          "dest": "chrome",
-          "help": "run chrome Mochitests",
           "default": False,
+          "help": "Run chrome mochitests.",
+          "suppress": True,
           }],
         [["--ipcplugins"],
          {"action": "store_true",
           "dest": "ipcplugins",
-          "help": "run ipcplugins Mochitests",
+          "help": "Run ipcplugins mochitests.",
           "default": False,
+          "suppress": True,
           }],
         [["--test-path"],
          {"dest": "testPath",
-          "help": "start in the given directory's tests",
           "default": "",
+          "help": "Run the given test or recursively run the given directory of tests.",
+          # if running from mach, a test_paths arg is exposed instead
+          "suppress": build_obj is not None,
           }],
         [["--bisect-chunk"],
          {"dest": "bisectChunk",
-          "help": "Specify the failing test name to find the previous tests that may be causing the failure.",
           "default": None,
+          "help": "Specify the failing test name to find the previous tests that may be "
+                  "causing the failure.",
           }],
         [["--start-at"],
          {"dest": "startAt",
-          "help": "skip over tests until reaching the given test",
           "default": "",
+          "help": "Start running the test sequence at this test.",
           }],
         [["--end-at"],
          {"dest": "endAt",
-          "help": "don't run any tests after the given one",
           "default": "",
+          "help": "Stop running the test sequence at this test.",
           }],
         [["--browser-chrome"],
          {"action": "store_true",
           "dest": "browserChrome",
+          "default": False,
           "help": "run browser chrome Mochitests",
-          "default": False,
+          "suppress": True,
           }],
         [["--subsuite"],
-         {"dest": "subsuite",
-          "help": "subsuite of tests to run",
-          "default": None,
+         {"default": None,
+          "help": "Subsuite of tests to run. Unlike tags, subsuites also remove tests from "
+                  "the default set. Only one can be specified at once.",
           }],
         [["--jetpack-package"],
          {"action": "store_true",
           "dest": "jetpackPackage",
-          "help": "run jetpack package tests",
+          "help": "Run jetpack package tests.",
           "default": False,
+          "suppress": True,
           }],
         [["--jetpack-addon"],
          {"action": "store_true",
           "dest": "jetpackAddon",
-          "help": "run jetpack addon tests",
+          "help": "Run jetpack addon tests.",
           "default": False,
+          "suppress": True,
           }],
         [["--webapprt-content"],
          {"action": "store_true",
           "dest": "webapprtContent",
-          "help": "run WebappRT content tests",
+          "help": "Run WebappRT content tests.",
           "default": False,
+          "suppress": True,
           }],
         [["--webapprt-chrome"],
          {"action": "store_true",
           "dest": "webapprtChrome",
-          "help": "run WebappRT chrome tests",
+          "help": "Run WebappRT chrome tests.",
           "default": False,
+          "suppress": True,
           }],
         [["--a11y"],
          {"action": "store_true",
-          "dest": "a11y",
-          "help": "run accessibility Mochitests",
+          "help": "Run accessibility Mochitests.",
           "default": False,
+          "suppress": True,
           }],
         [["--setenv"],
          {"action": "append",
           "dest": "environment",
           "metavar": "NAME=VALUE",
-          "help": "sets the given variable in the application's "
-          "environment",
           "default": [],
+          "help": "Sets the given variable in the application's environment.",
           }],
         [["--exclude-extension"],
          {"action": "append",
           "dest": "extensionsToExclude",
-          "help": "excludes the given extension from being installed "
-          "in the test profile",
           "default": [],
+          "help": "Excludes the given extension from being installed in the test profile.",
+          "suppress": True,
           }],
         [["--browser-arg"],
          {"action": "append",
           "dest": "browserArgs",
-          "metavar": "ARG",
-          "help": "provides an argument to the test application",
           "default": [],
+          "help": "Provides an argument to the test application (e.g Firefox).",
+          "suppress": True,
           }],
         [["--leak-threshold"],
          {"type": int,
           "dest": "defaultLeakThreshold",
-          "metavar": "THRESHOLD",
-          "help": "fail if the number of bytes leaked in default "
-          "processes through refcounted objects (or bytes "
-          "in classes with MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) "
-          "is greater than the given number",
           "default": 0,
+          "help": "Fail if the number of bytes leaked in default processes through "
+                  "refcounted objects (or bytes in classes with MOZ_COUNT_CTOR and "
+                  "MOZ_COUNT_DTOR) is greater than the given number.",
+          "suppress": True,
           }],
         [["--fatal-assertions"],
          {"action": "store_true",
           "dest": "fatalAssertions",
-          "help": "abort testing whenever an assertion is hit "
-          "(requires a debug build to be effective)",
           "default": False,
+          "help": "Abort testing whenever an assertion is hit (requires a debug build to "
+                  "be effective).",
+          "suppress": True,
           }],
         [["--extra-profile-file"],
          {"action": "append",
           "dest": "extraProfileFiles",
-          "help": "copy specified files/dirs to testing profile",
           "default": [],
+          "help": "Copy specified files/dirs to testing profile. Can be specified more "
+                  "than once.",
+          "suppress": True,
           }],
         [["--install-extension"],
          {"action": "append",
           "dest": "extensionsToInstall",
-          "help": "install the specified extension in the testing profile."
-          "The extension file's name should be <id>.xpi where <id> is"
-          "the extension's id as indicated in its install.rdf."
-          "An optional path can be specified too.",
           "default": [],
+          "help": "Install the specified extension in the testing profile. Can be a path "
+                  "to a .xpi file.",
           }],
         [["--profile-path"],
          {"dest": "profilePath",
-          "help": "Directory where the profile will be stored."
-          "This directory will be deleted after the tests are finished",
           "default": None,
+          "help": "Directory where the profile will be stored. This directory will be "
+                  "deleted after the tests are finished.",
+          "suppress": True,
           }],
         [["--testing-modules-dir"],
          {"dest": "testingModulesDir",
+          "default": None,
           "help": "Directory where testing-only JS modules are located.",
-          "default": None,
+          "suppress": True,
           }],
         [["--use-vmware-recording"],
          {"action": "store_true",
           "dest": "vmwareRecording",
-          "help": "enables recording while the application is running "
-          "inside a VMware Workstation 7.0 or later VM",
           "default": False,
+          "help": "Enables recording while the application is running inside a VMware "
+                  "Workstation 7.0 or later VM.",
+          "suppress": True,
           }],
         [["--repeat"],
          {"type": int,
-          "dest": "repeat",
-          "metavar": "REPEAT",
-          "help": "repeats the test or set of tests the given number of times, ie: repeat: 1 will run the test twice.",
           "default": 0,
+          "help": "Repeat the tests the given number of times.",
           }],
         [["--run-until-failure"],
          {"action": "store_true",
           "dest": "runUntilFailure",
-          "help": "Run tests repeatedly and stops on the first time a test fails. "
-          "Default cap is 30 runs, which can be overwritten with the --repeat parameter.",
           "default": False,
+          "help": "Run tests repeatedly but stop the first time a test fails. Default cap "
+                  "is 30 runs, which can be overridden with the --repeat parameter.",
           }],
         [["--manifest"],
          {"dest": "manifestFile",
-          "help": ".ini format of tests to run.",
           "default": None,
+          "help": "Path to a manifestparser (.ini formatted) manifest of tests to run.",
+          "suppress": True,
           }],
         [["--testrun-manifest-file"],
          {"dest": "testRunManifestFile",
-          "help": "Overrides the default filename of the tests.json manifest file that is created from the manifest and used by the test runners to run the tests. Only useful when running multiple test runs simulatenously on the same machine.",
           "default": 'tests.json',
+          "help": "Overrides the default filename of the tests.json manifest file that is "
+                  "generated by the harness and used by SimpleTest. Only useful when running "
+                  "multiple test runs simulatenously on the same machine.",
+          "suppress": True,
           }],
         [["--failure-file"],
          {"dest": "failureFile",
-          "help": "Filename of the output file where we can store a .json list of failures to be run in the future with --run-only-tests.",
           "default": None,
+          "help": "Filename of the output file where we can store a .json list of failures "
+                  "to be run in the future with --run-only-tests.",
+          "suppress": True,
           }],
         [["--run-slower"],
          {"action": "store_true",
           "dest": "runSlower",
-          "help": "Delay execution between test files.",
           "default": False,
+          "help": "Delay execution between tests.",
           }],
         [["--metro-immersive"],
          {"action": "store_true",
           "dest": "immersiveMode",
-          "help": "launches tests in immersive browser",
           "default": False,
+          "help": "Launches tests in an immersive browser.",
+          "suppress": True,
           }],
         [["--httpd-path"],
          {"dest": "httpdPath",
           "default": None,
-          "help": "path to the httpd.js file",
+          "help": "Path to the httpd.js file.",
+          "suppress": True,
           }],
         [["--setpref"],
          {"action": "append",
+          "metavar": "PREF=VALUE",
           "default": [],
           "dest": "extraPrefs",
-          "metavar": "PREF=VALUE",
-          "help": "defines an extra user preference",
+          "help": "Defines an extra user preference.",
           }],
         [["--jsdebugger"],
          {"action": "store_true",
           "default": False,
-          "dest": "jsdebugger",
-          "help": "open the browser debugger",
+          "help": "Start the browser JS debugger before running the test. Implies --no-autorun.",
           }],
         [["--debug-on-failure"],
          {"action": "store_true",
           "default": False,
           "dest": "debugOnFailure",
-          "help": "breaks execution and enters the JS debugger on a test failure. Should be used together with --jsdebugger."
+          "help": "Breaks execution and enters the JS debugger on a test failure. Should "
+                  "be used together with --jsdebugger."
           }],
         [["--e10s"],
          {"action": "store_true",
           "default": False,
-          "dest": "e10s",
           "help": "Run tests with electrolysis preferences and test filtering enabled.",
           }],
         [["--strict-content-sandbox"],
          {"action": "store_true",
           "default": False,
           "dest": "strictContentSandbox",
           "help": "Run tests with a more strict content sandbox (Windows only).",
+          "suppress": not mozinfo.isWin,
           }],
         [["--nested_oop"],
          {"action": "store_true",
           "default": False,
-          "dest": "nested_oop",
           "help": "Run tests with nested_oop preferences and test filtering enabled.",
           }],
+        [["--dmd"],
+         {"action": "store_true",
+          "default": False,
+          "help": "Run tests with DMD active.",
+          }],
         [["--dmd-path"],
          {"default": None,
           "dest": "dmdPath",
           "help": "Specifies the path to the directory containing the shared library for DMD.",
+          "suppress": True,
           }],
         [["--dump-output-directory"],
          {"default": None,
           "dest": "dumpOutputDirectory",
           "help": "Specifies the directory in which to place dumped memory reports.",
           }],
         [["--dump-about-memory-after-test"],
          {"action": "store_true",
           "default": False,
           "dest": "dumpAboutMemoryAfterTest",
-          "help": "Produce an about:memory dump after each test in the directory specified "
-          "by --dump-output-directory."
+          "help": "Dump an about:memory log after each test in the directory specified "
+                  "by --dump-output-directory.",
           }],
         [["--dump-dmd-after-test"],
          {"action": "store_true",
           "default": False,
           "dest": "dumpDMDAfterTest",
-          "help": "Produce a DMD dump after each test in the directory specified "
-          "by --dump-output-directory."
+          "help": "Dump a DMD log after each test in the directory specified "
+                  "by --dump-output-directory.",
           }],
         [["--slowscript"],
          {"action": "store_true",
           "default": False,
-          "dest": "slowscript",
           "help": "Do not set the JS_DISABLE_SLOW_SCRIPT_SIGNALS env variable; "
-          "when not set, recoverable but misleading SIGSEGV instances "
-          "may occur in Ion/Odin JIT code."
+                  "when not set, recoverable but misleading SIGSEGV instances "
+                  "may occur in Ion/Odin JIT code.",
           }],
         [["--screenshot-on-fail"],
          {"action": "store_true",
           "default": False,
           "dest": "screenshotOnFail",
-          "help": "Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory for storing the screenshots."
+          "help": "Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory "
+                  "for storing the screenshots."
           }],
         [["--quiet"],
          {"action": "store_true",
+          "dest": "quiet",
           "default": False,
-          "dest": "quiet",
-          "help": "Do not print test log lines unless a failure occurs."
+          "help": "Do not print test log lines unless a failure occurs.",
           }],
         [["--pidfile"],
          {"dest": "pidFile",
-          "help": "name of the pidfile to generate",
           "default": "",
+          "help": "Name of the pidfile to generate.",
+          "suppress": True,
           }],
         [["--use-test-media-devices"],
          {"action": "store_true",
           "default": False,
           "dest": "useTestMediaDevices",
           "help": "Use test media device drivers for media testing.",
           }],
         [["--gmp-path"],
          {"default": None,
-          "dest": "gmp_path",
           "help": "Path to fake GMP plugin. Will be deduced from the binary if not passed.",
+          "suppress": True,
           }],
         [["--xre-path"],
          {"dest": "xrePath",
           "default": None,    # individual scripts will set a sane default
-          "help": "absolute path to directory containing XRE (probably xulrunner)",
+          "help": "Absolute path to directory containing XRE (probably xulrunner).",
+          "suppress": True,
           }],
         [["--symbols-path"],
          {"dest": "symbolsPath",
           "default": None,
-          "help": "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols",
+          "help": "Absolute path to directory containing breakpad symbols, or the URL of a "
+                  "zip file containing symbols",
+          "suppress": True,
           }],
         [["--debugger"],
-         {"dest": "debugger",
-          "help": "use the given debugger to launch the application",
+         {"default": None,
+          "help": "Debugger binary to run tests in. Program name or path.",
           }],
         [["--debugger-args"],
          {"dest": "debuggerArgs",
-          "help": "pass the given args to the debugger _before_ the application on the command line",
+          "default": None,
+          "help": "Arguments to pass to the debugger.",
           }],
         [["--debugger-interactive"],
          {"action": "store_true",
           "dest": "debuggerInteractive",
-          "help": "prevents the test harness from redirecting stdout and stderr for interactive debuggers",
-          }],
-        [["--max-timeouts"],
-         {"type": int,
-          "dest": "maxTimeouts",
-          "help": "maximum number of timeouts permitted before halting testing",
           "default": None,
+          "help": "Prevents the test harness from redirecting stdout and stderr for "
+                  "interactive debuggers.",
+          "suppress": True,
           }],
         [["--tag"],
          {"action": "append",
           "dest": "test_tags",
           "default": None,
-          "help": "filter out tests that don't have the given tag. Can be "
-                  "used multiple times in which case the test must contain "
-                  "at least one of the given tags.",
+          "help": "Filter out tests that don't have the given tag. Can be used multiple "
+                  "times in which case the test must contain at least one of the given tags.",
           }],
         [["--enable-cpow-warnings"],
          {"action": "store_true",
           "dest": "enableCPOWWarnings",
-          "help": "enable logging of unsafe CPOW usage, which is disabled by default for tests",
+          "help": "Enable logging of unsafe CPOW usage, which is disabled by default for tests",
+          "suppress": True,
           }],
     ]
 
-    def __init__(self, **kwargs):
-        ArgumentParser.__init__(self, usage=self.__doc__, **kwargs)
-        for option, value in self.mochitest_options:
-            # Allocate new lists so references to original don't get mutated.
-            # allowing multiple uses within a single process.
-            if "default" in value and isinstance(value["default"], list):
-                value["default"] = []
-            self.add_argument(*option, **value)
+    defaults = {
+        # Bug 1065098 - The geckomediaplugin process fails to produce a leak
+        # log for some reason.
+        'ignoreMissingLeaks': ["geckomediaplugin"],
 
-    def verifyOptions(self, options, mochitest):
-        """ verify correct options and cleanup paths """
+        # Set server information on the args object
+        'webServer': '127.0.0.1',
+        'httpPort': DEFAULT_PORTS['http'],
+        'sslPort': DEFAULT_PORTS['https'],
+        'webSocketPort': '9988',
+        # The default websocket port is incorrect in mozprofile; it is
+        # set to the SSL proxy setting. See:
+        # see https://bugzilla.mozilla.org/show_bug.cgi?id=916517
+        # args.webSocketPort = DEFAULT_PORTS['ws']
+    }
+
+    def validate(self, parser, options, context):
+        """Validate generic options."""
 
         # for test manifest parsing.
         mozinfo.update({"strictContentSandbox": options.strictContentSandbox})
         # for test manifest parsing.
         mozinfo.update({"nested_oop": options.nested_oop})
 
-        if options.app is None:
-            if build_obj is not None:
-                options.app = build_obj.get_binary_path()
-            else:
-                self.error(
-                    "could not find the application path, --appname must be specified")
+        # b2g and android don't use 'app' the same way, so skip validation
+        if parser.app not in ('b2g', 'android'):
+            if options.app is None:
+                if build_obj:
+                    options.app = build_obj.get_binary_path()
+                else:
+                    parser.error(
+                        "could not find the application path, --appname must be specified")
+            elif options.app == "dist" and build_obj:
+                options.app = build_obj.get_binary_path(where='staged-package')
+
+            options.app = self.get_full_path(options.app, parser.oldcwd)
+            if not os.path.exists(options.app):
+                parser.error("Error: Path {} doesn't exist. Are you executing "
+                             "$objdir/_tests/testing/mochitest/runtests.py?".format(
+                                 options.app))
+
+        if options.gmp_path is None and options.app and build_obj:
+            # Need to fix the location of gmp_fake which might not be shipped in the binary
+            gmp_modules = (
+                ('gmp-fake', '1.0'),
+                ('gmp-clearkey', '0.1'),
+                ('gmp-fakeopenh264', '1.0')
+            )
+            options.gmp_path = os.pathsep.join(
+                os.path.join(build_obj.bindir, *p) for p in gmp_modules)
 
         if options.totalChunks is not None and options.thisChunk is None:
-            self.error(
+            parser.error(
                 "thisChunk must be specified when totalChunks is specified")
 
         if options.totalChunks:
             if not 1 <= options.thisChunk <= options.totalChunks:
-                self.error("thisChunk must be between 1 and totalChunks")
+                parser.error("thisChunk must be between 1 and totalChunks")
 
         if options.chunkByDir and options.chunkByRuntime:
-            self.error(
+            parser.error(
                 "can only use one of --chunk-by-dir or --chunk-by-runtime")
 
         if options.xrePath is None:
             # default xrePath to the app path if not provided
             # but only if an app path was explicitly provided
-            if options.app != self.get_default('app'):
+            if options.app != parser.get_default('app'):
                 options.xrePath = os.path.dirname(options.app)
                 if mozinfo.isMac:
                     options.xrePath = os.path.join(
                         os.path.dirname(
                             options.xrePath),
                         "Resources")
             elif build_obj is not None:
                 # otherwise default to dist/bin
                 options.xrePath = build_obj.bindir
             else:
-                self.error(
+                parser.error(
                     "could not find xre directory, --xre-path must be specified")
 
         # allow relative paths
-        options.xrePath = mochitest.getFullPath(options.xrePath)
+        options.xrePath = self.get_full_path(options.xrePath, parser.oldcwd)
         if options.profilePath:
-            options.profilePath = mochitest.getFullPath(options.profilePath)
-        options.app = mochitest.getFullPath(options.app)
-        if options.dmdPath is not None:
-            options.dmdPath = mochitest.getFullPath(options.dmdPath)
+            options.profilePath = self.get_full_path(options.profilePath, parser.oldcwd)
+
+        if options.dmdPath:
+            options.dmdPath = self.get_full_path(options.dmdPath, parser.oldcwd)
 
-        if not os.path.exists(options.app):
-            msg = """\
-            Error: Path %(app)s doesn't exist.
-            Are you executing $objdir/_tests/testing/mochitest/runtests.py?"""
-            self.error(msg % {"app": options.app})
-            return None
+        if options.dmd and not options.dmdPath:
+            if build_obj:
+                options.dmdPath = build_obj.bin_dir
+            else:
+                parser.error(
+                    "could not find dmd libraries, specify them with --dmd-path")
 
         if options.utilityPath:
-            options.utilityPath = mochitest.getFullPath(options.utilityPath)
+            options.utilityPath = self.get_full_path(options.utilityPath, parser.oldcwd)
 
         if options.certPath:
-            options.certPath = mochitest.getFullPath(options.certPath)
-
-        if options.symbolsPath and len(
-            urlparse(
-                options.symbolsPath).scheme) < 2:
-            options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
+            options.certPath = self.get_full_path(options.certPath, parser.oldcwd)
+        elif build_obj:
+            options.certPath = os.path.join(build_obj.topsrcdir, 'build', 'pgo', 'certs')
 
-        # Set server information on the options object
-        options.webServer = '127.0.0.1'
-        options.httpPort = DEFAULT_PORTS['http']
-        options.sslPort = DEFAULT_PORTS['https']
-        #        options.webSocketPort = DEFAULT_PORTS['ws']
-        # <- http://hg.mozilla.org/mozilla-central/file/b871dfb2186f/build/automation.py.in#l30
-        options.webSocketPort = str(9988)
-        # The default websocket port is incorrect in mozprofile; it is
-        # set to the SSL proxy setting. See:
-        # see https://bugzilla.mozilla.org/show_bug.cgi?id=916517
+        if options.symbolsPath and len(urlparse(options.symbolsPath).scheme) < 2:
+            options.symbolsPath = self.get_full_path(options.symbolsPath, parser.oldcwd)
+        elif not options.symbolsPath and build_obj:
+            options.symbolsPath = os.path.join(build_obj.distdir, 'crashreporter-symbols')
 
         if options.vmwareRecording:
             if not mozinfo.isWin:
-                self.error(
+                parser.error(
                     "use-vmware-recording is only supported on Windows.")
-            mochitest.vmwareHelperPath = os.path.join(
+            options.vmwareHelperPath = os.path.join(
                 options.utilityPath, VMWARE_RECORDING_HELPER_BASENAME + ".dll")
-            if not os.path.exists(mochitest.vmwareHelperPath):
-                self.error("%s not found, cannot automate VMware recording." %
-                           mochitest.vmwareHelperPath)
+            if not os.path.exists(options.vmwareHelperPath):
+                parser.error("%s not found, cannot automate VMware recording." %
+                             options.vmwareHelperPath)
 
         if options.webapprtContent and options.webapprtChrome:
-            self.error(
+            parser.error(
                 "Only one of --webapprt-content and --webapprt-chrome may be given.")
 
         if options.jsdebugger:
             options.extraPrefs += [
                 "devtools.debugger.remote-enabled=true",
                 "devtools.chrome.enabled=true",
                 "devtools.debugger.prompt-connection=false"
             ]
             options.autorun = False
 
         if options.debugOnFailure and not options.jsdebugger:
-            self.error(
-                "--debug-on-failure should be used together with --jsdebugger.")
+            parser.error(
+                "--debug-on-failure requires --jsdebugger.")
+
+        if options.debuggerArgs and not options.debugger:
+            parser.error(
+                "--debugger-args requires --debugger.")
 
-        # Try to guess the testing modules directory.
-        # This somewhat grotesque hack allows the buildbot machines to find the
-        # modules directory without having to configure the buildbot hosts. This
-        # code should never be executed in local runs because the build system
-        # should always set the flag that populates this variable. If buildbot ever
-        # passes this argument, this code can be deleted.
         if options.testingModulesDir is None:
-            possible = os.path.join(here, os.path.pardir, 'modules')
+            if build_obj:
+                options.testingModulesDir = os.path.join(
+                    build_obj.topobjdir, '_tests', 'modules')
+            else:
+                # Try to guess the testing modules directory.
+                # This somewhat grotesque hack allows the buildbot machines to find the
+                # modules directory without having to configure the buildbot hosts. This
+                # code should never be executed in local runs because the build system
+                # should always set the flag that populates this variable. If buildbot ever
+                # passes this argument, this code can be deleted.
+                possible = os.path.join(here, os.path.pardir, 'modules')
 
-            if os.path.isdir(possible):
-                options.testingModulesDir = possible
+                if os.path.isdir(possible):
+                    options.testingModulesDir = possible
+
+        if build_obj:
+            options.extraProfileFiles.append(os.path.join(build_obj.distdir, 'plugins'))
 
         # Even if buildbot is updated, we still want this, as the path we pass in
         # to the app must be absolute and have proper slashes.
         if options.testingModulesDir is not None:
             options.testingModulesDir = os.path.normpath(
                 options.testingModulesDir)
 
             if not os.path.isabs(options.testingModulesDir):
                 options.testingModulesDir = os.path.abspath(
                     options.testingModulesDir)
 
             if not os.path.isdir(options.testingModulesDir):
-                self.error('--testing-modules-dir not a directory: %s' %
-                           options.testingModulesDir)
+                parser.error('--testing-modules-dir not a directory: %s' %
+                             options.testingModulesDir)
 
             options.testingModulesDir = options.testingModulesDir.replace(
                 '\\',
                 '/')
             if options.testingModulesDir[-1] != '/':
                 options.testingModulesDir += '/'
 
         if options.immersiveMode:
             if not mozinfo.isWin:
-                self.error("immersive is only supported on Windows 8 and up.")
-            mochitest.immersiveHelperPath = os.path.join(
+                parser.error("immersive is only supported on Windows 8 and up.")
+            options.immersiveHelperPath = os.path.join(
                 options.utilityPath, "metrotestharness.exe")
-            if not os.path.exists(mochitest.immersiveHelperPath):
-                self.error("%s not found, cannot launch immersive tests." %
-                           mochitest.immersiveHelperPath)
+            if not os.path.exists(options.immersiveHelperPath):
+                parser.error("%s not found, cannot launch immersive tests." %
+                             options.immersiveHelperPath)
 
         if options.runUntilFailure:
             if not options.repeat:
                 options.repeat = 29
 
         if options.dumpOutputDirectory is None:
             options.dumpOutputDirectory = tempfile.gettempdir()
 
         if options.dumpAboutMemoryAfterTest or options.dumpDMDAfterTest:
             if not os.path.isdir(options.dumpOutputDirectory):
-                self.error('--dump-output-directory not a directory: %s' %
-                           options.dumpOutputDirectory)
+                parser.error('--dump-output-directory not a directory: %s' %
+                             options.dumpOutputDirectory)
 
         if options.useTestMediaDevices:
             if not mozinfo.isLinux:
-                self.error(
+                parser.error(
                     '--use-test-media-devices is only supported on Linux currently')
             for f in ['/usr/bin/gst-launch-0.10', '/usr/bin/pactl']:
                 if not os.path.isfile(f):
-                    self.error(
+                    parser.error(
                         'Missing binary %s required for '
                         '--use-test-media-devices' % f)
 
         if options.nested_oop:
             if not options.e10s:
                 options.e10s = True
         mozinfo.update({"e10s": options.e10s})  # for test manifest parsing.
 
         options.leakThresholds = {
             "default": options.defaultLeakThreshold,
             "tab": 25000,  # See dependencies of bug 1051230.
             # GMP rarely gets a log, but when it does, it leaks a little.
             "geckomediaplugin": 20000,
         }
 
-        # Bug 1065098 - The geckomediaplugin process fails to produce a leak
-        # log for some reason.
-        options.ignoreMissingLeaks = ["geckomediaplugin"]
-
         # Bug 1091917 - We exit early in tab processes on Windows, so we don't
         # get leak logs yet.
         if mozinfo.isWin:
             options.ignoreMissingLeaks.append("tab")
 
         # Bug 1121539 - OSX-only intermittent tab process leak in test_ipc.html
         if mozinfo.isMac:
             options.leakThresholds["tab"] = 100000
 
         return options
 
 
-class B2GOptions(MochitestOptions):
-    b2g_options = [
+class B2GArguments(ArgumentContainer):
+    """B2G specific arguments."""
+
+    args = [
         [["--b2gpath"],
          {"dest": "b2gPath",
-          "help": "path to B2G repo or qemu dir",
           "default": None,
+          "help": "Path to B2G repo or QEMU directory.",
+          "suppress": True,
           }],
         [["--desktop"],
          {"action": "store_true",
-          "dest": "desktop",
-          "help": "Run the tests on a B2G desktop build",
           "default": False,
+          "help": "Run the tests on a B2G desktop build.",
+          "suppress": True,
           }],
         [["--marionette"],
-         {"dest": "marionette",
+         {"default": None,
           "help": "host:port to use when connecting to Marionette",
-          "default": None,
           }],
         [["--emulator"],
-         {"dest": "emulator",
-          "help": "Architecture of emulator to use: x86 or arm",
-          "default": None,
+         {"default": None,
+          "help": "Architecture of emulator to use, x86 or arm",
+          "suppress": True,
           }],
         [["--wifi"],
-         {"dest": "wifi",
+         {"default": False,
           "help": "Devine wifi configuration for on device mochitest",
-          "default": False,
+          "suppress": True,
           }],
         [["--sdcard"],
-         {"dest": "sdcard",
+         {"default": "10MB",
           "help": "Define size of sdcard: 1MB, 50MB...etc",
-          "default": "10MB",
           }],
         [["--no-window"],
          {"action": "store_true",
           "dest": "noWindow",
+          "default": False,
           "help": "Pass --no-window to the emulator",
-          "default": False,
           }],
         [["--adbpath"],
          {"dest": "adbPath",
-          "help": "path to adb",
           "default": "adb",
+          "help": "Path to adb binary.",
+          "suppress": True,
           }],
         [["--deviceIP"],
          {"dest": "deviceIP",
-          "help": "ip address of remote device to test",
           "default": None,
+          "help": "IP address of remote device to test.",
+          "suppress": True,
           }],
         [["--devicePort"],
-         {"dest": "devicePort",
+         {"default": 20701,
           "help": "port of remote device to test",
-          "default": 20701,
+          "suppress": True,
           }],
         [["--remote-logfile"],
          {"dest": "remoteLogFile",
-          "help": "Name of log file on the device relative to the device root. \
-                  PLEASE ONLY USE A FILENAME.",
           "default": None,
+          "help": "Name of log file on the device relative to the device root. "
+                  "PLEASE ONLY USE A FILENAME.",
+          "suppress": True,
           }],
         [["--remote-webserver"],
          {"dest": "remoteWebServer",
-          "help": "ip address where the remote web server is hosted at",
           "default": None,
+          "help": "IP address where the remote web server is hosted.",
+          "suppress": True,
           }],
         [["--http-port"],
          {"dest": "httpPort",
-          "help": "ip address where the remote web server is hosted at",
           "default": DEFAULT_PORTS['http'],
+          "help": "Port used for http on the remote web server.",
+          "suppress": True,
           }],
         [["--ssl-port"],
          {"dest": "sslPort",
-          "help": "ip address where the remote web server is hosted at",
           "default": DEFAULT_PORTS['https'],
+          "help": "Port used for https on the remote web server.",
+          "suppress": True,
           }],
         [["--gecko-path"],
          {"dest": "geckoPath",
-          "help": "the path to a gecko distribution that should \
-                   be installed on the emulator prior to test",
           "default": None,
+          "help": "The path to a gecko distribution that should be installed on the emulator "
+                  "prior to test.",
+          "suppress": True,
           }],
         [["--profile"],
          {"dest": "profile",
-          "help": "for desktop testing, the path to the \
-                   gaia profile to use",
           "default": None,
+          "help": "For desktop testing, the path to the gaia profile to use.",
           }],
         [["--logdir"],
          {"dest": "logdir",
-          "help": "directory to store log files",
           "default": None,
+          "help": "Directory to store log files.",
           }],
         [['--busybox'],
          {"dest": 'busybox',
-          "help": "Path to busybox binary to install on device",
           "default": None,
+          "help": "Path to busybox binary to install on device.",
           }],
         [['--profile-data-dir'],
          {"dest": 'profile_data_dir',
-          "help": "Path to a directory containing preference and other \
-                   data to be installed into the profile",
           "default": os.path.join(here, 'profile_data'),
+          "help": "Path to a directory containing preference and other data to be installed "
+                  "into the profile.",
+          "suppress": True,
           }],
     ]
 
-    def __init__(self):
-        MochitestOptions.__init__(self)
+    defaults = {
+        'logFile': 'mochitest.log',
+        'extensionsToExclude': ['specialpowers'],
+        # See dependencies of bug 1038943.
+        'defaultLeakThreshold': 5536,
+    }
 
-        for option in self.b2g_options:
-            self.add_argument(*option[0], **option[1])
+    def validate(self, parser, options, context):
+        """Validate b2g options."""
 
-        defaults = {}
-        defaults["logFile"] = "mochitest.log"
-        defaults["autorun"] = True
-        defaults["closeWhenDone"] = True
-        defaults["extensionsToExclude"] = ["specialpowers"]
-        # See dependencies of bug 1038943.
-        defaults["defaultLeakThreshold"] = 5536
-        self.set_defaults(**defaults)
+        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')]
 
-    def verifyRemoteOptions(self, options):
         if options.remoteWebServer is None:
             if os.name != "nt":
                 options.remoteWebServer = moznetwork.get_ip()
             else:
-                self.error(
+                parser.error(
                     "You must specify a --remote-webserver=<ip address>")
         options.webServer = options.remoteWebServer
 
+        if not options.b2gPath and hasattr(context, 'b2g_home'):
+            options.b2gPath = context.b2g_home
+
+        if hasattr(context, 'device_name') and not options.emulator:
+            if context.device_name.startswith('emulator'):
+                options.emulator = 'x86' if 'x86' in context.device_name else 'arm'
+
         if options.geckoPath and not options.emulator:
-            self.error(
+            parser.error(
                 "You must specify --emulator if you specify --gecko-path")
 
         if options.logdir and not options.emulator:
-            self.error("You must specify --emulator if you specify --logdir")
+            parser.error("You must specify --emulator if you specify --logdir")
+        elif not options.logdir and options.emulator and build_obj:
+            options.logdir = os.path.join(
+                build_obj.topobjdir, '_tests', 'testing', 'mochitest')
+
+        if hasattr(context, 'xre_path'):
+            options.xrePath = context.xre_path
 
         if not os.path.isdir(options.xrePath):
-            self.error("--xre-path '%s' is not a directory" % options.xrePath)
+            parser.error("--xre-path '%s' is not a directory" % options.xrePath)
+
         xpcshell = os.path.join(options.xrePath, 'xpcshell')
         if not os.access(xpcshell, os.F_OK):
-            self.error('xpcshell not found at %s' % xpcshell)
+            parser.error('xpcshell not found at %s' % xpcshell)
+
         if self.elf_arm(xpcshell):
-            self.error('--xre-path points to an ARM version of xpcshell; it '
-                       'should instead point to a version that can run on '
-                       'your desktop')
+            parser.error('--xre-path points to an ARM version of xpcshell; it '
+                         'should instead point to a version that can run on '
+                         'your desktop')
+
+        if not options.httpdPath and build_obj:
+            options.httpdPath = os.path.join(
+                build_obj.topobjdir, '_tests', 'testing', 'mochitest')
+
+        # Bug 1071866 - B2G Mochitests do not always produce a leak log.
+        options.ignoreMissingLeaks.append("default")
+        # Bug 1070068 - Leak logging does not work for tab processes on B2G.
+        options.ignoreMissingLeaks.append("tab")
 
         if options.pidFile != "":
             f = open(options.pidFile, 'w')
             f.write("%s" % os.getpid())
             f.close()
 
         return options
 
-    def verifyOptions(self, options, mochitest):
-        # since we are reusing verifyOptions, it will exit if App is not found
-        temp = options.app
-        options.app = __file__
-        tempPort = options.httpPort
-        tempSSL = options.sslPort
-        tempIP = options.webServer
-        options = MochitestOptions.verifyOptions(self, options, mochitest)
-        options.webServer = tempIP
-        options.app = temp
-        options.sslPort = tempSSL
-        options.httpPort = tempPort
-
-        # Bug 1071866 - B2G Mochitests do not always produce a leak log.
-        options.ignoreMissingLeaks.append("default")
-
-        # Bug 1070068 - Leak logging does not work for tab processes on B2G.
-        options.ignoreMissingLeaks.append("tab")
-
-        return options
-
     def elf_arm(self, filename):
         data = open(filename, 'rb').read(20)
         return data[:4] == "\x7fELF" and ord(data[18]) == 40  # EM_ARM
 
 
-class RemoteOptions(MochitestOptions):
-    remote_options = [
+class AndroidArguments(ArgumentContainer):
+    """Android specific arguments."""
+
+    args = [
         [["--remote-app-path"],
          {"dest": "remoteAppPath",
           "help": "Path to remote executable relative to device root using \
                    only forward slashes. Either this or app must be specified \
                    but not both.",
           "default": None,
           }],
         [["--deviceIP"],
@@ -848,212 +948,266 @@ class RemoteOptions(MochitestOptions):
           "default": None,
           }],
         [["--deviceSerial"],
          {"dest": "deviceSerial",
           "help": "ip address of remote device to test",
           "default": None,
           }],
         [["--dm_trans"],
-         {"dest": "dm_trans",
-          "default": "sut",
-          "help": "the transport to use to communicate with device: \
-                   [adb|sut]; default=sut",
+         {"choices": ["adb", "sut"],
+          "default": "adb",
+          "help": "The transport to use for communication with the device [default: adb].",
+          "suppress": True,
           }],
         [["--devicePort"],
          {"dest": "devicePort",
           "type": int,
           "default": 20701,
           "help": "port of remote device to test",
           }],
         [["--remote-product-name"],
          {"dest": "remoteProductName",
           "default": "fennec",
           "help": "The executable's name of remote product to test - either \
                    fennec or firefox, defaults to fennec",
+          "suppress": True,
           }],
         [["--remote-logfile"],
          {"dest": "remoteLogFile",
           "default": None,
           "help": "Name of log file on the device relative to the device \
                    root. PLEASE ONLY USE A FILENAME.",
           }],
         [["--remote-webserver"],
          {"dest": "remoteWebServer",
           "default": None,
           "help": "ip address where the remote web server is hosted at",
           }],
         [["--http-port"],
          {"dest": "httpPort",
           "default": DEFAULT_PORTS['http'],
           "help": "http port of the remote web server",
+          "suppress": True,
           }],
         [["--ssl-port"],
          {"dest": "sslPort",
           "default": DEFAULT_PORTS['https'],
           "help": "ssl port of the remote web server",
+          "suppress": True,
           }],
         [["--robocop-ini"],
          {"dest": "robocopIni",
           "default": "",
           "help": "name of the .ini file containing the list of tests to run",
           }],
-        [["--robocop"],
-         {"dest": "robocop",
-          "default": "",
-          "help": "name of the .ini file containing the list of tests to run. \
-                   [DEPRECATED- please use --robocop-ini",
-          }],
         [["--robocop-apk"],
          {"dest": "robocopApk",
           "default": "",
           "help": "name of the Robocop APK to use for ADB test running",
           }],
-        [["--robocop-path"],
-         {"dest": "robocopPath",
-          "default": "",
-          "help": "Path to the folder where robocop.apk is located at. \
-                   Primarily used for ADB test running. \
-                   [DEPRECATED- please use --robocop-apk]",
-          }],
         [["--robocop-ids"],
          {"dest": "robocopIds",
           "default": "",
           "help": "name of the file containing the view ID map \
                    (fennec_ids.txt)",
           }],
         [["--remoteTestRoot"],
          {"dest": "remoteTestRoot",
           "default": None,
           "help": "remote directory to use as test root \
                    (eg. /mnt/sdcard/tests or /data/local/tests)",
+          "suppress": True,
           }],
     ]
 
-    def __init__(self, automation, **kwargs):
-        self._automation = automation or Automation()
-        MochitestOptions.__init__(self)
+    defaults = {
+        'dm': None,
+        'logFile': 'mochitest.log',
+        'utilityPath': None,
+    }
 
-        for option in self.remote_options:
-            self.add_argument(*option[0], **option[1])
+    def validate(self, parser, options, context):
+        """Validate android options."""
+
+        if build_obj:
+            options.log_mach = '-'
 
-        defaults = {}
-        defaults["logFile"] = "mochitest.log"
-        defaults["autorun"] = True
-        defaults["closeWhenDone"] = True
-        defaults["utilityPath"] = None
-        self.set_defaults(**defaults)
+        if options.dm_trans == "adb":
+            if options.deviceIP:
+                options.dm = DroidADB(
+                    options.deviceIP,
+                    options.devicePort,
+                    deviceRoot=options.remoteTestRoot)
+            elif options.deviceSerial:
+                options.dm = DroidADB(
+                    None,
+                    None,
+                    deviceSerial=options.deviceSerial,
+                    deviceRoot=options.remoteTestRoot)
+            else:
+                options.dm = DroidADB(deviceRoot=options.remoteTestRoot)
+        elif options.dm_trans == 'sut':
+            if options.deviceIP is None:
+                parser.error(
+                    "If --dm_trans = sut, you must provide a device IP")
 
-    def verifyRemoteOptions(self, options, automation):
-        options_logger = logging.getLogger('MochitestRemote')
+            options.dm = DroidSUT(
+                options.deviceIP,
+                options.devicePort,
+                deviceRoot=options.remoteTestRoot)
 
         if not options.remoteTestRoot:
-            options.remoteTestRoot = automation._devicemanager.deviceRoot
+            options.remoteTestRoot = options.dm.deviceRoot
 
         if options.remoteWebServer is None:
             if os.name != "nt":
                 options.remoteWebServer = moznetwork.get_ip()
             else:
-                options_logger.error(
+                parser.error(
                     "you must specify a --remote-webserver=<ip address>")
-                return None
 
         options.webServer = options.remoteWebServer
 
-        if (options.dm_trans == 'sut' and options.deviceIP is None):
-            options_logger.error(
-                "If --dm_trans = sut, you must provide a device IP")
-            return None
-
-        if (options.remoteLogFile is None):
+        if options.remoteLogFile is None:
             options.remoteLogFile = options.remoteTestRoot + \
                 '/logs/mochitest.log'
 
-        if (options.remoteLogFile.count('/') < 1):
+        if options.remoteLogFile.count('/') < 1:
             options.remoteLogFile = options.remoteTestRoot + \
                 '/' + options.remoteLogFile
 
-        if (options.remoteAppPath and options.app):
-            options_logger.error(
+        if options.remoteAppPath and options.app:
+            parser.error(
                 "You cannot specify both the remoteAppPath and the app setting")
-            return None
-        elif (options.remoteAppPath):
+        elif options.remoteAppPath:
             options.app = options.remoteTestRoot + "/" + options.remoteAppPath
-        elif (options.app is None):
-            # Neither remoteAppPath nor app are set -- error
-            options_logger.error("You must specify either appPath or app")
-            return None
+        elif options.app is None:
+            if build_obj:
+                options.app = build_obj.substs['ANDROID_PACKAGE_NAME']
+            else:
+                # Neither remoteAppPath nor app are set -- error
+                parser.error("You must specify either appPath or app")
+
+        if build_obj and 'MOZ_HOST_BIN' in os.environ:
+            options.xrePath = os.environ['MOZ_HOST_BIN']
 
         # Only reset the xrePath if it wasn't provided
-        if (options.xrePath is None):
+        if options.xrePath is None:
             options.xrePath = options.utilityPath
 
-        if (options.pidFile != ""):
+        if options.pidFile != "":
             f = open(options.pidFile, 'w')
             f.write("%s" % os.getpid())
             f.close()
 
-        # Robocop specific deprecated options.
-        if options.robocop:
-            if options.robocopIni:
-                options_logger.error(
-                    "can not use deprecated --robocop and replacement --robocop-ini together")
-                return None
-            options.robocopIni = options.robocop
-            del options.robocop
-
-        if options.robocopPath:
-            if options.robocopApk:
-                options_logger.error(
-                    "can not use deprecated --robocop-path and replacement --robocop-apk together")
-                return None
-            options.robocopApk = os.path.join(
-                options.robocopPath,
-                'robocop.apk')
-            del options.robocopPath
-
         # Robocop specific options
         if options.robocopIni != "":
             if not os.path.exists(options.robocopIni):
-                options_logger.error(
+                parser.error(
                     "Unable to find specified robocop .ini manifest '%s'" %
                     options.robocopIni)
-                return None
             options.robocopIni = os.path.abspath(options.robocopIni)
 
+            if not options.robocopApk and build_obj:
+                options.robocopApk = os.path.join(build_obj.topobjdir, 'build', 'mobile',
+                                                  'robocop', 'robocop-debug.apk')
+
         if options.robocopApk != "":
             if not os.path.exists(options.robocopApk):
-                options_logger.error(
+                parser.error(
                     "Unable to find robocop APK '%s'" %
                     options.robocopApk)
-                return None
             options.robocopApk = os.path.abspath(options.robocopApk)
 
         if options.robocopIds != "":
             if not os.path.exists(options.robocopIds):
-                options_logger.error(
+                parser.error(
                     "Unable to find specified robocop IDs file '%s'" %
                     options.robocopIds)
-                return None
             options.robocopIds = os.path.abspath(options.robocopIds)
 
         # allow us to keep original application around for cleanup while
         # running robocop via 'am'
         options.remoteappname = options.app
         return options
 
-    def verifyOptions(self, options, mochitest):
-        # since we are reusing verifyOptions, it will exit if App is not found
-        temp = options.app
-        options.app = __file__
-        tempPort = options.httpPort
-        tempSSL = options.sslPort
-        tempIP = options.webServer
-        # We are going to override this option later anyway, just pretend
-        # like it's not set for verification purposes.
-        options.dumpOutputDirectory = None
-        options = MochitestOptions.verifyOptions(self, options, mochitest)
-        options.webServer = tempIP
-        options.app = temp
-        options.sslPort = tempSSL
-        options.httpPort = tempPort
+
+container_map = {
+    'generic': [MochitestArguments],
+    'b2g': [MochitestArguments, B2GArguments],
+    'android': [MochitestArguments, AndroidArguments],
+}
+
+
+class MochitestArgumentParser(ArgumentParser):
+    """
+    Usage instructions for runtests.py.
+
+    All arguments are optional.
+    If --chrome is specified, chrome tests will be run instead of web content tests.
+    If --browser-chrome is specified, browser-chrome tests will be run instead of web content tests.
+    See <http://mochikit.com/doc/html/MochiKit/Logging.html> for details on the logging levels.
+    """
+
+    _containers = None
+    context = {}
+
+    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):
+                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(
+                self.app, ', '.join(container_map.keys())))
 
-        return options
+        defaults = {}
+        for container in self.containers:
+            defaults.update(container.defaults)
+            group = self.add_argument_group(container.__class__.__name__, container.__doc__)
+
+            for cli, kwargs in container.args:
+                # Allocate new lists so references to original don't get mutated.
+                # allowing multiple uses within a single process.
+                if "default" in kwargs and isinstance(kwargs['default'], list):
+                    kwargs["default"] = []
+
+                if 'suppress' in kwargs:
+                    if kwargs['suppress']:
+                        kwargs['help'] = SUPPRESS
+                    del kwargs['suppress']
+
+                group.add_argument(*cli, **kwargs)
+
+        self.set_defaults(**defaults)
+        structured.commandline.add_logging_group(self)
+
+    @property
+    def containers(self):
+        if self._containers:
+            return self._containers
+
+        containers = container_map[self.app]
+        self._containers = [c() for c in containers]
+        return self._containers
+
+    def validate(self, args):
+        for container in self.containers:
+            args = container.validate(self, args, self.context)
+        return args
+
+    def parse_args(self, *args, **kwargs):
+        return self.validate(ArgumentParser.parse_args(self, *args, **kwargs))
+
+    def parse_known_args(self, *args, **kwargs):
+        args, remainder = ArgumentParser.parse_known_args(self, *args, **kwargs)
+        return (self.validate(args), remainder)
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -48,23 +48,31 @@ from datetime import datetime
 from manifestparser import TestManifest
 from manifestparser.filters import (
     chunk_by_dir,
     chunk_by_runtime,
     chunk_by_slice,
     subsuite,
     tags,
 )
-from mochitest_options import MochitestOptions
+from mochitest_options import MochitestArgumentParser
 from mozprofile import Profile, Preferences
 from mozprofile.permissions import ServerLocations
 from urllib import quote_plus as encodeURIComponent
 from mozlog.structured.formatters import TbplFormatter
 from mozlog.structured import commandline
 
+here = os.path.abspath(os.path.dirname(__file__))
+
+try:
+    from mozbuild.base import MozbuildObject
+    build_obj = MozbuildObject.from_environment(cwd=here)
+except ImportError:
+    build_obj = None
+
 
 ###########################
 # Option for NSPR logging #
 ###########################
 
 # Set the desired log modules you want an NSPR log be produced by a try run for, or leave blank to disable the feature.
 # This will be passed to NSPR_LOG_MODULES environment variable. Try run will then put a download link for the log file
 # on tbpl.mozilla.org.
@@ -2590,34 +2598,39 @@ class Mochitest(MochitestUtilsMixin):
 
             rootdir = '/'.join(test['path'].split('/')[:-1])
             if rootdir not in dirlist:
                 dirlist.append(rootdir)
 
         return dirlist
 
 
-def main():
+def run_test_harness(options):
+    logger_options = {
+        key: value for key, value in vars(options).iteritems() if key.startswith('log')}
+    runner = Mochitest(logger_options)
+    result = runner.runTests(options)
+
+    # don't dump failures if running from automation as treeherder already displays them
+    if build_obj:
+        if runner.message_logger.errors:
+            result = 1
+            runner.message_logger.logger.warning("The following tests failed:")
+            for error in runner.message_logger.errors:
+                runner.message_logger.logger.log_raw(error)
+
+    runner.message_logger.finish()
+
+    return result
+
+
+def cli(args=sys.argv[1:]):
     # parse command line options
-    parser = MochitestOptions()
-    commandline.add_logging_group(parser)
-    options = parser.parse_args()
+    parser = MochitestArgumentParser(app='generic')
+    options = parser.parse_args(args)
     if options is None:
         # parsing error
         sys.exit(1)
-    logger_options = {
-        key: value for key,
-        value in vars(options).iteritems() if key.startswith('log')}
-    mochitest = Mochitest(logger_options)
-    options = parser.verifyOptions(options, mochitest)
-
-    options.utilityPath = mochitest.getFullPath(options.utilityPath)
-    options.certPath = mochitest.getFullPath(options.certPath)
-    if options.symbolsPath and len(urlparse(options.symbolsPath).scheme) < 2:
-        options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
-
-    return_code = mochitest.runTests(options)
-    mochitest.message_logger.finish()
-
-    sys.exit(return_code)
+
+    return run_test_harness(options)
 
 if __name__ == "__main__":
-    main()
+    sys.exit(cli())
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -12,17 +12,17 @@ import threading
 import traceback
 
 here = os.path.abspath(os.path.dirname(__file__))
 sys.path.insert(0, here)
 
 from automationutils import processLeakLog
 from runtests import Mochitest
 from runtests import MochitestUtilsMixin
-from mochitest_options import B2GOptions, MochitestOptions
+from mochitest_options import MochitestArgumentParser
 from marionette import Marionette
 from mozprofile import Profile, Preferences
 from mozlog import structured
 import mozinfo
 
 
 class B2GMochitest(MochitestUtilsMixin):
     marionette = None
@@ -412,46 +412,44 @@ class B2GDesktopMochitest(B2GMochitest, 
             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(parser, options):
+def run_remote_mochitests(options):
     # create our Marionette instance
     marionette_args = {
         'adb_path': options.adbPath,
         'emulator': options.emulator,
         'no_window': options.noWindow,
         'logdir': options.logdir,
         'busybox': options.busybox,
         'symbols_path': options.symbolsPath,
         'sdcard': options.sdcard,
         'homedir': options.b2gPath,
     }
     if options.marionette:
         host, port = options.marionette.split(':')
         marionette_args['host'] = host
         marionette_args['port'] = int(port)
 
-    options = parser.verifyRemoteOptions(options)
     if (options is None):
         print "ERROR: Invalid options specified, use --help for a list of valid options"
         sys.exit(1)
 
     mochitest = B2GDeviceMochitest(
         marionette_args,
         options,
         options.profile_data_dir,
         options.xrePath,
         remote_log_file=options.remoteLogFile)
 
-    options = parser.verifyOptions(options, mochitest)
     if (options is None):
         sys.exit(1)
 
     retVal = 1
     try:
         mochitest.cleanup(None, options)
         retVal = mochitest.run_tests(options)
     except:
@@ -464,52 +462,50 @@ def run_remote_mochitests(parser, option
             pass
         retVal = 1
 
     mochitest.message_logger.finish()
 
     sys.exit(retVal)
 
 
-def run_desktop_mochitests(parser, options):
+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)
-    options = MochitestOptions.verifyOptions(parser, options, mochitest)
     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']
 
     retVal = mochitest.runTests(options, onLaunch=mochitest.startTests)
     mochitest.message_logger.finish()
 
     sys.exit(retVal)
 
 
 def main():
-    parser = B2GOptions()
-    structured.commandline.add_logging_group(parser)
+    parser = MochitestArgumentParser(app='b2g')
     options = parser.parse_args()
 
     if options.desktop:
-        run_desktop_mochitests(parser, options)
+        run_desktop_mochitests(options)
     else:
-        run_remote_mochitests(parser, options)
+        run_remote_mochitests(options)
 
 if __name__ == "__main__":
     main()
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -13,23 +13,21 @@ 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 mochitest_options import RemoteOptions
-from mozlog import structured
+from mochitest_options import MochitestArgumentParser
 
 from manifestparser import TestManifest
 from manifestparser.filters import chunk_by_slice
 import devicemanager
-import droid
 import mozinfo
 
 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
 
 
 class MochiRemote(Mochitest):
     _automation = None
     _dm = None
@@ -456,67 +454,39 @@ class MochiRemote(Mochitest):
         kwargs['runSSLTunnel'] = False
 
         if 'quiet' in kwargs:
             kwargs.pop('quiet')
 
         return self._automation.runApp(*args, **kwargs)
 
 
-def main(args):
+def run_test_harness(options):
     message_logger = MessageLogger(logger=None)
     process_args = {'messageLogger': message_logger}
     auto = RemoteAutomation(None, "fennec", processArgs=process_args)
 
-    parser = RemoteOptions(auto)
-    structured.commandline.add_logging_group(parser)
-    options = parser.parse_args(args)
-
-    if (options.dm_trans == "adb"):
-        if (options.deviceIP):
-            dm = droid.DroidADB(
-                options.deviceIP,
-                options.devicePort,
-                deviceRoot=options.remoteTestRoot)
-        elif (options.deviceSerial):
-            dm = droid.DroidADB(
-                None,
-                None,
-                deviceSerial=options.deviceSerial,
-                deviceRoot=options.remoteTestRoot)
-        else:
-            dm = droid.DroidADB(deviceRoot=options.remoteTestRoot)
-    else:
-        dm = droid.DroidSUT(
-            options.deviceIP,
-            options.devicePort,
-            deviceRoot=options.remoteTestRoot)
-    auto.setDeviceManager(dm)
-    options = parser.verifyRemoteOptions(options, auto)
-
     if options is None:
         raise ValueError("Invalid options specified, use --help for a list of valid options")
 
+    dm = options.dm
+    auto.setDeviceManager(dm)
     mochitest = MochiRemote(auto, dm, options)
 
     log = mochitest.log
     message_logger.logger = log
     mochitest.message_logger = message_logger
 
     productPieces = options.remoteProductName.split('.')
     if (productPieces is not None):
         auto.setProduct(productPieces[0])
     else:
         auto.setProduct(options.remoteProductName)
     auto.setAppName(options.remoteappname)
 
-    options = parser.verifyOptions(options, mochitest)
-    if (options is None):
-        return 1
-
     logParent = os.path.dirname(options.remoteLogFile)
     dm.mkDir(logParent)
     auto.setRemoteLog(options.remoteLogFile)
     auto.setServerInfo(options.webServer, options.httpPort, options.sslPort)
 
     mochitest.printDeviceInfo()
 
     # Add Android version (SDK level) to mozinfo so that manifest entries
@@ -733,10 +703,17 @@ def main(args):
 
         mochitest.printDeviceInfo(printLogcat=True)
 
     message_logger.finish()
 
     return retVal
 
 
+def main(args=sys.argv[1:]):
+    parser = MochitestArgumentParser(app='android')
+    options = parser.parse_args(args)
+
+    return run_test_harness(options)
+
+
 if __name__ == "__main__":
-    sys.exit(main(sys.argv[1:]))
+    sys.exit(main())
--- a/testing/testsuite-targets.mk
+++ b/testing/testsuite-targets.mk
@@ -24,55 +24,55 @@ MOCHITESTS := mochitest-plain mochitest-
 mochitest:: $(MOCHITESTS)
 
 ifndef TEST_PACKAGE_NAME
 TEST_PACKAGE_NAME := $(ANDROID_PACKAGE_NAME)
 endif
 
 RUN_MOCHITEST_B2G_DESKTOP = \
   rm -f ./$@.log && \
-  $(PYTHON) _tests/testing/mochitest/runtestsb2g.py --autorun --close-when-done \
-    --console-level=INFO --log-tbpl=./$@.log \
+  $(PYTHON) _tests/testing/mochitest/runtestsb2g.py \
+    --log-tbpl=./$@.log \
     --desktop --profile ${GAIA_PROFILE_DIR} \
     --failure-file=$(abspath _tests/testing/mochitest/makefailures.json) \
     $(TEST_PATH_ARG) $(EXTRA_TEST_ARGS)
 
 RUN_MOCHITEST = \
   rm -f ./$@.log && \
-  $(PYTHON) _tests/testing/mochitest/runtests.py --autorun --close-when-done \
-    --console-level=INFO --log-tbpl=./$@.log \
+  $(PYTHON) _tests/testing/mochitest/runtests.py \
+    --log-tbpl=./$@.log \
     --failure-file=$(abspath _tests/testing/mochitest/makefailures.json) \
     --testing-modules-dir=$(abspath _tests/modules) \
     --extra-profile-file=$(DIST)/plugins \
     $(SYMBOLS_PATH) $(TEST_PATH_ARG) $(EXTRA_TEST_ARGS)
 
 RERUN_MOCHITEST = \
   rm -f ./$@.log && \
-  $(PYTHON) _tests/testing/mochitest/runtests.py --autorun --close-when-done \
-    --console-level=INFO --log-tbpl=./$@.log \
+  $(PYTHON) _tests/testing/mochitest/runtests.py \
+    --log-tbpl=./$@.log \
     --run-only-tests=makefailures.json \
     --testing-modules-dir=$(abspath _tests/modules) \
     --extra-profile-file=$(DIST)/plugins \
     $(SYMBOLS_PATH) $(TEST_PATH_ARG) $(EXTRA_TEST_ARGS)
 
 RUN_MOCHITEST_REMOTE = \
   rm -f ./$@.log && \
-  $(PYTHON) _tests/testing/mochitest/runtestsremote.py --autorun --close-when-done \
-    --console-level=INFO --log-tbpl=./$@.log $(DM_FLAGS) --dm_trans=$(DM_TRANS) \
+  $(PYTHON) _tests/testing/mochitest/runtestsremote.py \
+    --log-tbpl=./$@.log $(DM_FLAGS) --dm_trans=$(DM_TRANS) \
     --app=$(TEST_PACKAGE_NAME) --deviceIP=${TEST_DEVICE} --xre-path=${MOZ_HOST_BIN} \
     --testing-modules-dir=$(abspath _tests/modules) \
     $(SYMBOLS_PATH) $(TEST_PATH_ARG) $(EXTRA_TEST_ARGS)
 
 RUN_MOCHITEST_ROBOCOP = \
   rm -f ./$@.log && \
   $(PYTHON) _tests/testing/mochitest/runtestsremote.py \
     --robocop-apk=$(DEPTH)/build/mobile/robocop/robocop-debug.apk \
     --robocop-ids=$(DEPTH)/mobile/android/base/fennec_ids.txt \
     --robocop-ini=_tests/testing/mochitest/robocop.ini \
-    --console-level=INFO --log-tbpl=./$@.log $(DM_FLAGS) --dm_trans=$(DM_TRANS) \
+    --log-tbpl=./$@.log $(DM_FLAGS) --dm_trans=$(DM_TRANS) \
     --app=$(TEST_PACKAGE_NAME) --deviceIP=${TEST_DEVICE} --xre-path=${MOZ_HOST_BIN} \
     $(SYMBOLS_PATH) $(TEST_PATH_ARG) $(EXTRA_TEST_ARGS)
 
 ifndef NO_FAIL_ON_TEST_ERRORS
 define check_test_error_internal
   @errors=`grep 'TEST-UNEXPECTED-' $@.log` ;\
   if test "$$errors" ; then \
 	  echo '$@ failed:'; \