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