testing/mochitest/mach_commands.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/testing/mochitest/mach_commands.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,706 @@
     1.4 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.7 +
     1.8 +from __future__ import unicode_literals
     1.9 +
    1.10 +import logging
    1.11 +import mozpack.path
    1.12 +import os
    1.13 +import platform
    1.14 +import sys
    1.15 +import warnings
    1.16 +import which
    1.17 +
    1.18 +from mozbuild.base import (
    1.19 +    MachCommandBase,
    1.20 +    MachCommandConditions as conditions,
    1.21 +    MozbuildObject,
    1.22 +)
    1.23 +
    1.24 +from mach.decorators import (
    1.25 +    CommandArgument,
    1.26 +    CommandProvider,
    1.27 +    Command,
    1.28 +)
    1.29 +
    1.30 +from mach.logging import StructuredHumanFormatter
    1.31 +
    1.32 +ADB_NOT_FOUND = '''
    1.33 +The %s command requires the adb binary to be on your path.
    1.34 +
    1.35 +If you have a B2G build, this can be found in
    1.36 +'%s/out/host/<platform>/bin'.
    1.37 +'''.lstrip()
    1.38 +
    1.39 +GAIA_PROFILE_NOT_FOUND = '''
    1.40 +The %s command requires a non-debug gaia profile. Either pass in --profile,
    1.41 +or set the GAIA_PROFILE environment variable.
    1.42 +
    1.43 +If you do not have a non-debug gaia profile, you can build one:
    1.44 +    $ git clone https://github.com/mozilla-b2g/gaia
    1.45 +    $ cd gaia
    1.46 +    $ make
    1.47 +
    1.48 +The profile should be generated in a directory called 'profile'.
    1.49 +'''.lstrip()
    1.50 +
    1.51 +GAIA_PROFILE_IS_DEBUG = '''
    1.52 +The %s command requires a non-debug gaia profile. The specified profile,
    1.53 +%s, is a debug profile.
    1.54 +
    1.55 +If you do not have a non-debug gaia profile, you can build one:
    1.56 +    $ git clone https://github.com/mozilla-b2g/gaia
    1.57 +    $ cd gaia
    1.58 +    $ make
    1.59 +
    1.60 +The profile should be generated in a directory called 'profile'.
    1.61 +'''.lstrip()
    1.62 +
    1.63 +
    1.64 +class UnexpectedFilter(logging.Filter):
    1.65 +    def filter(self, record):
    1.66 +        msg = getattr(record, 'params', {}).get('msg', '')
    1.67 +        return 'TEST-UNEXPECTED-' in msg
    1.68 +
    1.69 +
    1.70 +class MochitestRunner(MozbuildObject):
    1.71 +    """Easily run mochitests.
    1.72 +
    1.73 +    This currently contains just the basics for running mochitests. We may want
    1.74 +    to hook up result parsing, etc.
    1.75 +    """
    1.76 +
    1.77 +    def get_webapp_runtime_path(self):
    1.78 +        import mozinfo
    1.79 +        appname = 'webapprt-stub' + mozinfo.info.get('bin_suffix', '')
    1.80 +        if sys.platform.startswith('darwin'):
    1.81 +            appname = os.path.join(self.distdir, self.substs['MOZ_MACBUNDLE_NAME'],
    1.82 +            'Contents', 'MacOS', appname)
    1.83 +        else:
    1.84 +            appname = os.path.join(self.distdir, 'bin', appname)
    1.85 +        return appname
    1.86 +
    1.87 +    def __init__(self, *args, **kwargs):
    1.88 +        MozbuildObject.__init__(self, *args, **kwargs)
    1.89 +
    1.90 +        # TODO Bug 794506 remove once mach integrates with virtualenv.
    1.91 +        build_path = os.path.join(self.topobjdir, 'build')
    1.92 +        if build_path not in sys.path:
    1.93 +            sys.path.append(build_path)
    1.94 +
    1.95 +        self.tests_dir = os.path.join(self.topobjdir, '_tests')
    1.96 +        self.mochitest_dir = os.path.join(self.tests_dir, 'testing', 'mochitest')
    1.97 +        self.bin_dir = os.path.join(self.topobjdir, 'dist', 'bin')
    1.98 +
    1.99 +    def run_b2g_test(self, test_paths=None, b2g_home=None, xre_path=None,
   1.100 +                     total_chunks=None, this_chunk=None, no_window=None,
   1.101 +                     **kwargs):
   1.102 +        """Runs a b2g mochitest.
   1.103 +
   1.104 +        test_paths is an enumerable of paths to tests. It can be a relative path
   1.105 +        from the top source directory, an absolute filename, or a directory
   1.106 +        containing test files.
   1.107 +        """
   1.108 +        # Need to call relpath before os.chdir() below.
   1.109 +        test_path = ''
   1.110 +        if test_paths:
   1.111 +            if len(test_paths) > 1:
   1.112 +                print('Warning: Only the first test path will be used.')
   1.113 +            test_path = self._wrap_path_argument(test_paths[0]).relpath()
   1.114 +
   1.115 +        # TODO without os.chdir, chained imports fail below
   1.116 +        os.chdir(self.mochitest_dir)
   1.117 +
   1.118 +        # The imp module can spew warnings if the modules below have
   1.119 +        # already been imported, ignore them.
   1.120 +        with warnings.catch_warnings():
   1.121 +            warnings.simplefilter('ignore')
   1.122 +
   1.123 +            import imp
   1.124 +            path = os.path.join(self.mochitest_dir, 'runtestsb2g.py')
   1.125 +            with open(path, 'r') as fh:
   1.126 +                imp.load_module('mochitest', fh, path,
   1.127 +                    ('.py', 'r', imp.PY_SOURCE))
   1.128 +
   1.129 +            import mochitest
   1.130 +            from mochitest_options import B2GOptions
   1.131 +
   1.132 +        parser = B2GOptions()
   1.133 +        options = parser.parse_args([])[0]
   1.134 +
   1.135 +        test_path_dir = False;
   1.136 +        if test_path:
   1.137 +            test_root_file = mozpack.path.join(self.mochitest_dir, 'tests', test_path)
   1.138 +            if not os.path.exists(test_root_file):
   1.139 +                print('Specified test path does not exist: %s' % test_root_file)
   1.140 +                return 1
   1.141 +            if os.path.isdir(test_root_file):
   1.142 +                test_path_dir = True;
   1.143 +            options.testPath = test_path
   1.144 +
   1.145 +        for k, v in kwargs.iteritems():
   1.146 +            setattr(options, k, v)
   1.147 +        options.noWindow = no_window
   1.148 +        options.totalChunks = total_chunks
   1.149 +        options.thisChunk = this_chunk
   1.150 +
   1.151 +        options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols')
   1.152 +
   1.153 +        options.consoleLevel = 'INFO'
   1.154 +        if conditions.is_b2g_desktop(self):
   1.155 +
   1.156 +            options.profile = options.profile or os.environ.get('GAIA_PROFILE')
   1.157 +            if not options.profile:
   1.158 +                print(GAIA_PROFILE_NOT_FOUND % 'mochitest-b2g-desktop')
   1.159 +                return 1
   1.160 +
   1.161 +            if os.path.isfile(os.path.join(options.profile, 'extensions', \
   1.162 +                    'httpd@gaiamobile.org')):
   1.163 +                print(GAIA_PROFILE_IS_DEBUG % ('mochitest-b2g-desktop',
   1.164 +                                               options.profile))
   1.165 +                return 1
   1.166 +
   1.167 +            options.desktop = True
   1.168 +            options.app = self.get_binary_path()
   1.169 +            if not options.app.endswith('-bin'):
   1.170 +                options.app = '%s-bin' % options.app
   1.171 +            if not os.path.isfile(options.app):
   1.172 +                options.app = options.app[:-len('-bin')]
   1.173 +
   1.174 +            return mochitest.run_desktop_mochitests(parser, options)
   1.175 +
   1.176 +        try:
   1.177 +            which.which('adb')
   1.178 +        except which.WhichError:
   1.179 +            # TODO Find adb automatically if it isn't on the path
   1.180 +            print(ADB_NOT_FOUND % ('mochitest-remote', b2g_home))
   1.181 +            return 1
   1.182 +
   1.183 +        options.b2gPath = b2g_home
   1.184 +        options.logcat_dir = self.mochitest_dir
   1.185 +        options.httpdPath = self.mochitest_dir
   1.186 +        options.xrePath = xre_path
   1.187 +        return mochitest.run_remote_mochitests(parser, options)
   1.188 +
   1.189 +    def run_desktop_test(self, context, suite=None, test_paths=None, debugger=None,
   1.190 +        debugger_args=None, slowscript=False, screenshot_on_fail = False, shuffle=False, keep_open=False,
   1.191 +        rerun_failures=False, no_autorun=False, repeat=0, run_until_failure=False,
   1.192 +        slow=False, chunk_by_dir=0, total_chunks=None, this_chunk=None,
   1.193 +        jsdebugger=False, debug_on_failure=False, start_at=None, end_at=None,
   1.194 +        e10s=False, dmd=False, dump_output_directory=None,
   1.195 +        dump_about_memory_after_test=False, dump_dmd_after_test=False,
   1.196 +        install_extension=None, quiet=False, environment=[], **kwargs):
   1.197 +        """Runs a mochitest.
   1.198 +
   1.199 +        test_paths are path to tests. They can be a relative path from the
   1.200 +        top source directory, an absolute filename, or a directory containing
   1.201 +        test files.
   1.202 +
   1.203 +        suite is the type of mochitest to run. It can be one of ('plain',
   1.204 +        'chrome', 'browser', 'metro', 'a11y').
   1.205 +
   1.206 +        debugger is a program name or path to a binary (presumably a debugger)
   1.207 +        to run the test in. e.g. 'gdb'
   1.208 +
   1.209 +        debugger_args are the arguments passed to the debugger.
   1.210 +
   1.211 +        slowscript is true if the user has requested the SIGSEGV mechanism of
   1.212 +        invoking the slow script dialog.
   1.213 +
   1.214 +        shuffle is whether test order should be shuffled (defaults to false).
   1.215 +
   1.216 +        keep_open denotes whether to keep the browser open after tests
   1.217 +        complete.
   1.218 +        """
   1.219 +        if rerun_failures and test_paths:
   1.220 +            print('Cannot specify both --rerun-failures and a test path.')
   1.221 +            return 1
   1.222 +
   1.223 +        # Need to call relpath before os.chdir() below.
   1.224 +        if test_paths:
   1.225 +            test_paths = [self._wrap_path_argument(p).relpath() for p in test_paths]
   1.226 +
   1.227 +        failure_file_path = os.path.join(self.statedir, 'mochitest_failures.json')
   1.228 +
   1.229 +        if rerun_failures and not os.path.exists(failure_file_path):
   1.230 +            print('No failure file present. Did you run mochitests before?')
   1.231 +            return 1
   1.232 +
   1.233 +        from StringIO import StringIO
   1.234 +
   1.235 +        # runtests.py is ambiguous, so we load the file/module manually.
   1.236 +        if 'mochitest' not in sys.modules:
   1.237 +            import imp
   1.238 +            path = os.path.join(self.mochitest_dir, 'runtests.py')
   1.239 +            with open(path, 'r') as fh:
   1.240 +                imp.load_module('mochitest', fh, path,
   1.241 +                    ('.py', 'r', imp.PY_SOURCE))
   1.242 +
   1.243 +        import mozinfo
   1.244 +        import mochitest
   1.245 +        from manifestparser import TestManifest
   1.246 +        from mozbuild.testing import TestResolver
   1.247 +
   1.248 +        # This is required to make other components happy. Sad, isn't it?
   1.249 +        os.chdir(self.topobjdir)
   1.250 +
   1.251 +        # Automation installs its own stream handler to stdout. Since we want
   1.252 +        # all logging to go through us, we just remove their handler.
   1.253 +        remove_handlers = [l for l in logging.getLogger().handlers
   1.254 +            if isinstance(l, logging.StreamHandler)]
   1.255 +        for handler in remove_handlers:
   1.256 +            logging.getLogger().removeHandler(handler)
   1.257 +
   1.258 +        runner = mochitest.Mochitest()
   1.259 +
   1.260 +        opts = mochitest.MochitestOptions()
   1.261 +        options, args = opts.parse_args([])
   1.262 +
   1.263 +        options.subsuite = ''
   1.264 +        flavor = suite
   1.265 +
   1.266 +        # Need to set the suite options before verifyOptions below.
   1.267 +        if suite == 'plain':
   1.268 +            # Don't need additional options for plain.
   1.269 +            flavor = 'mochitest'
   1.270 +        elif suite == 'chrome':
   1.271 +            options.chrome = True
   1.272 +        elif suite == 'browser':
   1.273 +            options.browserChrome = True
   1.274 +            flavor = 'browser-chrome'
   1.275 +        elif suite == 'devtools':
   1.276 +            options.browserChrome = True
   1.277 +            options.subsuite = 'devtools'
   1.278 +        elif suite == 'metro':
   1.279 +            options.immersiveMode = True
   1.280 +            options.browserChrome = True
   1.281 +        elif suite == 'a11y':
   1.282 +            options.a11y = True
   1.283 +        elif suite == 'webapprt-content':
   1.284 +            options.webapprtContent = True
   1.285 +            options.app = self.get_webapp_runtime_path()
   1.286 +        elif suite == 'webapprt-chrome':
   1.287 +            options.webapprtChrome = True
   1.288 +            options.app = self.get_webapp_runtime_path()
   1.289 +            options.browserArgs.append("-test-mode")
   1.290 +        else:
   1.291 +            raise Exception('None or unrecognized mochitest suite type.')
   1.292 +
   1.293 +        if dmd:
   1.294 +            options.dmdPath = self.bin_dir
   1.295 +
   1.296 +        options.autorun = not no_autorun
   1.297 +        options.closeWhenDone = not keep_open
   1.298 +        options.slowscript = slowscript
   1.299 +        options.screenshotOnFail = screenshot_on_fail
   1.300 +        options.shuffle = shuffle
   1.301 +        options.consoleLevel = 'INFO'
   1.302 +        options.repeat = repeat
   1.303 +        options.runUntilFailure = run_until_failure
   1.304 +        options.runSlower = slow
   1.305 +        options.testingModulesDir = os.path.join(self.tests_dir, 'modules')
   1.306 +        options.extraProfileFiles.append(os.path.join(self.distdir, 'plugins'))
   1.307 +        options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols')
   1.308 +        options.chunkByDir = chunk_by_dir
   1.309 +        options.totalChunks = total_chunks
   1.310 +        options.thisChunk = this_chunk
   1.311 +        options.jsdebugger = jsdebugger
   1.312 +        options.debugOnFailure = debug_on_failure
   1.313 +        options.startAt = start_at
   1.314 +        options.endAt = end_at
   1.315 +        options.e10s = e10s
   1.316 +        options.dumpAboutMemoryAfterTest = dump_about_memory_after_test
   1.317 +        options.dumpDMDAfterTest = dump_dmd_after_test
   1.318 +        options.dumpOutputDirectory = dump_output_directory
   1.319 +        options.quiet = quiet
   1.320 +        options.environment = environment
   1.321 +
   1.322 +        options.failureFile = failure_file_path
   1.323 +        if install_extension != None:
   1.324 +            options.extensionsToInstall = [os.path.join(self.topsrcdir,install_extension)]
   1.325 +
   1.326 +        for k, v in kwargs.iteritems():
   1.327 +            setattr(options, k, v)
   1.328 +
   1.329 +        if test_paths:
   1.330 +            resolver = self._spawn(TestResolver)
   1.331 +
   1.332 +            tests = list(resolver.resolve_tests(paths=test_paths, flavor=flavor,
   1.333 +                cwd=context.cwd))
   1.334 +
   1.335 +            if not tests:
   1.336 +                print('No tests could be found in the path specified. Please '
   1.337 +                    'specify a path that is a test file or is a directory '
   1.338 +                    'containing tests.')
   1.339 +                return 1
   1.340 +
   1.341 +            manifest = TestManifest()
   1.342 +            manifest.tests.extend(tests)
   1.343 +
   1.344 +            options.manifestFile = manifest
   1.345 +
   1.346 +        if rerun_failures:
   1.347 +            options.testManifest = failure_file_path
   1.348 +
   1.349 +        if debugger:
   1.350 +            options.debugger = debugger
   1.351 +
   1.352 +        if debugger_args:
   1.353 +            if options.debugger == None:
   1.354 +                print("--debugger-args passed, but no debugger specified.")
   1.355 +                return 1
   1.356 +            options.debuggerArgs = debugger_args
   1.357 +
   1.358 +        options = opts.verifyOptions(options, runner)
   1.359 +
   1.360 +        if options is None:
   1.361 +            raise Exception('mochitest option validator failed.')
   1.362 +
   1.363 +        # We need this to enable colorization of output.
   1.364 +        self.log_manager.enable_unstructured()
   1.365 +
   1.366 +        # Output processing is a little funky here. The old make targets
   1.367 +        # grepped the log output from TEST-UNEXPECTED-* and printed these lines
   1.368 +        # after test execution. Ideally the test runner would expose a Python
   1.369 +        # API for obtaining test results and we could just format failures
   1.370 +        # appropriately. Unfortunately, it doesn't yet do that. So, we capture
   1.371 +        # all output to a buffer then "grep" the buffer after test execution.
   1.372 +        # Bug 858197 tracks a Python API that would facilitate this.
   1.373 +        test_output = StringIO()
   1.374 +        handler = logging.StreamHandler(test_output)
   1.375 +        handler.addFilter(UnexpectedFilter())
   1.376 +        handler.setFormatter(StructuredHumanFormatter(0, write_times=False))
   1.377 +        logging.getLogger().addHandler(handler)
   1.378 +
   1.379 +        result = runner.runTests(options)
   1.380 +
   1.381 +        # Need to remove our buffering handler before we echo failures or else
   1.382 +        # it will catch them again!
   1.383 +        logging.getLogger().removeHandler(handler)
   1.384 +        self.log_manager.disable_unstructured()
   1.385 +
   1.386 +        if test_output.getvalue():
   1.387 +            result = 1
   1.388 +            for line in test_output.getvalue().splitlines():
   1.389 +                self.log(logging.INFO, 'unexpected', {'msg': line}, '{msg}')
   1.390 +
   1.391 +        return result
   1.392 +
   1.393 +
   1.394 +def MochitestCommand(func):
   1.395 +    """Decorator that adds shared command arguments to mochitest commands."""
   1.396 +
   1.397 +    # This employs light Python magic. Keep in mind a decorator is just a
   1.398 +    # function that takes a function, does something with it, then returns a
   1.399 +    # (modified) function. Here, we chain decorators onto the passed in
   1.400 +    # function.
   1.401 +
   1.402 +    debugger = CommandArgument('--debugger', '-d', metavar='DEBUGGER',
   1.403 +        help='Debugger binary to run test in. Program name or path.')
   1.404 +    func = debugger(func)
   1.405 +
   1.406 +    debugger_args = CommandArgument('--debugger-args',
   1.407 +        metavar='DEBUGGER_ARGS', help='Arguments to pass to the debugger.')
   1.408 +    func = debugger_args(func)
   1.409 +
   1.410 +    # Bug 933807 introduced JS_DISABLE_SLOW_SCRIPT_SIGNALS to avoid clever
   1.411 +    # segfaults induced by the slow-script-detecting logic for Ion/Odin JITted
   1.412 +    # code. If we don't pass this, the user will need to periodically type
   1.413 +    # "continue" to (safely) resume execution. There are ways to implement
   1.414 +    # automatic resuming; see the bug.
   1.415 +    slowscript = CommandArgument('--slowscript', action='store_true',
   1.416 +        help='Do not set the JS_DISABLE_SLOW_SCRIPT_SIGNALS env variable; when not set, recoverable but misleading SIGSEGV instances may occur in Ion/Odin JIT code')
   1.417 +    func = slowscript(func)
   1.418 +
   1.419 +    screenshot_on_fail = CommandArgument('--screenshot-on-fail', action='store_true',
   1.420 +        help='Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory for storing the screenshots.')
   1.421 +    func = screenshot_on_fail(func)
   1.422 +
   1.423 +    shuffle = CommandArgument('--shuffle', action='store_true',
   1.424 +        help='Shuffle execution order.')
   1.425 +    func = shuffle(func)
   1.426 +
   1.427 +    keep_open = CommandArgument('--keep-open', action='store_true',
   1.428 +        help='Keep the browser open after tests complete.')
   1.429 +    func = keep_open(func)
   1.430 +
   1.431 +    rerun = CommandArgument('--rerun-failures', action='store_true',
   1.432 +        help='Run only the tests that failed during the last test run.')
   1.433 +    func = rerun(func)
   1.434 +
   1.435 +    autorun = CommandArgument('--no-autorun', action='store_true',
   1.436 +        help='Do not starting running tests automatically.')
   1.437 +    func = autorun(func)
   1.438 +
   1.439 +    repeat = CommandArgument('--repeat', type=int, default=0,
   1.440 +        help='Repeat the test the given number of times.')
   1.441 +    func = repeat(func)
   1.442 +
   1.443 +    runUntilFailure = CommandArgument("--run-until-failure", action='store_true',
   1.444 +        help='Run tests repeatedly and stops on the first time a test fails. ' \
   1.445 +             'Default cap is 30 runs, which can be overwritten ' \
   1.446 +             'with the --repeat parameter.')
   1.447 +    func = runUntilFailure(func)
   1.448 +
   1.449 +    slow = CommandArgument('--slow', action='store_true',
   1.450 +        help='Delay execution between tests.')
   1.451 +    func = slow(func)
   1.452 +
   1.453 +    end_at = CommandArgument('--end-at', type=str,
   1.454 +        help='Stop running the test sequence at this test.')
   1.455 +    func = end_at(func)
   1.456 +
   1.457 +    start_at = CommandArgument('--start-at', type=str,
   1.458 +        help='Start running the test sequence at this test.')
   1.459 +    func = start_at(func)
   1.460 +
   1.461 +    chunk_dir = CommandArgument('--chunk-by-dir', type=int,
   1.462 +        help='Group tests together in chunks by this many top directories.')
   1.463 +    func = chunk_dir(func)
   1.464 +
   1.465 +    chunk_total = CommandArgument('--total-chunks', type=int,
   1.466 +        help='Total number of chunks to split tests into.')
   1.467 +    func = chunk_total(func)
   1.468 +
   1.469 +    this_chunk = CommandArgument('--this-chunk', type=int,
   1.470 +        help='If running tests by chunks, the number of the chunk to run.')
   1.471 +    func = this_chunk(func)
   1.472 +
   1.473 +    hide_subtests = CommandArgument('--hide-subtests', action='store_true',
   1.474 +        help='If specified, will only log subtest results on failure or timeout.')
   1.475 +    func = hide_subtests(func)
   1.476 +
   1.477 +    debug_on_failure = CommandArgument('--debug-on-failure', action='store_true',
   1.478 +        help='Breaks execution and enters the JS debugger on a test failure. ' \
   1.479 +             'Should be used together with --jsdebugger.')
   1.480 +    func = debug_on_failure(func)
   1.481 +
   1.482 +    jsdebugger = CommandArgument('--jsdebugger', action='store_true',
   1.483 +        help='Start the browser JS debugger before running the test. Implies --no-autorun.')
   1.484 +    func = jsdebugger(func)
   1.485 +
   1.486 +    this_chunk = CommandArgument('--e10s', action='store_true',
   1.487 +        help='Run tests with electrolysis preferences and test filtering enabled.')
   1.488 +    func = this_chunk(func)
   1.489 +
   1.490 +    dmd = CommandArgument('--dmd', action='store_true',
   1.491 +        help='Run tests with DMD active.')
   1.492 +    func = dmd(func)
   1.493 +
   1.494 +    dumpAboutMemory = CommandArgument('--dump-about-memory-after-test', action='store_true',
   1.495 +        help='Dump an about:memory log after every test.')
   1.496 +    func = dumpAboutMemory(func)
   1.497 +
   1.498 +    dumpDMD = CommandArgument('--dump-dmd-after-test', action='store_true',
   1.499 +        help='Dump a DMD log after every test.')
   1.500 +    func = dumpDMD(func)
   1.501 +
   1.502 +    dumpOutputDirectory = CommandArgument('--dump-output-directory', action='store',
   1.503 +        help='Specifies the directory in which to place dumped memory reports.')
   1.504 +    func = dumpOutputDirectory(func)
   1.505 +
   1.506 +    path = CommandArgument('test_paths', default=None, nargs='*',
   1.507 +        metavar='TEST',
   1.508 +        help='Test to run. Can be specified as a single file, a ' \
   1.509 +            'directory, or omitted. If omitted, the entire test suite is ' \
   1.510 +            'executed.')
   1.511 +    func = path(func)
   1.512 +
   1.513 +    install_extension = CommandArgument('--install-extension',
   1.514 +        help='Install given extension before running selected tests. ' \
   1.515 +            'Parameter is a path to xpi file.')
   1.516 +    func = install_extension(func)
   1.517 +
   1.518 +    quiet = CommandArgument('--quiet', default=False, action='store_true',
   1.519 +        help='Do not print test log lines unless a failure occurs.')
   1.520 +    func = quiet(func)
   1.521 +
   1.522 +    setenv = CommandArgument('--setenv', default=[], action='append',
   1.523 +                             metavar='NAME=VALUE', dest='environment',
   1.524 +                             help="Sets the given variable in the application's environment")
   1.525 +    func = setenv(func)
   1.526 +
   1.527 +    return func
   1.528 +
   1.529 +def B2GCommand(func):
   1.530 +    """Decorator that adds shared command arguments to b2g mochitest commands."""
   1.531 +
   1.532 +    busybox = CommandArgument('--busybox', default=None,
   1.533 +        help='Path to busybox binary to install on device')
   1.534 +    func = busybox(func)
   1.535 +
   1.536 +    logcatdir = CommandArgument('--logcat-dir', default=None,
   1.537 +        help='directory to store logcat dump files')
   1.538 +    func = logcatdir(func)
   1.539 +
   1.540 +    profile = CommandArgument('--profile', default=None,
   1.541 +        help='for desktop testing, the path to the \
   1.542 +              gaia profile to use')
   1.543 +    func = profile(func)
   1.544 +
   1.545 +    geckopath = CommandArgument('--gecko-path', default=None,
   1.546 +        help='the path to a gecko distribution that should \
   1.547 +              be installed on the emulator prior to test')
   1.548 +    func = geckopath(func)
   1.549 +
   1.550 +    nowindow = CommandArgument('--no-window', action='store_true', default=False,
   1.551 +        help='Pass --no-window to the emulator')
   1.552 +    func = nowindow(func)
   1.553 +
   1.554 +    sdcard = CommandArgument('--sdcard', default="10MB",
   1.555 +        help='Define size of sdcard: 1MB, 50MB...etc')
   1.556 +    func = sdcard(func)
   1.557 +
   1.558 +    emulator = CommandArgument('--emulator', default='arm',
   1.559 +        help='Architecture of emulator to use: x86 or arm')
   1.560 +    func = emulator(func)
   1.561 +
   1.562 +    marionette = CommandArgument('--marionette', default=None,
   1.563 +        help='host:port to use when connecting to Marionette')
   1.564 +    func = marionette(func)
   1.565 +
   1.566 +    chunk_total = CommandArgument('--total-chunks', type=int,
   1.567 +        help='Total number of chunks to split tests into.')
   1.568 +    func = chunk_total(func)
   1.569 +
   1.570 +    this_chunk = CommandArgument('--this-chunk', type=int,
   1.571 +        help='If running tests by chunks, the number of the chunk to run.')
   1.572 +    func = this_chunk(func)
   1.573 +
   1.574 +    hide_subtests = CommandArgument('--hide-subtests', action='store_true',
   1.575 +        help='If specified, will only log subtest results on failure or timeout.')
   1.576 +    func = hide_subtests(func)
   1.577 +
   1.578 +    path = CommandArgument('test_paths', default=None, nargs='*',
   1.579 +        metavar='TEST',
   1.580 +        help='Test to run. Can be specified as a single file, a ' \
   1.581 +            'directory, or omitted. If omitted, the entire test suite is ' \
   1.582 +            'executed.')
   1.583 +    func = path(func)
   1.584 +
   1.585 +    return func
   1.586 +
   1.587 +
   1.588 +
   1.589 +@CommandProvider
   1.590 +class MachCommands(MachCommandBase):
   1.591 +    @Command('mochitest-plain', category='testing',
   1.592 +        conditions=[conditions.is_firefox],
   1.593 +        description='Run a plain mochitest.')
   1.594 +    @MochitestCommand
   1.595 +    def run_mochitest_plain(self, test_paths, **kwargs):
   1.596 +        return self.run_mochitest(test_paths, 'plain', **kwargs)
   1.597 +
   1.598 +    @Command('mochitest-chrome', category='testing',
   1.599 +        conditions=[conditions.is_firefox],
   1.600 +        description='Run a chrome mochitest.')
   1.601 +    @MochitestCommand
   1.602 +    def run_mochitest_chrome(self, test_paths, **kwargs):
   1.603 +        return self.run_mochitest(test_paths, 'chrome', **kwargs)
   1.604 +
   1.605 +    @Command('mochitest-browser', category='testing',
   1.606 +        conditions=[conditions.is_firefox],
   1.607 +        description='Run a mochitest with browser chrome.')
   1.608 +    @MochitestCommand
   1.609 +    def run_mochitest_browser(self, test_paths, **kwargs):
   1.610 +        return self.run_mochitest(test_paths, 'browser', **kwargs)
   1.611 +
   1.612 +    @Command('mochitest-devtools', category='testing',
   1.613 +        conditions=[conditions.is_firefox],
   1.614 +        description='Run a devtools mochitest with browser chrome.')
   1.615 +    @MochitestCommand
   1.616 +    def run_mochitest_devtools(self, test_paths, **kwargs):
   1.617 +        return self.run_mochitest(test_paths, 'devtools', **kwargs)
   1.618 +
   1.619 +    @Command('mochitest-metro', category='testing',
   1.620 +        conditions=[conditions.is_firefox],
   1.621 +        description='Run a mochitest with metro browser chrome.')
   1.622 +    @MochitestCommand
   1.623 +    def run_mochitest_metro(self, test_paths, **kwargs):
   1.624 +        return self.run_mochitest(test_paths, 'metro', **kwargs)
   1.625 +
   1.626 +    @Command('mochitest-a11y', category='testing',
   1.627 +        conditions=[conditions.is_firefox],
   1.628 +        description='Run an a11y mochitest.')
   1.629 +    @MochitestCommand
   1.630 +    def run_mochitest_a11y(self, test_paths, **kwargs):
   1.631 +        return self.run_mochitest(test_paths, 'a11y', **kwargs)
   1.632 +
   1.633 +    @Command('webapprt-test-chrome', category='testing',
   1.634 +        conditions=[conditions.is_firefox],
   1.635 +        description='Run a webapprt chrome mochitest.')
   1.636 +    @MochitestCommand
   1.637 +    def run_mochitest_webapprt_chrome(self, test_paths, **kwargs):
   1.638 +        return self.run_mochitest(test_paths, 'webapprt-chrome', **kwargs)
   1.639 +
   1.640 +    @Command('webapprt-test-content', category='testing',
   1.641 +        conditions=[conditions.is_firefox],
   1.642 +        description='Run a webapprt content mochitest.')
   1.643 +    @MochitestCommand
   1.644 +    def run_mochitest_webapprt_content(self, test_paths, **kwargs):
   1.645 +        return self.run_mochitest(test_paths, 'webapprt-content', **kwargs)
   1.646 +
   1.647 +    def run_mochitest(self, test_paths, flavor, **kwargs):
   1.648 +        from mozbuild.controller.building import BuildDriver
   1.649 +
   1.650 +        self._ensure_state_subdir_exists('.')
   1.651 +
   1.652 +        driver = self._spawn(BuildDriver)
   1.653 +        driver.install_tests(remove=False)
   1.654 +
   1.655 +        mochitest = self._spawn(MochitestRunner)
   1.656 +
   1.657 +        return mochitest.run_desktop_test(self._mach_context,
   1.658 +            test_paths=test_paths, suite=flavor, **kwargs)
   1.659 +
   1.660 +
   1.661 +# TODO For now b2g commands will only work with the emulator,
   1.662 +# they should be modified to work with all devices.
   1.663 +def is_emulator(cls):
   1.664 +    """Emulator needs to be configured."""
   1.665 +    return cls.device_name.find('emulator') == 0
   1.666 +
   1.667 +
   1.668 +@CommandProvider
   1.669 +class B2GCommands(MachCommandBase):
   1.670 +    """So far these are only mochitest plain. They are
   1.671 +    implemented separately because their command lines
   1.672 +    are completely different.
   1.673 +    """
   1.674 +    def __init__(self, context):
   1.675 +        MachCommandBase.__init__(self, context)
   1.676 +
   1.677 +        for attr in ('b2g_home', 'xre_path', 'device_name'):
   1.678 +            setattr(self, attr, getattr(context, attr, None))
   1.679 +
   1.680 +    @Command('mochitest-remote', category='testing',
   1.681 +        description='Run a remote mochitest.',
   1.682 +        conditions=[conditions.is_b2g, is_emulator])
   1.683 +    @B2GCommand
   1.684 +    def run_mochitest_remote(self, test_paths, **kwargs):
   1.685 +        from mozbuild.controller.building import BuildDriver
   1.686 +
   1.687 +        self._ensure_state_subdir_exists('.')
   1.688 +
   1.689 +        driver = self._spawn(BuildDriver)
   1.690 +        driver.install_tests(remove=False)
   1.691 +
   1.692 +        mochitest = self._spawn(MochitestRunner)
   1.693 +        return mochitest.run_b2g_test(b2g_home=self.b2g_home,
   1.694 +                xre_path=self.xre_path, test_paths=test_paths, **kwargs)
   1.695 +
   1.696 +    @Command('mochitest-b2g-desktop', category='testing',
   1.697 +        conditions=[conditions.is_b2g_desktop],
   1.698 +        description='Run a b2g desktop mochitest.')
   1.699 +    @B2GCommand
   1.700 +    def run_mochitest_b2g_desktop(self, test_paths, **kwargs):
   1.701 +        from mozbuild.controller.building import BuildDriver
   1.702 +
   1.703 +        self._ensure_state_subdir_exists('.')
   1.704 +
   1.705 +        driver = self._spawn(BuildDriver)
   1.706 +        driver.install_tests(remove=False)
   1.707 +
   1.708 +        mochitest = self._spawn(MochitestRunner)
   1.709 +        return mochitest.run_b2g_test(test_paths=test_paths, **kwargs)

mercurial