blob: 617f4326ae0dfaf56922b1f0b182c51afb79da23 [file] [log] [blame]
# 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))
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