Bug 1193257 - Make xpcshell harness command line arguments path filters for tests
authorJames Graham <james@hoppipolla.co.uk>
Tue, 18 Aug 2015 19:06:33 +0100
changeset 532219 21f79da79fdb1304d5a9558161d43e69c2186247
parent 532218 63abff28a76e144be330476670a19ff513c23236
child 532220 5f10234c3cbc713c6bbb02a14ad7dbacd684bc8c
push id84127
push userjames@hoppipolla.co.uk
push dateWed, 19 Aug 2015 23:13:45 +0000
treeherdertry@fab790176c30 [default view] [failures only]
bugs1193257
milestone43.0a1
Bug 1193257 - Make xpcshell harness command line arguments path filters for tests
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
@@ -103,18 +103,19 @@ 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": {'options': ["--xpcshell=%(abs_app_dir)s/" + XPCSHELL_NAME,
+                                 "--manifest=tests/xpcshell/tests/all-test-dirs.list"],
+                     'tests': []},
     },
     "all_cppunittest_suites": {
         "cppunittest": ['tests/cppunittest']
     },
     "all_jittest_suites": {
         "jittest": [],
         "jittest1": ["--total-chunks=2", "--this-chunk=1"],
         "jittest2": ["--total-chunks=2", "--this-chunk=2"],
--- 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,189 +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,
-                 # 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)
-            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.')
+        self.run_suite(**kwargs)
 
-        # Dynamically write out a manifest holding all the discovered tests.
-        manifest = TestManifest()
-        manifest.tests.extend(tests)
-
-        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,
-        }
-
-        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):
+    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,
-            '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
 
@@ -228,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.
@@ -341,129 +253,89 @@ 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('--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)
@@ -92,25 +94,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"
@@ -203,17 +205,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
@@ -480,169 +482,108 @@ 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)
+    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,38 @@ 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
 
     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
 
 # --------------------------------------------------------------
 
 # TODO: perhaps this should be in a more generally shared location?
 # This regex matches all of the C0 and C1 control characters
@@ -433,17 +436,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
@@ -627,17 +630,17 @@ class XPCShellTestThread(Thread):
         if self.app_dir_key and self.app_dir_key in self.test_object:
             rel_app_dir = self.test_object[self.app_dir_key]
             rel_app_dir = os.path.join(self.xrePath, rel_app_dir)
             self.appPath = os.path.abspath(rel_app_dir)
         else:
             self.appPath = None
 
         test_dir = os.path.dirname(path)
-        self.buildXpcsCmd(test_dir)
+        self.buildXpcsCmd()
         head_files, tail_files = self.getHeadAndTailFiles(self.test_object)
         cmdH = self.buildCmdHead(head_files, tail_files, self.xpcsCmd)
 
         # 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()
 
@@ -785,40 +788,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
 
@@ -921,41 +944,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):
@@ -1048,37 +1046,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, utility_path=None, **otherOptions):
+                 test_tags=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.
@@ -1093,31 +1091,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)
@@ -1136,34 +1141,28 @@ 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.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()
@@ -1208,17 +1207,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 = []
@@ -1236,17 +1235,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)
@@ -1429,130 +1428,32 @@ 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("--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 options.xpcshell is None:
+        options.xpcshell = "xpcshell"
+
+    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,200 @@
+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("--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