# Copyright 2022 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. from __future__ import annotations import argparse import logging import re from typing import TYPE_CHECKING, Optional, TextIO, Tuple, cast from crossbench import path as pth from crossbench import plt from crossbench.browsers.attributes import BrowserAttributes from crossbench.browsers.browser import Browser from crossbench.browsers.browser_helper import convert_flags_to_label from crossbench.browsers.viewport import Viewport from crossbench.flags.chrome import ChromeFeatures, ChromeFlags from crossbench.types import JsonDict if TYPE_CHECKING: from crossbench.browsers.settings import Settings from crossbench.flags.base import Flags, FlagsData from crossbench.flags.js_flags import JSFlags from crossbench.runner.groups.session import BrowserSessionRunGroup class Chromium(Browser): MIN_HEADLESS_NEW_VERSION: int = 112 DEFAULT_FLAGS: Tuple[str, ...] = ( "--no-default-browser-check", "--disable-component-update", "--disable-sync", "--disable-extensions", "--no-first-run", # This could be enabled via feature-flags as well. "--disable-search-engine-choice-screen", ) FLAGS_FOR_DISABLING_BACKGROUND_INTERVENTIONS: Tuple[str, ...] = ( "--disable-background-timer-throttling", "--disable-renderer-backgrounding", ) # All flags that might affect how finch / field-trials are loaded. FIELD_TRIAL_FLAGS: Tuple[str, ...] = ( "--force-fieldtrials", "--variations-server-url", "--variations-insecure-server-url", "--variations-test-seed-path", "--enable-field-trial-config", "--disable-variations-safe-mode", ) NO_EXPERIMENTS_FLAGS: Tuple[str, ...] = ( "--no-experiments", "--enable-benchmarking", "--disable-field-trial-config", ) @classmethod def default_path(cls, platform: plt.Platform) -> pth.AnyPath: return platform.search_app_or_executable( "Chromium", macos=["Chromium.app"], linux=["google-chromium", "chromium"], win=["Google/Chromium/Application/chromium.exe"]) @classmethod def default_flags(cls, initial_data: FlagsData = None) -> ChromeFlags: return ChromeFlags(initial_data) def __init__(self, label: str, path: pth.AnyPath, settings: Optional[Settings] = None): super().__init__(label, path, settings=settings) self._stdout_log_file: Optional[TextIO] = None assert isinstance(self._flags, ChromeFlags) def _setup_flags(self, settings: Settings) -> ChromeFlags: flags: Flags = settings.flags js_flags: Flags = settings.js_flags self._flags = self.default_flags(self.DEFAULT_FLAGS) self._flags.update(flags) if "--allow-background-interventions" in self._flags.data: # The --allow-background-interventions flag should have no value. assert self._flags.get("--allow-background-interventions") is None else: self._flags.update(self.FLAGS_FOR_DISABLING_BACKGROUND_INTERVENTIONS) # Explicitly disable field-trials by default on all chrome flavours: # By default field-trials are enabled on non-Chrome branded builds, but # are auto-enabled on everything else. This gives very confusing results # when comparing local builds to official binaries. field_trial_flags = [ flag for flag in self.FIELD_TRIAL_FLAGS if flag in self._flags ] if not field_trial_flags: logging.info("Disabling experiments/finch/field-trials for %s", self) for flag in self.NO_EXPERIMENTS_FLAGS: self._flags.set(flag) else: logging.warning("Running with field-trials or finch experiments.") no_finch_flags = [ flag for flag in self.NO_EXPERIMENTS_FLAGS if flag in self._flags ] if no_finch_flags: raise argparse.ArgumentTypeError( "Conflicting flag groups set: " f"{field_trial_flags} vs {no_finch_flags}.\n" "Cannot enable and disable finch / field-trials at the same time.") self.js_flags.update(js_flags) self._maybe_disable_gpu_compositing() return self._flags def _maybe_disable_gpu_compositing(self) -> None: # Chrome Remote Desktop provide no GPU and older chrome versions # don't handle this well. if self.major_version > 92 or ("CHROME_REMOTE_DESKTOP_SESSION" not in self.platform.environ): return self.flags.set("--disable-gpu-compositing") self.flags.set("--no-sandbox") def _setup_cache_dir(self, settings: Settings) -> None: cache_dir = settings.cache_dir if cache_dir is None: maybe_cache_dir = self._flags.get("--user-data-dir", None) if maybe_cache_dir: cache_dir = pth.AnyPath(maybe_cache_dir) if cache_dir is None: self.cache_dir = self.platform.mkdtemp(prefix=self.type_name) self.clear_cache_dir = True else: self.cache_dir = cache_dir self.clear_cache_dir = False def _extract_version(self) -> str: assert self.path version_string = self.platform.app_version(self.path) # Sample output: "Chromium 90.0.4430.212 dev" => "90.0.4430.212" matches = re.findall(r"[\d\.]+", version_string) if not matches: raise ValueError( f"Could not extract version number from '{version_string}' " f"for '{self.path}'") return str(matches[0]) @property def type_name(self) -> str: return "chromium" @property def attributes(self) -> BrowserAttributes: return BrowserAttributes.CHROMIUM | BrowserAttributes.CHROMIUM_BASED @property def is_headless(self) -> bool: return "--headless" in self._flags @property def chrome_log_file(self) -> pth.AnyPath: assert self.log_file return self.log_file.with_suffix(f".{self.type_name}.log") @property def flags(self) -> ChromeFlags: return cast(ChromeFlags, self._flags) @property def js_flags(self) -> JSFlags: return cast(ChromeFlags, self._flags).js_flags @property def features(self) -> ChromeFeatures: return cast(ChromeFlags, self._flags).features def details_json(self) -> JsonDict: details: JsonDict = super().details_json() if self.log_file: log = cast(JsonDict, details["log"]) log[self.type_name] = str(self.chrome_log_file) log["stdout"] = str(self.stdout_log_file) details["js_flags"] = tuple(self.js_flags) return details def _get_browser_flags_for_session( self, session: BrowserSessionRunGroup) -> Tuple[str, ...]: js_flags_copy = self.js_flags.copy() js_flags_copy.update(session.extra_js_flags) flags_copy = self.flags.copy() flags_copy.update(session.extra_flags) flags_copy.update(self.network.extra_flags(self.attributes)) self._handle_viewport_flags(flags_copy) if len(js_flags_copy): flags_copy["--js-flags"] = str(js_flags_copy) if user_data_dir := self.flags.get("--user-data-dir"): assert user_data_dir == str( self.cache_dir), (f"--user-data-dir path: {user_data_dir} was passed " f"but does not match cache-dir: {self.cache_dir}") if self.cache_dir: flags_copy["--user-data-dir"] = str(self.cache_dir) if self.log_file: flags_copy.set("--enable-logging") flags_copy["--log-file"] = str(self.chrome_log_file) flags_copy = self._filter_flags_for_run(flags_copy) return tuple(flags_copy) def _handle_viewport_flags(self, flags: Flags) -> None: self._sync_viewport_flag(flags, "--start-fullscreen", self.viewport.is_fullscreen, Viewport.FULLSCREEN) self._sync_viewport_flag(flags, "--start-maximized", self.viewport.is_maximized, Viewport.MAXIMIZED) self._sync_viewport_flag(flags, "--headless", self.viewport.is_headless, Viewport.HEADLESS) # M112 added --headless=new as replacement for --headless if "--headless" in flags and (self.major_version >= self.MIN_HEADLESS_NEW_VERSION): if flags["--headless"] is None: logging.info("Replacing --headless with --headless=new") flags.set("--headless", "new", override=True) if self.viewport.is_default: update_viewport = False width, height = self.viewport.size x, y = self.viewport.position if "--window-size" in flags: update_viewport = True width, height = map(int, flags["--window-size"].split(",")) if "--window-position" in flags: update_viewport = True x, y = map(int, flags["--window-position"].split(",")) if update_viewport: self.viewport = Viewport(width, height, x, y) if self.viewport.has_size: flags["--window-size"] = f"{self.viewport.width},{self.viewport.height}" flags["--window-position"] = f"{self.viewport.x},{self.viewport.y}" else: for flag in ("--window-position", "--window-size"): if flag in flags: flag_value = flags[flag] raise ValueError(f"Viewport {self.viewport} conflicts with flag " f"{flag}={flag_value}") def get_label_from_flags(self) -> str: return convert_flags_to_label(*self.flags, *self.js_flags) def quit(self) -> None: super().quit() if self._stdout_log_file: self._stdout_log_file.close() self._stdout_log_file = None