• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 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 re
8from typing import Dict, Final, Optional, Tuple
9
10from crossbench.browsers.version import (BrowserVersion, BrowserVersionChannel,
11                                         PartialBrowserVersionError)
12
13
14class ChromiumVersion(BrowserVersion):
15  _PARTS_LEN: Final[int] = 4
16  _VERSION_RE = re.compile(
17      r"(?P<prefix>[^\d]*)"
18      r"(?P<version>\d{2,3}(\.(\d{1,4}|X)){0,3})? ?"
19      r"(?P<suffix>.*)", re.I)
20  _VALID_SUFFIX_MATCH = re.compile(r"[^.\d]+", re.I)
21  _CHANNEL_LOOKUP: Dict[str, BrowserVersionChannel] = {
22      "any": BrowserVersionChannel.ANY,
23      "extended": BrowserVersionChannel.LTS,
24      "stable": BrowserVersionChannel.STABLE,
25      "beta": BrowserVersionChannel.BETA,
26      "dev": BrowserVersionChannel.ALPHA,
27      "canary": BrowserVersionChannel.PRE_ALPHA,
28  }
29  _CHANNEL_NAME_LOOKUP: Dict[BrowserVersionChannel, str] = {
30      channel: name for name, channel in _CHANNEL_LOOKUP.items()
31  }
32  _CHANNEL_RE = re.compile("|".join(_CHANNEL_LOOKUP.keys()), re.I)
33
34  @classmethod
35  def _parse(
36      cls,
37      full_version: str) -> Tuple[Tuple[int, ...], BrowserVersionChannel, str]:
38    matches = cls._VERSION_RE.fullmatch(full_version.strip(),)
39    if not matches:
40      raise cls.parse_error("Could not extract version number.", full_version)
41    channel_str = cls._parse_channel(full_version)
42    version_str = matches["version"]
43    if not version_str and not channel_str:
44      raise cls.parse_error("Got empty version match.", full_version)
45    prefix = matches["prefix"]
46    if not cls._validate_prefix(prefix):
47      raise cls.parse_error(f"Wrong prefix {repr(prefix)}", full_version)
48    suffix = matches["suffix"]
49    if not cls._validate_suffix(suffix):
50      raise cls.parse_error(f"Wrong suffix {repr(suffix)}", full_version)
51
52    if not version_str:
53      return cls._channel_version(channel_str, full_version)
54    return cls._numbered_version(version_str, full_version)
55
56  @classmethod
57  def _parse_channel(cls, full_version: str) -> str:
58    if matches := cls._CHANNEL_RE.search(full_version):
59      return matches[0]
60    return ""
61
62  @classmethod
63  def _channel_version(
64      cls, channel_str: str,
65      full_version: str) -> Tuple[Tuple[int, ...], BrowserVersionChannel, str]:
66    channel = cls._parse_exact_channel(channel_str, full_version)
67    version_str = ""
68    return tuple(), channel, version_str
69
70  @classmethod
71  def _numbered_version(
72      cls, version_str: str,
73      full_version: str) -> Tuple[Tuple[int, ...], BrowserVersionChannel, str]:
74    channel: BrowserVersionChannel = cls._parse_default_channel(full_version)
75
76    parts_str = version_str.split(".")
77    if len(parts_str) > cls._PARTS_LEN:
78      raise cls.parse_error(f"Too many version parts {parts_str}", full_version)
79    if len(parts_str) != 1 and len(parts_str) != cls._PARTS_LEN:
80      raise cls.parse_error(
81          f"Incomplete chrome version number, need {cls._PARTS_LEN} parts",
82          full_version)
83    # Remove .X from the input version.
84    while parts_str[-1] == "X":
85      parts_str.pop()
86    try:
87      parts = tuple(map(int, parts_str))
88    except ValueError as e:
89      raise cls.parse_error(
90          f"Could not parse version parts {repr(version_str)}",
91          full_version) from e
92    if not parts_str:
93      raise cls.parse_error("Need at least one version number part.",
94                            full_version)
95    if len(parts_str) == 1:
96      version_str = f"M{parts_str[0]}"
97    else:
98      padding = ("X",) * (cls._PARTS_LEN - len(parts))
99      version_str = ".".join(map(str, parts + padding))
100    return parts, channel, version_str
101
102  @classmethod
103  def _validate_prefix(cls, prefix: Optional[str]) -> bool:
104    if not prefix:
105      return True
106    prefix = prefix.lower()
107    if prefix.strip() == "m":
108      return True
109    return "chromium " in prefix or "chromium-" in prefix
110
111  @classmethod
112  def _parse_exact_channel(cls, channel_str: str,
113                           full_version: str) -> BrowserVersionChannel:
114    if channel := cls._CHANNEL_LOOKUP.get(channel_str.lower()):
115      return channel
116    raise cls.parse_error(f"Unknown channel {repr(channel_str)}", full_version)
117
118  @classmethod
119  def _parse_default_channel(cls, full_version: str) -> BrowserVersionChannel:
120    version_lower: str = full_version.lower()
121    for channel_name, channel_obj in cls._CHANNEL_LOOKUP.items():
122      if channel_name in version_lower:
123        return channel_obj
124    return BrowserVersionChannel.STABLE
125
126  @classmethod
127  def _validate_suffix(cls, suffix: Optional[str]) -> bool:
128    if not suffix:
129      return True
130    return bool(cls._VALID_SUFFIX_MATCH.fullmatch(suffix))
131
132  @property
133  def key(self) -> Tuple[Tuple[int, ...], BrowserVersionChannel]:
134    return (self.comparable_parts(self._PARTS_LEN), self._channel)
135
136  @property
137  def has_complete_parts(self) -> bool:
138    return len(self.parts) == 4
139
140  @property
141  def build(self) -> int:
142    if len(self._parts) <= 2:
143      raise PartialBrowserVersionError()
144    return self._parts[2]
145
146  @property
147  def patch(self) -> int:
148    if len(self._parts) <= 3:
149      raise PartialBrowserVersionError()
150    return self._parts[3]
151
152  @property
153  def is_dev(self) -> bool:
154    return self.is_alpha
155
156  @property
157  def is_canary(self) -> bool:
158    return self.is_pre_alpha
159
160  def _channel_name(self, channel: BrowserVersionChannel) -> str:
161    if name := self._CHANNEL_NAME_LOOKUP[channel]:
162      return name
163    raise ValueError(f"Unsupported channel: {channel}")
164
165
166class ChromeDriverVersion(ChromiumVersion):
167  _EMPTY_COMMIT_HASH: Final = "0000000000000000000000000000000000000000"
168
169  @classmethod
170  def _validate_prefix(cls, prefix: Optional[str]) -> bool:
171    if not prefix:
172      return False
173    return prefix.lower() in ("chromedriver ", "chromedriver-")
174
175  @classmethod
176  def _parse_default_channel(cls, full_version: str) -> BrowserVersionChannel:
177    if cls._EMPTY_COMMIT_HASH in full_version:
178      return BrowserVersionChannel.PRE_ALPHA
179    return BrowserVersionChannel.STABLE
180
181  @classmethod
182  def _validate_suffix(cls, suffix: Optional[str]) -> bool:
183    # TODO: extract commit hash / branch info from newer versions
184    return True
185