1# Copyright 2024 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from __future__ import annotations 6 7import abc 8import logging 9from typing import Dict, Iterable, Iterator, Optional, Tuple 10 11from ordered_set import OrderedSet 12 13from crossbench import path as pth 14from crossbench.flags.base import Flags, FlagsData, Freezable 15from crossbench.flags.js_flags import JSFlags 16from crossbench.flags.known_js_flags import KNOWN_JS_FLAGS 17 18 19class ChromeFlags(Flags): 20 """Specialized Flags for Chrome/Chromium-based browser. 21 22 This has special treatment for --js-flags and the feature flags: 23 --enable-features/--disable-features 24 --enable-blink-features/--disable-blink-features 25 """ 26 _JS_FLAG = "--js-flags" 27 28 def __init__(self, initial_data: FlagsData = None) -> None: 29 self._features = ChromeFeatures() 30 self._blink_features = ChromeBlinkFeatures() 31 self._js_flags = JSFlags() 32 super().__init__(initial_data) 33 34 def freeze(self) -> ChromeFlags: 35 super().freeze() 36 self._js_flags.freeze() 37 self._features.freeze() 38 self._blink_features.freeze() 39 return self 40 41 def __getitem__(self, key): 42 if key == self._JS_FLAG and self._js_flags: 43 return self._js_flags 44 if key == ChromeFeatures.ENABLE_FLAG and self._features.enabled: 45 return self._features.enabled_str() 46 if key == ChromeFeatures.DISABLE_FLAG and self._features.disabled: 47 return self._features.disabled_str() 48 if key == ChromeBlinkFeatures.ENABLE_FLAG and self._blink_features.enabled: 49 return self._blink_features.enabled_str() 50 if (key == ChromeBlinkFeatures.DISABLE_FLAG and 51 self._blink_features.disabled): 52 return self._blink_features.disabled_str() 53 return super().__getitem__(key) 54 55 def _set(self, 56 flag_name: str, 57 flag_value: Optional[str] = None, 58 override: bool = False) -> None: 59 self.assert_not_frozen() 60 # pylint: disable=signature-differs 61 if flag_name == ChromeFeatures.ENABLE_FLAG: 62 if flag_value is None: 63 raise ValueError(f"{ChromeFeatures.ENABLE_FLAG} cannot be None") 64 for feature in flag_value.split(","): 65 self._features.enable(feature) 66 elif flag_name == ChromeFeatures.DISABLE_FLAG: 67 if flag_value is None: 68 raise ValueError(f"{ChromeFeatures.DISABLE_FLAG} cannot be None") 69 for feature in flag_value.split(","): 70 self._features.disable(feature) 71 elif flag_name == ChromeBlinkFeatures.ENABLE_FLAG: 72 if flag_value is None: 73 raise ValueError(f"{ChromeBlinkFeatures.ENABLE_FLAG} cannot be None") 74 for feature in flag_value.split(","): 75 self._blink_features.enable(feature) 76 elif flag_name == ChromeBlinkFeatures.DISABLE_FLAG: 77 if flag_value is None: 78 raise ValueError(f"{ChromeBlinkFeatures.DISABLE_FLAG} cannot be None") 79 for feature in flag_value.split(","): 80 self._blink_features.disable(feature) 81 elif flag_name == self._JS_FLAG: 82 if flag_value is None: 83 raise ValueError(f"{self._JS_FLAG} cannot be None") 84 self._set_js_flag(flag_value, override) 85 else: 86 flag_value = self._verify_flag(flag_name, flag_value) 87 super()._set(flag_name, flag_value, override) 88 89 def _set_js_flag(self, raw_js_flags: str, override: bool) -> None: 90 new_js_flags = JSFlags(self._js_flags) 91 for js_flag_name, js_flag_value in JSFlags.parse(raw_js_flags).items(): 92 new_js_flags.set(js_flag_name, js_flag_value, override=override) 93 self._js_flags.update(new_js_flags) 94 95 def _verify_flag(self, name: str, value: Optional[str]) -> Optional[str]: 96 if candidate := self._find_misspelled_flag(name): 97 logging.error( 98 "Potentially misspelled flag: '%s'. " 99 "Did you mean to use %s ?", name, candidate) 100 if candidate := self._find_js_flag(name): 101 js_flags = JSFlags() 102 js_flags.set(candidate, value) 103 logging.error( 104 "Got potential V8 flag that should be used as " 105 "--js-flags=%s", js_flags) 106 if name == "--user-data-dir": 107 if not value or not value.strip(): 108 raise ValueError("--user-data-dir cannot be the empty string.") 109 # TODO: support remote platforms 110 expanded_dir = str(pth.LocalPath(value).expanduser()) 111 if expanded_dir != value: 112 logging.warning( 113 "Chrome Flags: auto-expanding --user-data-dir from '%s' to '%s'", 114 value, expanded_dir) 115 return expanded_dir 116 return value 117 118 def _find_misspelled_flag(self, name: str) -> Optional[str]: 119 if name in ("--enable-feature", "--enabled-feature", "--enabled-features"): 120 return "--enable-features" 121 if name in ("--disable-feature", "--disabled-feature", 122 "--disabled-features"): 123 return "--disable-features" 124 if name in ("--enable-blink-feature", "--enabled-blink-feature", 125 "--enabled-blink-features"): 126 return "--enable-blink-features" 127 if name in ("--disable-blink-feature", "--disabled-blink-feature", 128 "--disabled-blink-features"): 129 return "--disable-blink-features" 130 return None 131 132 def _find_js_flag(self, name: str) -> Optional[str]: 133 normalized_name = name 134 if name.startswith("--no-"): 135 normalized_name = f"--{name[5:]}" 136 elif name.startswith("--no"): 137 normalized_name = f"--{name[4:]}" 138 if normalized_name in KNOWN_JS_FLAGS: 139 return name 140 return None 141 142 @property 143 def features(self) -> ChromeFeatures: 144 return self._features 145 146 @property 147 def blink_features(self) -> ChromeBlinkFeatures: 148 return self._blink_features 149 150 @property 151 def js_flags(self) -> JSFlags: 152 return self._js_flags 153 154 def merge(self, other: FlagsData) -> None: 155 if not isinstance(other, ChromeFlags): 156 other = ChromeFlags(other) 157 self.features.merge(other.features) 158 self.blink_features.merge(other.blink_features) 159 self.js_flags.merge(other.js_flags) 160 for name, value in other.base_items(): 161 self.set(name, value) 162 163 def base_items(self) -> Iterable[Tuple[str, Optional[str]]]: 164 yield from super().items() 165 166 def items(self) -> Iterable[Tuple[str, Optional[str]]]: 167 yield from self.base_items() 168 if self._js_flags: 169 yield (self._JS_FLAG, str(self.js_flags)) 170 yield from self.features.items() 171 yield from self.blink_features.items() 172 173 def __bool__(self) -> bool: 174 return bool(self.data) or bool(self._js_flags) or bool( 175 self._features) or bool(self._blink_features) 176 177 178class ChromeBaseFeatures(Freezable, abc.ABC): 179 ENABLE_FLAG: str = "" 180 DISABLE_FLAG: str = "" 181 182 def __init__(self) -> None: 183 super().__init__() 184 self._enabled: Dict[str, Optional[str]] = {} 185 self._disabled: OrderedSet[str] = OrderedSet() 186 187 @property 188 def is_empty(self) -> bool: 189 return len(self._enabled) == 0 and len(self._disabled) == 0 190 191 @property 192 def enabled(self) -> Dict[str, Optional[str]]: 193 return dict(self._enabled) 194 195 @property 196 def disabled(self) -> OrderedSet[str]: 197 return OrderedSet(self._disabled) 198 199 def _parse_feature(self, feature: str) -> Tuple[str, Optional[str]]: 200 if not feature: 201 raise ValueError("Cannot parse empty feature") 202 if "," in feature: 203 raise ValueError(f"{repr(feature)} contains multiple features. " 204 "Please split them first.") 205 return self._parse_feature_parts(feature) 206 207 @abc.abstractmethod 208 def _parse_feature_parts(self, feature: str) -> Tuple[str, Optional[str]]: 209 pass 210 211 def enable(self, feature: str) -> None: 212 name, value = self._parse_feature(feature) 213 self._enable(name, value) 214 215 def _enable(self, name: str, value: Optional[str]) -> None: 216 self.assert_not_frozen() 217 if name in self._disabled: 218 raise ValueError( 219 f"Cannot enable previously disabled feature={repr(name)}") 220 if name in self._enabled: 221 prev_value = self._enabled[name] 222 if value != prev_value: 223 raise ValueError("Cannot set conflicting values " 224 f"({repr(prev_value)}, vs. {repr(value)}) " 225 f"for the same feature={repr(name)}") 226 else: 227 self._enabled[name] = value 228 229 def disable(self, feature: str) -> None: 230 self.assert_not_frozen() 231 name, _ = self._parse_feature(feature) 232 if name in self._enabled: 233 raise ValueError( 234 f"Cannot disable previously enabled feature={repr(name)}") 235 self._disabled.add(name) 236 237 def update(self, other: ChromeBaseFeatures) -> None: 238 if not isinstance(other, type(self)): 239 raise TypeError(f"Cannot merge {type(self)} with {type(other)}") 240 for disabled in other.disabled: 241 self.disable(disabled) 242 for name, value in other.enabled.items(): 243 self._enable(name, value) 244 245 def merge(self, other: ChromeBaseFeatures) -> None: 246 self.update(other) 247 248 def items(self) -> Iterable[Tuple[str, str]]: 249 if self._enabled: 250 yield (self.ENABLE_FLAG, self.enabled_str()) 251 if self._disabled: 252 yield (self.DISABLE_FLAG, self.disabled_str()) 253 254 def enabled_str(self) -> str: 255 return ",".join( 256 k if v is None else f"{k}{v}" for k, v in self._enabled.items()) 257 258 def disabled_str(self) -> str: 259 return ",".join(self._disabled) 260 261 def __iter__(self) -> Iterator[str]: 262 for flag_name, features_str in self.items(): 263 yield f"{flag_name}={features_str}" 264 265 def __bool__(self): 266 return bool(self._enabled) or bool(self._disabled) 267 268 def __str__(self) -> str: 269 return " ".join(self) 270 271 272class ChromeFeatures(ChromeBaseFeatures): 273 """ 274 Chrome Features set, throws if features are enabled and disabled at the same 275 time. 276 Examples: 277 --disable-features="MyFeature1" 278 --enable-features="MyFeature1,MyFeature2" 279 --enable-features="MyFeature1:k1/v1/k2/v2,MyFeature2" 280 --enable-features="MyFeature3<Trial2:k1/v1/k2/v2" 281 """ 282 283 ENABLE_FLAG: str = "--enable-features" 284 DISABLE_FLAG: str = "--disable-features" 285 286 def _parse_feature_parts(self, feature: str) -> Tuple[str, Optional[str]]: 287 parts = feature.split("<") 288 if len(parts) == 2: 289 return (parts[0], "<" + parts[1]) 290 if len(parts) != 1: 291 raise ValueError(f"Invalid number of feature parts: {repr(parts)}") 292 parts = feature.split(":") 293 if len(parts) == 2: 294 return (parts[0], ":" + parts[1]) 295 if len(parts) != 1: 296 raise ValueError(f"Invalid number of feature parts: {repr(parts)}") 297 return (feature, None) 298 299 300class ChromeBlinkFeatures(ChromeBaseFeatures): 301 """ 302 Chrome Features set, throws if features are enabled and disabled at the same 303 time. 304 Examples: 305 --disable-blink-features="MyFeature1" 306 --enable-blink-features="MyFeature1,MyFeature2" 307 """ 308 309 ENABLE_FLAG: str = "--enable-blink-features" 310 DISABLE_FLAG: str = "--disable-blink-features" 311 312 def _parse_feature_parts(self, feature: str) -> Tuple[str, Optional[str]]: 313 if "<" in feature or ":" in feature: 314 raise ValueError("blink features do not have params, " 315 f"but found param separator in {repr(feature)}") 316 return (feature, None) 317