• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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