blob: 0449da6fcfc8abca45cf49bc7020a30dcb01ecdc [file] [log] [blame]
# Copyright 2024 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 abc
import logging
from typing import Dict, Iterable, Iterator, Optional, Tuple
from ordered_set import OrderedSet
from crossbench import path as pth
from crossbench.flags.base import Flags, Freezable
from crossbench.flags.js_flags import JSFlags
from crossbench.flags.known_js_flags import KNOWN_JS_FLAGS
class ChromeFlags(Flags):
"""Specialized Flags for Chrome/Chromium-based browser.
This has special treatment for --js-flags and the feature flags:
--enable-features/--disable-features
--enable-blink-features/--disable-blink-features
"""
_JS_FLAG = "--js-flags"
def __init__(self, initial_data: Flags.InitialDataType = None) -> None:
self._features = ChromeFeatures()
self._blink_features = ChromeBlinkFeatures()
self._js_flags = JSFlags()
super().__init__(initial_data)
def freeze(self) -> ChromeFlags:
super().freeze()
self._js_flags.freeze()
self._features.freeze()
self._blink_features.freeze()
return self
def __getitem__(self, key):
if key == self._JS_FLAG and self._js_flags:
return self._js_flags
if key == ChromeFeatures.ENABLE_FLAG and self._features.enabled:
return self._features.enabled_str()
if key == ChromeFeatures.DISABLE_FLAG and self._features.disabled:
return self._features.disabled_str()
if key == ChromeBlinkFeatures.ENABLE_FLAG and self._blink_features.enabled:
return self._blink_features.enabled_str()
if (key == ChromeBlinkFeatures.DISABLE_FLAG and
self._blink_features.disabled):
return self._blink_features.disabled_str()
return super().__getitem__(key)
def _set(self,
flag_name: str,
flag_value: Optional[str] = None,
override: bool = False) -> None:
self.assert_not_frozen()
# pylint: disable=signature-differs
if flag_name == ChromeFeatures.ENABLE_FLAG:
if flag_value is None:
raise ValueError(f"{ChromeFeatures.ENABLE_FLAG} cannot be None")
for feature in flag_value.split(","):
self._features.enable(feature)
elif flag_name == ChromeFeatures.DISABLE_FLAG:
if flag_value is None:
raise ValueError(f"{ChromeFeatures.DISABLE_FLAG} cannot be None")
for feature in flag_value.split(","):
self._features.disable(feature)
elif flag_name == ChromeBlinkFeatures.ENABLE_FLAG:
if flag_value is None:
raise ValueError(f"{ChromeBlinkFeatures.ENABLE_FLAG} cannot be None")
for feature in flag_value.split(","):
self._blink_features.enable(feature)
elif flag_name == ChromeBlinkFeatures.DISABLE_FLAG:
if flag_value is None:
raise ValueError(f"{ChromeBlinkFeatures.DISABLE_FLAG} cannot be None")
for feature in flag_value.split(","):
self._blink_features.disable(feature)
elif flag_name == self._JS_FLAG:
if flag_value is None:
raise ValueError(f"{self._JS_FLAG} cannot be None")
self._set_js_flag(flag_value, override)
else:
flag_value = self._verify_flag(flag_name, flag_value)
super()._set(flag_name, flag_value, override)
def _set_js_flag(self, raw_js_flags: str, override: bool) -> None:
new_js_flags = JSFlags(self._js_flags)
for js_flag_name, js_flag_value in JSFlags.parse(raw_js_flags).items():
new_js_flags.set(js_flag_name, js_flag_value, override=override)
self._js_flags.update(new_js_flags)
def _verify_flag(self, name: str, value: Optional[str]) -> Optional[str]:
if candidate := self._find_misspelled_flag(name):
logging.error(
"Potentially misspelled flag: '%s'. "
"Did you mean to use %s ?", name, candidate)
if candidate := self._find_js_flag(name):
js_flags = JSFlags()
js_flags.set(candidate, value)
logging.error(
"Got potential V8 flag that should be used as "
"--js-flags=%s", js_flags)
if name == "--user-data-dir":
if not value or not value.strip():
raise ValueError("--user-data-dir cannot be the empty string.")
# TODO: support remote platforms
expanded_dir = str(pth.LocalPath(value).expanduser())
if expanded_dir != value:
logging.warning(
"Chrome Flags: auto-expanding --user-data-dir from '%s' to '%s'",
value, expanded_dir)
return expanded_dir
return value
def _find_misspelled_flag(self, name: str) -> Optional[str]:
if name in ("--enable-feature", "--enabled-feature", "--enabled-features"):
return "--enable-features"
if name in ("--disable-feature", "--disabled-feature",
"--disabled-features"):
return "--disable-features"
if name in ("--enable-blink-feature", "--enabled-blink-feature",
"--enabled-blink-features"):
return "--enable-blink-features"
if name in ("--disable-blink-feature", "--disabled-blink-feature",
"--disabled-blink-features"):
return "--disable-blink-features"
return None
def _find_js_flag(self, name: str) -> Optional[str]:
normalized_name = name
if name.startswith("--no-"):
normalized_name = f"--{name[5:]}"
elif name.startswith("--no"):
normalized_name = f"--{name[4:]}"
if normalized_name in KNOWN_JS_FLAGS:
return name
return None
@property
def features(self) -> ChromeFeatures:
return self._features
@property
def blink_features(self) -> ChromeBlinkFeatures:
return self._blink_features
@property
def js_flags(self) -> JSFlags:
return self._js_flags
def merge(self, other: Flags.InitialDataType) -> None:
if not isinstance(other, ChromeFlags):
other = ChromeFlags(other)
self.features.merge(other.features)
self.blink_features.merge(other.blink_features)
self.js_flags.merge(other.js_flags)
for name, value in other.base_items():
self.set(name, value)
def base_items(self) -> Iterable[Tuple[str, Optional[str]]]:
yield from super().items()
def items(self) -> Iterable[Tuple[str, Optional[str]]]:
yield from self.base_items()
if self._js_flags:
yield (self._JS_FLAG, str(self.js_flags))
yield from self.features.items()
yield from self.blink_features.items()
def __bool__(self) -> bool:
return bool(self.data) or bool(self._js_flags) or bool(
self._features) or bool(self._blink_features)
class ChromeBaseFeatures(Freezable, abc.ABC):
ENABLE_FLAG: str = ""
DISABLE_FLAG: str = ""
def __init__(self) -> None:
super().__init__()
self._enabled: Dict[str, Optional[str]] = {}
self._disabled: OrderedSet[str] = OrderedSet()
@property
def is_empty(self) -> bool:
return len(self._enabled) == 0 and len(self._disabled) == 0
@property
def enabled(self) -> Dict[str, Optional[str]]:
return dict(self._enabled)
@property
def disabled(self) -> OrderedSet[str]:
return OrderedSet(self._disabled)
def _parse_feature(self, feature: str) -> Tuple[str, Optional[str]]:
if not feature:
raise ValueError("Cannot parse empty feature")
if "," in feature:
raise ValueError(f"{repr(feature)} contains multiple features. "
"Please split them first.")
return self._parse_feature_parts(feature)
@abc.abstractmethod
def _parse_feature_parts(self, feature: str) -> Tuple[str, Optional[str]]:
pass
def enable(self, feature: str) -> None:
name, value = self._parse_feature(feature)
self._enable(name, value)
def _enable(self, name: str, value: Optional[str]) -> None:
self.assert_not_frozen()
if name in self._disabled:
raise ValueError(
f"Cannot enable previously disabled feature={repr(name)}")
if name in self._enabled:
prev_value = self._enabled[name]
if value != prev_value:
raise ValueError("Cannot set conflicting values "
f"({repr(prev_value)}, vs. {repr(value)}) "
f"for the same feature={repr(name)}")
else:
self._enabled[name] = value
def disable(self, feature: str) -> None:
self.assert_not_frozen()
name, _ = self._parse_feature(feature)
if name in self._enabled:
raise ValueError(
f"Cannot disable previously enabled feature={repr(name)}")
self._disabled.add(name)
def update(self, other: ChromeBaseFeatures) -> None:
if not isinstance(other, type(self)):
raise TypeError(f"Cannot merge {type(self)} with {type(other)}")
for disabled in other.disabled:
self.disable(disabled)
for name, value in other.enabled.items():
self._enable(name, value)
def merge(self, other: ChromeBaseFeatures) -> None:
self.update(other)
def items(self) -> Iterable[Tuple[str, str]]:
if self._enabled:
yield (self.ENABLE_FLAG, self.enabled_str())
if self._disabled:
yield (self.DISABLE_FLAG, self.disabled_str())
def enabled_str(self) -> str:
return ",".join(
k if v is None else f"{k}{v}" for k, v in self._enabled.items())
def disabled_str(self) -> str:
return ",".join(self._disabled)
def __iter__(self) -> Iterator[str]:
for flag_name, features_str in self.items():
yield f"{flag_name}={features_str}"
def __bool__(self):
return bool(self._enabled) or bool(self._disabled)
def __str__(self) -> str:
return " ".join(self)
class ChromeFeatures(ChromeBaseFeatures):
"""
Chrome Features set, throws if features are enabled and disabled at the same
time.
Examples:
--disable-features="MyFeature1"
--enable-features="MyFeature1,MyFeature2"
--enable-features="MyFeature1:k1/v1/k2/v2,MyFeature2"
--enable-features="MyFeature3<Trial2:k1/v1/k2/v2"
"""
ENABLE_FLAG: str = "--enable-features"
DISABLE_FLAG: str = "--disable-features"
def _parse_feature_parts(self, feature: str) -> Tuple[str, Optional[str]]:
parts = feature.split("<")
if len(parts) == 2:
return (parts[0], "<" + parts[1])
if len(parts) != 1:
raise ValueError(f"Invalid number of feature parts: {repr(parts)}")
parts = feature.split(":")
if len(parts) == 2:
return (parts[0], ":" + parts[1])
if len(parts) != 1:
raise ValueError(f"Invalid number of feature parts: {repr(parts)}")
return (feature, None)
class ChromeBlinkFeatures(ChromeBaseFeatures):
"""
Chrome Features set, throws if features are enabled and disabled at the same
time.
Examples:
--disable-blink-features="MyFeature1"
--enable-blink-features="MyFeature1,MyFeature2"
"""
ENABLE_FLAG: str = "--enable-blink-features"
DISABLE_FLAG: str = "--disable-blink-features"
def _parse_feature_parts(self, feature: str) -> Tuple[str, Optional[str]]:
if "<" in feature or ":" in feature:
raise ValueError("blink features do not have params, "
f"but found param separator in {repr(feature)}")
return (feature, None)