chromium/third_party/wpt_tools/wpt/tools/wpt/run.py

# mypy: allow-untyped-defs

import argparse
import os
import platform
import subprocess
import sys
from shutil import copyfile, which
from typing import ClassVar, Tuple, Type

wpt_root = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
sys.path.insert(0, os.path.abspath(os.path.join(wpt_root, "tools")))

from . import browser, install, testfiles
from ..serve import serve

logger = None


class WptrunError(Exception):
    pass


class WptrunnerHelpAction(argparse.Action):
    def __init__(self,
                 option_strings,
                 dest=argparse.SUPPRESS,
                 default=argparse.SUPPRESS,
                 help=None):
        super().__init__(
            option_strings=option_strings,
            dest=dest,
            default=default,
            nargs=0,
            help=help)

    def __call__(self, parser, namespace, values, option_string=None):
        from wptrunner import wptcommandline
        wptparser = wptcommandline.create_parser()
        wptparser.usage = parser.usage
        wptparser.print_help()
        parser.exit()


def create_parser():
    from wptrunner import wptcommandline

    parser = argparse.ArgumentParser(add_help=False, parents=[install.channel_args])
    parser.add_argument("product", action="store",
                        help="Browser to run tests in")
    parser.add_argument("--affected", action="store", default=None,
                        help="Run affected tests since revish")
    parser.add_argument("--yes", "-y", dest="prompt", action="store_false", default=True,
                        help="Don't prompt before installing components")
    parser.add_argument("--install-browser", action="store_true",
                        help="Install the browser from the release channel specified by --channel "
                        "(or the nightly channel by default).")
    parser.add_argument("--install-webdriver", action="store_true",
                        help="Install WebDriver from the release channel specified by --channel "
                        "(or the nightly channel by default).")
    parser.add_argument("--logcat-dir", action="store", default=None,
                        help="Directory to write Android logcat files to")
    parser._add_container_actions(wptcommandline.create_parser())
    return parser


def exit(msg=None):
    if msg:
        logger.critical(msg)
        sys.exit(1)
    else:
        sys.exit(0)


def args_general(kwargs):

    def set_if_none(name, value):
        if kwargs.get(name) is None:
            kwargs[name] = value
            logger.info("Set %s to %s" % (name, value))

    set_if_none("tests_root", wpt_root)
    set_if_none("metadata_root", wpt_root)
    set_if_none("manifest_update", True)
    set_if_none("manifest_download", True)

    if kwargs["ssl_type"] in (None, "pregenerated"):
        cert_root = os.path.join(wpt_root, "tools", "certs")
        if kwargs["ca_cert_path"] is None:
            kwargs["ca_cert_path"] = os.path.join(cert_root, "cacert.pem")

        if kwargs["host_key_path"] is None:
            kwargs["host_key_path"] = os.path.join(cert_root, "web-platform.test.key")

        if kwargs["host_cert_path"] is None:
            kwargs["host_cert_path"] = os.path.join(cert_root, "web-platform.test.pem")
    elif kwargs["ssl_type"] == "openssl":
        if not which(kwargs["openssl_binary"]):
            if os.uname()[0] == "Windows":
                raise WptrunError("""OpenSSL binary not found. If you need HTTPS tests, install OpenSSL from

https://slproweb.com/products/Win32OpenSSL.html

Ensuring that libraries are added to /bin and add the resulting bin directory to
your PATH.

Otherwise run with --ssl-type=none""")
            else:
                raise WptrunError("""OpenSSL not found. If you don't need HTTPS support run with --ssl-type=none,
otherwise install OpenSSL and ensure that it's on your $PATH.""")


def check_environ(product):
    if product not in ("android_webview", "chrome", "chrome_android", "chrome_ios",
                       "edge", "firefox", "firefox_android", "headless_shell",
                       "ladybird", "servo", "wktr"):
        config_builder = serve.build_config(os.path.join(wpt_root, "config.json"))
        # Override the ports to avoid looking for free ports
        config_builder.ssl = {"type": "none"}
        config_builder.ports = {"http": [8000]}

        is_windows = platform.uname()[0] == "Windows"

        with config_builder as config:
            expected_hosts = set(config.domains_set)
            if is_windows:
                expected_hosts.update(config.not_domains_set)

        missing_hosts = set(expected_hosts)
        if is_windows:
            hosts_path = r"%s\System32\drivers\etc\hosts" % os.environ.get(
                "SystemRoot", r"C:\Windows")
        else:
            hosts_path = "/etc/hosts"

        if os.path.abspath(os.curdir) == wpt_root:
            wpt_path = "wpt"
        else:
            wpt_path = os.path.join(wpt_root, "wpt")

        with open(hosts_path) as f:
            for line in f:
                line = line.split("#", 1)[0].strip()
                parts = line.split()
                hosts = parts[1:]
                for host in hosts:
                    missing_hosts.discard(host)
            if missing_hosts:
                if is_windows:
                    message = """Missing hosts file configuration. Run

python %s make-hosts-file | Out-File %s -Encoding ascii -Append

in PowerShell with Administrator privileges.""" % (wpt_path, hosts_path)
                else:
                    message = """Missing hosts file configuration. Run

%s make-hosts-file | sudo tee -a %s""" % ("./wpt" if wpt_path == "wpt" else wpt_path,
                                          hosts_path)
                raise WptrunError(message)


class AndroidLogcat:
    def __init__(self, adb_path, base_path=None):
        self.adb_path = adb_path
        self.base_path = base_path if base_path is not None else os.curdir
        self.procs = {}

    def start(self, device_serial):
        """
        Start recording logcat. Writes logcat to the upload directory.
        """
        # Start logcat for the device. The adb process runs until the
        # corresponding device is stopped. Output is written directly to
        # the blobber upload directory so that it is uploaded automatically
        # at the end of the job.
        if device_serial in self.procs:
            logger.warning(f"Logcat for {device_serial} already started")
            return

        logcat_path = os.path.join(self.base_path, f"logcat-{device_serial}.log")
        out_file = open(logcat_path, "w")
        cmd = [
            self.adb_path,
            "-s",
            device_serial,
            "logcat",
            "-v",
            "threadtime",
            "Trace:S",
            "StrictMode:S",
            "ExchangeService:S",
        ]
        logger.debug(" ".join(cmd))
        proc = subprocess.Popen(
            cmd, stdout=out_file, stdin=subprocess.PIPE
        )
        logger.info(f"Started logcat for device {device_serial} pid {proc.pid}")
        self.procs[device_serial] = (proc, out_file)

    def stop(self, device_serial=None):
        """
        Stop logcat process started by logcat_start.
        """
        if device_serial is None:
            for key in list(self.procs.keys()):
                self.stop(key)
            return

        proc, out_file = self.procs.get(device_serial, (None, None))
        if proc is not None:
            try:
                proc.kill()
                out_file.close()
            finally:
                del self.procs[device_serial]


class BrowserSetup:
    name: ClassVar[str]
    browser_cls: ClassVar[Type[browser.Browser]]

    def __init__(self, venv, prompt=True):
        self.browser = self.browser_cls(logger)
        self.venv = venv
        self.prompt = prompt

    def prompt_install(self, component):
        if not self.prompt:
            return True
        while True:
            resp = input("Download and install %s [Y/n]? " % component).strip().lower()
            if not resp or resp == "y":
                return True
            elif resp == "n":
                return False

    def install(self, channel=None):
        if self.prompt_install(self.name):
            return self.browser.install(self.venv.path, channel)

    def requirements(self):
        if self.browser.requirements:
            return [os.path.join(wpt_root, "tools", "wptrunner", self.browser.requirements)]
        return []

    def setup(self, kwargs):
        self.setup_kwargs(kwargs)

    def teardown(self):
        pass


def safe_unsetenv(env_var):
    """Safely remove an environment variable.

    Python3 does not support os.unsetenv in Windows for python<3.9, so we better
    remove the variable directly from os.environ.
    """
    try:
        del os.environ[env_var]
    except KeyError:
        pass


class Firefox(BrowserSetup):
    name = "firefox"
    browser_cls = browser.Firefox

    def setup_kwargs(self, kwargs):
        if kwargs["binary"] is None:
            if kwargs["browser_channel"] is None:
                kwargs["browser_channel"] = "nightly"
                logger.info("No browser channel specified. Running nightly instead.")

            binary = self.browser.find_binary(self.venv.path,
                                              kwargs["browser_channel"])
            if binary is None:
                raise WptrunError("""Firefox binary not found on $PATH.

Install Firefox or use --binary to set the binary path""")
            kwargs["binary"] = binary

        if kwargs["certutil_binary"] is None and kwargs["ssl_type"] != "none":
            certutil = self.browser.find_certutil()

            if certutil is None:
                # Can't download this for now because it's missing the libnss3 library
                logger.info("""Can't find certutil, certificates will not be checked.
Consider installing certutil via your OS package manager or directly.""")
            else:
                logger.info("Using certutil %s" % certutil)

            kwargs["certutil_binary"] = certutil

        if kwargs["webdriver_binary"] is None and "wdspec" in kwargs["test_types"]:
            webdriver_binary = None
            if not kwargs["install_webdriver"]:
                webdriver_binary = self.browser.find_webdriver()

            if webdriver_binary is None:
                install = self.prompt_install("geckodriver")

                if install:
                    logger.info("Downloading geckodriver")
                    webdriver_binary = self.browser.install_webdriver(
                        dest=self.venv.bin_path,
                        channel=kwargs["browser_channel"],
                        browser_binary=kwargs["binary"])
            else:
                logger.info("Using webdriver binary %s" % webdriver_binary)

            if webdriver_binary:
                kwargs["webdriver_binary"] = webdriver_binary
            else:
                logger.info("Unable to find or install geckodriver, skipping wdspec tests")
                kwargs["test_types"].remove("wdspec")

        if kwargs["prefs_root"] is None:
            prefs_root = self.browser.install_prefs(kwargs["binary"],
                                                    self.venv.path,
                                                    channel=kwargs["browser_channel"])
            kwargs["prefs_root"] = prefs_root

        if kwargs["headless"] is None and not kwargs["debug_test"]:
            kwargs["headless"] = True
            logger.info("Running in headless mode, pass --no-headless to disable")

        if kwargs["browser_channel"] == "nightly" and kwargs["enable_webtransport_h3"] is None:
            kwargs["enable_webtransport_h3"] = True

        # Turn off Firefox WebRTC ICE logging on WPT (turned on by mozrunner)
        safe_unsetenv('R_LOG_LEVEL')
        safe_unsetenv('R_LOG_DESTINATION')
        safe_unsetenv('R_LOG_VERBOSE')

        # Allow WebRTC tests to call getUserMedia.
        kwargs["extra_prefs"].append("media.navigator.streams.fake=true")


class FirefoxAndroid(BrowserSetup):
    name = "firefox_android"
    browser_cls = browser.FirefoxAndroid

    def setup_kwargs(self, kwargs):
        from . import android
        import mozdevice

        # We don't support multiple channels for android yet
        if kwargs["browser_channel"] is None:
            kwargs["browser_channel"] = "nightly"

        if kwargs["prefs_root"] is None:
            prefs_root = self.browser.install_prefs(kwargs["binary"],
                                                    self.venv.path,
                                                    channel=kwargs["browser_channel"])
            kwargs["prefs_root"] = prefs_root

        if kwargs["package_name"] is None:
            kwargs["package_name"] = "org.mozilla.geckoview.test_runner"
        app = kwargs["package_name"]

        if not kwargs["device_serial"]:
            kwargs["device_serial"] = ["emulator-5554"]

        if kwargs["webdriver_binary"] is None and "wdspec" in kwargs["test_types"]:
            webdriver_binary = None
            if not kwargs["install_webdriver"]:
                webdriver_binary = self.browser.find_webdriver()

            if webdriver_binary is None:
                install = self.prompt_install("geckodriver")

                if install:
                    logger.info("Downloading geckodriver")
                    webdriver_binary = self.browser.install_webdriver(
                        dest=self.venv.bin_path,
                        channel=kwargs["browser_channel"],
                        browser_binary=kwargs["binary"])
            else:
                logger.info("Using webdriver binary %s" % webdriver_binary)

            if webdriver_binary:
                kwargs["webdriver_binary"] = webdriver_binary
            else:
                logger.info("Unable to find or install geckodriver, skipping wdspec tests")
                kwargs["test_types"].remove("wdspec")

        if kwargs["adb_binary"] is None:
            if "ADB_PATH" not in os.environ:
                adb_path = os.path.join(android.get_paths(None)["sdk"],
                                        "platform-tools",
                                        "adb")
                os.environ["ADB_PATH"] = adb_path
            kwargs["adb_binary"] = os.environ["ADB_PATH"]

        self._logcat = AndroidLogcat(kwargs["adb_binary"], base_path=kwargs["logcat_dir"])

        for device_serial in kwargs["device_serial"]:
            if device_serial.startswith("emulator-"):
                # We're running on an emulator so ensure that's set up
                android.start(logger,
                              reinstall=False,
                              device_serial=device_serial,
                              prompt=kwargs["prompt"])

        for device_serial in kwargs["device_serial"]:
            device = mozdevice.ADBDeviceFactory(adb=kwargs["adb_binary"],
                                                device=device_serial)
            self._logcat.start(device_serial)
            max_retries = 5
            last_exception = None
            if self.browser.apk_path:
                device.uninstall_app(app)
                for i in range(max_retries + 1):
                    logger.info(f"Installing {app} on {device_serial} "
                                f"attempt {i + 1}/{max_retries + 1}")
                    try:
                        # Temporarily replace mozdevice function with custom code
                        # that passes in the `--no-incremental` option
                        cmd = ["install", "--no-incremental", self.browser.apk_path]
                        logger.debug(" ".join(cmd))
                        data = device.command_output(cmd, timeout=120)
                        if data.find("Success") == -1:
                            raise mozdevice.ADBError(f"Install failed for {self.browser.apk_path}."
                                                     f" Got: {data}")
                    except Exception as e:
                        last_exception = e
                    else:
                        break
                else:
                    assert last_exception is not None
                    raise WptrunError(f"Failed to install {app} on device {device_serial} "
                                      f"after {max_retries} retries") from last_exception
            elif not device.is_app_installed(app):
                raise WptrunError(f"app {app} not installed on device {device_serial}")


    def teardown(self):
        from . import android

        if hasattr(self, "_logcat"):
            emulator_log = os.path.join(android.get_paths(None)["sdk"],
                                        ".android",
                                        "emulator.log")
            if os.path.exists(emulator_log):
                dest_path = os.path.join(self._logcat.base_path, "emulator.log")
                copyfile(emulator_log, dest_path)

            self._logcat.stop()


class Chrome(BrowserSetup):
    name = "chrome"
    browser_cls: ClassVar[Type[browser.ChromeChromiumBase]] = browser.Chrome
    experimental_channels: ClassVar[Tuple[str, ...]] = ("dev", "canary")

    def setup_kwargs(self, kwargs):
        browser_channel = kwargs["browser_channel"]
        if kwargs["binary"] is None:
            binary = self.browser.find_binary(venv_path=self.venv.path, channel=browser_channel)
            if binary:
                kwargs["binary"] = binary
            else:
                raise WptrunError(f"Unable to locate {self.name.capitalize()} binary")

        if kwargs["mojojs_path"]:
            kwargs["enable_mojojs"] = True
            logger.info("--mojojs-path is provided, enabling MojoJS")
        else:
            path = self.browser.install_mojojs(dest=self.venv.path,
                                               browser_binary=kwargs["binary"])
            if path:
                kwargs["mojojs_path"] = path
                kwargs["enable_mojojs"] = True
                logger.info(f"MojoJS enabled automatically (mojojs_path: {path})")
            else:
                kwargs["enable_mojojs"] = False
                logger.info("MojoJS is disabled for this run.")

        if kwargs["webdriver_binary"] is None:
            webdriver_binary = None
            if not kwargs["install_webdriver"]:
                webdriver_binary = self.browser.find_webdriver(self.venv.bin_path)
                if webdriver_binary and not self.browser.webdriver_supports_browser(
                        webdriver_binary, kwargs["binary"], browser_channel):
                    webdriver_binary = None

            if webdriver_binary is None:
                install = self.prompt_install("chromedriver")

                if install:
                    webdriver_binary = self.browser.install_webdriver(
                        dest=self.venv.bin_path,
                        channel=browser_channel,
                        browser_binary=kwargs["binary"],
                    )
            else:
                logger.info("Using webdriver binary %s" % webdriver_binary)

            if webdriver_binary:
                kwargs["webdriver_binary"] = webdriver_binary
            else:
                raise WptrunError("Unable to locate or install matching ChromeDriver binary")
        if kwargs["headless"] is None and not kwargs["debug_test"]:
            kwargs["headless"] = True
            logger.info("Running in headless mode, pass --no-headless to disable")
        if browser_channel in self.experimental_channels:
            # HACK(Hexcles): work around https://github.com/web-platform-tests/wpt/issues/16448
            kwargs["webdriver_args"].append("--disable-build-check")
            if kwargs["enable_experimental"] is None:
                logger.info(
                    "Automatically turning on experimental features for Chrome Dev/Canary or Chromium trunk")
                kwargs["enable_experimental"] = True
            if kwargs["enable_webtransport_h3"] is None:
                # To start the WebTransport over HTTP/3 test server.
                kwargs["enable_webtransport_h3"] = True
        if os.getenv("TASKCLUSTER_ROOT_URL"):
            # We are on Taskcluster, where our Docker container does not have
            # enough capabilities to run Chrome with sandboxing. (gh-20133)
            kwargs["binary_args"].append("--no-sandbox")


class HeadlessShell(BrowserSetup):
    name = "headless_shell"
    browser_cls = browser.HeadlessShell
    experimental_channels = ("dev", "canary", "nightly")

    def setup_kwargs(self, kwargs):
        browser_channel = kwargs["browser_channel"]
        if kwargs["binary"] is None:
            binary = self.browser.find_binary(venv_path=self.venv.path, channel=browser_channel)
            if binary:
                kwargs["binary"] = binary
            else:
                raise WptrunError(f"Unable to locate {self.name!r} binary")

        if kwargs["mojojs_path"]:
            kwargs["enable_mojojs"] = True
            logger.info("--mojojs-path is provided, enabling MojoJS")
        elif kwargs["enable_mojojs"]:
            logger.warning(f"Cannot install MojoJS for {self.name}, "
                           "which does not return version information. "
                           "Provide '--mojojs-path' explicitly instead.")
            logger.warning("MojoJS is disabled for this run.")

        # Never pause after test, since headless shell is not interactive.
        kwargs["pause_after_test"] = False
        # Don't add a `--headless` switch.
        kwargs["headless"] = False

        if kwargs["enable_webtransport_h3"] is None:
            kwargs["enable_webtransport_h3"] = True


class Chromium(Chrome):
    name = "chromium"
    browser_cls: ClassVar[Type[browser.ChromeChromiumBase]] = browser.Chromium
    experimental_channels = ("nightly",)


class ChromeAndroidBase(BrowserSetup):
    experimental_channels = ("dev", "canary")

    def setup_kwargs(self, kwargs):
        if kwargs.get("device_serial"):
            self.browser.device_serial = kwargs["device_serial"]
        if kwargs.get("adb_binary"):
            self.browser.adb_binary = kwargs["adb_binary"]
        browser_channel = kwargs["browser_channel"]
        if kwargs["package_name"] is None:
            kwargs["package_name"] = self.browser.find_binary(
                channel=browser_channel)
        if not kwargs["device_serial"]:
            kwargs["device_serial"] = ["emulator-5554"]
        if kwargs["webdriver_binary"] is None:
            webdriver_binary = None
            if not kwargs["install_webdriver"]:
                webdriver_binary = self.browser.find_webdriver()

            if webdriver_binary is None:
                install = self.prompt_install("chromedriver")

                if install:
                    logger.info("Downloading chromedriver")
                    webdriver_binary = self.browser.install_webdriver(
                        dest=self.venv.bin_path,
                        channel=browser_channel,
                        browser_binary=kwargs["package_name"],
                    )
            else:
                logger.info("Using webdriver binary %s" % webdriver_binary)

            if webdriver_binary:
                kwargs["webdriver_binary"] = webdriver_binary
            else:
                raise WptrunError("Unable to locate or install chromedriver binary")


class ChromeAndroid(ChromeAndroidBase):
    name = "chrome_android"
    browser_cls = browser.ChromeAndroid

    def setup_kwargs(self, kwargs):
        super().setup_kwargs(kwargs)
        if kwargs["browser_channel"] in self.experimental_channels:
            # HACK(Hexcles): work around https://github.com/web-platform-tests/wpt/issues/16448
            kwargs["webdriver_args"].append("--disable-build-check")
            if kwargs["enable_experimental"] is None:
                logger.info("Automatically turning on experimental features for Chrome Dev/Canary")
                kwargs["enable_experimental"] = True


class ChromeiOS(BrowserSetup):
    name = "chrome_ios"
    browser_cls = browser.ChromeiOS

    def setup_kwargs(self, kwargs):
        if kwargs["webdriver_binary"] is None:
            raise WptrunError("Unable to locate or install chromedriver binary")


class AndroidWebview(ChromeAndroidBase):
    name = "android_webview"
    browser_cls = browser.AndroidWebview

    def setup_kwargs(self, kwargs):
        if kwargs["mojojs_path"]:
            kwargs["enable_mojojs"] = True
            logger.info("--mojojs-path is provided, enabling MojoJS")


class Opera(BrowserSetup):
    name = "opera"
    browser_cls = browser.Opera

    def setup_kwargs(self, kwargs):
        if kwargs["webdriver_binary"] is None:
            webdriver_binary = None
            if not kwargs["install_webdriver"]:
                webdriver_binary = self.browser.find_webdriver()

            if webdriver_binary is None:
                install = self.prompt_install("operadriver")

                if install:
                    logger.info("Downloading operadriver")
                    webdriver_binary = self.browser.install_webdriver(
                        dest=self.venv.bin_path,
                        channel=kwargs["browser_channel"])
            else:
                logger.info("Using webdriver binary %s" % webdriver_binary)

            if webdriver_binary:
                kwargs["webdriver_binary"] = webdriver_binary
            else:
                raise WptrunError("Unable to locate or install operadriver binary")


class Edge(BrowserSetup):
    name = "MicrosoftEdge"
    browser_cls = browser.Edge
    experimental_channels: ClassVar[Tuple[str, ...]] = ("dev", "canary")

    def setup_kwargs(self, kwargs):
        browser_channel = kwargs["browser_channel"]
        if kwargs["binary"] is None:
            binary = self.browser.find_binary(venv_path=self.venv.path, channel=browser_channel)
            if binary:
                kwargs["binary"] = binary
            else:
                raise WptrunError(f"Unable to locate {self.name.capitalize()} binary")

        if kwargs["mojojs_path"]:
            kwargs["enable_mojojs"] = True
            logger.info("--mojojs-path is provided, enabling MojoJS")
        else:
            path = self.browser.install_mojojs(dest=self.venv.path,
                                               browser_binary=kwargs["binary"])
            if path:
                kwargs["mojojs_path"] = path
                kwargs["enable_mojojs"] = True
                logger.info(f"MojoJS enabled automatically (mojojs_path: {path})")
            else:
                kwargs["enable_mojojs"] = False
                logger.info("MojoJS is disabled for this run.")

        if kwargs["webdriver_binary"] is None:
            webdriver_binary = None
            if not kwargs["install_webdriver"]:
                webdriver_binary = self.browser.find_webdriver(self.venv.bin_path)
                if webdriver_binary and not self.browser.webdriver_supports_browser(
                        webdriver_binary, kwargs["binary"], browser_channel):
                    webdriver_binary = None

            if webdriver_binary is None:
                install = self.prompt_install("msedgedriver")

                if install:
                    webdriver_binary = self.browser.install_webdriver(
                        dest=self.venv.bin_path,
                        channel=browser_channel,
                        browser_binary=kwargs["binary"],
                    )
            else:
                logger.info("Using webdriver binary %s" % webdriver_binary)

            if webdriver_binary:
                kwargs["webdriver_binary"] = webdriver_binary
            else:
                raise WptrunError("Unable to locate or install matching msedgedriver binary")
        if browser_channel in self.experimental_channels:
            # HACK(Hexcles): work around https://github.com/web-platform-tests/wpt/issues/16448
            kwargs["webdriver_args"].append("--disable-build-check")
            if kwargs["enable_experimental"] is None:
                logger.info(
                    "Automatically turning on experimental features for Microsoft Edge Dev/Canary")
                kwargs["enable_experimental"] = True
            if kwargs["enable_webtransport_h3"] is None:
                # To start the WebTransport over HTTP/3 test server.
                kwargs["enable_webtransport_h3"] = True
        if os.getenv("TASKCLUSTER_ROOT_URL"):
            # We are on Taskcluster, where our Docker container does not have
            # enough capabilities to run Microsoft Edge with sandboxing. (gh-20133)
            kwargs["binary_args"].append("--no-sandbox")


class Safari(BrowserSetup):
    name = "safari"
    browser_cls = browser.Safari

    def install(self, channel=None):
        raise NotImplementedError

    def setup_kwargs(self, kwargs):
        if kwargs["webdriver_binary"] is None:
            webdriver_binary = self.browser.find_webdriver(channel=kwargs["browser_channel"])

            if webdriver_binary is None:
                raise WptrunError("Unable to locate safaridriver binary")

            kwargs["webdriver_binary"] = webdriver_binary


class Sauce(BrowserSetup):
    name = "sauce"
    browser_cls = browser.Sauce

    def install(self, channel=None):
        raise NotImplementedError

    def setup_kwargs(self, kwargs):
        if kwargs["sauce_browser"] is None:
            raise WptrunError("Missing required argument --sauce-browser")
        if kwargs["sauce_version"] is None:
            raise WptrunError("Missing required argument --sauce-version")
        kwargs["test_types"] = ["testharness", "reftest"]


class Servo(BrowserSetup):
    name = "servo"
    browser_cls = browser.Servo

    def install(self, channel=None):
        if self.prompt_install(self.name):
            return self.browser.install(self.venv.path)

    def setup_kwargs(self, kwargs):
        if kwargs["binary"] is None:
            binary = self.browser.find_binary(self.venv.path, None)

            if binary is None:
                raise WptrunError("Unable to find servo binary in PATH")
            kwargs["binary"] = binary


class ServoWebDriver(Servo):
    name = "servodriver"
    browser_cls = browser.ServoWebDriver


class WebKit(BrowserSetup):
    name = "webkit"
    browser_cls = browser.WebKit

    def install(self, channel=None):
        raise NotImplementedError

    def setup_kwargs(self, kwargs):
        pass

class Ladybird(BrowserSetup):
    name = "ladybird"
    browser_cls = browser.Ladybird

    def install(self, channel=None):
        raise NotImplementedError

    def setup_kwargs(self, kwargs):
        pass

class WebKitTestRunner(BrowserSetup):
    name = "wktr"
    browser_cls = browser.WebKitTestRunner

    def install(self, channel=None):
        if self.prompt_install(self.name):
            return self.browser.install(self.venv.path, channel=channel)

    def setup_kwargs(self, kwargs):
        if kwargs["binary"] is None:
            binary = self.browser.find_binary(self.venv.path, channel=kwargs["browser_channel"])

            if binary is None:
                raise WptrunError("Unable to find binary in PATH")
            kwargs["binary"] = binary


class WebKitGTKMiniBrowser(BrowserSetup):
    name = "webkitgtk_minibrowser"
    browser_cls = browser.WebKitGTKMiniBrowser

    def install(self, channel=None):
        if self.prompt_install(self.name):
            return self.browser.install(self.venv.path, channel, self.prompt)

    def setup_kwargs(self, kwargs):
        if kwargs["binary"] is None:
            binary = self.browser.find_binary(
                venv_path=self.venv.path, channel=kwargs["browser_channel"])

            if binary is None:
                raise WptrunError("Unable to find MiniBrowser binary")
            kwargs["binary"] = binary

        if kwargs["webdriver_binary"] is None:
            webdriver_binary = self.browser.find_webdriver(
                venv_path=self.venv.path, channel=kwargs["browser_channel"])

            if webdriver_binary is None:
                raise WptrunError("Unable to find WebKitWebDriver in PATH")
            kwargs["webdriver_binary"] = webdriver_binary


class Epiphany(BrowserSetup):
    name = "epiphany"
    browser_cls = browser.Epiphany

    def install(self, channel=None):
        raise NotImplementedError

    def setup_kwargs(self, kwargs):
        if kwargs["binary"] is None:
            binary = self.browser.find_binary()

            if binary is None:
                raise WptrunError("Unable to find epiphany in PATH")
            kwargs["binary"] = binary

        if kwargs["webdriver_binary"] is None:
            webdriver_binary = self.browser.find_webdriver()

            if webdriver_binary is None:
                raise WptrunError("Unable to find WebKitWebDriver in PATH")
            kwargs["webdriver_binary"] = webdriver_binary


product_setup = {
    "android_webview": AndroidWebview,
    "firefox": Firefox,
    "firefox_android": FirefoxAndroid,
    "chrome": Chrome,
    "chrome_android": ChromeAndroid,
    "chrome_ios": ChromeiOS,
    "chromium": Chromium,
    "edge": Edge,
    "headless_shell": HeadlessShell,
    "safari": Safari,
    "servo": Servo,
    "servodriver": ServoWebDriver,
    "sauce": Sauce,
    "opera": Opera,
    "webkit": WebKit,
    "wktr": WebKitTestRunner,
    "webkitgtk_minibrowser": WebKitGTKMiniBrowser,
    "epiphany": Epiphany,
    "ladybird": Ladybird,
}


def setup_logging(kwargs, default_config=None, formatter_defaults=None):
    import mozlog
    from wptrunner import wptrunner

    global logger

    # Use the grouped formatter by default where mozlog 3.9+ is installed
    if default_config is None:
        if hasattr(mozlog.formatters, "GroupingFormatter"):
            default_formatter = "grouped"
        else:
            default_formatter = "mach"
        default_config = {default_formatter: sys.stdout}
    wptrunner.setup_logging(kwargs, default_config, formatter_defaults=formatter_defaults)
    logger = wptrunner.logger
    return logger


def setup_wptrunner(venv, **kwargs):
    from wptrunner import wptcommandline

    kwargs = kwargs.copy()

    kwargs["product"] = kwargs["product"].replace("-", "_")

    check_environ(kwargs["product"])
    args_general(kwargs)

    if kwargs["product"] not in product_setup:
        if kwargs["product"] == "edgechromium":
            raise WptrunError("edgechromium has been renamed to edge.")

        raise WptrunError("Unsupported product %s" % kwargs["product"])

    setup_cls = product_setup[kwargs["product"]](venv, kwargs["prompt"])
    if not venv.skip_virtualenv_setup:
        requirements = [os.path.join(wpt_root, "tools", "wptrunner", "requirements.txt")]
        requirements.extend(setup_cls.requirements())
        venv.install_requirements(*requirements)

    affected_revish = kwargs.get("affected")
    if affected_revish is not None:
        files_changed, _ = testfiles.files_changed(
            affected_revish, include_uncommitted=True, include_new=True)
        # TODO: Perhaps use wptrunner.testloader.ManifestLoader here
        # and remove the manifest-related code from testfiles.
        # https://github.com/web-platform-tests/wpt/issues/14421
        tests_changed, tests_affected = testfiles.affected_testfiles(
            files_changed, manifest_path=kwargs.get("manifest_path"), manifest_update=kwargs["manifest_update"])
        test_list = tests_changed | tests_affected
        logger.info("Identified %s affected tests" % len(test_list))
        test_list = [os.path.relpath(item, wpt_root) for item in test_list]
        kwargs["test_list"] += test_list
        kwargs["default_exclude"] = True

    if kwargs["install_browser"] and not kwargs["channel"]:
        logger.info("--install-browser is given but --channel is not set, default to nightly channel")
        kwargs["channel"] = "nightly"

    if kwargs["channel"]:
        channel = install.get_channel(kwargs["product"], kwargs["channel"])
        if channel is not None:
            if channel != kwargs["channel"]:
                logger.info("Interpreting channel '%s' as '%s'" % (kwargs["channel"],
                                                                   channel))
            kwargs["browser_channel"] = channel
        else:
            logger.info("Valid channels for %s not known; using argument unmodified" %
                        kwargs["product"])
            kwargs["browser_channel"] = kwargs["channel"]

    if kwargs["install_browser"]:
        logger.info("Installing browser")
        kwargs["binary"] = setup_cls.install(channel=channel)

    setup_cls.setup(kwargs)

    # Remove kwargs we handle here
    wptrunner_kwargs = kwargs.copy()
    for kwarg in ["affected",
                  "install_browser",
                  "install_webdriver",
                  "channel",
                  "prompt",
                  "logcat_dir"]:
        del wptrunner_kwargs[kwarg]

    wptcommandline.check_args(wptrunner_kwargs)

    # Only update browser_version if it was not given as a command line
    # argument, so that it can be overridden on the command line.
    if not wptrunner_kwargs["browser_version"]:
        wptrunner_kwargs["browser_version"] = setup_cls.browser.version(
            binary=wptrunner_kwargs.get("binary") or wptrunner_kwargs.get("package_name"),
            webdriver_binary=wptrunner_kwargs.get("webdriver_binary"),
        )

    return setup_cls, wptrunner_kwargs


def run(venv, **kwargs):
    setup_logging(kwargs)

    setup_cls, wptrunner_kwargs = setup_wptrunner(venv, **kwargs)

    try:
        rv = run_single(venv, **wptrunner_kwargs) > 0
    finally:
        setup_cls.teardown()

    return rv


def run_single(venv, **kwargs):
    from wptrunner import wptrunner
    return wptrunner.start(**kwargs)