Bug 1193257 - Make xpcshell harness command line arguments path filters for tests, r=ahal
authorJames Graham <james@hoppipolla.co.uk>
Thu, 27 Aug 2015 13:05:50 +0100
changeset 295629 9dfa459ee7e564b5f2bb79b03e9742a9394a24bc
parent 295628 46b3878338c22fa1ba8e662fa754adb272fec08e
child 295630 5c715c7f22c32df4c1425a26d4389ffb761f6a70
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersahal
bugs1193257
milestone43.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 1193257 - Make xpcshell harness command line arguments path filters for tests, r=ahal
testing/mozharness/configs/unittests/linux_unittest.py
testing/xpcshell/Makefile.in
testing/xpcshell/mach_commands.py
testing/xpcshell/remotexpcshelltests.py
testing/xpcshell/runtestsb2g.py
testing/xpcshell/runxpcshelltests.py
testing/xpcshell/xpcshellcommandline.py
--- a/testing/mozharness/configs/unittests/linux_unittest.py
+++ b/testing/mozharness/configs/unittests/linux_unittest.py
@@ -216,21 +216,23 @@ config = {
                                   'MOZ_DISABLE_CONTEXT_SHARING_GLX': '1'},
                           'options': ['--setpref=browser.tabs.remote=true',
                                       '--setpref=browser.tabs.remote.autostart=true',
                                       '--setpref=layers.offmainthreadcomposition.testing.enabled=true',
                                       '--setpref=layers.async-pan-zoom.enabled=true',
                                       'tests/reftest/tests/testing/crashtest/crashtests.list']},
     },
     "all_xpcshell_suites": {
-        "xpcshell": ["--manifest=tests/xpcshell/tests/all-test-dirs.list",
-                     "%(abs_app_dir)s/" + XPCSHELL_NAME],
-        "xpcshell-addons": ["--manifest=tests/xpcshell/tests/all-test-dirs.list",
-                            "--tag=addons",
-                            "%(abs_app_dir)s/" + XPCSHELL_NAME]
+        "xpcshell": {'options': ["--xpcshell=%(abs_app_dir)s/" + XPCSHELL_NAME,
+                                 "--manifest=tests/xpcshell/tests/all-test-dirs.list"],
+                     'tests': []},
+        "xpcshell-addons": {'options': ["--xpcshell=%(abs_app_dir)s/" + XPCSHELL_NAME,
+                                        "--manifest=tests/xpcshell/tests/all-test-dirs.list",
+                                        "--tag=addons"],
+                            'tests': []}
     },
     "all_cppunittest_suites": {
         "cppunittest": ['tests/cppunittest']
     },
     "all_gtest_suites": {
         "gtest": []
     },
     "all_jittest_suites": {
--- a/testing/xpcshell/Makefile.in
+++ b/testing/xpcshell/Makefile.in
@@ -5,16 +5,17 @@
 
 include $(topsrcdir)/config/rules.mk
 
 # Harness files from the srcdir
 TEST_HARNESS_FILES := \
   runxpcshelltests.py \
   remotexpcshelltests.py \
   runtestsb2g.py \
+  xpcshellcommandline.py \
   head.js \
   node-spdy \
   moz-spdy \
   node-http2 \
   moz-http2 \
   $(NULL)
 
 # Extra files needed from $(topsrcdir)/build
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -20,192 +20,113 @@ from mozbuild.base import (
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
-_parser = argparse.ArgumentParser()
-structured.commandline.add_logging_group(_parser)
+from xpcshellcommandline import parser_desktop, parser_remote, parser_b2g
 
 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()
 
 BUSYBOX_URLS = {
     'arm': 'http://www.busybox.net/downloads/binaries/latest/busybox-armv7l',
     'x86': 'http://www.busybox.net/downloads/binaries/latest/busybox-i686'
 }
 
+here = os.path.abspath(os.path.dirname(__file__))
 
 if sys.version_info[0] < 3:
     unicode_type = unicode
 else:
     unicode_type = str
 
 # This should probably be consolidated with similar classes in other test
 # runners.
 class InvalidTestPathError(Exception):
     """Exception raised when the test path is not valid."""
 
 
 class XPCShellRunner(MozbuildObject):
     """Run xpcshell tests."""
     def run_suite(self, **kwargs):
-        from manifestparser import TestManifest
-        manifest = TestManifest(manifests=[os.path.join(self.topobjdir,
-            '_tests', 'xpcshell', 'xpcshell.ini')])
-
-        return self._run_xpcshell_harness(manifest=manifest, **kwargs)
+        return self._run_xpcshell_harness(**kwargs)
 
-    def run_test(self, test_paths, interactive=False,
-                 keep_going=False, sequential=False, shuffle=False,
-                 debugger=None, debuggerArgs=None, debuggerInteractive=None,
-                 jsDebugger=False, jsDebuggerPort=None,
-                 rerun_failures=False, test_objects=None, verbose=False,
-                 log=None, test_tags=None, dump_tests=None,
-                 # ignore parameters from other platforms' options
-                 **kwargs):
+    def run_test(self, **kwargs):
         """Runs an individual xpcshell test."""
         from mozbuild.testing import TestResolver
         from manifestparser import TestManifest
 
         # TODO Bug 794506 remove once mach integrates with virtualenv.
         build_path = os.path.join(self.topobjdir, 'build')
         if build_path not in sys.path:
             sys.path.append(build_path)
 
         src_build_path = os.path.join(self.topsrcdir, 'mozilla', 'build')
         if os.path.isdir(src_build_path):
             sys.path.append(src_build_path)
 
-        if test_paths == 'all':
-            self.run_suite(interactive=interactive,
-                           keep_going=keep_going, shuffle=shuffle, sequential=sequential,
-                           debugger=debugger, debuggerArgs=debuggerArgs,
-                           debuggerInteractive=debuggerInteractive,
-                           jsDebugger=jsDebugger, jsDebuggerPort=jsDebuggerPort,
-                           rerun_failures=rerun_failures,
-                           verbose=verbose, log=log, test_tags=test_tags, dump_tests=dump_tests)
-            return
-        elif test_paths:
-            test_paths = [self._wrap_path_argument(p).relpath() for p in test_paths]
-
-        if test_objects:
-            tests = test_objects
-        else:
-            resolver = self._spawn(TestResolver)
-            tests = list(resolver.resolve_tests(paths=test_paths,
-                flavor='xpcshell'))
-
-        if not tests:
-            raise InvalidTestPathError('We could not find an xpcshell test '
-                'for the passed test path. Please select a path that is '
-                'a test file or is a directory containing xpcshell tests.')
-
-        # Dynamically write out a manifest holding all the discovered tests.
-        manifest = TestManifest()
-        manifest.tests.extend(tests)
+        self.run_suite(**kwargs)
 
-        args = {
-            'interactive': interactive,
-            'keep_going': keep_going,
-            'shuffle': shuffle,
-            'sequential': sequential,
-            'debugger': debugger,
-            'debuggerArgs': debuggerArgs,
-            'debuggerInteractive': debuggerInteractive,
-            'jsDebugger': jsDebugger,
-            'jsDebuggerPort': jsDebuggerPort,
-            'rerun_failures': rerun_failures,
-            'manifest': manifest,
-            'verbose': verbose,
-            'log': log,
-            'test_tags': test_tags,
-            'dump_tests': dump_tests,
-        }
 
-        return self._run_xpcshell_harness(**args)
-
-    def _run_xpcshell_harness(self, manifest,
-                              test_path=None, shuffle=False, interactive=False,
-                              keep_going=False, sequential=False,
-                              debugger=None, debuggerArgs=None, debuggerInteractive=None,
-                              jsDebugger=False, jsDebuggerPort=None,
-                              rerun_failures=False, verbose=False, log=None, test_tags=None,
-                              dump_tests=None):
-
+    def _run_xpcshell_harness(self, **kwargs):
         # Obtain a reference to the xpcshell test runner.
         import runxpcshelltests
 
+        log = kwargs.pop("log")
+
         xpcshell = runxpcshelltests.XPCShellTests(log=log)
         self.log_manager.enable_unstructured()
 
         tests_dir = os.path.join(self.topobjdir, '_tests', 'xpcshell')
         modules_dir = os.path.join(self.topobjdir, '_tests', 'modules')
         # We want output from the test to be written immediately if we are only
         # running a single test.
-        single_test = (test_path is not None or
+        single_test = (kwargs["testPaths"] is not None or
                        (manifest and len(manifest.test_paths())==1))
-        sequential = sequential or single_test
+        sequential = kwargs["sequential"] or single_test
+
+        if kwargs["xpcshell"] is None:
+            kwargs["xpcshell"] = self.get_binary_path('xpcshell')
+
+        if kwargs["mozInfo"] is None:
+            kwargs["mozInfo"] = os.path.join(self.topobjdir, 'mozinfo.json')
+
+        if kwargs["symbolsPath"] is None:
+            kwargs["symbolsPath"] = os.path.join(self.distdir, 'crashreporter-symbols')
 
-        args = {
-            'manifest': manifest,
-            'xpcshell': self.get_binary_path('xpcshell'),
-            'mozInfo': os.path.join(self.topobjdir, 'mozinfo.json'),
-            'symbolsPath': os.path.join(self.distdir, 'crashreporter-symbols'),
-            'interactive': interactive,
-            'keepGoing': keep_going,
-            'logfiles': False,
-            'sequential': sequential,
-            'shuffle': shuffle,
-            'testingModulesDir': modules_dir,
-            'profileName': 'firefox',
-            'verbose': verbose or single_test,
-            'xunitFilename': os.path.join(self.statedir, 'xpchsell.xunit.xml'),
-            'xunitName': 'xpcshell',
-            'pluginsPath': os.path.join(self.distdir, 'plugins'),
-            'debugger': debugger,
-            'debuggerArgs': debuggerArgs,
-            'debuggerInteractive': debuggerInteractive,
-            'jsDebugger': jsDebugger,
-            'jsDebuggerPort': jsDebuggerPort,
-            'test_tags': test_tags,
-            'dump_tests': dump_tests,
-            'utility_path': self.bindir,
-        }
+        if kwargs["logfiles"] is None:
+            kwargs["logfiles"] = False
+
+        if kwargs["profileName"] is None:
+            kwargs["profileName"] = "firefox"
+
+        if kwargs["pluginsPath"] is None:
+            kwargs['pluginsPath'] = os.path.join(self.distdir, 'plugins')
 
-        if test_path is not None:
-            args['testPath'] = test_path
+        if kwargs["utility_path"] is None:
+            kwargs['utility_path'] = self.bindir
 
-        # A failure manifest is written by default. If --rerun-failures is
-        # specified and a prior failure manifest is found, the prior manifest
-        # will be run. A new failure manifest is always written over any
-        # prior failure manifest.
-        failure_manifest_path = os.path.join(self.statedir, 'xpcshell.failures.ini')
-        rerun_manifest_path = os.path.join(self.statedir, 'xpcshell.rerun.ini')
-        if os.path.exists(failure_manifest_path) and rerun_failures:
-            shutil.move(failure_manifest_path, rerun_manifest_path)
-            args['manifest'] = rerun_manifest_path
-        elif os.path.exists(failure_manifest_path):
-            os.remove(failure_manifest_path)
-        elif rerun_failures:
-            print("No failures were found to re-run.")
-            return 0
-        args['failureManifest'] = failure_manifest_path
+        if kwargs["manifest"] is None:
+            kwargs["manifest"] = os.path.join(tests_dir, "xpcshell.ini")
+
+        if kwargs["failure_manifest"] is None:
+            kwargs["failure_manifest"] = os.path.join(self.statedir, 'xpcshell.failures.ini')
 
         # Python through 2.7.2 has issues with unicode in some of the
         # arguments. Work around that.
         filtered_args = {}
-        for k, v in args.items():
+        for k, v in kwargs.iteritems():
             if isinstance(v, unicode_type):
                 v = v.encode('utf-8')
 
             if isinstance(k, unicode_type):
                 k = k.encode('utf-8')
 
             filtered_args[k] = v
 
@@ -231,83 +152,71 @@ class AndroidXPCShellRunner(MozbuildObje
         else:
             if ip:
                 dm = mozdevice.DroidSUT(ip, port, deviceRoot=remote_test_root)
             else:
                 raise Exception("You must provide a device IP to connect to via the --ip option")
         return dm
 
     """Run Android xpcshell tests."""
-    def run_test(self,
-                 test_paths, keep_going,
-                 devicemanager, ip, port, remote_test_root, no_setup, local_apk,
-                 test_objects=None, log=None,
-                 # ignore parameters from other platforms' options
-                 **kwargs):
+    def run_test(self, **kwargs):
         # TODO Bug 794506 remove once mach integrates with virtualenv.
         build_path = os.path.join(self.topobjdir, 'build')
         if build_path not in sys.path:
             sys.path.append(build_path)
 
         import remotexpcshelltests
 
-        dm = self.get_devicemanager(devicemanager, ip, port, remote_test_root)
+        dm = self.get_devicemanager(kwargs["dm_trans"], kwargs["deviceIP"], kwargs["devicePort"],
+                                    kwargs["remoteTestRoot"])
+
+        log = kwargs.pop("log")
+        self.log_manager.enable_unstructured()
+
+        if kwargs["xpcshell"] is None:
+            kwargs["xpcshell"] = "xpcshell"
+
+        if not kwargs["objdir"]:
+            kwargs["objdir"] = self.topobjdir
+
+        if not kwargs["localLib"]:
+            kwargs["localLib"] = os.path.join(self.topobjdir, 'dist/fennec')
+
+        if not kwargs["localBin"]:
+            kwargs["localBin"] = os.path.join(self.topobjdir, 'dist/bin')
 
-        options = remotexpcshelltests.RemoteXPCShellOptions()
-        options.shuffle = False
-        options.sequential = True
-        options.interactive = False
-        options.debugger = None
-        options.debuggerArgs = None
-        options.setup = not no_setup
-        options.keepGoing = keep_going
-        options.objdir = self.topobjdir
-        options.localLib = os.path.join(self.topobjdir, 'dist/fennec')
-        options.localBin = os.path.join(self.topobjdir, 'dist/bin')
-        options.testingModulesDir = os.path.join(self.topobjdir, '_tests/modules')
-        options.mozInfo = os.path.join(self.topobjdir, 'mozinfo.json')
-        options.manifest = os.path.join(self.topobjdir, '_tests/xpcshell/xpcshell.ini')
-        options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols')
-        if local_apk:
-            options.localAPK = local_apk
-        else:
-            for file in os.listdir(os.path.join(options.objdir, "dist")):
-                if file.endswith(".apk") and file.startswith("fennec"):
-                    options.localAPK = os.path.join(options.objdir, "dist")
-                    options.localAPK = os.path.join(options.localAPK, file)
-                    print ("using APK: " + options.localAPK)
+        if not kwargs["testingModulesDir"]:
+            kwargs["testingModulesDir"] = os.path.join(self.topobjdir, '_tests/modules')
+
+        if not kwargs["mozInfo"]:
+            kwargs["mozInfo"] = os.path.join(self.topobjdir, 'mozinfo.json')
+
+        if not kwargs["manifest"]:
+            kwargs["manifest"] = os.path.join(self.topobjdir, '_tests/xpcshell/xpcshell.ini')
+
+        if not kwargs["symbolsPath"]:
+            kwargs["symbolsPath"] = os.path.join(self.distdir, 'crashreporter-symbols')
+
+        if not kwargs["localAPK"]:
+            for file_name in os.listdir(os.path.join(kwargs["objdir"], "dist")):
+                if file_name.endswith(".apk") and file_name.startswith("fennec"):
+                    kwargs["localAPK"] = os.path.join(kwargs["objdir"], "dist", file_name)
+                    print ("using APK: %s" % kwargs["localAPK"])
                     break
             else:
                 raise Exception("You must specify an APK")
 
-        if test_paths == 'all':
-            testdirs = []
-            options.testPath = None
-            options.verbose = False
-        elif test_objects:
-            if len(test_objects) > 1:
-                print('Warning: only the first test will be used.')
-            testdirs = test_objects[0]['dir_relpath']
-            options.testPath = test_objects[0]['path']
-            options.verbose = True
-        else:
-            if len(test_paths) > 1:
-                print('Warning: only the first test path argument will be used.')
-            testdirs = test_paths[0]
-            options.testPath = test_paths[0]
-            options.verbose = True
+        options = argparse.Namespace(**kwargs)
+        xpcshell = remotexpcshelltests.XPCShellRemote(dm, options, log)
 
-        xpcshell = remotexpcshelltests.XPCShellRemote(dm, options, testdirs, log)
+        result = xpcshell.runTests(testClass=remotexpcshelltests.RemoteXPCShellTestThread,
+                                   mobileArgs=xpcshell.mobileArgs,
+                                   **vars(options))
 
-        result = xpcshell.runTests(xpcshell='xpcshell',
-                      testClass=remotexpcshelltests.RemoteXPCShellTestThread,
-                      testdirs=testdirs,
-                      mobileArgs=xpcshell.mobileArgs,
-                      **options.__dict__)
-
+        self.log_manager.disable_unstructured()
 
         return int(not result)
 
 class B2GXPCShellRunner(MozbuildObject):
     def __init__(self, *args, **kwargs):
         MozbuildObject.__init__(self, *args, **kwargs)
 
         # TODO Bug 794506 remove once mach integrates with virtualenv.
@@ -344,131 +253,90 @@ class B2GXPCShellRunner(MozbuildObject):
             print('There was a problem downloading busybox. Proceeding without it,' \
                   'initial setup will be slow.')
             return
 
         with open(busybox_path, 'wb') as f:
             f.write(data.read())
         return busybox_path
 
-    def run_test(self, test_paths, b2g_home=None, busybox=None, device_name=None,
-                 test_objects=None, log=None,
-                 # ignore parameters from other platforms' options
-                 **kwargs):
+    def run_test(self, **kwargs):
         try:
             import which
             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', kwargs["b2g_home"]))
             sys.exit(1)
 
-        test_path = None
-        if test_objects:
-            if len(test_objects) > 1:
-                print('Warning: Only the first test will be used.')
-
-            test_path = self._wrap_path_argument(test_objects[0]['path'])
-        elif 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()
-
         import runtestsb2g
-        parser = runtestsb2g.B2GOptions()
-        options, args = parser.parse_args([])
+
+        log = kwargs.pop("log")
+        self.log_manager.enable_unstructured()
+
+        if kwargs["xpcshell"] is None:
+            kwargs["xpcshell"] = "xpcshell"
+        if kwargs["b2g_path"] is None:
+            kwargs["b2g_path"] = kwargs["b2g_home"]
+        if kwargs["busybox"] is None:
+            kwargs["busybox"] = os.environ.get('BUSYBOX')
+        if kwargs["busybox"] is None:
+            kwargs["busybox"] = self._download_busybox(kwargs["b2g_home"], kwargs["emulator"])
 
-        options.b2g_path = b2g_home
-        options.busybox = busybox or os.environ.get('BUSYBOX')
-        options.localLib = self.bin_dir
-        options.localBin = self.bin_dir
-        options.logdir = self.xpcshell_dir
-        options.manifest = os.path.join(self.xpcshell_dir, 'xpcshell.ini')
-        options.mozInfo = os.path.join(self.topobjdir, 'mozinfo.json')
-        options.objdir = self.topobjdir
-        options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols'),
-        options.testingModulesDir = os.path.join(self.tests_dir, 'modules')
-        options.testPath = test_path
-        options.use_device_libs = True
+        if kwargs["localLib"] is None:
+            kwargs["localLib"] = self.bin_dir
+        if kwargs["localBin"] is None:
+            kwargs["localBin"] = self.bin_dir
+        if kwargs["logdir"] is None:
+            kwargs["logdir"] = self.xpcshell_dir
+        if kwargs["manifest"] is None:
+            kwargs["manifest"] = os.path.join(self.xpcshell_dir, 'xpcshell.ini')
+        if kwargs["mozInfo"] is None:
+            kwargs["mozInfo"] = os.path.join(self.topobjdir, 'mozinfo.json')
+        if kwargs["objdir"] is None:
+            kwargs["objdir"] = self.topobjdir
+        if kwargs["symbolsPath"] is None:
+            kwargs["symbolsPath"] = os.path.join(self.distdir, 'crashreporter-symbols')
+        if kwargs["testingModulesDir"] is None:
+            kwargs["testingModulesDir"] = os.path.join(self.tests_dir, 'modules')
+        if kwargs["use_device_libs"] is None:
+            kwargs["use_device_libs"] = True
 
-        options.emulator = 'arm'
-        if device_name.startswith('emulator'):
-            if 'x86' in device_name:
-                options.emulator = 'x86'
+        if kwargs["device_name"].startswith('emulator') and 'x86' in kwargs["device_name"]:
+            kwargs["emulator"] = 'x86'
+
+        parser = parser_b2g()
+        options = argparse.Namespace(**kwargs)
+        rv = runtestsb2g.run_remote_xpcshell(parser, options, log)
 
-        if not options.busybox:
-            options.busybox = self._download_busybox(b2g_home, options.emulator)
+        self.log_manager.disable_unstructured()
+        return rv
 
-        return runtestsb2g.run_remote_xpcshell(parser, options, args, log)
+def get_parser():
+    build_obj = MozbuildObject.from_environment(cwd=here)
+    if conditions.is_android(build_obj):
+        return parser_remote()
+    elif conditions.is_b2g(build_obj):
+        return parser_b2g()
+    else:
+        return parser_desktop()
 
 @CommandProvider
 class MachCommands(MachCommandBase):
     def __init__(self, context):
         MachCommandBase.__init__(self, context)
 
         for attr in ('b2g_home', 'device_name'):
             setattr(self, attr, getattr(context, attr, None))
 
     @Command('xpcshell-test', category='testing',
-        description='Run XPCOM Shell tests (API direct unit testing)',
-        conditions=[lambda *args: True],
-        parser=_parser)
-    @CommandArgument('test_paths', default='all', nargs='*', metavar='TEST',
-        help='Test to run. Can be specified as a single JS file, a directory, '
-             'or omitted. If omitted, the entire test suite is executed.')
-    @CommandArgument('--verbose', '-v', action='store_true',
-        help='Provide full output from each test process.')
-    @CommandArgument("--debugger", default=None, metavar='DEBUGGER',
-                     help = "Run xpcshell under the given debugger.")
-    @CommandArgument("--debugger-args", default=None, metavar='ARGS', type=str,
-                     dest = "debuggerArgs",
-                     help = "pass the given args to the debugger _before_ "
-                            "the application on the command line")
-    @CommandArgument("--debugger-interactive", action = "store_true",
-                     dest = "debuggerInteractive",
-                     help = "prevents the test harness from redirecting "
-                            "stdout and stderr for interactive debuggers")
-    @CommandArgument("--jsdebugger", dest="jsDebugger", action="store_true",
-                     help="Waits for a devtools JS debugger to connect before "
-                          "starting the test.")
-    @CommandArgument("--jsdebugger-port", dest="jsDebuggerPort",
-                     type=int, default=6000,
-                     help="The port to listen on for a debugger connection if "
-                          "--jsdebugger is specified (default=6000).")
-    @CommandArgument('--interactive', '-i', action='store_true',
-        help='Open an xpcshell prompt before running tests.')
-    @CommandArgument('--keep-going', '-k', action='store_true',
-        help='Continue running tests after a SIGINT is received.')
-    @CommandArgument('--sequential', action='store_true',
-        help='Run the tests sequentially.')
-    @CommandArgument('--shuffle', '-s', action='store_true',
-        help='Randomize the execution order of tests.')
-    @CommandArgument('--rerun-failures', action='store_true',
-        help='Reruns failures from last time.')
-    @CommandArgument('--tag', action='append', dest='test_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.')
-    @CommandArgument('--dump-tests', default=None, type=str, dest='dump_tests',
-        help='Specify path to a filename to dump all the tests that will be run')
-    @CommandArgument('--devicemanager', default='adb', type=str,
-        help='(Android) Type of devicemanager to use for communication: adb or sut')
-    @CommandArgument('--ip', type=str, default=None,
-        help='(Android) IP address of device')
-    @CommandArgument('--port', type=int, default=20701,
-        help='(Android) Port of device')
-    @CommandArgument('--remote_test_root', type=str, default=None,
-        help='(Android) Remote test root such as /mnt/sdcard or /data/local')
-    @CommandArgument('--no-setup', action='store_true',
-        help='(Android) Do not copy files to device')
-    @CommandArgument('--local-apk', type=str, default=None,
-        help='(Android) Use specified Fennec APK')
-    @CommandArgument('--busybox', type=str, default=None,
-        help='(B2G) Path to busybox binary (speeds up installation of tests).')
+             description='Run XPCOM Shell tests (API direct unit testing)',
+             conditions=[lambda *args: True],
+             parser=get_parser)
+
     def run_xpcshell_test(self, **params):
         from mozbuild.controller.building import BuildDriver
 
         # We should probably have a utility function to ensure the tree is
         # ready to run tests. Until then, we just create the state dir (in
         # case the tree wasn't built with mach).
         self._ensure_state_subdir_exists('.')
 
--- a/testing/xpcshell/remotexpcshelltests.py
+++ b/testing/xpcshell/remotexpcshelltests.py
@@ -11,16 +11,18 @@ import runxpcshelltests as xpcshell
 import tempfile
 from zipfile import ZipFile
 from mozlog import commandline
 import shutil
 import mozdevice
 import mozfile
 import mozinfo
 
+from xpcshellcommandline import parser_remote
+
 here = os.path.dirname(os.path.abspath(__file__))
 
 def remoteJoin(path1, path2):
     return posixpath.join(path1, path2)
 
 class RemoteXPCShellTestThread(xpcshell.XPCShellTestThread):
     def __init__(self, *args, **kwargs):
         xpcshell.XPCShellTestThread.__init__(self, *args, **kwargs)
@@ -100,25 +102,25 @@ class RemoteXPCShellTestThread(xpcshell.
 
                 yield path
 
         self.remoteHere = self.remoteForLocal(test['here'])
 
         return (list(sanitize_list(test['head'], 'head')),
                 list(sanitize_list(test['tail'], 'tail')))
 
-    def buildXpcsCmd(self, testdir):
+    def buildXpcsCmd(self):
         # change base class' paths to remote paths and use base class to build command
         self.xpcshell = remoteJoin(self.remoteBinDir, "xpcw")
         self.headJSPath = remoteJoin(self.remoteScriptsDir, 'head.js')
         self.httpdJSPath = remoteJoin(self.remoteComponentsDir, 'httpd.js')
         self.httpdManifest = remoteJoin(self.remoteComponentsDir, 'httpd.manifest')
         self.testingModulesDir = self.remoteModulesDir
         self.testharnessdir = self.remoteScriptsDir
-        xpcshell.XPCShellTestThread.buildXpcsCmd(self, testdir)
+        xpcshell.XPCShellTestThread.buildXpcsCmd(self)
         # remove "-g <dir> -a <dir>" and add "--greomni <apk>"
         del(self.xpcsCmd[1:5])
         if self.options.localAPK:
             self.xpcsCmd.insert(3, '--greomni')
             self.xpcsCmd.insert(4, self.remoteAPK)
 
         if self.remoteDebugger:
             # for example, "/data/local/gdbserver" "localhost:12345"
@@ -211,17 +213,17 @@ class RemoteXPCShellTestThread(xpcshell.
             if f is not None:
                 f.close()
 
 
 # A specialization of XPCShellTests that runs tests on an Android device
 # via devicemanager.
 class XPCShellRemote(xpcshell.XPCShellTests, object):
 
-    def __init__(self, devmgr, options, args, log):
+    def __init__(self, devmgr, options, log):
         xpcshell.XPCShellTests.__init__(self, log)
 
         # Add Android version (SDK level) to mozinfo so that manifest entries
         # can be conditional on android_version.
         androidVersion = devmgr.shellCheckOutput(['getprop', 'ro.build.version.sdk'])
         mozinfo.info['android_version'] = androidVersion
 
         self.localLib = options.localLib
@@ -512,169 +514,111 @@ class XPCShellRemote(xpcshell.XPCShellTe
             # Foopies have an older mozdevice ver without retryLimit
             self.device.pushDir(self.xpcDir, self.remoteScriptsDir)
 
     def setupMinidumpDir(self):
         if self.device.dirExists(self.remoteMinidumpDir):
             self.device.removeDir(self.remoteMinidumpDir)
         self.device.mkDir(self.remoteMinidumpDir)
 
-    def buildTestList(self, test_tags=None):
-        xpcshell.XPCShellTests.buildTestList(self, test_tags=test_tags)
+    def buildTestList(self, test_tags=None, test_paths=None):
+        xpcshell.XPCShellTests.buildTestList(self, test_tags=test_tags, test_paths=test_paths)
         uniqueTestPaths = set([])
         for test in self.alltests:
             uniqueTestPaths.add(test['here'])
         for testdir in uniqueTestPaths:
             abbrevTestDir = os.path.relpath(testdir, self.xpcDir)
             remoteScriptDir = remoteJoin(self.remoteScriptsDir, abbrevTestDir)
             self.pathMapping.append(PathMapping(testdir, remoteScriptDir))
 
-class RemoteXPCShellOptions(xpcshell.XPCShellOptions):
-
-    def __init__(self):
-        xpcshell.XPCShellOptions.__init__(self)
-        defaults = {}
-
-        self.add_option("--deviceIP", action="store",
-                        type = "string", dest = "deviceIP",
-                        help = "ip address of remote device to test")
-        defaults["deviceIP"] = None
-
-        self.add_option("--devicePort", action="store",
-                        type = "string", dest = "devicePort",
-                        help = "port of remote device to test")
-        defaults["devicePort"] = 20701
-
-        self.add_option("--dm_trans", action="store",
-                        type = "string", dest = "dm_trans",
-                        help = "the transport to use to communicate with device: [adb|sut]; default=sut")
-        defaults["dm_trans"] = "sut"
-
-        self.add_option("--objdir", action="store",
-                        type = "string", dest = "objdir",
-                        help = "local objdir, containing xpcshell binaries")
-        defaults["objdir"] = None
-
-        self.add_option("--apk", action="store",
-                        type = "string", dest = "localAPK",
-                        help = "local path to Fennec APK")
-        defaults["localAPK"] = None
-
-        self.add_option("--noSetup", action="store_false",
-                        dest = "setup",
-                        help = "do not copy any files to device (to be used only if device is already setup)")
-        defaults["setup"] = True
-
-        self.add_option("--local-lib-dir", action="store",
-                        type = "string", dest = "localLib",
-                        help = "local path to library directory")
-        defaults["localLib"] = None
+def verifyRemoteOptions(parser, options):
+    if options.localLib is None:
+        if options.localAPK and options.objdir:
+            for path in ['dist/fennec', 'fennec/lib']:
+                options.localLib = os.path.join(options.objdir, path)
+                if os.path.isdir(options.localLib):
+                    break
+            else:
+                parser.error("Couldn't find local library dir, specify --local-lib-dir")
+        elif options.objdir:
+            options.localLib = os.path.join(options.objdir, 'dist/bin')
+        elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
+            # assume tests are being run from a tests.zip
+            options.localLib = os.path.abspath(os.path.join(here, '..', 'bin'))
+        else:
+            parser.error("Couldn't find local library dir, specify --local-lib-dir")
 
-        self.add_option("--local-bin-dir", action="store",
-                        type = "string", dest = "localBin",
-                        help = "local path to bin directory")
-        defaults["localBin"] = None
-
-        self.add_option("--remoteTestRoot", action = "store",
-                    type = "string", dest = "remoteTestRoot",
-                    help = "remote directory to use as test root (eg. /mnt/sdcard/tests or /data/local/tests)")
-        defaults["remoteTestRoot"] = None
-
-        self.set_defaults(**defaults)
-
-    def verifyRemoteOptions(self, options):
-        if options.localLib is None:
-            if options.localAPK and options.objdir:
-                for path in ['dist/fennec', 'fennec/lib']:
-                    options.localLib = os.path.join(options.objdir, path)
-                    if os.path.isdir(options.localLib):
-                        break
-                else:
-                    self.error("Couldn't find local library dir, specify --local-lib-dir")
-            elif options.objdir:
-                options.localLib = os.path.join(options.objdir, 'dist/bin')
-            elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
-                # assume tests are being run from a tests.zip
-                options.localLib = os.path.abspath(os.path.join(here, '..', 'bin'))
+    if options.localBin is None:
+        if options.objdir:
+            for path in ['dist/bin', 'bin']:
+                options.localBin = os.path.join(options.objdir, path)
+                if os.path.isdir(options.localBin):
+                    break
             else:
-                self.error("Couldn't find local library dir, specify --local-lib-dir")
-
-        if options.localBin is None:
-            if options.objdir:
-                for path in ['dist/bin', 'bin']:
-                    options.localBin = os.path.join(options.objdir, path)
-                    if os.path.isdir(options.localBin):
-                        break
-                else:
-                    self.error("Couldn't find local binary dir, specify --local-bin-dir")
-            elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
-                # assume tests are being run from a tests.zip
-                options.localBin = os.path.abspath(os.path.join(here, '..', 'bin'))
-            else:
-                self.error("Couldn't find local binary dir, specify --local-bin-dir")
-        return options
+                parser.error("Couldn't find local binary dir, specify --local-bin-dir")
+        elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
+            # assume tests are being run from a tests.zip
+            options.localBin = os.path.abspath(os.path.join(here, '..', 'bin'))
+        else:
+            parser.error("Couldn't find local binary dir, specify --local-bin-dir")
+    return options
 
 class PathMapping:
 
     def __init__(self, localDir, remoteDir):
         self.local = localDir
         self.remote = remoteDir
 
 def main():
-
     if sys.version_info < (2,7):
         print >>sys.stderr, "Error: You must use python version 2.7 or newer but less than 3.0"
         sys.exit(1)
 
-    parser = RemoteXPCShellOptions()
+    parser = parser_remote()
     commandline.add_logging_group(parser)
-    options, args = parser.parse_args()
+    options = parser.parse_args()
     if not options.localAPK:
         for file in os.listdir(os.path.join(options.objdir, "dist")):
             if (file.endswith(".apk") and file.startswith("fennec")):
                 options.localAPK = os.path.join(options.objdir, "dist")
                 options.localAPK = os.path.join(options.localAPK, file)
                 print >>sys.stderr, "using APK: " + options.localAPK
                 break
         else:
             print >>sys.stderr, "Error: please specify an APK"
             sys.exit(1)
 
-    options = parser.verifyRemoteOptions(options)
+    options = verifyRemoteOptions(parser, options)
     log = commandline.setup_logging("Remote XPCShell",
                                     options,
                                     {"tbpl": sys.stdout})
 
-    if len(args) < 1 and options.manifest is None:
-        print >>sys.stderr, """Usage: %s <test dirs>
-             or: %s --manifest=test.manifest """ % (sys.argv[0], sys.argv[0])
-        sys.exit(1)
-
     if options.dm_trans == "adb":
         if options.deviceIP:
             dm = mozdevice.DroidADB(options.deviceIP, options.devicePort, packageName=None, deviceRoot=options.remoteTestRoot)
         else:
             dm = mozdevice.DroidADB(packageName=None, deviceRoot=options.remoteTestRoot)
     else:
         if not options.deviceIP:
             print "Error: you must provide a device IP to connect to via the --device option"
             sys.exit(1)
         dm = mozdevice.DroidSUT(options.deviceIP, options.devicePort, deviceRoot=options.remoteTestRoot)
 
     if options.interactive and not options.testPath:
         print >>sys.stderr, "Error: You must specify a test filename in interactive mode!"
         sys.exit(1)
 
-    xpcsh = XPCShellRemote(dm, options, args, log)
+    if options.xpcshell is None:
+        options.xpcshell = "xpcshell"
+
+    xpcsh = XPCShellRemote(dm, options, log)
 
     # we don't run concurrent tests on mobile
     options.sequential = True
 
-    if not xpcsh.runTests(xpcshell='xpcshell',
-                          testClass=RemoteXPCShellTestThread,
-                          testdirs=args[0:],
+    if not xpcsh.runTests(testClass=RemoteXPCShellTestThread,
                           mobileArgs=xpcsh.mobileArgs,
-                          **options.__dict__):
+                          **vars(options)):
         sys.exit(1)
 
 
 if __name__ == '__main__':
     main()
--- a/testing/xpcshell/runtestsb2g.py
+++ b/testing/xpcshell/runtestsb2g.py
@@ -4,24 +4,25 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import sys
 import os
 sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))))
 
 import traceback
-from remotexpcshelltests import RemoteXPCShellTestThread, XPCShellRemote, RemoteXPCShellOptions
+import remotexpcshelltests
+from remotexpcshelltests import RemoteXPCShellTestThread, XPCShellRemote
 from mozdevice import devicemanagerADB, DMError
 from mozlog import commandline
 
 DEVICE_TEST_ROOT = '/data/local/tests'
 
-
 from marionette import Marionette
+from xpcshellcommandline import parser_b2g
 
 class B2GXPCShellTestThread(RemoteXPCShellTestThread):
     # Overridden
     def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None):
         try:
             # This returns 1 even when tests pass - hardcode returncode to 0 (bug 773703)
             outputFile = RemoteXPCShellTestThread.launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=timeout)
             self.shellReturnCode = 0
@@ -70,97 +71,29 @@ class B2GXPCShellRemote(XPCShellRemote):
     def pushLibs(self):
         if not self.options.use_device_libs:
             count = XPCShellRemote.pushLibs(self)
             if not count:
                 # couldn't find any libs, fallback to device libs
                 self.env['LD_LIBRARY_PATH'] = '/system/b2g'
                 self.options.use_device_libs = True
 
-class B2GOptions(RemoteXPCShellOptions):
-
-    def __init__(self):
-        RemoteXPCShellOptions.__init__(self)
-        defaults = {}
-
-        self.add_option('--b2gpath', action='store',
-                        type='string', dest='b2g_path',
-                        help="Path to B2G repo or qemu dir")
-        defaults['b2g_path'] = None
-
-        self.add_option('--emupath', action='store',
-                        type='string', dest='emu_path',
-                        help="Path to emulator folder (if different "
-                                                      "from b2gpath")
+def verifyRemoteOptions(parser, options):
+    if options.b2g_path is None:
+        parser.error("Need to specify a --b2gpath")
 
-        self.add_option('--no-clean', action='store_false',
-                        dest='clean',
-                        help="Do not clean TESTROOT. Saves [lots of] time")
-        defaults['clean'] = True
-
-        defaults['emu_path'] = None
-
-        self.add_option('--emulator', action='store',
-                        type='string', dest='emulator',
-                        help="Architecture of emulator to use: x86 or arm")
-        defaults['emulator'] = None
-
-        self.add_option('--no-window', action='store_true',
-                        dest='no_window',
-                        help="Pass --no-window to the emulator")
-        defaults['no_window'] = False
-
-        self.add_option('--adbpath', action='store',
-                        type='string', dest='adb_path',
-                        help="Path to adb")
-        defaults['adb_path'] = 'adb'
+    if options.geckoPath and not options.emulator:
+        parser.error("You must specify --emulator if you specify --gecko-path")
 
-        self.add_option('--address', action='store',
-                        type='string', dest='address',
-                        help="host:port of running Gecko instance to connect to")
-        defaults['address'] = None
+    if options.logdir and not options.emulator:
+        parser.error("You must specify --emulator if you specify --logdir")
+    return remotexpcshelltests.verifyRemoteOptions(parser, options)
 
-        self.add_option('--use-device-libs', action='store_true',
-                        dest='use_device_libs',
-                        help="Don't push .so's")
-        defaults['use_device_libs'] = False
-        self.add_option("--gecko-path", action="store",
-                        type="string", dest="geckoPath",
-                        help="the path to a gecko distribution that should "
-                        "be installed on the emulator prior to test")
-        defaults["geckoPath"] = None
-        self.add_option("--logdir", action="store",
-                        type="string", dest="logdir",
-                        help="directory to store log files")
-        defaults["logdir"] = None
-        self.add_option('--busybox', action='store',
-                        type='string', dest='busybox',
-                        help="Path to busybox binary to install on device")
-        defaults['busybox'] = None
-
-        defaults["remoteTestRoot"] = DEVICE_TEST_ROOT
-        defaults['dm_trans'] = 'adb'
-        defaults['debugger'] = None
-        defaults['debuggerArgs'] = None
-
-        self.set_defaults(**defaults)
-
-    def verifyRemoteOptions(self, options):
-        if options.b2g_path is None:
-            self.error("Need to specify a --b2gpath")
-
-        if options.geckoPath and not options.emulator:
-            self.error("You must specify --emulator if you specify --gecko-path")
-
-        if options.logdir and not options.emulator:
-            self.error("You must specify --emulator if you specify --logdir")
-        return RemoteXPCShellOptions.verifyRemoteOptions(self, options)
-
-def run_remote_xpcshell(parser, options, args, log):
-    options = parser.verifyRemoteOptions(options)
+def run_remote_xpcshell(parser, options, log):
+    options = verifyRemoteOptions(parser, options)
 
     # Create the Marionette instance
     kwargs = {}
     if options.emulator:
         kwargs['emulator'] = options.emulator
         if options.no_window:
             kwargs['noWindow'] = True
         if options.geckoPath:
@@ -190,39 +123,41 @@ def run_remote_xpcshell(parser, options,
         if options.deviceIP:
             kwargs['host'] = options.deviceIP
             kwargs['port'] = options.devicePort
         kwargs['deviceRoot'] = options.remoteTestRoot
         dm = devicemanagerADB.DeviceManagerADB(**kwargs)
 
     if not options.remoteTestRoot:
         options.remoteTestRoot = dm.deviceRoot
-    xpcsh = B2GXPCShellRemote(dm, options, args, log)
+    xpcsh = B2GXPCShellRemote(dm, options, log)
 
     # we don't run concurrent tests on mobile
     options.sequential = True
 
+    if options.xpcshell is None:
+        options.xpcshell = "xpcshell"
+
     try:
-        if not xpcsh.runTests(xpcshell='xpcshell', testdirs=args[0:],
-                                 testClass=B2GXPCShellTestThread,
-                                 mobileArgs=xpcsh.mobileArgs,
-                                 **options.__dict__):
+        if not xpcsh.runTests(testClass=B2GXPCShellTestThread,
+                              mobileArgs=xpcsh.mobileArgs,
+                              **vars(options)):
             sys.exit(1)
     except:
         print "Automation Error: Exception caught while running tests"
         traceback.print_exc()
         sys.exit(1)
 
 def main():
-    parser = B2GOptions()
+    parser = parser_b2g()
     commandline.add_logging_group(parser)
-    options, args = parser.parse_args()
+    options = parser.parse_args()
     log = commandline.setup_logging("Remote XPCShell",
                                     options,
                                     {"tbpl": sys.stdout})
-    run_remote_xpcshell(parser, options, args, log)
+    run_remote_xpcshell(parser, options, log)
 
 # You usually run this like :
 # python runtestsb2g.py --emulator arm --b2gpath $B2GPATH --manifest $MANIFEST [--xre-path $MOZ_HOST_BIN
 #                                                                               --adbpath $ADB_PATH
 #                                                                               ...]
 if __name__ == '__main__':
     main()
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -18,33 +18,36 @@ import shutil
 import signal
 import sys
 import time
 import traceback
 
 from collections import deque, namedtuple
 from distutils import dir_util
 from multiprocessing import cpu_count
-from optparse import OptionParser
+from argparse import ArgumentParser
 from subprocess import Popen, PIPE, STDOUT
 from tempfile import mkdtemp, gettempdir
 from threading import (
     Timer,
     Thread,
     Event,
     current_thread,
 )
 
 try:
     import psutil
     HAVE_PSUTIL = True
 except Exception:
     HAVE_PSUTIL = False
 
 from automation import Automation
+from xpcshellcommandline import parser_desktop
+
+SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
 
 HARNESS_TIMEOUT = 5 * 60
 
 # benchmarking on tbpl revealed that this works best for now
 NUM_THREADS = int(cpu_count() * 4)
 
 EXPECTED_LOG_ACTIONS = set([
     "test_status",
@@ -57,17 +60,17 @@ EXPECTED_LOG_ACTIONS = set([
 here = os.path.dirname(__file__)
 mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase'))
 
 if os.path.isdir(mozbase):
     for package in os.listdir(mozbase):
         sys.path.append(os.path.join(mozbase, package))
 
 from manifestparser import TestManifest
-from manifestparser.filters import chunk_by_slice, tags
+from manifestparser.filters import chunk_by_slice, tags, pathprefix
 from mozlog import commandline
 import mozcrash
 import mozinfo
 from mozrunner.utils import get_stack_fixer_function
 
 # --------------------------------------------------------------
 
 # TODO: perhaps this should be in a more generally shared location?
@@ -407,17 +410,17 @@ class XPCShellTestThread(Thread):
 
                 yield path
 
         headlist = test_object['head'] if 'head' in test_object else ''
         taillist = test_object['tail'] if 'tail' in test_object else ''
         return (list(sanitize_list(headlist, 'head')),
                 list(sanitize_list(taillist, 'tail')))
 
-    def buildXpcsCmd(self, testdir):
+    def buildXpcsCmd(self):
         """
           Load the root head.js file as the first file in our test path, before other head, test, and tail files.
           On a remote system, we overload this to add additional command line arguments, so this gets overloaded.
         """
         # - NOTE: if you rename/add any of the constants set here, update
         #   do_load_child_test_harness() in head.js
         if not self.appPath:
             self.appPath = self.xrePath
@@ -609,17 +612,17 @@ class XPCShellTestThread(Thread):
         test_dir = os.path.dirname(path)
 
         # Create a profile and a temp dir that the JS harness can stick
         # a profile and temporary data in
         self.profileDir = self.setupProfileDir()
         self.tempDir = self.setupTempDir()
         self.mozInfoJSPath = self.setupMozinfoJS()
 
-        self.buildXpcsCmd(test_dir)
+        self.buildXpcsCmd()
         head_files, tail_files = self.getHeadAndTailFiles(self.test_object)
         cmdH = self.buildCmdHead(head_files, tail_files, self.xpcsCmd)
 
         # The test file will have to be loaded after the head files.
         cmdT = self.buildCmdTestFile(path)
 
         args = self.xpcsRunArgs[:]
         if 'debug' in self.test_object:
@@ -762,40 +765,60 @@ class XPCShellTestThread(Thread):
 class XPCShellTests(object):
 
     def __init__(self, log=None):
         """ Initializes node status and logger. """
         self.log = log
         self.harness_timeout = HARNESS_TIMEOUT
         self.nodeProc = {}
 
-    def buildTestList(self, test_tags=None):
+    def getTestManifest(self, manifest):
+        if isinstance(manifest, TestManifest):
+            return manifest
+        elif manifest is not None:
+            manifest = os.path.normpath(os.path.abspath(manifest))
+            if os.path.isfile(manifest):
+                return TestManifest([manifest], strict=True)
+            else:
+                ini_path = os.path.join(manifest, "xpcshell.ini")
+        else:
+            ini_path = os.path.join(SCRIPT_DIR, "tests", "xpcshell.ini")
+
+        if os.path.exists(ini_path):
+            return TestManifest([ini_path], strict=True)
+        else:
+            print >> sys.stderr, ("Failed to find manifest at %s; use --manifest "
+                                  "to set path explicitly." % (ini_path,))
+            sys.exit(1)
+
+    def buildTestList(self, test_tags=None, test_paths=None):
         """
           read the xpcshell.ini manifest and set self.alltests to be
           an array of test objects.
 
           if we are chunking tests, it will be done here as well
         """
-        if isinstance(self.manifest, TestManifest):
-            mp = self.manifest
+
+        if test_paths is None:
+            test_paths = []
+
+        if len(test_paths) == 1 and test_paths[0].endswith(".js"):
+            self.singleFile = os.path.basename(test_paths[0])
         else:
-            mp = TestManifest(strict=True)
-            if self.manifest is None:
-                for testdir in self.testdirs:
-                    if testdir:
-                        mp.read(os.path.join(testdir, 'xpcshell.ini'))
-            else:
-                mp.read(self.manifest)
+            self.singleFile = None
 
-        self.buildTestPath()
+        mp = self.getTestManifest(self.manifest)
 
         filters = []
         if test_tags:
             filters.append(tags(test_tags))
 
+        if test_paths:
+            filters.append(pathprefix(test_paths))
+
         if self.singleFile is None and self.totalChunks > 1:
             filters.append(chunk_by_slice(self.thisChunk, self.totalChunks))
         try:
             self.alltests = mp.active_tests(filters=filters, **mozinfo.info)
         except TypeError:
             sys.stderr.write("*** offending mozinfo.info: %s\n" % repr(mozinfo.info))
             raise
 
@@ -912,41 +935,16 @@ class XPCShellTests(object):
             else:
                 if sys.platform == 'os2emx':
                     pStdout = None
                 else:
                     pStdout = PIPE
                 pStderr = STDOUT
         return pStdout, pStderr
 
-    def buildTestPath(self):
-        """
-          If we specifiy a testpath, set the self.testPath variable to be the given directory or file.
-
-          |testPath| will be the optional path only, or |None|.
-          |singleFile| will be the optional test only, or |None|.
-        """
-        self.singleFile = None
-        if self.testPath is not None:
-            if self.testPath.endswith('.js'):
-            # Split into path and file.
-                if self.testPath.find('/') == -1:
-                    # Test only.
-                    self.singleFile = self.testPath
-                else:
-                    # Both path and test.
-                    # Reuse |testPath| temporarily.
-                    self.testPath = self.testPath.rsplit('/', 1)
-                    self.singleFile = self.testPath[1]
-                    self.testPath = self.testPath[0]
-            else:
-                # Path only.
-                # Simply remove optional ending separator.
-                self.testPath = self.testPath.rstrip("/")
-
     def verifyDirPath(self, dirname):
         """
           Simple wrapper to get the absolute path for a given directory name.
           On a remote system, we need to overload this to work on the remote filesystem.
         """
         return os.path.abspath(dirname)
 
     def trySetupNode(self):
@@ -1039,37 +1037,37 @@ class XPCShellTests(object):
 
         relpath_key = 'file_relpath' if 'file_relpath' in test_object else 'relpath'
         path = test_object[relpath_key].replace('\\', '/');
         if 'dupe-manifest' in test_object and 'ancestor-manifest' in test_object:
             return '%s:%s' % (os.path.basename(test_object['ancestor-manifest']), path)
         return path
 
     def runTests(self, xpcshell=None, xrePath=None, appPath=None, symbolsPath=None,
-                 manifest=None, testdirs=None, testPath=None, mobileArgs=None,
+                 manifest=None, testPaths=None, mobileArgs=None,
                  interactive=False, verbose=False, keepGoing=False, logfiles=True,
                  thisChunk=1, totalChunks=1, debugger=None,
                  debuggerArgs=None, debuggerInteractive=False,
                  profileName=None, mozInfo=None, sequential=False, shuffle=False,
                  testingModulesDir=None, pluginsPath=None,
                  testClass=XPCShellTestThread, failureManifest=None,
                  log=None, stream=None, jsDebugger=False, jsDebuggerPort=0,
-                 test_tags=None, dump_tests=None, utility_path=None, **otherOptions):
+                 test_tags=None, dump_tests=None, utility_path=None,
+                 rerun_failures=False, failure_manifest=None, **otherOptions):
         """Run xpcshell tests.
 
         |xpcshell|, is the xpcshell executable to use to run the tests.
         |xrePath|, if provided, is the path to the XRE to use.
         |appPath|, if provided, is the path to an application directory.
         |symbolsPath|, if provided is the path to a directory containing
           breakpad symbols for processing crashes in tests.
         |manifest|, if provided, is a file containing a list of
           test directories to run.
-        |testdirs|, if provided, is a list of absolute paths of test directories.
-          No-manifest only option.
-        |testPath|, if provided, indicates a single path and/or test to run.
+        |testPaths|, if provided, is a list of paths to files or directories containing
+                     tests to run.
         |pluginsPath|, if provided, custom plugins directory to be returned from
           the xpcshell dir svc provider for NS_APP_PLUGINS_DIR_LIST.
         |interactive|, if set to True, indicates to provide an xpcshell prompt
           instead of automatically executing the test.
         |verbose|, if set to True, will cause stdout/stderr from tests to
           be printed always
         |logfiles|, if set to False, indicates not to save output to log files.
           Non-interactive only option.
@@ -1084,31 +1082,38 @@ class XPCShellTests(object):
         |shuffle|, if True, execute tests in random order.
         |testingModulesDir|, if provided, specifies where JS modules reside.
           xpcshell will register a resource handler mapping this path.
         |otherOptions| may be present for the convenience of subclasses
         """
 
         global gotSIGINT
 
-        if testdirs is None:
-            testdirs = []
-
         # Try to guess modules directory.
         # This somewhat grotesque hack allows the buildbot machines to find the
         # modules directory without having to configure the buildbot hosts. This
         # code path should never be executed in local runs because the build system
         # should always set this argument.
         if not testingModulesDir:
             ourDir = os.path.dirname(__file__)
             possible = os.path.join(ourDir, os.path.pardir, 'modules')
 
             if os.path.isdir(possible):
                 testingModulesDir = possible
 
+        if rerun_failures:
+            if os.path.exists(failure_manifest):
+                rerun_manifest = os.path.join(os.path.dirname(failure_manifest), "rerun.ini")
+                shutil.copyfile(failure_manifest, rerun_manifest)
+                os.remove(failure_manifest)
+                manifest = rerun_manifest
+            else:
+                print >> sys.stderr, "No failures were found to re-run."
+                sys.exit(1)
+
         if testingModulesDir:
             # The resource loader expects native paths. Depending on how we were
             # invoked, a UNIX style path may sneak in on Windows. We try to
             # normalize that.
             testingModulesDir = os.path.normpath(testingModulesDir)
 
             if not os.path.isabs(testingModulesDir):
                 testingModulesDir = os.path.abspath(testingModulesDir)
@@ -1127,35 +1132,29 @@ class XPCShellTests(object):
             JSDebuggerInfo = namedtuple('JSDebuggerInfo', ['port'])
             self.jsDebuggerInfo = JSDebuggerInfo(port=jsDebuggerPort)
 
         self.xpcshell = xpcshell
         self.xrePath = xrePath
         self.appPath = appPath
         self.symbolsPath = symbolsPath
         self.manifest = manifest
-        self.testdirs = testdirs
-        self.testPath = testPath
         self.dump_tests = dump_tests
         self.interactive = interactive
         self.verbose = verbose
         self.keepGoing = keepGoing
         self.logfiles = logfiles
         self.totalChunks = totalChunks
         self.thisChunk = thisChunk
         self.profileName = profileName or "xpcshell"
         self.mozInfo = mozInfo
         self.testingModulesDir = testingModulesDir
         self.pluginsPath = pluginsPath
         self.sequential = sequential
-
-        if not testdirs and not manifest:
-            # nothing to test!
-            self.log.error("Error: No test dirs or test manifest specified!")
-            return False
+        self.failure_manifest = failure_manifest
 
         self.testCount = 0
         self.passCount = 0
         self.failCount = 0
         self.todoCount = 0
 
         self.setAbsPath()
         self.buildXpcsRunArgs()
@@ -1198,17 +1197,17 @@ class XPCShellTests(object):
             appDirKey = self.mozInfo["appname"] + "-appdir"
 
         # We have to do this before we build the test list so we know whether or
         # not to run tests that depend on having the node spdy server
         self.trySetupNode()
 
         pStdout, pStderr = self.getPipes()
 
-        self.buildTestList(test_tags)
+        self.buildTestList(test_tags, testPaths)
         if self.singleFile:
             self.sequential = True
 
         if shuffle:
             random.shuffle(self.alltests)
 
         self.cleanup_dir_list = []
         self.try_again_list = []
@@ -1226,17 +1225,17 @@ class XPCShellTests(object):
             'testharnessdir': self.testharnessdir,
             'profileName': self.profileName,
             'singleFile': self.singleFile,
             'env': self.env, # making a copy of this in the testthreads
             'symbolsPath': self.symbolsPath,
             'logfiles': self.logfiles,
             'xpcshell': self.xpcshell,
             'xpcsRunArgs': self.xpcsRunArgs,
-            'failureManifest': failureManifest,
+            'failureManifest': self.failure_manifest,
             'harness_timeout': self.harness_timeout,
             'stack_fixer_function': self.stack_fixer_function,
         }
 
         if self.sequential:
             # Allow user to kill hung xpcshell subprocess with SIGINT
             # when we are only running tests sequentially.
             signal.signal(signal.SIGINT, markGotSIGINT)
@@ -1419,133 +1418,30 @@ class XPCShellTests(object):
         if gotSIGINT and not keepGoing:
             self.log.error("TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so stopped run. " \
                            "(Use --keep-going to keep running tests after killing one with SIGINT)")
             return False
 
         self.log.suite_end()
         return self.failCount == 0
 
-class XPCShellOptions(OptionParser):
-    def __init__(self):
-        """Process command line arguments and call runTests() to do the real work."""
-        OptionParser.__init__(self)
-        self.add_option("--app-path",
-                        type="string", dest="appPath", default=None,
-                        help="application directory (as opposed to XRE directory)")
-        self.add_option("--interactive",
-                        action="store_true", dest="interactive", default=False,
-                        help="don't automatically run tests, drop to an xpcshell prompt")
-        self.add_option("--verbose",
-                        action="store_true", dest="verbose", default=False,
-                        help="always print stdout and stderr from tests")
-        self.add_option("--keep-going",
-                        action="store_true", dest="keepGoing", default=False,
-                        help="continue running tests after test killed with control-C (SIGINT)")
-        self.add_option("--logfiles",
-                        action="store_true", dest="logfiles", default=True,
-                        help="create log files (default, only used to override --no-logfiles)")
-        self.add_option("--dump-tests",
-                        type="string", dest="dump_tests", default=None,
-                        help="Specify path to a filename to dump all the tests that will be run")
-        self.add_option("--manifest",
-                        type="string", dest="manifest", default=None,
-                        help="Manifest of test directories to use")
-        self.add_option("--no-logfiles",
-                        action="store_false", dest="logfiles",
-                        help="don't create log files")
-        self.add_option("--sequential",
-                        action="store_true", dest="sequential", default=False,
-                        help="Run all tests sequentially")
-        self.add_option("--test-path",
-                        type="string", dest="testPath", default=None,
-                        help="single path and/or test filename to test")
-        self.add_option("--testing-modules-dir",
-                        dest="testingModulesDir", default=None,
-                        help="Directory where testing modules are located.")
-        self.add_option("--test-plugin-path",
-                        type="string", dest="pluginsPath", default=None,
-                        help="Path to the location of a plugins directory containing the test plugin or plugins required for tests. "
-                             "By default xpcshell's dir svc provider returns gre/plugins. Use test-plugin-path to add a directory "
-                             "to return for NS_APP_PLUGINS_DIR_LIST when queried.")
-        self.add_option("--total-chunks",
-                        type = "int", dest = "totalChunks", default=1,
-                        help = "how many chunks to split the tests up into")
-        self.add_option("--this-chunk",
-                        type = "int", dest = "thisChunk", default=1,
-                        help = "which chunk to run between 1 and --total-chunks")
-        self.add_option("--profile-name",
-                        type = "string", dest="profileName", default=None,
-                        help="name of application profile being tested")
-        self.add_option("--build-info-json",
-                        type = "string", dest="mozInfo", default=None,
-                        help="path to a mozinfo.json including information about the build configuration. defaults to looking for mozinfo.json next to the script.")
-        self.add_option("--shuffle",
-                        action="store_true", dest="shuffle", default=False,
-                        help="Execute tests in random order")
-        self.add_option("--failure-manifest", dest="failureManifest",
-                        action="store",
-                        help="path to file where failure manifest will be written.")
-        self.add_option("--xre-path",
-                        action = "store", type = "string", dest = "xrePath",
-                        # individual scripts will set a sane default
-                        default = None,
-                        help = "absolute path to directory containing XRE (probably xulrunner)")
-        self.add_option("--symbols-path",
-                        action = "store", type = "string", dest = "symbolsPath",
-                        default = None,
-                        help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
-        self.add_option("--debugger",
-                        action = "store", dest = "debugger",
-                        help = "use the given debugger to launch the application")
-        self.add_option("--debugger-args",
-                        action = "store", dest = "debuggerArgs",
-                        help = "pass the given args to the debugger _before_ "
-                           "the application on the command line")
-        self.add_option("--debugger-interactive",
-                        action = "store_true", dest = "debuggerInteractive",
-                        help = "prevents the test harness from redirecting "
-                          "stdout and stderr for interactive debuggers")
-        self.add_option("--jsdebugger", dest="jsDebugger", action="store_true",
-                        help="Waits for a devtools JS debugger to connect before "
-                             "starting the test.")
-        self.add_option("--jsdebugger-port", type="int", dest="jsDebuggerPort",
-                        default=6000,
-                        help="The port to listen on for a debugger connection if "
-                             "--jsdebugger is specified.")
-        self.add_option("--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.")
-        self.add_option("--utility-path",
-                        action="store", dest="utility_path",
-                        default=None,
-                        help="Path to a directory containing utility programs, such "
-                             "as stack fixer scripts.")
 
 def main():
-    parser = XPCShellOptions()
+    parser = parser_desktop()
     commandline.add_logging_group(parser)
-    options, args = parser.parse_args()
-
+    options = parser.parse_args()
 
     log = commandline.setup_logging("XPCShell", options, {"tbpl": sys.stdout})
 
-    if len(args) < 2 and options.manifest is None or \
-       (len(args) < 1 and options.manifest is not None):
-        print >>sys.stderr, """Usage: %s <path to xpcshell> <test dirs>
-              or: %s --manifest=test.manifest <path to xpcshell>""" % (sys.argv[0],
-                                                              sys.argv[0])
-        sys.exit(1)
+    if options.xpcshell is None:
+        print >> sys.stderr, """Must provide path to xpcshell using --xpcshell"""
 
     xpcsh = XPCShellTests(log)
 
     if options.interactive and not options.testPath:
         print >>sys.stderr, "Error: You must specify a test filename in interactive mode!"
         sys.exit(1)
 
-    if not xpcsh.runTests(args[0], testdirs=args[1:], **options.__dict__):
+    if not xpcsh.runTests(**vars(options)):
         sys.exit(1)
 
 if __name__ == '__main__':
     main()
new file mode 100644
--- /dev/null
+++ b/testing/xpcshell/xpcshellcommandline.py
@@ -0,0 +1,202 @@
+import argparse
+
+def add_common_arguments(parser):
+    parser.add_argument("--app-path",
+                        type=unicode, dest="appPath", default=None,
+                        help="application directory (as opposed to XRE directory)")
+    parser.add_argument("--interactive",
+                        action="store_true", dest="interactive", default=False,
+                        help="don't automatically run tests, drop to an xpcshell prompt")
+    parser.add_argument("--verbose",
+                        action="store_true", dest="verbose", default=False,
+                        help="always print stdout and stderr from tests")
+    parser.add_argument("--keep-going",
+                        action="store_true", dest="keepGoing", default=False,
+                        help="continue running tests after test killed with control-C (SIGINT)")
+    parser.add_argument("--logfiles",
+                        action="store_true", dest="logfiles", default=True,
+                        help="create log files (default, only used to override --no-logfiles)")
+    parser.add_argument("--dump-tests", type=str, dest="dump_tests", default=None,
+                        help="Specify path to a filename to dump all the tests that will be run")
+    parser.add_argument("--manifest",
+                        type=unicode, dest="manifest", default=None,
+                        help="Manifest of test directories to use")
+    parser.add_argument("--no-logfiles",
+                        action="store_false", dest="logfiles",
+                        help="don't create log files")
+    parser.add_argument("--sequential",
+                        action="store_true", dest="sequential", default=False,
+                        help="Run all tests sequentially")
+    parser.add_argument("--testing-modules-dir",
+                        dest="testingModulesDir", default=None,
+                        help="Directory where testing modules are located.")
+    parser.add_argument("--test-plugin-path",
+                        type=str, dest="pluginsPath", default=None,
+                        help="Path to the location of a plugins directory containing the test plugin or plugins required for tests. "
+                        "By default xpcshell's dir svc provider returns gre/plugins. Use test-plugin-path to add a directory "
+                        "to return for NS_APP_PLUGINS_DIR_LIST when queried.")
+    parser.add_argument("--total-chunks",
+                        type=int, dest="totalChunks", default=1,
+                        help="how many chunks to split the tests up into")
+    parser.add_argument("--this-chunk",
+                        type=int, dest="thisChunk", default=1,
+                        help="which chunk to run between 1 and --total-chunks")
+    parser.add_argument("--profile-name",
+                        type=str, dest="profileName", default=None,
+                        help="name of application profile being tested")
+    parser.add_argument("--build-info-json",
+                        type=str, dest="mozInfo", default=None,
+                        help="path to a mozinfo.json including information about the build configuration. defaults to looking for mozinfo.json next to the script.")
+    parser.add_argument("--shuffle",
+                        action="store_true", dest="shuffle", default=False,
+                        help="Execute tests in random order")
+    parser.add_argument("--xre-path",
+                        action="store", type=str, dest="xrePath",
+                        # individual scripts will set a sane default
+                        default=None,
+                        help="absolute path to directory containing XRE (probably xulrunner)")
+    parser.add_argument("--symbols-path",
+                        action="store", type=str, dest="symbolsPath",
+                        default=None,
+                        help="absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
+    parser.add_argument("--debugger",
+                        action="store", dest="debugger",
+                        help="use the given debugger to launch the application")
+    parser.add_argument("--debugger-args",
+                        action="store", dest="debuggerArgs",
+                        help="pass the given args to the debugger _before_ "
+                        "the application on the command line")
+    parser.add_argument("--debugger-interactive",
+                        action="store_true", dest="debuggerInteractive",
+                        help="prevents the test harness from redirecting "
+                        "stdout and stderr for interactive debuggers")
+    parser.add_argument("--jsdebugger", dest="jsDebugger", action="store_true",
+                        help="Waits for a devtools JS debugger to connect before "
+                        "starting the test.")
+    parser.add_argument("--jsdebugger-port", type=int, dest="jsDebuggerPort",
+                        default=6000,
+                        help="The port to listen on for a debugger connection if "
+                        "--jsdebugger is specified.")
+    parser.add_argument("--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.")
+    parser.add_argument("--utility-path",
+                        action="store", dest="utility_path",
+                        default=None,
+                        help="Path to a directory containing utility programs, such "
+                        "as stack fixer scripts.")
+    parser.add_argument("--xpcshell",
+                        action="store", dest="xpcshell",
+                        default=None,
+                        help="Path to xpcshell binary")
+    # This argument can be just present, or the path to a manifest file. The
+    # just-present case is usually used for mach which can provide a default
+    # path to the failure file from the previous run
+    parser.add_argument("--rerun-failures",
+                        action="store_true",
+                        help="Rerun failures from the previous run, if any")
+    parser.add_argument("--failure-manifest",
+                        action="store",
+                        help="Path to a manifest file from which to rerun failures "
+                        "(with --rerun-failure) or in which to record failed tests")
+    parser.add_argument("testPaths", nargs="*", default=None,
+                        help="Paths of tests to run.")
+
+def add_remote_arguments(parser):
+    parser.add_argument("--deviceIP", action="store", type=str, dest="deviceIP",
+                        help="ip address of remote device to test")
+
+    parser.add_argument("--devicePort", action="store", type=str, dest="devicePort",
+                        default=20701, help="port of remote device to test")
+
+    parser.add_argument("--dm_trans", action="store", type=str, dest="dm_trans",
+                        choices=["adb", "sut"], default="sut",
+                        help="the transport to use to communicate with device: [adb|sut]; default=sut")
+
+    parser.add_argument("--objdir", action="store", type=str, dest="objdir",
+                        help="local objdir, containing xpcshell binaries")
+
+
+    parser.add_argument("--apk", action="store", type=str, dest="localAPK",
+                        help="local path to Fennec APK")
+
+
+    parser.add_argument("--noSetup", action="store_false", dest="setup", default=True,
+                        help="do not copy any files to device (to be used only if device is already setup)")
+
+    parser.add_argument("--local-lib-dir", action="store", type=str, dest="localLib",
+                        help="local path to library directory")
+
+    parser.add_argument("--local-bin-dir", action="store", type=str, dest="localBin",
+                        help="local path to bin directory")
+
+    parser.add_argument("--remoteTestRoot", action="store", type=str, dest="remoteTestRoot",
+                        help="remote directory to use as test root (eg. /mnt/sdcard/tests or /data/local/tests)")
+
+def add_b2g_arguments(parser):
+    parser.add_argument('--b2gpath', action='store', type=str, dest='b2g_path',
+                        help="Path to B2G repo or qemu dir")
+
+    parser.add_argument('--emupath', action='store', type=str, dest='emu_path',
+                        help="Path to emulator folder (if different "
+                        "from b2gpath")
+
+    parser.add_argument('--no-clean', action='store_false', dest='clean', default=True,
+                        help="Do not clean TESTROOT. Saves [lots of] time")
+
+    parser.add_argument('--emulator', action='store', type=str, dest='emulator',
+                        default="arm", choices=["x86", "arm"],
+                        help="Architecture of emulator to use: x86 or arm")
+
+    parser.add_argument('--no-window', action='store_true', dest='no_window', default=False,
+                        help="Pass --no-window to the emulator")
+
+    parser.add_argument('--adbpath', action='store', type=str, dest='adb_path',
+                        default="adb", help="Path to adb")
+
+    parser.add_argument('--address', action='store', type=str, dest='address',
+                        help="host:port of running Gecko instance to connect to")
+
+    parser.add_argument('--use-device-libs', action='store_true', dest='use_device_libs',
+                        default=None, help="Don't push .so's")
+
+    parser.add_argument("--gecko-path", action="store", type=str, dest="geckoPath",
+                        help="the path to a gecko distribution that should "
+                        "be installed on the emulator prior to test")
+
+    parser.add_argument("--logdir", action="store", type=str, dest="logdir",
+                        help="directory to store log files")
+
+    parser.add_argument('--busybox', action='store', type=str, dest='busybox',
+                        help="Path to busybox binary to install on device")
+
+    parser.set_defaults(remoteTestRoot="/data/local/tests",
+                        dm_trans="adb")
+
+def parser_desktop():
+    parser = argparse.ArgumentParser()
+    add_common_arguments(parser)
+    return parser
+
+def parser_remote():
+    parser = argparse.ArgumentParser()
+    common = parser.add_argument_group("Common Options")
+    add_common_arguments(common)
+    remote = parser.add_argument_group("Remote Options")
+    add_remote_arguments(remote)
+
+    return parser
+
+def parser_b2g():
+    parser = argparse.ArgumentParser()
+    common = parser.add_argument_group("Common Options")
+    add_common_arguments(common)
+    remote = parser.add_argument_group("Remote Options")
+    add_remote_arguments(remote)
+    b2g = parser.add_argument_group("B2G Options")
+    add_b2g_arguments(b2g)
+
+    return parser