tor-browser

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

mach_commands.py (20398B)


      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 functools
      6 import logging
      7 import os
      8 import sys
      9 import warnings
     10 from argparse import Namespace
     11 from collections import defaultdict
     12 
     13 from mach.decorators import Command, CommandArgument
     14 from mozbuild.base import MachCommandConditions as conditions
     15 from mozbuild.base import MozbuildObject
     16 from mozfile import load_source
     17 
     18 here = os.path.abspath(os.path.dirname(__file__))
     19 
     20 
     21 ENG_BUILD_REQUIRED = """
     22 The mochitest command requires an engineering build. It may be the case that
     23 VARIANT=user or PRODUCTION=1 were set. Try re-building with VARIANT=eng:
     24 
     25    $ VARIANT=eng ./build.sh
     26 
     27 There should be an app called 'test-container.gaiamobile.org' located in
     28 {}.
     29 """.lstrip()
     30 
     31 SUPPORTED_TESTS_NOT_FOUND = """
     32 The mochitest command could not find any supported tests to run! The
     33 following flavors and subsuites were found, but are either not supported on
     34 {} builds, or were excluded on the command line:
     35 
     36 {}
     37 
     38 Double check the command line you used, and make sure you are running in
     39 context of the proper build. To switch build contexts, either run |mach|
     40 from the appropriate objdir, or export the correct mozconfig:
     41 
     42    $ export MOZCONFIG=path/to/mozconfig
     43 """.lstrip()
     44 
     45 TESTS_NOT_FOUND = """
     46 The mochitest command could not find any mochitests under the following
     47 test path(s):
     48 
     49 {}
     50 
     51 Please check spelling and make sure there are mochitests living there.
     52 """.lstrip()
     53 
     54 SUPPORTED_APPS = ["firefox", "android", "thunderbird", "ios"]
     55 
     56 parser = None
     57 
     58 
     59 class MochitestRunner(MozbuildObject):
     60    """Easily run mochitests.
     61 
     62    This currently contains just the basics for running mochitests. We may want
     63    to hook up result parsing, etc.
     64    """
     65 
     66    def __init__(self, *args, **kwargs):
     67        MozbuildObject.__init__(self, *args, **kwargs)
     68 
     69        # TODO Bug 794506 remove once mach integrates with virtualenv.
     70        build_path = os.path.join(self.topobjdir, "build")
     71        if build_path not in sys.path:
     72            sys.path.append(build_path)
     73 
     74        self.tests_dir = os.path.join(self.topobjdir, "_tests")
     75        self.mochitest_dir = os.path.join(self.tests_dir, "testing", "mochitest")
     76        self.bin_dir = os.path.join(self.topobjdir, "dist", "bin")
     77 
     78    def resolve_tests(self, test_paths, test_objects=None, cwd=None):
     79        if test_objects:
     80            return test_objects
     81 
     82        from moztest.resolve import TestResolver
     83 
     84        resolver = self._spawn(TestResolver)
     85        tests = list(resolver.resolve_tests(paths=test_paths, cwd=cwd))
     86        return tests
     87 
     88    def run_desktop_test(self, command_context, tests=None, **kwargs):
     89        """Runs a mochitest."""
     90        # runtests.py is ambiguous, so we load the file/module manually.
     91        if "mochitest" not in sys.modules:
     92            path = os.path.join(self.mochitest_dir, "runtests.py")
     93            load_source("mochitest", path)
     94 
     95        import mochitest
     96 
     97        # This is required to make other components happy. Sad, isn't it?
     98        os.chdir(self.topobjdir)
     99 
    100        # Automation installs its own stream handler to stdout. Since we want
    101        # all logging to go through us, we just remove their handler.
    102        remove_handlers = [
    103            l
    104            for l in logging.getLogger().handlers
    105            if isinstance(l, logging.StreamHandler)
    106        ]
    107        for handler in remove_handlers:
    108            logging.getLogger().removeHandler(handler)
    109 
    110        options = Namespace(**kwargs)
    111        options.topsrcdir = self.topsrcdir
    112        options.topobjdir = self.topobjdir
    113 
    114        from manifestparser import TestManifest
    115 
    116        if tests and not options.manifestFile:
    117            manifest = TestManifest()
    118            manifest.tests.extend(tests)
    119            options.manifestFile = manifest
    120 
    121            # When developing mochitest-plain tests, it's often useful to be able to
    122            # refresh the page to pick up modifications. Therefore leave the browser
    123            # open if only running a single mochitest-plain test. This behaviour can
    124            # be overridden by passing in --keep-open=false.
    125            if (
    126                len(tests) == 1
    127                and options.keep_open is None
    128                and not options.headless
    129                and getattr(options, "flavor", "plain") == "plain"
    130            ):
    131                options.keep_open = True
    132 
    133        # We need this to enable colorization of output.
    134        self.log_manager.enable_unstructured()
    135        result = mochitest.run_test_harness(parser, options)
    136        self.log_manager.disable_unstructured()
    137        return result
    138 
    139    def run_android_test(self, command_context, tests, **kwargs):
    140        host_ret = verify_host_bin()
    141        if host_ret != 0:
    142            return host_ret
    143 
    144        path = os.path.join(self.mochitest_dir, "runtestsremote.py")
    145        load_source("runtestsremote", path)
    146 
    147        import runtestsremote
    148 
    149        options = Namespace(**kwargs)
    150 
    151        from manifestparser import TestManifest
    152 
    153        if tests and not options.manifestFile:
    154            manifest = TestManifest()
    155            manifest.tests.extend(tests)
    156            options.manifestFile = manifest
    157 
    158        return runtestsremote.run_test_harness(parser, options)
    159 
    160    def run_ios_test(self, command_context, tests, **kwargs):
    161        host_ret = verify_host_bin()
    162        if host_ret != 0:
    163            return host_ret
    164 
    165        path = os.path.join(self.mochitest_dir, "runtestsremoteios.py")
    166        load_source("runtestsremoteios", path)
    167 
    168        import runtestsremoteios
    169 
    170        options = Namespace(**kwargs)
    171 
    172        from manifestparser import TestManifest
    173 
    174        if tests and not options.manifestFile:
    175            manifest = TestManifest()
    176            manifest.tests.extend(tests)
    177            options.manifestFile = manifest
    178 
    179        return runtestsremoteios.run_test_harness(parser, options)
    180 
    181    def run_geckoview_junit_test(self, context, **kwargs):
    182        host_ret = verify_host_bin()
    183        if host_ret != 0:
    184            return host_ret
    185 
    186        import runjunit
    187 
    188        options = Namespace(**kwargs)
    189 
    190        return runjunit.run_test_harness(parser, options)
    191 
    192 
    193 # parser
    194 
    195 
    196 def setup_argument_parser():
    197    build_obj = MozbuildObject.from_environment(cwd=here)
    198 
    199    build_path = os.path.join(build_obj.topobjdir, "build")
    200    if build_path not in sys.path:
    201        sys.path.append(build_path)
    202 
    203    mochitest_dir = os.path.join(build_obj.topobjdir, "_tests", "testing", "mochitest")
    204 
    205    with warnings.catch_warnings():
    206        warnings.simplefilter("ignore")
    207 
    208        path = os.path.join(build_obj.topobjdir, mochitest_dir, "runtests.py")
    209        if not os.path.exists(path):
    210            path = os.path.join(here, "runtests.py")
    211 
    212        load_source("mochitest", path)
    213 
    214        from mochitest_options import MochitestArgumentParser
    215 
    216    if conditions.is_android(build_obj):
    217        # On Android, check for a connected device (and offer to start an
    218        # emulator if appropriate) before running tests. This check must
    219        # be done in this admittedly awkward place because
    220        # MochitestArgumentParser initialization fails if no device is found.
    221        from mozrunner.devices.android_device import (
    222            InstallIntent,
    223            verify_android_device,
    224        )
    225 
    226        # verify device and xre
    227        verify_android_device(build_obj, install=InstallIntent.NO, xre=True)
    228 
    229    if conditions.is_ios(build_obj):
    230        # On iOS, check for a connected device before running tests.
    231        from mozrunner.devices.ios_device import verify_ios_device
    232 
    233        # verify device and xre
    234        verify_ios_device(build_obj, install=False, xre=True)
    235 
    236    global parser
    237    parser = MochitestArgumentParser()
    238    return parser
    239 
    240 
    241 def setup_junit_argument_parser():
    242    build_obj = MozbuildObject.from_environment(cwd=here)
    243 
    244    build_path = os.path.join(build_obj.topobjdir, "build")
    245    if build_path not in sys.path:
    246        sys.path.append(build_path)
    247 
    248    mochitest_dir = os.path.join(build_obj.topobjdir, "_tests", "testing", "mochitest")
    249 
    250    with warnings.catch_warnings():
    251        warnings.simplefilter("ignore")
    252 
    253        # runtests.py contains MochitestDesktop, required by runjunit
    254        path = os.path.join(build_obj.topobjdir, mochitest_dir, "runtests.py")
    255        if not os.path.exists(path):
    256            path = os.path.join(here, "runtests.py")
    257 
    258        load_source("mochitest", path)
    259 
    260        import runjunit
    261        from mozrunner.devices.android_device import (
    262            InstallIntent,
    263            verify_android_device,
    264        )
    265 
    266        verify_android_device(
    267            build_obj, install=InstallIntent.NO, xre=True, network=True
    268        )
    269 
    270    global parser
    271    parser = runjunit.JunitArgumentParser()
    272    return parser
    273 
    274 
    275 def verify_host_bin():
    276    # validate MOZ_HOST_BIN environment variables for Android tests
    277    xpcshell_binary = "xpcshell"
    278    if os.name == "nt":
    279        xpcshell_binary = "xpcshell.exe"
    280    MOZ_HOST_BIN = os.environ.get("MOZ_HOST_BIN")
    281    if not MOZ_HOST_BIN:
    282        print(
    283            "environment variable MOZ_HOST_BIN must be set to a directory containing host "
    284            "%s" % xpcshell_binary
    285        )
    286        return 1
    287    elif not os.path.isdir(MOZ_HOST_BIN):
    288        print("$MOZ_HOST_BIN does not specify a directory")
    289        return 1
    290    elif not os.path.isfile(os.path.join(MOZ_HOST_BIN, xpcshell_binary)):
    291        print("$MOZ_HOST_BIN/%s does not exist" % xpcshell_binary)
    292        return 1
    293    return 0
    294 
    295 
    296 @Command(
    297    "mochitest",
    298    category="testing",
    299    conditions=[functools.partial(conditions.is_buildapp_in, apps=SUPPORTED_APPS)],
    300    description="Run any flavor of mochitest (integration test).",
    301    parser=setup_argument_parser,
    302 )
    303 def run_mochitest_general(
    304    command_context, flavor=None, test_objects=None, resolve_tests=True, **kwargs
    305 ):
    306    from mochitest_options import ALL_FLAVORS
    307    from mozlog.commandline import setup_logging
    308    from mozlog.handlers import ResourceHandler, StreamHandler
    309    from moztest.resolve import get_suite_definition
    310 
    311    # TODO: This is only strictly necessary while mochitest is using Python
    312    # 2 and can be removed once the command is migrated to Python 3.
    313    command_context.activate_virtualenv()
    314 
    315    buildapp = None
    316    for app in SUPPORTED_APPS:
    317        if conditions.is_buildapp_in(command_context, apps=[app]):
    318            buildapp = app
    319            break
    320 
    321    # Force the buildapp to be android if requested
    322    if kwargs.get("android"):
    323        buildapp = "android"
    324 
    325    flavors = None
    326    if flavor:
    327        for fname, fobj in ALL_FLAVORS.items():
    328            if flavor in fobj["aliases"]:
    329                if buildapp not in fobj["enabled_apps"]:
    330                    continue
    331                flavors = [fname]
    332                break
    333    else:
    334        flavors = [f for f, v in ALL_FLAVORS.items() if buildapp in v["enabled_apps"]]
    335 
    336    from mozbuild.controller.building import BuildDriver
    337 
    338    command_context._ensure_state_subdir_exists(".")
    339 
    340    test_paths = kwargs["test_paths"]
    341    kwargs["test_paths"] = []
    342 
    343    if kwargs.get("debugger", None):
    344        import mozdebug
    345 
    346        if not mozdebug.get_debugger_info(kwargs.get("debugger")):
    347            sys.exit(1)
    348 
    349    mochitest = command_context._spawn(MochitestRunner)
    350    tests = []
    351    if resolve_tests:
    352        tests = mochitest.resolve_tests(
    353            test_paths, test_objects, cwd=command_context._mach_context.cwd
    354        )
    355 
    356    if not kwargs.get("log"):
    357        # Create shared logger
    358        format_args = {"level": command_context._mach_context.settings["test"]["level"]}
    359        if len(tests) == 1:
    360            format_args["verbose"] = True
    361            format_args["compact"] = False
    362 
    363        default_format = command_context._mach_context.settings["test"]["format"]
    364        log = setup_logging(
    365            "mach-mochitest", kwargs, {default_format: sys.stdout}, format_args
    366        )
    367        kwargs["log"] = log
    368        for handler in log.handlers:
    369            if isinstance(handler, StreamHandler):
    370                handler.formatter.inner.summary_on_shutdown = True
    371 
    372        log.add_handler(ResourceHandler(command_context))
    373 
    374    driver = command_context._spawn(BuildDriver)
    375    driver.install_tests()
    376 
    377    subsuite = kwargs.get("subsuite")
    378    if subsuite == "default":
    379        kwargs["subsuite"] = None
    380 
    381    suites = defaultdict(list)
    382    is_webrtc_tag_present = False
    383    unsupported = set()
    384    for test in tests:
    385        # Check if we're running a webrtc test so we can enable webrtc
    386        # specific test logic later if needed.
    387        if "webrtc" in test.get("tags", ""):
    388            is_webrtc_tag_present = True
    389 
    390        # Filter out non-mochitests and unsupported flavors.
    391        if test["flavor"] not in ALL_FLAVORS:
    392            continue
    393 
    394        key = (test["flavor"], test.get("subsuite", ""))
    395        if test["flavor"] not in flavors:
    396            unsupported.add(key)
    397            continue
    398 
    399        if subsuite == "default":
    400            # "--subsuite default" means only run tests that don't have a subsuite
    401            if test.get("subsuite"):
    402                unsupported.add(key)
    403                continue
    404        elif subsuite and test.get("subsuite", "") != subsuite:
    405            unsupported.add(key)
    406            continue
    407 
    408        suites[key].append(test)
    409 
    410    # Only webrtc mochitests in the media suite need the websocketprocessbridge.
    411    if ("mochitest", "media") in suites and is_webrtc_tag_present:
    412        req = os.path.join(
    413            "testing",
    414            "tools",
    415            "websocketprocessbridge",
    416            "websocketprocessbridge_requirements_3.txt",
    417        )
    418        command_context.virtualenv_manager.activate()
    419        command_context.virtualenv_manager.install_pip_requirements(
    420            req, require_hashes=False
    421        )
    422 
    423        # sys.executable is used to start the websocketprocessbridge, though for some
    424        # reason it doesn't get set when calling `activate_this.py` in the virtualenv.
    425        sys.executable = command_context.virtualenv_manager.python_path
    426 
    427    if ("browser-chrome", "a11y") in suites and sys.platform == "win32":
    428        # Only Windows a11y browser tests need this.
    429        req = os.path.join(
    430            "accessible",
    431            "tests",
    432            "browser",
    433            "windows",
    434            "a11y_setup_requirements.txt",
    435        )
    436        command_context.virtualenv_manager.activate()
    437        command_context.virtualenv_manager.install_pip_requirements(
    438            req, require_hashes=False
    439        )
    440 
    441    # This is a hack to introduce an option in mach to not send
    442    # filtered tests to the mochitest harness. Mochitest harness will read
    443    # the master manifest in that case.
    444    if not resolve_tests:
    445        for flavor_name in flavors:
    446            key = (flavor_name, kwargs.get("subsuite"))
    447            suites[key] = []
    448 
    449    if not suites:
    450        # Make it very clear why no tests were found
    451        if not unsupported:
    452            print(
    453                TESTS_NOT_FOUND.format(
    454                    "\n".join(sorted(list(test_paths or test_objects)))
    455                )
    456            )
    457            return 1
    458 
    459        msg = []
    460        for f, s in unsupported:
    461            fobj = ALL_FLAVORS[f]
    462            apps = fobj["enabled_apps"]
    463            name = fobj["aliases"][0]
    464            if s:
    465                name = f"{name} --subsuite {s}"
    466 
    467            if buildapp not in apps:
    468                reason = "requires {}".format(" or ".join(apps))
    469            else:
    470                reason = "excluded by the command line"
    471            msg.append(f"    mochitest -f {name} ({reason})")
    472        print(SUPPORTED_TESTS_NOT_FOUND.format(buildapp, "\n".join(sorted(msg))))
    473        return 1
    474 
    475    if buildapp == "android":
    476        from mozrunner.devices.android_device import (
    477            InstallIntent,
    478            get_adb_path,
    479            verify_android_device,
    480        )
    481 
    482        app = kwargs.get("app")
    483        if not app:
    484            app = "org.mozilla.geckoview.test_runner"
    485        device_serial = kwargs.get("deviceSerial")
    486        install = InstallIntent.NO if kwargs.get("no_install") else InstallIntent.YES
    487        aab = kwargs.get("aab")
    488 
    489        # verify installation
    490        verify_android_device(
    491            command_context,
    492            install=install,
    493            xre=False,
    494            network=True,
    495            app=app,
    496            aab=aab,
    497            device_serial=device_serial,
    498        )
    499 
    500        if not kwargs["adbPath"]:
    501            kwargs["adbPath"] = get_adb_path(command_context)
    502 
    503        run_mochitest = mochitest.run_android_test
    504    elif buildapp == "ios":
    505        from mozrunner.devices.ios_device import verify_ios_device
    506 
    507        app = kwargs.get("app")
    508        if not app:
    509            app = "org.mozilla.ios.GeckoTestBrowser"
    510        install = not kwargs.get("no_install")
    511 
    512        # verify installation
    513        verify_ios_device(command_context, install=install, xre=False, app=app)
    514 
    515        run_mochitest = mochitest.run_ios_test
    516    else:
    517        run_mochitest = mochitest.run_desktop_test
    518 
    519    overall = None
    520    for (test_flavor, subsuite), tests in sorted(suites.items()):
    521        suite_name, suite = get_suite_definition(test_flavor, subsuite)
    522        if "test_paths" in suite["kwargs"]:
    523            del suite["kwargs"]["test_paths"]
    524 
    525        harness_args = kwargs.copy()
    526        harness_args.update(suite["kwargs"])
    527        # Pass in the full suite name as defined in moztest/resolve.py in case
    528        # chunk-by-runtime is called, in which case runtime information for
    529        # specific mochitest suite has to be loaded. See Bug 1637463.
    530        harness_args.update({"suite_name": suite_name})
    531 
    532        result = run_mochitest(
    533            command_context._mach_context, tests=tests, **harness_args
    534        )
    535 
    536        if result:
    537            overall = result
    538 
    539        # Halt tests on keyboard interrupt
    540        if result == -1:
    541            break
    542 
    543    # Only shutdown the logger if we created it
    544    if kwargs["log"].name == "mach-mochitest":
    545        kwargs["log"].shutdown()
    546 
    547    return overall
    548 
    549 
    550 @Command(
    551    "geckoview-junit",
    552    category="testing",
    553    conditions=[conditions.is_android],
    554    description="Run remote geckoview junit tests.",
    555    parser=setup_junit_argument_parser,
    556 )
    557 @CommandArgument(
    558    "--no-install",
    559    help="Do not try to install application on device before "
    560    + "running (default: False)",
    561    action="store_true",
    562    default=False,
    563 )
    564 def run_junit(command_context, no_install, **kwargs):
    565    command_context._ensure_state_subdir_exists(".")
    566 
    567    from mozrunner.devices.android_device import (
    568        InstallIntent,
    569        get_adb_path,
    570        verify_android_device,
    571    )
    572 
    573    # verify installation
    574    app = kwargs.get("app")
    575    device_serial = kwargs.get("deviceSerial")
    576    verify_android_device(
    577        command_context,
    578        install=InstallIntent.NO if no_install else InstallIntent.YES,
    579        xre=False,
    580        app=app,
    581        device_serial=device_serial,
    582    )
    583 
    584    if not kwargs.get("adbPath"):
    585        kwargs["adbPath"] = get_adb_path(command_context)
    586 
    587    if not kwargs.get("log"):
    588        from mozlog.commandline import setup_logging
    589 
    590        format_args = {"level": command_context._mach_context.settings["test"]["level"]}
    591        default_format = command_context._mach_context.settings["test"]["format"]
    592        kwargs["log"] = setup_logging(
    593            "mach-mochitest", kwargs, {default_format: sys.stdout}, format_args
    594        )
    595 
    596    if kwargs.get("mach_test") and kwargs.get("test_objects"):
    597        test_classes = []
    598        test_objects = kwargs.get("test_objects")
    599        for test_object in test_objects:
    600            test_classes.append(classname_for_test(test_object["name"]))
    601        kwargs["test_filters"] = test_classes
    602 
    603    mochitest = command_context._spawn(MochitestRunner)
    604    return mochitest.run_geckoview_junit_test(command_context._mach_context, **kwargs)
    605 
    606 
    607 def classname_for_test(test):
    608    """Convert path of test file to gradle recognized test suite name"""
    609    test_path = os.path.join(
    610        "mobile",
    611        "android",
    612        "geckoview",
    613        "src",
    614        "androidTest",
    615        "java",
    616    )
    617    return (
    618        os.path.normpath(test)
    619        .split(os.path.normpath(test_path))[-1]
    620        .removeprefix(os.path.sep)
    621        .replace(os.path.sep, ".")
    622        .removesuffix(".kt")
    623    )