tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

mochitest_options.py (54413B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import json
      6 import os
      7 import sys
      8 import tempfile
      9 from abc import ABCMeta, abstractmethod, abstractproperty
     10 from argparse import SUPPRESS, ArgumentParser
     11 from itertools import chain
     12 from shutil import which
     13 from urllib.parse import urlparse
     14 
     15 import mozinfo
     16 import mozlog
     17 import moznetwork
     18 from mozprofile import DEFAULT_PORTS
     19 
     20 here = os.path.abspath(os.path.dirname(__file__))
     21 
     22 try:
     23    from mozbuild.base import MachCommandConditions as conditions
     24    from mozbuild.base import MozbuildObject
     25 
     26    build_obj = MozbuildObject.from_environment(cwd=here)
     27 except ImportError:
     28    build_obj = None
     29    conditions = None
     30 
     31 
     32 # Maps test flavors to data needed to run them
     33 ALL_FLAVORS = {
     34    "mochitest": {
     35        "suite": "plain",
     36        "aliases": ("plain", "mochitest"),
     37        "enabled_apps": ("firefox", "android", "ios"),
     38        "extra_args": {
     39            "flavor": "plain",
     40        },
     41        "install_subdir": "tests",
     42    },
     43    "chrome": {
     44        "suite": "chrome",
     45        "aliases": ("chrome", "mochitest-chrome"),
     46        "enabled_apps": ("firefox"),
     47        "extra_args": {
     48            "flavor": "chrome",
     49        },
     50    },
     51    "browser-chrome": {
     52        "suite": "browser",
     53        "aliases": ("browser", "browser-chrome", "mochitest-browser-chrome", "bc"),
     54        "enabled_apps": ("firefox", "thunderbird"),
     55        "extra_args": {
     56            "flavor": "browser",
     57        },
     58    },
     59    "a11y": {
     60        "suite": "a11y",
     61        "aliases": ("a11y", "mochitest-a11y", "accessibility"),
     62        "enabled_apps": ("firefox",),
     63        "extra_args": {
     64            "flavor": "a11y",
     65        },
     66    },
     67 }
     68 SUPPORTED_FLAVORS = list(
     69    chain.from_iterable([f["aliases"] for f in ALL_FLAVORS.values()])
     70 )
     71 CANONICAL_FLAVORS = sorted([f["aliases"][0] for f in ALL_FLAVORS.values()])
     72 
     73 
     74 def strtobool(value: str):
     75    # Copied from `mach.util` since `mach.util` is not guaranteed to be available
     76    # Reimplementation of distutils.util.strtobool
     77    # https://docs.python.org/3.9/distutils/apiref.html#distutils.util.strtobool
     78    true_vals = ("y", "yes", "t", "true", "on", "1")
     79    false_vals = ("n", "no", "f", "false", "off", "0")
     80 
     81    value = value.lower()
     82    if value in true_vals:
     83        return 1
     84    if value in false_vals:
     85        return 0
     86 
     87    raise ValueError(f"Expected one of: {', '.join(true_vals + false_vals)}")
     88 
     89 
     90 def get_default_valgrind_suppression_files():
     91    # We are trying to locate files in the source tree.  So if we
     92    # don't know where the source tree is, we must give up.
     93    #
     94    # When this is being run by |mach mochitest --valgrind ...|, it is
     95    # expected that |build_obj| is not None, and so the logic below will
     96    # select the correct suppression files.
     97    #
     98    # When this is run from mozharness, |build_obj| is None, and we expect
     99    # that testing/mozharness/configs/unittests/linux_unittests.py will
    100    # select the correct suppression files (and paths to them) and
    101    # will specify them using the --valgrind-supp-files= flag.  Hence this
    102    # function will not get called when running from mozharness.
    103    #
    104    # Note: keep these Valgrind .sup file names consistent with those
    105    # in testing/mozharness/configs/unittests/linux_unittest.py.
    106    if build_obj is None or build_obj.topsrcdir is None:
    107        return []
    108 
    109    supps_path = os.path.join(build_obj.topsrcdir, "build", "valgrind")
    110 
    111    rv = []
    112    if mozinfo.os == "linux":
    113        if mozinfo.processor == "x86_64":
    114            rv.append(os.path.join(supps_path, "x86_64-pc-linux-gnu.sup"))
    115            rv.append(os.path.join(supps_path, "cross-architecture.sup"))
    116        elif mozinfo.processor == "x86":
    117            rv.append(os.path.join(supps_path, "i386-pc-linux-gnu.sup"))
    118            rv.append(os.path.join(supps_path, "cross-architecture.sup"))
    119 
    120    return rv
    121 
    122 
    123 class ArgumentContainer(metaclass=ABCMeta):
    124    @abstractproperty
    125    def args(self):
    126        pass
    127 
    128    @abstractproperty
    129    def defaults(self):
    130        pass
    131 
    132    @abstractmethod
    133    def validate(self, parser, args, context):
    134        pass
    135 
    136    def get_full_path(self, path, cwd):
    137        """Get an absolute path relative to cwd."""
    138        return os.path.normpath(os.path.join(cwd, os.path.expanduser(path)))
    139 
    140 
    141 class MochitestArguments(ArgumentContainer):
    142    """General mochitest arguments."""
    143 
    144    LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "FATAL")
    145 
    146    args = [
    147        [
    148            ["test_paths"],
    149            {
    150                "nargs": "*",
    151                "metavar": "TEST",
    152                "default": [],
    153                "help": "Test to run. Can be a single test file or a directory of tests "
    154                "(to run recursively). If omitted, the entire suite is run.",
    155            },
    156        ],
    157        [
    158            ["-f", "--flavor"],
    159            {
    160                "choices": SUPPORTED_FLAVORS,
    161                "metavar": "{{{}}}".format(", ".join(CANONICAL_FLAVORS)),
    162                "default": None,
    163                "help": "Only run tests of this flavor.",
    164            },
    165        ],
    166        [
    167            ["--keep-open"],
    168            {
    169                "nargs": "?",
    170                "type": strtobool,
    171                "const": "true",
    172                "default": None,
    173                "help": "Always keep the browser open after tests complete. Or always close the "
    174                "browser with --keep-open=false",
    175            },
    176        ],
    177        [
    178            ["--appname"],
    179            {
    180                "dest": "app",
    181                "default": None,
    182                "help": (
    183                    "Override the default binary used to run tests with the path provided, e.g "
    184                    "/usr/bin/firefox. If you have run ./mach package beforehand, you can "
    185                    "specify 'dist' to run tests against the distribution bundle's binary."
    186                ),
    187            },
    188        ],
    189        [
    190            ["--android"],
    191            {
    192                "action": "store_true",
    193                "default": False,
    194                "help": "Force an android test run.",
    195            },
    196        ],
    197        [
    198            ["--utility-path"],
    199            {
    200                "dest": "utilityPath",
    201                "default": build_obj.bindir if build_obj is not None else None,
    202                "help": "absolute path to directory containing utility programs "
    203                "(xpcshell, ssltunnel, certutil)",
    204                "suppress": True,
    205            },
    206        ],
    207        [
    208            ["--certificate-path"],
    209            {
    210                "dest": "certPath",
    211                "default": None,
    212                "help": "absolute path to directory containing certificate store to use testing profile",  # NOQA: E501
    213                "suppress": True,
    214            },
    215        ],
    216        [
    217            ["--no-autorun"],
    218            {
    219                "action": "store_false",
    220                "dest": "autorun",
    221                "default": True,
    222                "help": "Do not start running tests automatically.",
    223            },
    224        ],
    225        [
    226            ["--timeout"],
    227            {
    228                "type": int,
    229                "default": None,
    230                "help": "The per-test timeout in seconds (default: 60 seconds).",
    231            },
    232        ],
    233        [
    234            ["--max-timeouts"],
    235            {
    236                "type": int,
    237                "dest": "maxTimeouts",
    238                "default": None,
    239                "help": "The maximum number of timeouts permitted before halting testing.",
    240            },
    241        ],
    242        [
    243            ["--total-chunks"],
    244            {
    245                "type": int,
    246                "dest": "totalChunks",
    247                "help": "Total number of chunks to split tests into.",
    248                "default": None,
    249            },
    250        ],
    251        [
    252            ["--this-chunk"],
    253            {
    254                "type": int,
    255                "dest": "thisChunk",
    256                "help": "If running tests by chunks, the chunk number to run.",
    257                "default": None,
    258            },
    259        ],
    260        [
    261            ["--chunk-by-runtime"],
    262            {
    263                "action": "store_true",
    264                "dest": "chunkByRuntime",
    265                "help": "Group tests such that each chunk has roughly the same runtime.",
    266                "default": False,
    267            },
    268        ],
    269        [
    270            ["--chunk-by-dir"],
    271            {
    272                "type": int,
    273                "dest": "chunkByDir",
    274                "help": "Group tests together in the same chunk that are in the same top "
    275                "chunkByDir directories.",
    276                "default": 0,
    277            },
    278        ],
    279        [
    280            ["--run-by-manifest"],
    281            {
    282                "action": "store_true",
    283                "dest": "runByManifest",
    284                "help": "Run each manifest in a single browser instance with a fresh profile.",
    285                "default": False,
    286                "suppress": True,
    287            },
    288        ],
    289        [
    290            ["--shuffle"],
    291            {
    292                "action": "store_true",
    293                "help": "Shuffle execution order of tests.",
    294                "default": False,
    295            },
    296        ],
    297        [
    298            ["--console-level"],
    299            {
    300                "dest": "consoleLevel",
    301                "choices": LOG_LEVELS,
    302                "default": "INFO",
    303                "help": "One of {} to determine the level of console logging.".format(
    304                    ", ".join(LOG_LEVELS)
    305                ),
    306                "suppress": True,
    307            },
    308        ],
    309        [
    310            ["--bisect-chunk"],
    311            {
    312                "dest": "bisectChunk",
    313                "default": None,
    314                "help": "Specify the failing test name to find the previous tests that may be "
    315                "causing the failure.",
    316            },
    317        ],
    318        [
    319            ["--start-at"],
    320            {
    321                "dest": "startAt",
    322                "default": "",
    323                "help": "Start running the test sequence at this test.",
    324            },
    325        ],
    326        [
    327            ["--end-at"],
    328            {
    329                "dest": "endAt",
    330                "default": "",
    331                "help": "Stop running the test sequence at this test.",
    332            },
    333        ],
    334        [
    335            ["--subsuite"],
    336            {
    337                "default": None,
    338                "help": "Subsuite of tests to run. Unlike tags, subsuites also remove tests from "
    339                "the default set. Only one can be specified at once.",
    340            },
    341        ],
    342        [
    343            ["--setenv"],
    344            {
    345                "action": "append",
    346                "dest": "environment",
    347                "metavar": "NAME=VALUE",
    348                "default": [],
    349                "help": "Sets the given variable in the application's environment.",
    350            },
    351        ],
    352        [
    353            ["--exclude-extension"],
    354            {
    355                "action": "append",
    356                "dest": "extensionsToExclude",
    357                "default": [],
    358                "help": "Excludes the given extension from being installed in the test profile.",
    359                "suppress": True,
    360            },
    361        ],
    362        [
    363            ["--browser-arg"],
    364            {
    365                "action": "append",
    366                "dest": "browserArgs",
    367                "default": [],
    368                "help": "Provides an argument to the test application (e.g Firefox).",
    369                "suppress": True,
    370            },
    371        ],
    372        [
    373            ["--leak-threshold"],
    374            {
    375                "type": int,
    376                "dest": "defaultLeakThreshold",
    377                "default": 0,
    378                "help": "Fail if the number of bytes leaked in default processes through "
    379                "refcounted objects (or bytes in classes with MOZ_COUNT_CTOR and "
    380                "MOZ_COUNT_DTOR) is greater than the given number.",
    381                "suppress": True,
    382            },
    383        ],
    384        [
    385            ["--fatal-assertions"],
    386            {
    387                "action": "store_true",
    388                "dest": "fatalAssertions",
    389                "default": False,
    390                "help": "Abort testing whenever an assertion is hit (requires a debug build to "
    391                "be effective).",
    392                "suppress": True,
    393            },
    394        ],
    395        [
    396            ["--extra-profile-file"],
    397            {
    398                "action": "append",
    399                "dest": "extraProfileFiles",
    400                "default": [],
    401                "help": "Copy specified files/dirs to testing profile. Can be specified more "
    402                "than once.",
    403                "suppress": True,
    404            },
    405        ],
    406        [
    407            ["--install-extension"],
    408            {
    409                "action": "append",
    410                "dest": "extensionsToInstall",
    411                "default": [],
    412                "help": "Install the specified extension in the testing profile. Can be a path "
    413                "to a .xpi file.",
    414            },
    415        ],
    416        [
    417            ["--profile-path"],
    418            {
    419                "dest": "profilePath",
    420                "default": None,
    421                "help": "Directory where the profile will be stored. This directory will be "
    422                "deleted after the tests are finished.",
    423                "suppress": True,
    424            },
    425        ],
    426        [
    427            ["--conditioned-profile"],
    428            {
    429                "dest": "conditionedProfile",
    430                "action": "store_true",
    431                "default": False,
    432                "help": "Download and run with a full conditioned profile.",
    433            },
    434        ],
    435        [
    436            ["--testing-modules-dir"],
    437            {
    438                "dest": "testingModulesDir",
    439                "default": None,
    440                "help": "Directory where testing-only JS modules are located.",
    441                "suppress": True,
    442            },
    443        ],
    444        [
    445            ["--repeat"],
    446            {
    447                "type": int,
    448                "default": 0,
    449                "help": "Repeat the tests the given number of times.",
    450            },
    451        ],
    452        [
    453            ["--run-until-failure"],
    454            {
    455                "action": "store_true",
    456                "dest": "runUntilFailure",
    457                "default": False,
    458                "help": "Run tests repeatedly but stop the first time a test fails. Default cap "
    459                "is 30 runs, which can be overridden with the --repeat parameter.",
    460            },
    461        ],
    462        [
    463            ["--manifest"],
    464            {
    465                "dest": "manifestFile",
    466                "default": None,
    467                "help": "Path to a manifestparser (.ini formatted) manifest of tests to run.",
    468                "suppress": True,
    469            },
    470        ],
    471        [
    472            ["--extra-mozinfo-json"],
    473            {
    474                "dest": "extra_mozinfo_json",
    475                "default": None,
    476                "help": "Filter tests based on a given mozinfo file.",
    477                "suppress": True,
    478            },
    479        ],
    480        [
    481            ["--testrun-manifest-file"],
    482            {
    483                "dest": "testRunManifestFile",
    484                "default": "tests.json",
    485                "help": "Overrides the default filename of the tests.json manifest file that is "
    486                "generated by the harness and used by SimpleTest. Only useful when running "
    487                "multiple test runs simulatenously on the same machine.",
    488                "suppress": True,
    489            },
    490        ],
    491        [
    492            ["--dump-tests"],
    493            {
    494                "dest": "dump_tests",
    495                "default": None,
    496                "help": "Specify path to a filename to dump all the tests that will be run",
    497                "suppress": True,
    498            },
    499        ],
    500        [
    501            ["--failure-file"],
    502            {
    503                "dest": "failureFile",
    504                "default": None,
    505                "help": "Filename of the output file where we can store a .json list of failures "
    506                "to be run in the future with --run-only-tests.",
    507                "suppress": True,
    508            },
    509        ],
    510        [
    511            ["--run-slower"],
    512            {
    513                "action": "store_true",
    514                "dest": "runSlower",
    515                "default": False,
    516                "help": "Delay execution between tests.",
    517            },
    518        ],
    519        [
    520            ["--httpd-path"],
    521            {
    522                "dest": "httpdPath",
    523                "default": None,
    524                "help": "Path to the httpd.js file.",
    525                "suppress": True,
    526            },
    527        ],
    528        [
    529            ["--use-http3-server"],
    530            {
    531                "dest": "useHttp3Server",
    532                "default": False,
    533                "help": "Whether to use the Http3 server",
    534                "action": "store_true",
    535            },
    536        ],
    537        [
    538            ["--use-http2-server"],
    539            {
    540                "dest": "useHttp2Server",
    541                "default": False,
    542                "help": "Whether to use the Http2 server",
    543                "action": "store_true",
    544            },
    545        ],
    546        [
    547            ["--setpref"],
    548            {
    549                "action": "append",
    550                "metavar": "PREF=VALUE",
    551                "default": [],
    552                "dest": "extraPrefs",
    553                "help": "Defines an extra user preference.",
    554            },
    555        ],
    556        [
    557            ["--jsconsole"],
    558            {
    559                "action": "store_true",
    560                "default": False,
    561                "help": "Open the Browser Console.",
    562            },
    563        ],
    564        [
    565            ["--jsdebugger"],
    566            {
    567                "action": "store_true",
    568                "default": False,
    569                "help": "Start the browser JS debugger before running the test.",
    570            },
    571        ],
    572        [
    573            ["--jsdebugger-path"],
    574            {
    575                "default": None,
    576                "dest": "jsdebuggerPath",
    577                "help": "Path to a Firefox binary that will be used to run the toolbox. Should "
    578                "be used together with --jsdebugger.",
    579            },
    580        ],
    581        [
    582            ["--debug-on-failure"],
    583            {
    584                "action": "store_true",
    585                "default": False,
    586                "dest": "debugOnFailure",
    587                "help": "Breaks execution and enters the JS debugger on a test failure. Should "
    588                "be used together with --jsdebugger.",
    589            },
    590        ],
    591        [
    592            ["--disable-e10s"],
    593            {
    594                "action": "store_false",
    595                "default": True,
    596                "dest": "e10s",
    597                "help": "Run tests with electrolysis preferences and test filtering disabled.",
    598            },
    599        ],
    600        [
    601            ["--enable-a11y-checks"],
    602            {
    603                "action": "store_true",
    604                "default": False,
    605                "dest": "a11y_checks",
    606                "help": "Run tests with accessibility checks enabled.",
    607            },
    608        ],
    609        [
    610            ["--disable-fission"],
    611            {
    612                "action": "store_true",
    613                "default": False,
    614                "dest": "disable_fission",
    615                "help": "Run tests with fission (site isolation) disabled.",
    616            },
    617        ],
    618        [
    619            ["--enable-xorigin-tests"],
    620            {
    621                "action": "store_true",
    622                "default": False,
    623                "dest": "xOriginTests",
    624                "help": "Run tests in a cross origin iframe.",
    625            },
    626        ],
    627        [
    628            ["--store-chrome-manifest"],
    629            {
    630                "action": "store",
    631                "help": "Destination path to write a copy of any chrome manifest "
    632                "written by the harness.",
    633                "default": None,
    634                "suppress": True,
    635            },
    636        ],
    637        [
    638            ["--jscov-dir-prefix"],
    639            {
    640                "action": "store",
    641                "help": "Directory to store per-test line coverage data as json "
    642                "(browser-chrome only). To emit lcov formatted data, set "
    643                "JS_CODE_COVERAGE_OUTPUT_DIR in the environment.",
    644                "default": None,
    645                "suppress": True,
    646            },
    647        ],
    648        [
    649            ["--dmd"],
    650            {
    651                "action": "store_true",
    652                "default": False,
    653                "help": "Run tests with DMD active.",
    654            },
    655        ],
    656        [
    657            ["--dump-output-directory"],
    658            {
    659                "default": None,
    660                "dest": "dumpOutputDirectory",
    661                "help": "Specifies the directory in which to place dumped memory reports.",
    662            },
    663        ],
    664        [
    665            ["--dump-about-memory-after-test"],
    666            {
    667                "action": "store_true",
    668                "default": False,
    669                "dest": "dumpAboutMemoryAfterTest",
    670                "help": "Dump an about:memory log after each test in the directory specified "
    671                "by --dump-output-directory.",
    672            },
    673        ],
    674        [
    675            ["--dump-dmd-after-test"],
    676            {
    677                "action": "store_true",
    678                "default": False,
    679                "dest": "dumpDMDAfterTest",
    680                "help": "Dump a DMD log (and an accompanying about:memory log) after each test. "
    681                "These will be dumped into your default temp directory, NOT the directory "
    682                "specified by --dump-output-directory. The logs are numbered by test, and "
    683                "each test will include output that indicates the DMD output filename.",
    684            },
    685        ],
    686        [
    687            ["--screenshot-on-fail"],
    688            {
    689                "action": "store_true",
    690                "default": False,
    691                "dest": "screenshotOnFail",
    692                "help": "Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory "  # NOQA: E501
    693                "for storing the screenshots.",
    694            },
    695        ],
    696        [
    697            ["--quiet"],
    698            {
    699                "action": "store_true",
    700                "dest": "quiet",
    701                "default": False,
    702                "help": "Do not print test log lines unless a failure occurs.",
    703            },
    704        ],
    705        [
    706            ["--headless"],
    707            {
    708                "action": "store_true",
    709                "dest": "headless",
    710                "default": False,
    711                "help": "Run tests in headless mode.",
    712            },
    713        ],
    714        [
    715            ["--pidfile"],
    716            {
    717                "dest": "pidFile",
    718                "default": "",
    719                "help": "Name of the pidfile to generate.",
    720                "suppress": True,
    721            },
    722        ],
    723        [
    724            ["--use-test-media-devices"],
    725            {
    726                "action": "store_true",
    727                "default": False,
    728                "dest": "useTestMediaDevices",
    729                "help": "Use test media device drivers for media testing.",
    730            },
    731        ],
    732        [
    733            ["--gmp-path"],
    734            {
    735                "default": None,
    736                "help": "Path to fake GMP plugin. Will be deduced from the binary if not passed.",
    737                "suppress": True,
    738            },
    739        ],
    740        [
    741            ["--xre-path"],
    742            {
    743                "dest": "xrePath",
    744                "default": None,  # individual scripts will set a sane default
    745                "help": "Absolute path to directory containing XRE (probably xulrunner).",
    746                "suppress": True,
    747            },
    748        ],
    749        [
    750            ["--symbols-path"],
    751            {
    752                "dest": "symbolsPath",
    753                "default": None,
    754                "help": "Absolute path to directory containing breakpad symbols, or the URL of a "
    755                "zip file containing symbols",
    756                "suppress": True,
    757            },
    758        ],
    759        [
    760            ["--debugger"],
    761            {
    762                "default": None,
    763                "help": "Debugger binary to run tests in. Program name or path.",
    764            },
    765        ],
    766        [
    767            ["--debugger-args"],
    768            {
    769                "dest": "debuggerArgs",
    770                "default": None,
    771                "help": "Arguments to pass to the debugger.",
    772            },
    773        ],
    774        [
    775            ["--valgrind"],
    776            {
    777                "default": None,
    778                "help": "Valgrind binary to run tests with. Program name or path.",
    779            },
    780        ],
    781        [
    782            ["--valgrind-args"],
    783            {
    784                "dest": "valgrindArgs",
    785                "default": None,
    786                "help": "Comma-separated list of extra arguments to pass to Valgrind.",
    787            },
    788        ],
    789        [
    790            ["--valgrind-supp-files"],
    791            {
    792                "dest": "valgrindSuppFiles",
    793                "default": None,
    794                "help": "Comma-separated list of suppression files to pass to Valgrind.",
    795            },
    796        ],
    797        [
    798            ["--debugger-interactive"],
    799            {
    800                "action": "store_true",
    801                "dest": "debuggerInteractive",
    802                "default": None,
    803                "help": "Prevents the test harness from redirecting stdout and stderr for "
    804                "interactive debuggers.",
    805                "suppress": True,
    806            },
    807        ],
    808        [
    809            ["--tag"],
    810            {
    811                "action": "append",
    812                "dest": "test_tags",
    813                "default": None,
    814                "help": "Filter out tests that don't have the given tag. Can be used multiple "
    815                "times in which case the test must contain at least one of the given tags.",
    816            },
    817        ],
    818        [
    819            ["--marionette"],
    820            {
    821                "default": None,
    822                "help": "host:port to use when connecting to Marionette",
    823            },
    824        ],
    825        [
    826            ["--marionette-socket-timeout"],
    827            {
    828                "default": None,
    829                "help": "Timeout while waiting to receive a message from the marionette server.",
    830                "suppress": True,
    831            },
    832        ],
    833        [
    834            ["--marionette-startup-timeout"],
    835            {
    836                "default": None,
    837                "help": "Timeout while waiting for marionette server startup.",
    838                "suppress": True,
    839            },
    840        ],
    841        [
    842            ["--cleanup-crashes"],
    843            {
    844                "action": "store_true",
    845                "dest": "cleanupCrashes",
    846                "default": False,
    847                "help": "Delete pending crash reports before running tests.",
    848                "suppress": True,
    849            },
    850        ],
    851        [
    852            ["--websocket-process-bridge-port"],
    853            {
    854                "default": "8191",
    855                "dest": "websocket_process_bridge_port",
    856                "help": "Port for websocket/process bridge. Default 8191.",
    857            },
    858        ],
    859        [
    860            ["--failure-pattern-file"],
    861            {
    862                "default": None,
    863                "dest": "failure_pattern_file",
    864                "help": "File describes all failure patterns of the tests.",
    865                "suppress": True,
    866            },
    867        ],
    868        [
    869            ["--sandbox-read-whitelist"],
    870            {
    871                "default": [],
    872                "dest": "sandboxReadWhitelist",
    873                "action": "append",
    874                "help": "Path to add to the sandbox whitelist.",
    875                "suppress": True,
    876            },
    877        ],
    878        [
    879            ["--verify"],
    880            {
    881                "action": "store_true",
    882                "default": False,
    883                "help": "Run tests in verification mode: Run many times in different "
    884                "ways, to see if there are intermittent failures.",
    885            },
    886        ],
    887        [
    888            ["--verify-fission"],
    889            {
    890                "action": "store_true",
    891                "default": False,
    892                "help": "Run tests once without Fission, once with Fission",
    893            },
    894        ],
    895        [
    896            ["--verify-max-time"],
    897            {
    898                "type": int,
    899                "default": 3600,
    900                "help": "Maximum time, in seconds, to run in --verify mode.",
    901            },
    902        ],
    903        [
    904            ["--profiler"],
    905            {
    906                "action": "store_true",
    907                "dest": "profiler",
    908                "default": False,
    909                "help": "Run the Firefox Profiler and get a performance profile of the "
    910                "mochitest. This is useful to find performance issues, and also "
    911                "to see what exactly the test is doing. To get profiler options run: "
    912                "`MOZ_PROFILER_HELP=1 ./mach run`",
    913            },
    914        ],
    915        [
    916            ["--profiler-save-only"],
    917            {
    918                "action": "store_true",
    919                "dest": "profilerSaveOnly",
    920                "default": False,
    921                "help": "Run the Firefox Profiler and save it to the path specified by the "
    922                "MOZ_UPLOAD_DIR environment variable.",
    923            },
    924        ],
    925        [
    926            ["--run-failures"],
    927            {
    928                "action": "store",
    929                "dest": "runFailures",
    930                "default": "",
    931                "help": "Run fail-if/skip-if tests that match a keyword given.",
    932            },
    933        ],
    934        [
    935            ["--timeout-as-pass"],
    936            {
    937                "action": "store_true",
    938                "dest": "timeoutAsPass",
    939                "default": False,
    940                "help": "treat harness level timeouts as passing (used for quarantine jobs).",
    941            },
    942        ],
    943        [
    944            ["--crash-as-pass"],
    945            {
    946                "action": "store_true",
    947                "dest": "crashAsPass",
    948                "default": False,
    949                "help": "treat harness level crashes as passing (used for quarantine jobs).",
    950            },
    951        ],
    952        [
    953            ["--compare-preferences"],
    954            {
    955                "action": "store_true",
    956                "dest": "comparePrefs",
    957                "default": False,
    958                "help": "Compare preferences at the end of each test and report changed ones as failures.",
    959            },
    960        ],
    961        [
    962            ["--restart-after-failure"],
    963            {
    964                "action": "store_true",
    965                "dest": "restartAfterFailure",
    966                "default": False,
    967                "help": "Terminate the session on first failure and restart where you left off.",
    968            },
    969        ],
    970        [
    971            ["--variant"],
    972            {
    973                "dest": "variant",
    974                "default": "",
    975                "help": "use specified variant for any harness level changes.",
    976            },
    977        ],
    978    ]
    979 
    980    defaults = {
    981        # Bug 1065098 - The gmplugin process fails to produce a leak
    982        # log for some reason.
    983        "ignoreMissingLeaks": ["gmplugin"],
    984        "extensionsToExclude": ["specialpowers"],
    985        # Set server information on the args object
    986        "webServer": "127.0.0.1",
    987        "httpPort": DEFAULT_PORTS["http"],
    988        "sslPort": DEFAULT_PORTS["https"],
    989        "webSocketPort": DEFAULT_PORTS["ws"],
    990        "webSocketSSLPort": DEFAULT_PORTS["wss"],
    991        # The default websocket port is incorrect in mozprofile; it is
    992        # set to the SSL proxy setting. See:
    993        # see https://bugzilla.mozilla.org/show_bug.cgi?id=916517
    994        # args.webSocketPort = DEFAULT_PORTS['ws']
    995    }
    996 
    997    def validate(self, parser, options, context):
    998        """Validate generic options."""
    999 
   1000        # and android/iOS doesn't use 'app' the same way, so skip validation
   1001        if parser.app not in ("android", "ios"):
   1002            if options.app is None:
   1003                if build_obj:
   1004                    from mozbuild.base import BinaryNotFoundException
   1005 
   1006                    try:
   1007                        options.app = build_obj.get_binary_path()
   1008                    except BinaryNotFoundException as e:
   1009                        print(f"{e}\n\n{e.help()}\n")
   1010                        sys.exit(1)
   1011                else:
   1012                    parser.error(
   1013                        "could not find the application path, --appname must be specified"
   1014                    )
   1015            elif options.app == "dist" and build_obj:
   1016                options.app = build_obj.get_binary_path(where="staged-package")
   1017 
   1018            options.app = self.get_full_path(options.app, parser.oldcwd)
   1019            if not os.path.exists(options.app):
   1020                parser.error(
   1021                    f"Error: Path {options.app} doesn't exist. Are you executing "
   1022                    "$objdir/_tests/testing/mochitest/runtests.py?"
   1023                )
   1024 
   1025        if options.flavor is None:
   1026            options.flavor = "plain"
   1027 
   1028        for value in ALL_FLAVORS.values():
   1029            if options.flavor in value["aliases"]:
   1030                options.flavor = value["suite"]
   1031                break
   1032 
   1033        if options.gmp_path is None and options.app and build_obj:
   1034            # Need to fix the location of gmp_fake which might not be shipped in the binary
   1035            gmp_modules = (
   1036                ("gmp-fake", "1.0"),
   1037                ("gmp-clearkey", "0.1"),
   1038                ("gmp-fakeopenh264", "1.0"),
   1039            )
   1040            options.gmp_path = os.pathsep.join(
   1041                os.path.join(build_obj.bindir, *p) for p in gmp_modules
   1042            )
   1043 
   1044        if options.totalChunks is not None and options.thisChunk is None:
   1045            parser.error("thisChunk must be specified when totalChunks is specified")
   1046 
   1047        if options.extra_mozinfo_json:
   1048            if not os.path.isfile(options.extra_mozinfo_json):
   1049                parser.error(
   1050                    "Error: couldn't find mozinfo.json at '%s'."
   1051                    % options.extra_mozinfo_json
   1052                )
   1053 
   1054            options.extra_mozinfo_json = json.load(open(options.extra_mozinfo_json))
   1055 
   1056        if options.totalChunks:
   1057            if not 1 <= options.thisChunk <= options.totalChunks:
   1058                parser.error("thisChunk must be between 1 and totalChunks")
   1059 
   1060        if options.chunkByDir and options.chunkByRuntime:
   1061            parser.error("can only use one of --chunk-by-dir or --chunk-by-runtime")
   1062 
   1063        if options.xrePath is None:
   1064            # default xrePath to the app path if not provided
   1065            # but only if an app path was explicitly provided
   1066            if options.app != parser.get_default("app"):
   1067                options.xrePath = os.path.dirname(options.app)
   1068                if mozinfo.isMac:
   1069                    options.xrePath = os.path.join(
   1070                        os.path.dirname(options.xrePath), "Resources"
   1071                    )
   1072            elif build_obj is not None:
   1073                # otherwise default to dist/bin
   1074                options.xrePath = build_obj.bindir
   1075            else:
   1076                parser.error(
   1077                    "could not find xre directory, --xre-path must be specified"
   1078                )
   1079 
   1080        # allow relative paths
   1081        if options.xrePath:
   1082            options.xrePath = self.get_full_path(options.xrePath, parser.oldcwd)
   1083 
   1084        if options.profilePath:
   1085            options.profilePath = self.get_full_path(options.profilePath, parser.oldcwd)
   1086 
   1087        if options.utilityPath:
   1088            options.utilityPath = self.get_full_path(options.utilityPath, parser.oldcwd)
   1089 
   1090        if options.certPath:
   1091            options.certPath = self.get_full_path(options.certPath, parser.oldcwd)
   1092        elif build_obj:
   1093            options.certPath = os.path.join(
   1094                build_obj.topsrcdir, "build", "pgo", "certs"
   1095            )
   1096 
   1097        if options.symbolsPath and len(urlparse(options.symbolsPath).scheme) < 2:
   1098            options.symbolsPath = self.get_full_path(options.symbolsPath, parser.oldcwd)
   1099        elif not options.symbolsPath and build_obj:
   1100            options.symbolsPath = os.path.join(
   1101                build_obj.distdir, "crashreporter-symbols"
   1102            )
   1103 
   1104        if options.debugOnFailure and not options.jsdebugger:
   1105            parser.error("--debug-on-failure requires --jsdebugger.")
   1106 
   1107        if options.jsdebuggerPath and not options.jsdebugger:
   1108            parser.error("--jsdebugger-path requires --jsdebugger.")
   1109 
   1110        if options.debuggerArgs and not options.debugger:
   1111            parser.error("--debugger-args requires --debugger.")
   1112 
   1113        if options.valgrind or options.debugger:
   1114            # valgrind and some debuggers may cause Gecko to start slowly. Make sure
   1115            # marionette waits long enough to connect.
   1116            options.marionette_startup_timeout = 900
   1117            options.marionette_socket_timeout = 540
   1118 
   1119        if options.store_chrome_manifest:
   1120            options.store_chrome_manifest = os.path.abspath(
   1121                options.store_chrome_manifest
   1122            )
   1123            if not os.path.isdir(os.path.dirname(options.store_chrome_manifest)):
   1124                parser.error(
   1125                    "directory for %s does not exist as a destination to copy a "
   1126                    "chrome manifest." % options.store_chrome_manifest
   1127                )
   1128 
   1129        if options.jscov_dir_prefix:
   1130            options.jscov_dir_prefix = os.path.abspath(options.jscov_dir_prefix)
   1131            if not os.path.isdir(options.jscov_dir_prefix):
   1132                parser.error(
   1133                    "directory %s does not exist as a destination for coverage "
   1134                    "data." % options.jscov_dir_prefix
   1135                )
   1136 
   1137        if options.testingModulesDir is None:
   1138            # Try to guess the testing modules directory.
   1139            possible = [os.path.join(here, os.path.pardir, "modules")]
   1140            if build_obj:
   1141                possible.insert(
   1142                    0, os.path.join(build_obj.topobjdir, "_tests", "modules")
   1143                )
   1144 
   1145            for p in possible:
   1146                if os.path.isdir(p):
   1147                    options.testingModulesDir = p
   1148                    break
   1149 
   1150        # Paths to specialpowers and mochijar from the tests archive.
   1151        options.stagedAddons = [
   1152            os.path.join(here, "extensions", "specialpowers"),
   1153            os.path.join(here, "mochijar"),
   1154        ]
   1155        if build_obj:
   1156            objdir_xpi_stage = os.path.join(build_obj.distdir, "xpi-stage")
   1157            if os.path.isdir(objdir_xpi_stage):
   1158                options.stagedAddons = [
   1159                    os.path.join(objdir_xpi_stage, "specialpowers"),
   1160                    os.path.join(objdir_xpi_stage, "mochijar"),
   1161                ]
   1162            plugins_dir = os.path.join(build_obj.distdir, "plugins")
   1163            if (
   1164                os.path.isdir(plugins_dir)
   1165                and plugins_dir not in options.extraProfileFiles
   1166            ):
   1167                options.extraProfileFiles.append(plugins_dir)
   1168 
   1169        # Even if buildbot is updated, we still want this, as the path we pass in
   1170        # to the app must be absolute and have proper slashes.
   1171        if options.testingModulesDir is not None:
   1172            options.testingModulesDir = os.path.normpath(options.testingModulesDir)
   1173 
   1174            if not os.path.isabs(options.testingModulesDir):
   1175                options.testingModulesDir = os.path.abspath(options.testingModulesDir)
   1176 
   1177            if not os.path.isdir(options.testingModulesDir):
   1178                parser.error(
   1179                    "--testing-modules-dir not a directory: %s"
   1180                    % options.testingModulesDir
   1181                )
   1182 
   1183            options.testingModulesDir = options.testingModulesDir.replace("\\", "/")
   1184            if options.testingModulesDir[-1] != "/":
   1185                options.testingModulesDir += "/"
   1186 
   1187        if options.runUntilFailure:
   1188            if not options.repeat:
   1189                options.repeat = 29
   1190 
   1191        if options.dumpOutputDirectory is None:
   1192            options.dumpOutputDirectory = tempfile.gettempdir()
   1193 
   1194        if options.dumpAboutMemoryAfterTest or options.dumpDMDAfterTest:
   1195            if not os.path.isdir(options.dumpOutputDirectory):
   1196                parser.error(
   1197                    "--dump-output-directory not a directory: %s"
   1198                    % options.dumpOutputDirectory
   1199                )
   1200 
   1201        if options.useTestMediaDevices:
   1202            if not mozinfo.isLinux:
   1203                parser.error(
   1204                    "--use-test-media-devices is only supported on Linux currently"
   1205                )
   1206 
   1207            gst01 = which("gst-launch-0.1")
   1208            gst010 = which("gst-launch-0.10")
   1209            gst10 = which("gst-launch-1.0")
   1210            pactl = which("pactl")
   1211 
   1212            if not (gst01 or gst10 or gst010):
   1213                parser.error(
   1214                    "Missing gst-launch-{0.1,0.10,1.0}, required for "
   1215                    "--use-test-media-devices"
   1216                )
   1217 
   1218            if not pactl:
   1219                parser.error(
   1220                    "Missing binary pactl required for --use-test-media-devices"
   1221                )
   1222 
   1223        # The a11y and chrome flavors can't run with e10s.
   1224        if options.flavor in ("a11y", "chrome") and options.e10s:
   1225            parser.error(
   1226                f"mochitest-{options.flavor} does not support e10s, try again with "
   1227                "--disable-e10s."
   1228            )
   1229 
   1230        # If e10s explicitly disabled and no fission option specified, disable fission
   1231        if (not options.e10s) and (not options.disable_fission):
   1232            options.disable_fission = True
   1233 
   1234        options.leakThresholds = {
   1235            "default": options.defaultLeakThreshold,
   1236            "tab": options.defaultLeakThreshold,
   1237            "forkserver": options.defaultLeakThreshold,
   1238            # GMP rarely gets a log, but when it does, it leaks a little.
   1239            "gmplugin": 20000,
   1240        }
   1241 
   1242        # See the dependencies of bug 1401764.
   1243        if mozinfo.isWin:
   1244            options.leakThresholds["tab"] = 1000
   1245 
   1246        # XXX We can't normalize test_paths in the non build_obj case here,
   1247        # because testRoot depends on the flavor, which is determined by the
   1248        # mach command and therefore not finalized yet. Conversely, test paths
   1249        # need to be normalized here for the mach case.
   1250        if options.test_paths and build_obj:
   1251            # Normalize test paths so they are relative to test root
   1252            options.test_paths = [
   1253                build_obj._wrap_path_argument(p).relpath() for p in options.test_paths
   1254            ]
   1255 
   1256        return options
   1257 
   1258 
   1259 class AndroidArguments(ArgumentContainer):
   1260    """Android specific arguments."""
   1261 
   1262    args = [
   1263        [
   1264            ["--no-install"],
   1265            {
   1266                "action": "store_true",
   1267                "default": False,
   1268                "help": "Skip the installation of the APK.",
   1269            },
   1270        ],
   1271        [
   1272            ["--aab"],
   1273            {
   1274                "action": "store_true",
   1275                "default": False,
   1276                "help": "Install the test_runner app using AAB.",
   1277            },
   1278        ],
   1279        [
   1280            ["--deviceSerial"],
   1281            {
   1282                "dest": "deviceSerial",
   1283                "help": "adb serial number of remote device. This is required "
   1284                "when more than one device is connected to the host. "
   1285                "Use 'adb devices' to see connected devices.",
   1286                "default": None,
   1287            },
   1288        ],
   1289        [
   1290            ["--adbpath"],
   1291            {
   1292                "dest": "adbPath",
   1293                "default": None,
   1294                "help": "Path to adb binary.",
   1295                "suppress": True,
   1296            },
   1297        ],
   1298        [
   1299            ["--activity"],
   1300            {
   1301                "dest": "appActivity",
   1302                "default": "TestRunnerActivity",
   1303                "help": (
   1304                    "Specify the android app activity that should be used (e.g. "
   1305                    "GeckoViewActivity for org.mozilla.geckoview_example). Uses "
   1306                    "TestRunnerActivity by default for org.mozilla.geckoview.test_"
   1307                    "runner"
   1308                ),
   1309            },
   1310        ],
   1311        [
   1312            ["--remote-webserver"],
   1313            {
   1314                "dest": "remoteWebServer",
   1315                "default": None,
   1316                "help": "IP address of the remote web server.",
   1317            },
   1318        ],
   1319        [
   1320            ["--http-port"],
   1321            {
   1322                "dest": "httpPort",
   1323                "default": DEFAULT_PORTS["http"],
   1324                "help": "http port of the remote web server.",
   1325                "suppress": True,
   1326            },
   1327        ],
   1328        [
   1329            ["--ssl-port"],
   1330            {
   1331                "dest": "sslPort",
   1332                "default": DEFAULT_PORTS["https"],
   1333                "help": "ssl port of the remote web server.",
   1334                "suppress": True,
   1335            },
   1336        ],
   1337        [
   1338            ["--remoteTestRoot"],
   1339            {
   1340                "dest": "remoteTestRoot",
   1341                "default": None,
   1342                "help": "Remote directory to use as test root "
   1343                "(eg. /data/local/tmp/test_root).",
   1344                "suppress": True,
   1345            },
   1346        ],
   1347        [
   1348            ["--enable-coverage"],
   1349            {
   1350                "action": "store_true",
   1351                "default": False,
   1352                "help": "Enable collecting code coverage information when running "
   1353                "junit tests.",
   1354            },
   1355        ],
   1356        [
   1357            ["--coverage-output-dir"],
   1358            {
   1359                "action": "store",
   1360                "default": None,
   1361                "help": "When using --enable-java-coverage, save the code coverage report "
   1362                "files to this directory.",
   1363            },
   1364        ],
   1365    ]
   1366 
   1367    defaults = {
   1368        # we don't want to exclude specialpowers on android just yet
   1369        "extensionsToExclude": [],
   1370        # mochijar doesn't get installed via marionette on android
   1371        "extensionsToInstall": [os.path.join(here, "mochijar")],
   1372        "logFile": "mochitest.log",
   1373        "utilityPath": None,
   1374    }
   1375 
   1376    def validate(self, parser, options, context):
   1377        """Validate android options."""
   1378 
   1379        if build_obj:
   1380            options.log_mach = "-"
   1381 
   1382            objdir_xpi_stage = os.path.join(build_obj.distdir, "xpi-stage")
   1383            if os.path.isdir(objdir_xpi_stage):
   1384                options.extensionsToInstall = [
   1385                    os.path.join(objdir_xpi_stage, "mochijar"),
   1386                    os.path.join(objdir_xpi_stage, "specialpowers"),
   1387                ]
   1388 
   1389        if options.remoteWebServer is None:
   1390            options.remoteWebServer = moznetwork.get_ip()
   1391 
   1392        options.webServer = options.remoteWebServer
   1393 
   1394        if options.app is None:
   1395            options.app = "org.mozilla.geckoview.test_runner"
   1396 
   1397        if build_obj and "MOZ_HOST_BIN" in os.environ:
   1398            options.xrePath = os.environ["MOZ_HOST_BIN"]
   1399 
   1400        # Only reset the xrePath if it wasn't provided
   1401        if options.xrePath is None:
   1402            options.xrePath = options.utilityPath
   1403 
   1404        if build_obj:
   1405            options.topsrcdir = build_obj.topsrcdir
   1406 
   1407        if options.pidFile != "":
   1408            f = open(options.pidFile, "w")
   1409            f.write("%s" % os.getpid())
   1410            f.close()
   1411 
   1412        if options.coverage_output_dir and not options.enable_coverage:
   1413            parser.error("--coverage-output-dir must be used with --enable-coverage")
   1414        if options.enable_coverage:
   1415            if not options.autorun:
   1416                parser.error("--enable-coverage cannot be used with --no-autorun")
   1417            if not options.coverage_output_dir:
   1418                parser.error(
   1419                    "--coverage-output-dir must be specified when using --enable-coverage"
   1420                )
   1421            parent_dir = os.path.dirname(options.coverage_output_dir)
   1422            if not os.path.isdir(options.coverage_output_dir):
   1423                parser.error(
   1424                    "The directory for the coverage output does not exist: %s"
   1425                    % parent_dir
   1426                )
   1427 
   1428        # allow us to keep original application around for cleanup while
   1429        # running tests
   1430        options.remoteappname = options.app
   1431        return options
   1432 
   1433 
   1434 class IosArguments(ArgumentContainer):
   1435    """Ios specific arguments."""
   1436 
   1437    args = [
   1438        [
   1439            ["--no-install"],
   1440            {
   1441                "action": "store_true",
   1442                "default": False,
   1443                "help": "Skip the installation of the app.",
   1444            },
   1445        ],
   1446        # FIXME: Support something like --deviceSerial.
   1447        [
   1448            ["--remote-webserver"],
   1449            {
   1450                "dest": "remoteWebServer",
   1451                "default": None,
   1452                "help": "IP address of the remote web server.",
   1453            },
   1454        ],
   1455        [
   1456            ["--http-port"],
   1457            {
   1458                "dest": "httpPort",
   1459                "default": DEFAULT_PORTS["http"],
   1460                "help": "http port of the remote web server.",
   1461                "suppress": True,
   1462            },
   1463        ],
   1464        [
   1465            ["--ssl-port"],
   1466            {
   1467                "dest": "sslPort",
   1468                "default": DEFAULT_PORTS["https"],
   1469                "help": "ssl port of the remote web server.",
   1470                "suppress": True,
   1471            },
   1472        ],
   1473        [
   1474            ["--remoteTestRoot"],
   1475            {
   1476                "dest": "remoteTestRoot",
   1477                "default": None,
   1478                "help": "Remote directory to use as test root "
   1479                "(eg. /data/local/tmp/test_root).",
   1480                "suppress": True,
   1481            },
   1482        ],
   1483    ]
   1484 
   1485    defaults = {
   1486        # we don't want to exclude specialpowers on iOS just yet
   1487        "extensionsToExclude": [],
   1488        # mochijar doesn't get installed via marionette on iOS
   1489        "extensionsToInstall": [os.path.join(here, "mochijar")],
   1490        "logFile": "mochitest.log",
   1491        "utilityPath": None,
   1492    }
   1493 
   1494    def validate(self, parser, options, context):
   1495        """Validate iOS options."""
   1496 
   1497        if build_obj:
   1498            options.log_mach = "-"
   1499 
   1500            objdir_xpi_stage = os.path.join(build_obj.distdir, "xpi-stage")
   1501            if os.path.isdir(objdir_xpi_stage):
   1502                options.extensionsToInstall = [
   1503                    os.path.join(objdir_xpi_stage, "mochijar"),
   1504                    os.path.join(objdir_xpi_stage, "specialpowers"),
   1505                ]
   1506 
   1507        if options.remoteWebServer is None:
   1508            options.remoteWebServer = moznetwork.get_ip()
   1509 
   1510        options.webServer = options.remoteWebServer
   1511 
   1512        if options.app is None:
   1513            options.app = "org.mozilla.ios.GeckoTestBrowser"
   1514 
   1515        if build_obj and "MOZ_HOST_BIN" in os.environ:
   1516            options.xrePath = os.environ["MOZ_HOST_BIN"]
   1517 
   1518        # Only reset the xrePath if it wasn't provided
   1519        if options.xrePath is None:
   1520            options.xrePath = options.utilityPath
   1521 
   1522        if build_obj:
   1523            options.topsrcdir = build_obj.topsrcdir
   1524 
   1525        if options.pidFile != "":
   1526            f = open(options.pidFile, "w")
   1527            f.write("%s" % os.getpid())
   1528            f.close()
   1529 
   1530        # allow us to keep original application around for cleanup while
   1531        # running tests
   1532        options.remoteappname = options.app
   1533        return options
   1534 
   1535 
   1536 container_map = {
   1537    "generic": [MochitestArguments],
   1538    "android": [MochitestArguments, AndroidArguments],
   1539    "ios": [MochitestArguments, IosArguments],
   1540 }
   1541 
   1542 
   1543 class MochitestArgumentParser(ArgumentParser):
   1544    """%(prog)s [options] [test paths]"""
   1545 
   1546    _containers = None
   1547    context = {}
   1548 
   1549    def __init__(self, app=None, **kwargs):
   1550        ArgumentParser.__init__(
   1551            self, usage=self.__doc__, conflict_handler="resolve", **kwargs
   1552        )
   1553 
   1554        self.oldcwd = os.getcwd()
   1555        self.app = app
   1556 
   1557        mozlog.commandline.add_logging_group(self)
   1558 
   1559        self.build_args()
   1560 
   1561    @property
   1562    def containers(self):
   1563        if self._containers:
   1564            return self._containers
   1565 
   1566        containers = container_map[self.app]
   1567        self._containers = [c() for c in containers]
   1568        return self._containers
   1569 
   1570    def validate(self, args):
   1571        for container in self.containers:
   1572            args = container.validate(self, args, self.context)
   1573        return args
   1574 
   1575    def build_args(self, args=None):
   1576        if args and not self.app and any("--android" == arg for arg in args):
   1577            self.app = "android"
   1578 
   1579        if not self.app and build_obj:
   1580            if conditions.is_android(build_obj):
   1581                self.app = "android"
   1582            if conditions.is_ios(build_obj):
   1583                self.app = "ios"
   1584        if not self.app:
   1585            # platform can't be determined and app wasn't specified explicitly,
   1586            # so just use generic arguments and hope for the best
   1587            self.app = "generic"
   1588 
   1589        if self.app not in container_map:
   1590            self.error(
   1591                "Unrecognized app '{}'! Must be one of: {}".format(
   1592                    self.app, ", ".join(container_map.keys())
   1593                )
   1594            )
   1595 
   1596        defaults = {}
   1597        for container in self.containers:
   1598            defaults.update(container.defaults)
   1599            group = self.add_argument_group(
   1600                container.__class__.__name__, container.__doc__
   1601            )
   1602 
   1603            for cli, kwargs in container.args:
   1604                # Allocate new lists so references to original don't get mutated.
   1605                # allowing multiple uses within a single process.
   1606                if "default" in kwargs and isinstance(kwargs["default"], list):
   1607                    kwargs["default"] = []
   1608 
   1609                if "suppress" in kwargs:
   1610                    if kwargs["suppress"]:
   1611                        kwargs["help"] = SUPPRESS
   1612                    del kwargs["suppress"]
   1613 
   1614                group.add_argument(*cli, **kwargs)
   1615 
   1616        self.set_defaults(**defaults)
   1617 
   1618    def parse_known_args(self, args=None, namespace=None):
   1619        return super().parse_known_args(args, namespace)