tor-browser

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

run.py (38907B)


      1 # mypy: allow-untyped-defs
      2 
      3 import argparse
      4 import os
      5 import platform
      6 import subprocess
      7 import sys
      8 from shutil import copyfile, which
      9 from typing import ClassVar, Tuple, Type
     10 
     11 wpt_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
     12 sys.path.insert(0, os.path.abspath(os.path.join(wpt_root, "tools")))
     13 
     14 from . import browser, install, testfiles
     15 from ..serve import serve
     16 
     17 logger = None
     18 
     19 
     20 class WptrunError(Exception):
     21    pass
     22 
     23 
     24 class WptrunnerHelpAction(argparse.Action):
     25    def __init__(self,
     26                 option_strings,
     27                 dest=argparse.SUPPRESS,
     28                 default=argparse.SUPPRESS,
     29                 help=None):
     30        super().__init__(
     31            option_strings=option_strings,
     32            dest=dest,
     33            default=default,
     34            nargs=0,
     35            help=help)
     36 
     37    def __call__(self, parser, namespace, values, option_string=None):
     38        from wptrunner import wptcommandline
     39        wptparser = wptcommandline.create_parser()
     40        wptparser.usage = parser.usage
     41        wptparser.print_help()
     42        parser.exit()
     43 
     44 
     45 def create_parser():
     46    from wptrunner import wptcommandline
     47 
     48    parser = argparse.ArgumentParser(add_help=False, parents=[install.channel_args])
     49    parser.add_argument("product", help="Browser to run tests in")
     50    parser.add_argument("--affected", help="Run affected tests since revish")
     51    parser.add_argument("--yes", "-y", dest="prompt", action="store_false",
     52                        help="Don't prompt before installing components")
     53    parser.add_argument("--install-browser", action="store_true",
     54                        help="Install the browser from the release channel specified by --channel "
     55                        "(or the nightly channel by default).")
     56    parser.add_argument("--install-webdriver", action="store_true",
     57                        help="Install WebDriver from the release channel specified by --channel "
     58                        "(or the nightly channel by default).")
     59    parser.add_argument("--logcat-dir",
     60                        help="Directory to write Android logcat files to")
     61    parser._add_container_actions(wptcommandline.create_parser())
     62    return parser
     63 
     64 
     65 def exit(msg=None):
     66    if msg:
     67        logger.critical(msg)
     68        sys.exit(1)
     69    else:
     70        sys.exit(0)
     71 
     72 
     73 def args_general(kwargs):
     74 
     75    def set_if_none(name, value):
     76        if kwargs.get(name) is None:
     77            kwargs[name] = value
     78            logger.info("Set %s to %s" % (name, value))
     79 
     80    set_if_none("tests_root", wpt_root)
     81    set_if_none("metadata_root", wpt_root)
     82    set_if_none("manifest_update", True)
     83    set_if_none("manifest_download", True)
     84 
     85    if kwargs["ssl_type"] in (None, "pregenerated"):
     86        cert_root = os.path.join(wpt_root, "tools", "certs")
     87        if kwargs["ca_cert_path"] is None:
     88            kwargs["ca_cert_path"] = os.path.join(cert_root, "cacert.pem")
     89 
     90        if kwargs["host_key_path"] is None:
     91            kwargs["host_key_path"] = os.path.join(cert_root, "web-platform.test.key")
     92 
     93        if kwargs["host_cert_path"] is None:
     94            kwargs["host_cert_path"] = os.path.join(cert_root, "web-platform.test.pem")
     95    elif kwargs["ssl_type"] == "openssl":
     96        if not which(kwargs["openssl_binary"]):
     97            if os.uname()[0] == "Windows":
     98                raise WptrunError("""OpenSSL binary not found. If you need HTTPS tests, install OpenSSL from
     99 
    100 https://slproweb.com/products/Win32OpenSSL.html
    101 
    102 Ensuring that libraries are added to /bin and add the resulting bin directory to
    103 your PATH.
    104 
    105 Otherwise run with --ssl-type=none""")
    106            else:
    107                raise WptrunError("""OpenSSL not found. If you don't need HTTPS support run with --ssl-type=none,
    108 otherwise install OpenSSL and ensure that it's on your $PATH.""")
    109 
    110 
    111 def check_environ(product):
    112    if product not in ("android_webview", "chrome", "chrome_android", "chrome_ios",
    113                       "edge", "firefox", "firefox_android", "headless_shell",
    114                       "ladybird", "servo", "wktr"):
    115        config_builder = serve.build_config(os.path.join(wpt_root, "config.json"))
    116        # Override the ports to avoid looking for free ports
    117        config_builder.ssl = {"type": "none"}
    118        config_builder.ports = {"http": [8000]}
    119 
    120        is_windows = platform.uname()[0] == "Windows"
    121 
    122        with config_builder as config:
    123            expected_hosts = set(config.domains_set)
    124            if is_windows:
    125                expected_hosts.update(config.not_domains_set)
    126 
    127        missing_hosts = set(expected_hosts)
    128        if is_windows:
    129            hosts_path = r"%s\System32\drivers\etc\hosts" % os.environ.get(
    130                "SystemRoot", r"C:\Windows")
    131        else:
    132            hosts_path = "/etc/hosts"
    133 
    134        if os.path.abspath(os.curdir) == wpt_root:
    135            wpt_path = "wpt"
    136        else:
    137            wpt_path = os.path.join(wpt_root, "wpt")
    138 
    139        with open(hosts_path) as f:
    140            for line in f:
    141                line = line.split("#", 1)[0].strip()
    142                parts = line.split()
    143                hosts = parts[1:]
    144                for host in hosts:
    145                    missing_hosts.discard(host)
    146            if missing_hosts:
    147                if is_windows:
    148                    message = """Missing hosts file configuration. Run
    149 
    150 python %s make-hosts-file | Out-File %s -Encoding ascii -Append
    151 
    152 in PowerShell with Administrator privileges.""" % (wpt_path, hosts_path)
    153                else:
    154                    message = """Missing hosts file configuration. Run
    155 
    156 %s make-hosts-file | sudo tee -a %s""" % ("./wpt" if wpt_path == "wpt" else wpt_path,
    157                                          hosts_path)
    158                raise WptrunError(message)
    159 
    160 
    161 class AndroidLogcat:
    162    def __init__(self, adb_path, base_path=None):
    163        self.adb_path = adb_path
    164        self.base_path = base_path if base_path is not None else os.curdir
    165        self.procs = {}
    166 
    167    def start(self, device_serial):
    168        """
    169        Start recording logcat. Writes logcat to the upload directory.
    170        """
    171        # Start logcat for the device. The adb process runs until the
    172        # corresponding device is stopped. Output is written directly to
    173        # the blobber upload directory so that it is uploaded automatically
    174        # at the end of the job.
    175        if device_serial in self.procs:
    176            logger.warning(f"Logcat for {device_serial} already started")
    177            return
    178 
    179        logcat_path = os.path.join(self.base_path, f"logcat-{device_serial}.log")
    180        out_file = open(logcat_path, "w")
    181        cmd = [
    182            self.adb_path,
    183            "-s",
    184            device_serial,
    185            "logcat",
    186            "-v",
    187            "threadtime",
    188            "Trace:S",
    189            "StrictMode:S",
    190            "ExchangeService:S",
    191        ]
    192        logger.debug(" ".join(cmd))
    193        proc = subprocess.Popen(
    194            cmd, stdout=out_file, stdin=subprocess.PIPE
    195        )
    196        logger.info(f"Started logcat for device {device_serial} pid {proc.pid}")
    197        self.procs[device_serial] = (proc, out_file)
    198 
    199    def stop(self, device_serial=None):
    200        """
    201        Stop logcat process started by logcat_start.
    202        """
    203        if device_serial is None:
    204            for key in list(self.procs.keys()):
    205                self.stop(key)
    206            return
    207 
    208        proc, out_file = self.procs.get(device_serial, (None, None))
    209        if proc is not None:
    210            try:
    211                proc.kill()
    212                out_file.close()
    213            finally:
    214                del self.procs[device_serial]
    215 
    216 
    217 class BrowserSetup:
    218    name: ClassVar[str]
    219    browser_cls: ClassVar[Type[browser.Browser]]
    220 
    221    def __init__(self, venv, prompt=True):
    222        self.browser = self.browser_cls(logger)
    223        self.venv = venv
    224        self.prompt = prompt
    225 
    226    def prompt_install(self, component):
    227        if not self.prompt:
    228            return True
    229        while True:
    230            resp = input("Download and install %s [Y/n]? " % component).strip().lower()
    231            if not resp or resp == "y":
    232                return True
    233            elif resp == "n":
    234                return False
    235 
    236    def install(self, channel=None):
    237        if self.prompt_install(self.name):
    238            return self.browser.install(self.venv.path, channel)
    239 
    240    def requirements(self):
    241        if self.browser.requirements:
    242            return [os.path.join(wpt_root, "tools", "wptrunner", self.browser.requirements)]
    243        return []
    244 
    245    def setup(self, kwargs):
    246        self.setup_kwargs(kwargs)
    247 
    248    def teardown(self):
    249        pass
    250 
    251 
    252 def safe_unsetenv(env_var):
    253    """Safely remove an environment variable.
    254 
    255    Python3 does not support os.unsetenv in Windows for python<3.9, so we better
    256    remove the variable directly from os.environ.
    257    """
    258    try:
    259        del os.environ[env_var]
    260    except KeyError:
    261        pass
    262 
    263 
    264 class Firefox(BrowserSetup):
    265    name = "firefox"
    266    browser_cls = browser.Firefox
    267 
    268    def setup_kwargs(self, kwargs):
    269        if kwargs["binary"] is None:
    270            if kwargs["browser_channel"] is None:
    271                kwargs["browser_channel"] = "nightly"
    272                logger.info("No browser channel specified. Running nightly instead.")
    273 
    274            binary = self.browser.find_binary(self.venv.path,
    275                                              kwargs["browser_channel"])
    276            if binary is None:
    277                raise WptrunError("""Firefox binary not found on $PATH.
    278 
    279 Install Firefox or use --binary to set the binary path""")
    280            kwargs["binary"] = binary
    281 
    282        if kwargs["certutil_binary"] is None and kwargs["ssl_type"] != "none":
    283            certutil = self.browser.find_certutil()
    284 
    285            if certutil is None:
    286                # Can't download this for now because it's missing the libnss3 library
    287                logger.info("""Can't find certutil, certificates will not be checked.
    288 Consider installing certutil via your OS package manager or directly.""")
    289            else:
    290                logger.info("Using certutil %s" % certutil)
    291 
    292            kwargs["certutil_binary"] = certutil
    293 
    294        if kwargs["webdriver_binary"] is None and "wdspec" in kwargs["test_types"]:
    295            webdriver_binary = None
    296            if not kwargs["install_webdriver"]:
    297                webdriver_binary = self.browser.find_webdriver()
    298 
    299            if webdriver_binary is None:
    300                install = self.prompt_install("geckodriver")
    301 
    302                if install:
    303                    logger.info("Downloading geckodriver")
    304                    webdriver_binary = self.browser.install_webdriver(
    305                        dest=self.venv.bin_path,
    306                        channel=kwargs["browser_channel"],
    307                        browser_binary=kwargs["binary"])
    308            else:
    309                logger.info("Using webdriver binary %s" % webdriver_binary)
    310 
    311            if webdriver_binary:
    312                kwargs["webdriver_binary"] = webdriver_binary
    313            else:
    314                logger.info("Unable to find or install geckodriver, skipping wdspec tests")
    315                kwargs["test_types"].remove("wdspec")
    316 
    317        if kwargs["prefs_root"] is None:
    318            prefs_root = self.browser.install_prefs(kwargs["binary"],
    319                                                    self.venv.path,
    320                                                    channel=kwargs["browser_channel"])
    321            kwargs["prefs_root"] = prefs_root
    322 
    323        if kwargs["headless"] is None and not kwargs["debug_test"]:
    324            kwargs["headless"] = True
    325            logger.info("Running in headless mode, pass --no-headless to disable")
    326 
    327        if kwargs["browser_channel"] == "nightly" and kwargs["enable_webtransport_h3"] is None:
    328            kwargs["enable_webtransport_h3"] = True
    329 
    330        # Turn off Firefox WebRTC ICE logging on WPT (turned on by mozrunner)
    331        safe_unsetenv('R_LOG_LEVEL')
    332        safe_unsetenv('R_LOG_DESTINATION')
    333        safe_unsetenv('R_LOG_VERBOSE')
    334 
    335        # Allow WebRTC tests to call getUserMedia.
    336        kwargs["extra_prefs"].append("media.navigator.streams.fake=true")
    337 
    338        kwargs["enable_webtransport_h3"] = True
    339 
    340 class FirefoxAndroid(BrowserSetup):
    341    name = "firefox_android"
    342    browser_cls = browser.FirefoxAndroid
    343 
    344    def setup_kwargs(self, kwargs):
    345        from . import android
    346        import mozdevice
    347 
    348        # We don't support multiple channels for android yet
    349        if kwargs["browser_channel"] is None:
    350            kwargs["browser_channel"] = "nightly"
    351 
    352        if kwargs["prefs_root"] is None:
    353            prefs_root = self.browser.install_prefs(kwargs["binary"],
    354                                                    self.venv.path,
    355                                                    channel=kwargs["browser_channel"])
    356            kwargs["prefs_root"] = prefs_root
    357 
    358        if kwargs["package_name"] is None:
    359            kwargs["package_name"] = "org.mozilla.geckoview.test_runner"
    360        app = kwargs["package_name"]
    361 
    362        if not kwargs["device_serial"]:
    363            kwargs["device_serial"] = ["emulator-5554"]
    364 
    365        if kwargs["webdriver_binary"] is None and "wdspec" in kwargs["test_types"]:
    366            webdriver_binary = None
    367            if not kwargs["install_webdriver"]:
    368                webdriver_binary = self.browser.find_webdriver()
    369 
    370            if webdriver_binary is None:
    371                install = self.prompt_install("geckodriver")
    372 
    373                if install:
    374                    logger.info("Downloading geckodriver")
    375                    webdriver_binary = self.browser.install_webdriver(
    376                        dest=self.venv.bin_path,
    377                        channel=kwargs["browser_channel"],
    378                        browser_binary=kwargs["binary"])
    379            else:
    380                logger.info("Using webdriver binary %s" % webdriver_binary)
    381 
    382            if webdriver_binary:
    383                kwargs["webdriver_binary"] = webdriver_binary
    384            else:
    385                logger.info("Unable to find or install geckodriver, skipping wdspec tests")
    386                kwargs["test_types"].remove("wdspec")
    387 
    388        if kwargs["adb_binary"] is None:
    389            if "ADB_PATH" not in os.environ:
    390                adb_path = os.path.join(android.get_paths(None)["sdk"],
    391                                        "platform-tools",
    392                                        "adb")
    393                os.environ["ADB_PATH"] = adb_path
    394            kwargs["adb_binary"] = os.environ["ADB_PATH"]
    395 
    396        self._logcat = AndroidLogcat(kwargs["adb_binary"], base_path=kwargs["logcat_dir"])
    397 
    398        for device_serial in kwargs["device_serial"]:
    399            if device_serial.startswith("emulator-"):
    400                # We're running on an emulator so ensure that's set up
    401                android.start(logger,
    402                              reinstall=False,
    403                              device_serial=device_serial,
    404                              prompt=kwargs["prompt"])
    405 
    406        for device_serial in kwargs["device_serial"]:
    407            device = mozdevice.ADBDeviceFactory(adb=kwargs["adb_binary"],
    408                                                device=device_serial)
    409            self._logcat.start(device_serial)
    410            max_retries = 5
    411            last_exception = None
    412            if self.browser.apk_path:
    413                device.uninstall_app(app)
    414                for i in range(max_retries + 1):
    415                    logger.info(f"Installing {app} on {device_serial} "
    416                                f"attempt {i + 1}/{max_retries + 1}")
    417                    try:
    418                        # Temporarily replace mozdevice function with custom code
    419                        # that passes in the `--no-incremental` option
    420                        cmd = ["install", "--no-incremental", self.browser.apk_path]
    421                        logger.debug(" ".join(cmd))
    422                        data = device.command_output(cmd, timeout=120)
    423                        if data.find("Success") == -1:
    424                            raise mozdevice.ADBError(f"Install failed for {self.browser.apk_path}."
    425                                                     f" Got: {data}")
    426                    except Exception as e:
    427                        last_exception = e
    428                    else:
    429                        break
    430                else:
    431                    assert last_exception is not None
    432                    raise WptrunError(f"Failed to install {app} on device {device_serial} "
    433                                      f"after {max_retries} retries") from last_exception
    434            elif not device.is_app_installed(app):
    435                raise WptrunError(f"app {app} not installed on device {device_serial}")
    436 
    437        kwargs["enable_webtransport_h3"] = True
    438 
    439    def teardown(self):
    440        from . import android
    441 
    442        if hasattr(self, "_logcat"):
    443            emulator_log = os.path.join(android.get_paths(None)["sdk"],
    444                                        ".android",
    445                                        "emulator.log")
    446            if os.path.exists(emulator_log):
    447                dest_path = os.path.join(self._logcat.base_path, "emulator.log")
    448                copyfile(emulator_log, dest_path)
    449 
    450            self._logcat.stop()
    451 
    452 
    453 class Chrome(BrowserSetup):
    454    name = "chrome"
    455    browser_cls: ClassVar[Type[browser.ChromeChromiumBase]] = browser.Chrome
    456    experimental_channels: ClassVar[Tuple[str, ...]] = ("dev", "canary")
    457 
    458    def setup_kwargs(self, kwargs):
    459        browser_channel = kwargs["browser_channel"]
    460        if kwargs["binary"] is None:
    461            binary = self.browser.find_binary(venv_path=self.venv.path, channel=browser_channel)
    462            if binary:
    463                kwargs["binary"] = binary
    464            else:
    465                raise WptrunError(f"Unable to locate {self.name.capitalize()} binary")
    466 
    467        if kwargs["mojojs_path"]:
    468            kwargs["enable_mojojs"] = True
    469            logger.info("--mojojs-path is provided, enabling MojoJS")
    470        else:
    471            path = self.browser.install_mojojs(dest=self.venv.path,
    472                                               browser_binary=kwargs["binary"])
    473            if path:
    474                kwargs["mojojs_path"] = path
    475                kwargs["enable_mojojs"] = True
    476                logger.info(f"MojoJS enabled automatically (mojojs_path: {path})")
    477            else:
    478                kwargs["enable_mojojs"] = False
    479                logger.info("MojoJS is disabled for this run.")
    480 
    481        if kwargs["webdriver_binary"] is None:
    482            webdriver_binary = None
    483            if not kwargs["install_webdriver"]:
    484                webdriver_binary = self.browser.find_webdriver(self.venv.bin_path)
    485                if webdriver_binary and not self.browser.webdriver_supports_browser(
    486                        webdriver_binary, kwargs["binary"], browser_channel):
    487                    webdriver_binary = None
    488 
    489            if webdriver_binary is None:
    490                install = self.prompt_install("chromedriver")
    491 
    492                if install:
    493                    webdriver_binary = self.browser.install_webdriver(
    494                        dest=self.venv.bin_path,
    495                        channel=browser_channel,
    496                        browser_binary=kwargs["binary"],
    497                    )
    498            else:
    499                logger.info("Using webdriver binary %s" % webdriver_binary)
    500 
    501            if webdriver_binary:
    502                kwargs["webdriver_binary"] = webdriver_binary
    503            else:
    504                raise WptrunError("Unable to locate or install matching ChromeDriver binary")
    505        if kwargs["headless"] is None and not kwargs["debug_test"]:
    506            kwargs["headless"] = True
    507            logger.info("Running in headless mode, pass --no-headless to disable")
    508        if browser_channel in self.experimental_channels:
    509            # HACK(Hexcles): work around https://github.com/web-platform-tests/wpt/issues/16448
    510            kwargs["webdriver_args"].append("--disable-build-check")
    511            if kwargs["enable_experimental"] is None:
    512                logger.info(
    513                    "Automatically turning on experimental features for Chrome Dev/Canary or Chromium trunk")
    514                kwargs["enable_experimental"] = True
    515            if kwargs["enable_webtransport_h3"] is None:
    516                # To start the WebTransport over HTTP/3 test server.
    517                kwargs["enable_webtransport_h3"] = True
    518        elif browser_channel is not None:
    519            # browser_channel is not set when running WPT in chromium
    520            kwargs["enable_experimental"] = False
    521        if os.getenv("TASKCLUSTER_ROOT_URL"):
    522            # We are on Taskcluster, where our Docker container does not have
    523            # enough capabilities to run Chrome with sandboxing. (gh-20133)
    524            kwargs["binary_args"].append("--no-sandbox")
    525 
    526 
    527 class HeadlessShell(BrowserSetup):
    528    name = "headless_shell"
    529    browser_cls = browser.HeadlessShell
    530    experimental_channels = ("dev", "canary", "nightly")
    531 
    532    def setup_kwargs(self, kwargs):
    533        browser_channel = kwargs["browser_channel"]
    534        if kwargs["binary"] is None:
    535            binary = self.browser.find_binary(venv_path=self.venv.path, channel=browser_channel)
    536            if binary:
    537                kwargs["binary"] = binary
    538            else:
    539                raise WptrunError(f"Unable to locate {self.name!r} binary")
    540 
    541        if kwargs["mojojs_path"]:
    542            kwargs["enable_mojojs"] = True
    543            logger.info("--mojojs-path is provided, enabling MojoJS")
    544        elif kwargs["enable_mojojs"]:
    545            logger.warning(f"Cannot install MojoJS for {self.name}, "
    546                           "which does not return version information. "
    547                           "Provide '--mojojs-path' explicitly instead.")
    548            logger.warning("MojoJS is disabled for this run.")
    549 
    550        # Never pause after test, since headless shell is not interactive.
    551        kwargs["pause_after_test"] = False
    552        # Don't add a `--headless` switch.
    553        kwargs["headless"] = False
    554 
    555        if kwargs["enable_webtransport_h3"] is None:
    556            kwargs["enable_webtransport_h3"] = True
    557 
    558 
    559 class Chromium(Chrome):
    560    name = "chromium"
    561    browser_cls: ClassVar[Type[browser.ChromeChromiumBase]] = browser.Chromium
    562    experimental_channels = ("nightly",)
    563 
    564 
    565 class ChromeAndroidBase(BrowserSetup):
    566    experimental_channels = ("dev", "canary")
    567 
    568    def setup_kwargs(self, kwargs):
    569        if kwargs.get("device_serial"):
    570            self.browser.device_serial = kwargs["device_serial"]
    571        if kwargs.get("adb_binary"):
    572            self.browser.adb_binary = kwargs["adb_binary"]
    573        browser_channel = kwargs["browser_channel"]
    574        if kwargs["package_name"] is None:
    575            kwargs["package_name"] = self.browser.find_binary(
    576                channel=browser_channel)
    577        if not kwargs["device_serial"]:
    578            kwargs["device_serial"] = ["emulator-5554"]
    579        if kwargs["webdriver_binary"] is None:
    580            webdriver_binary = None
    581            if not kwargs["install_webdriver"]:
    582                webdriver_binary = self.browser.find_webdriver()
    583 
    584            if webdriver_binary is None:
    585                install = self.prompt_install("chromedriver")
    586 
    587                if install:
    588                    logger.info("Downloading chromedriver")
    589                    webdriver_binary = self.browser.install_webdriver(
    590                        dest=self.venv.bin_path,
    591                        channel=browser_channel,
    592                        browser_binary=kwargs["package_name"],
    593                    )
    594            else:
    595                logger.info("Using webdriver binary %s" % webdriver_binary)
    596 
    597            if webdriver_binary:
    598                kwargs["webdriver_binary"] = webdriver_binary
    599            else:
    600                raise WptrunError("Unable to locate or install chromedriver binary")
    601 
    602 
    603 class ChromeAndroid(ChromeAndroidBase):
    604    name = "chrome_android"
    605    browser_cls = browser.ChromeAndroid
    606 
    607    def setup_kwargs(self, kwargs):
    608        super().setup_kwargs(kwargs)
    609        if kwargs["browser_channel"] in self.experimental_channels:
    610            # HACK(Hexcles): work around https://github.com/web-platform-tests/wpt/issues/16448
    611            kwargs["webdriver_args"].append("--disable-build-check")
    612            if kwargs["enable_experimental"] is None:
    613                logger.info("Automatically turning on experimental features for Chrome Dev/Canary")
    614                kwargs["enable_experimental"] = True
    615 
    616 
    617 class ChromeiOS(BrowserSetup):
    618    name = "chrome_ios"
    619    browser_cls = browser.ChromeiOS
    620 
    621    def setup_kwargs(self, kwargs):
    622        if kwargs["webdriver_binary"] is None:
    623            raise WptrunError("Unable to locate or install chromedriver binary")
    624 
    625 
    626 class AndroidWebview(ChromeAndroidBase):
    627    name = "android_webview"
    628    browser_cls = browser.AndroidWebview
    629 
    630    def setup_kwargs(self, kwargs):
    631        if kwargs["mojojs_path"]:
    632            kwargs["enable_mojojs"] = True
    633            logger.info("--mojojs-path is provided, enabling MojoJS")
    634 
    635 
    636 class Opera(BrowserSetup):
    637    name = "opera"
    638    browser_cls = browser.Opera
    639 
    640    def setup_kwargs(self, kwargs):
    641        if kwargs["webdriver_binary"] is None:
    642            webdriver_binary = None
    643            if not kwargs["install_webdriver"]:
    644                webdriver_binary = self.browser.find_webdriver()
    645 
    646            if webdriver_binary is None:
    647                install = self.prompt_install("operadriver")
    648 
    649                if install:
    650                    logger.info("Downloading operadriver")
    651                    webdriver_binary = self.browser.install_webdriver(
    652                        dest=self.venv.bin_path,
    653                        channel=kwargs["browser_channel"])
    654            else:
    655                logger.info("Using webdriver binary %s" % webdriver_binary)
    656 
    657            if webdriver_binary:
    658                kwargs["webdriver_binary"] = webdriver_binary
    659            else:
    660                raise WptrunError("Unable to locate or install operadriver binary")
    661 
    662 
    663 class Edge(BrowserSetup):
    664    name = "MicrosoftEdge"
    665    browser_cls = browser.Edge
    666    experimental_channels: ClassVar[Tuple[str, ...]] = ("dev", "canary")
    667 
    668    def setup_kwargs(self, kwargs):
    669        browser_channel = kwargs["browser_channel"]
    670        if kwargs["binary"] is None:
    671            binary = self.browser.find_binary(venv_path=self.venv.path, channel=browser_channel)
    672            if binary:
    673                kwargs["binary"] = binary
    674            else:
    675                raise WptrunError(f"Unable to locate {self.name.capitalize()} binary")
    676 
    677        if kwargs["mojojs_path"]:
    678            kwargs["enable_mojojs"] = True
    679            logger.info("--mojojs-path is provided, enabling MojoJS")
    680        else:
    681            path = self.browser.install_mojojs(dest=self.venv.path,
    682                                               browser_binary=kwargs["binary"])
    683            if path:
    684                kwargs["mojojs_path"] = path
    685                kwargs["enable_mojojs"] = True
    686                logger.info(f"MojoJS enabled automatically (mojojs_path: {path})")
    687            else:
    688                kwargs["enable_mojojs"] = False
    689                logger.info("MojoJS is disabled for this run.")
    690 
    691        if kwargs["webdriver_binary"] is None:
    692            webdriver_binary = None
    693            if not kwargs["install_webdriver"]:
    694                webdriver_binary = self.browser.find_webdriver(self.venv.bin_path)
    695                if webdriver_binary and not self.browser.webdriver_supports_browser(
    696                        webdriver_binary, kwargs["binary"], browser_channel):
    697                    webdriver_binary = None
    698 
    699            if webdriver_binary is None:
    700                install = self.prompt_install("msedgedriver")
    701 
    702                if install:
    703                    webdriver_binary = self.browser.install_webdriver(
    704                        dest=self.venv.bin_path,
    705                        channel=browser_channel,
    706                        browser_binary=kwargs["binary"],
    707                    )
    708            else:
    709                logger.info("Using webdriver binary %s" % webdriver_binary)
    710 
    711            if webdriver_binary:
    712                kwargs["webdriver_binary"] = webdriver_binary
    713            else:
    714                raise WptrunError("Unable to locate or install matching msedgedriver binary")
    715        if browser_channel in self.experimental_channels:
    716            # HACK(Hexcles): work around https://github.com/web-platform-tests/wpt/issues/16448
    717            kwargs["webdriver_args"].append("--disable-build-check")
    718            if kwargs["enable_experimental"] is None:
    719                logger.info(
    720                    "Automatically turning on experimental features for Microsoft Edge Dev/Canary")
    721                kwargs["enable_experimental"] = True
    722            if kwargs["enable_webtransport_h3"] is None:
    723                # To start the WebTransport over HTTP/3 test server.
    724                kwargs["enable_webtransport_h3"] = True
    725        if os.getenv("TASKCLUSTER_ROOT_URL"):
    726            # We are on Taskcluster, where our Docker container does not have
    727            # enough capabilities to run Microsoft Edge with sandboxing. (gh-20133)
    728            kwargs["binary_args"].append("--no-sandbox")
    729 
    730 
    731 class Safari(BrowserSetup):
    732    name = "safari"
    733    browser_cls = browser.Safari
    734 
    735    def install(self, channel=None):
    736        raise NotImplementedError
    737 
    738    def setup_kwargs(self, kwargs):
    739        if kwargs["webdriver_binary"] is None:
    740            webdriver_binary = self.browser.find_webdriver(channel=kwargs["browser_channel"])
    741 
    742            if webdriver_binary is None:
    743                raise WptrunError("Unable to locate safaridriver binary")
    744 
    745            kwargs["webdriver_binary"] = webdriver_binary
    746 
    747 
    748 class Sauce(BrowserSetup):
    749    name = "sauce"
    750    browser_cls = browser.Sauce
    751 
    752    def install(self, channel=None):
    753        raise NotImplementedError
    754 
    755    def setup_kwargs(self, kwargs):
    756        if kwargs["sauce_browser"] is None:
    757            raise WptrunError("Missing required argument --sauce-browser")
    758        if kwargs["sauce_version"] is None:
    759            raise WptrunError("Missing required argument --sauce-version")
    760        kwargs["test_types"] = ["testharness", "reftest"]
    761 
    762 
    763 class Servo(BrowserSetup):
    764    name = "servo"
    765    browser_cls = browser.Servo
    766 
    767    def install(self, channel=None):
    768        if self.prompt_install(self.name):
    769            return self.browser.install(self.venv.path)
    770 
    771    def setup_kwargs(self, kwargs):
    772        if kwargs["binary"] is None:
    773            binary = self.browser.find_binary(self.venv.path, None)
    774 
    775            if binary is None:
    776                raise WptrunError("Unable to find servo binary in PATH")
    777            kwargs["binary"] = binary
    778 
    779 
    780 class ServoWebDriver(Servo):
    781    name = "servodriver"
    782    browser_cls = browser.ServoWebDriver
    783 
    784 
    785 class WebKit(BrowserSetup):
    786    name = "webkit"
    787    browser_cls = browser.WebKit
    788 
    789    def install(self, channel=None):
    790        raise NotImplementedError
    791 
    792    def setup_kwargs(self, kwargs):
    793        pass
    794 
    795 class Ladybird(BrowserSetup):
    796    name = "ladybird"
    797    browser_cls = browser.Ladybird
    798 
    799    def install(self, channel=None):
    800        raise NotImplementedError
    801 
    802    def setup_kwargs(self, kwargs):
    803        pass
    804 
    805 class WebKitTestRunner(BrowserSetup):
    806    name = "wktr"
    807    browser_cls = browser.WebKitTestRunner
    808 
    809    def install(self, channel=None):
    810        if self.prompt_install(self.name):
    811            return self.browser.install(self.venv.path, channel=channel)
    812 
    813    def setup_kwargs(self, kwargs):
    814        if kwargs["binary"] is None:
    815            binary = self.browser.find_binary(self.venv.path, channel=kwargs["browser_channel"])
    816 
    817            if binary is None:
    818                raise WptrunError("Unable to find binary in PATH")
    819            kwargs["binary"] = binary
    820 
    821 
    822 class WebKitGlibBaseMiniBrowser(BrowserSetup):
    823    """ Base class for WebKitGTKMiniBrowser and WPEWebKitMiniBrowser """
    824 
    825    def install(self, channel=None):
    826        if self.prompt_install(self.name):
    827            return self.browser.install(self.venv.path, channel, self.prompt)
    828 
    829    def setup_kwargs(self, kwargs):
    830        if kwargs["binary"] is None:
    831            binary = self.browser.find_binary(
    832                venv_path=self.venv.path, channel=kwargs["browser_channel"])
    833 
    834            if binary is None:
    835                raise WptrunError("Unable to find MiniBrowser binary")
    836            kwargs["binary"] = binary
    837 
    838        if kwargs["webdriver_binary"] is None:
    839            webdriver_binary = self.browser.find_webdriver(
    840                venv_path=self.venv.path, channel=kwargs["browser_channel"])
    841 
    842            if webdriver_binary is None:
    843                raise WptrunError('Unable to find "%s" binary in PATH' % self.browser_cls.WEBDRIVER_BINARY_NAME)
    844            kwargs["webdriver_binary"] = webdriver_binary
    845 
    846 
    847 class WebKitGTKMiniBrowser(WebKitGlibBaseMiniBrowser):
    848    name = "webkitgtk_minibrowser"
    849    browser_cls = browser.WebKitGTKMiniBrowser
    850 
    851 
    852 class WPEWebKitMiniBrowser(WebKitGlibBaseMiniBrowser):
    853    name = "wpewebkit_minibrowser"
    854    browser_cls = browser.WPEWebKitMiniBrowser
    855 
    856    def setup_kwargs(self, kwargs):
    857        if kwargs["headless"]:
    858            kwargs["binary_args"].append("--headless")
    859        super().setup_kwargs(kwargs)
    860 
    861 
    862 class Epiphany(BrowserSetup):
    863    name = "epiphany"
    864    browser_cls = browser.Epiphany
    865 
    866    def install(self, channel=None):
    867        raise NotImplementedError
    868 
    869    def setup_kwargs(self, kwargs):
    870        if kwargs["binary"] is None:
    871            binary = self.browser.find_binary()
    872 
    873            if binary is None:
    874                raise WptrunError("Unable to find epiphany in PATH")
    875            kwargs["binary"] = binary
    876 
    877        if kwargs["webdriver_binary"] is None:
    878            webdriver_binary = self.browser.find_webdriver()
    879 
    880            if webdriver_binary is None:
    881                raise WptrunError("Unable to find WebKitWebDriver in PATH")
    882            kwargs["webdriver_binary"] = webdriver_binary
    883 
    884 
    885 product_setup = {
    886    "android_webview": AndroidWebview,
    887    "firefox": Firefox,
    888    "firefox_android": FirefoxAndroid,
    889    "chrome": Chrome,
    890    "chrome_android": ChromeAndroid,
    891    "chrome_ios": ChromeiOS,
    892    "chromium": Chromium,
    893    "edge": Edge,
    894    "headless_shell": HeadlessShell,
    895    "safari": Safari,
    896    "servo": Servo,
    897    "servodriver": ServoWebDriver,
    898    "sauce": Sauce,
    899    "opera": Opera,
    900    "webkit": WebKit,
    901    "wktr": WebKitTestRunner,
    902    "webkitgtk_minibrowser": WebKitGTKMiniBrowser,
    903    "wpewebkit_minibrowser": WPEWebKitMiniBrowser,
    904    "epiphany": Epiphany,
    905    "ladybird": Ladybird,
    906 }
    907 
    908 
    909 def setup_logging(kwargs, default_config=None, formatter_defaults=None):
    910    import mozlog
    911    from wptrunner import wptrunner
    912 
    913    global logger
    914 
    915    # Use the grouped formatter by default where mozlog 3.9+ is installed
    916    if default_config is None:
    917        if hasattr(mozlog.formatters, "GroupingFormatter"):
    918            default_formatter = "grouped"
    919        else:
    920            default_formatter = "mach"
    921        default_config = {default_formatter: sys.stdout}
    922    wptrunner.setup_logging(kwargs, default_config, formatter_defaults=formatter_defaults)
    923    logger = wptrunner.logger
    924    return logger
    925 
    926 
    927 def setup_wptrunner(venv, **kwargs):
    928    from wptrunner import wptcommandline
    929 
    930    kwargs = kwargs.copy()
    931 
    932    kwargs["product"] = kwargs["product"].replace("-", "_")
    933 
    934    check_environ(kwargs["product"])
    935    args_general(kwargs)
    936 
    937    if kwargs["product"] not in product_setup:
    938        if kwargs["product"] == "edgechromium":
    939            raise WptrunError("edgechromium has been renamed to edge.")
    940 
    941        raise WptrunError("Unsupported product %s" % kwargs["product"])
    942 
    943    setup_cls = product_setup[kwargs["product"]](venv, kwargs["prompt"])
    944    if not venv.skip_virtualenv_setup:
    945        requirements = [os.path.join(wpt_root, "tools", "wptrunner", "requirements.txt")]
    946        requirements.extend(setup_cls.requirements())
    947        venv.install_requirements(*requirements)
    948 
    949    affected_revish = kwargs.get("affected")
    950    if affected_revish is not None:
    951        files_changed, _ = testfiles.files_changed(
    952            affected_revish, include_uncommitted=True, include_new=True)
    953        # TODO: Perhaps use wptrunner.testloader.ManifestLoader here
    954        # and remove the manifest-related code from testfiles.
    955        # https://github.com/web-platform-tests/wpt/issues/14421
    956        tests_changed, tests_affected = testfiles.affected_testfiles(
    957            files_changed, manifest_path=kwargs.get("manifest_path"), manifest_update=kwargs["manifest_update"])
    958        test_list = tests_changed | tests_affected
    959        logger.info("Identified %s affected tests" % len(test_list))
    960        test_list = [os.path.relpath(item, wpt_root) for item in test_list]
    961        kwargs["test_list"] += test_list
    962        kwargs["default_exclude"] = True
    963 
    964    if kwargs["install_browser"] and not kwargs["channel"]:
    965        logger.info("--install-browser is given but --channel is not set, default to nightly channel")
    966        kwargs["channel"] = "nightly"
    967 
    968    if kwargs["channel"]:
    969        channel = install.get_channel(kwargs["product"], kwargs["channel"])
    970        if channel is not None:
    971            if channel != kwargs["channel"]:
    972                logger.info("Interpreting channel '%s' as '%s'" % (kwargs["channel"],
    973                                                                   channel))
    974            kwargs["browser_channel"] = channel
    975        else:
    976            logger.info("Valid channels for %s not known; using argument unmodified" %
    977                        kwargs["product"])
    978            kwargs["browser_channel"] = kwargs["channel"]
    979 
    980    if kwargs["install_browser"]:
    981        logger.info("Installing browser")
    982        kwargs["binary"] = setup_cls.install(channel=kwargs["browser_channel"])
    983 
    984    setup_cls.setup(kwargs)
    985 
    986    # Remove kwargs we handle here
    987    wptrunner_kwargs = kwargs.copy()
    988    for kwarg in ["affected",
    989                  "install_browser",
    990                  "install_webdriver",
    991                  "channel",
    992                  "prompt",
    993                  "logcat_dir"]:
    994        del wptrunner_kwargs[kwarg]
    995 
    996    wptcommandline.check_args(wptrunner_kwargs)
    997 
    998    # Only update browser_version if it was not given as a command line
    999    # argument, so that it can be overridden on the command line.
   1000    if not wptrunner_kwargs["browser_version"]:
   1001        wptrunner_kwargs["browser_version"] = setup_cls.browser.version(
   1002            binary=wptrunner_kwargs.get("binary") or wptrunner_kwargs.get("package_name"),
   1003            webdriver_binary=wptrunner_kwargs.get("webdriver_binary"),
   1004        )
   1005 
   1006    return setup_cls, wptrunner_kwargs
   1007 
   1008 
   1009 def run(venv, **kwargs):
   1010    setup_logging(kwargs)
   1011 
   1012    setup_cls, wptrunner_kwargs = setup_wptrunner(venv, **kwargs)
   1013 
   1014    try:
   1015        rv = run_single(venv, **wptrunner_kwargs)
   1016    finally:
   1017        setup_cls.teardown()
   1018 
   1019    return rv
   1020 
   1021 
   1022 def run_single(venv, **kwargs):
   1023    from wptrunner import wptrunner
   1024    return wptrunner.start(**kwargs)