# Copyright 2023 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 re from typing import Dict, Final, Optional, Tuple from crossbench.browsers.version import (BrowserVersion, BrowserVersionChannel, PartialBrowserVersionError) class ChromiumVersion(BrowserVersion): _PARTS_LEN: Final[int] = 4 _VERSION_RE = re.compile( r"(?P[^\d]*)" r"(?P\d{2,3}(\.(\d{1,4}|X)){0,3})? ?" r"(?P.*)", re.I) _VALID_SUFFIX_MATCH = re.compile(r"[^.\d]+", re.I) _CHANNEL_LOOKUP: Dict[str, BrowserVersionChannel] = { "any": BrowserVersionChannel.ANY, "extended": BrowserVersionChannel.LTS, "stable": BrowserVersionChannel.STABLE, "beta": BrowserVersionChannel.BETA, "dev": BrowserVersionChannel.ALPHA, "canary": BrowserVersionChannel.PRE_ALPHA, } _CHANNEL_NAME_LOOKUP: Dict[BrowserVersionChannel, str] = { channel: name for name, channel in _CHANNEL_LOOKUP.items() } _CHANNEL_RE = re.compile("|".join(_CHANNEL_LOOKUP.keys()), re.I) @classmethod def _parse( cls, full_version: str) -> Tuple[Tuple[int, ...], BrowserVersionChannel, str]: matches = cls._VERSION_RE.fullmatch(full_version.strip(),) if not matches: raise cls.parse_error("Could not extract version number.", full_version) channel_str = cls._parse_channel(full_version) version_str = matches["version"] if not version_str and not channel_str: raise cls.parse_error("Got empty version match.", full_version) prefix = matches["prefix"] if not cls._validate_prefix(prefix): raise cls.parse_error(f"Wrong prefix {repr(prefix)}", full_version) suffix = matches["suffix"] if not cls._validate_suffix(suffix): raise cls.parse_error(f"Wrong suffix {repr(suffix)}", full_version) if not version_str: return cls._channel_version(channel_str, full_version) return cls._numbered_version(version_str, full_version) @classmethod def _parse_channel(cls, full_version: str) -> str: if matches := cls._CHANNEL_RE.search(full_version): return matches[0] return "" @classmethod def _channel_version( cls, channel_str: str, full_version: str) -> Tuple[Tuple[int, ...], BrowserVersionChannel, str]: channel = cls._parse_exact_channel(channel_str, full_version) version_str = "" return tuple(), channel, version_str @classmethod def _numbered_version( cls, version_str: str, full_version: str) -> Tuple[Tuple[int, ...], BrowserVersionChannel, str]: channel: BrowserVersionChannel = cls._parse_default_channel(full_version) parts_str = version_str.split(".") if len(parts_str) > cls._PARTS_LEN: raise cls.parse_error(f"Too many version parts {parts_str}", full_version) if len(parts_str) != 1 and len(parts_str) != cls._PARTS_LEN: raise cls.parse_error( f"Incomplete chrome version number, need {cls._PARTS_LEN} parts", full_version) # Remove .X from the input version. while parts_str[-1] == "X": parts_str.pop() try: parts = tuple(map(int, parts_str)) except ValueError as e: raise cls.parse_error( f"Could not parse version parts {repr(version_str)}", full_version) from e if not parts_str: raise cls.parse_error("Need at least one version number part.", full_version) if len(parts_str) == 1: version_str = f"M{parts_str[0]}" else: padding = ("X",) * (cls._PARTS_LEN - len(parts)) version_str = ".".join(map(str, parts + padding)) return parts, channel, version_str @classmethod def _validate_prefix(cls, prefix: Optional[str]) -> bool: if not prefix: return True prefix = prefix.lower() if prefix.strip() == "m": return True return "chromium " in prefix or "chromium-" in prefix @classmethod def _parse_exact_channel(cls, channel_str: str, full_version: str) -> BrowserVersionChannel: if channel := cls._CHANNEL_LOOKUP.get(channel_str.lower()): return channel raise cls.parse_error(f"Unknown channel {repr(channel_str)}", full_version) @classmethod def _parse_default_channel(cls, full_version: str) -> BrowserVersionChannel: version_lower: str = full_version.lower() for channel_name, channel_obj in cls._CHANNEL_LOOKUP.items(): if channel_name in version_lower: return channel_obj return BrowserVersionChannel.STABLE @classmethod def _validate_suffix(cls, suffix: Optional[str]) -> bool: if not suffix: return True return bool(cls._VALID_SUFFIX_MATCH.fullmatch(suffix)) @property def key(self) -> Tuple[Tuple[int, ...], BrowserVersionChannel]: return (self.comparable_parts(self._PARTS_LEN), self._channel) @property def has_complete_parts(self) -> bool: return len(self.parts) == 4 @property def build(self) -> int: if len(self._parts) <= 2: raise PartialBrowserVersionError() return self._parts[2] @property def patch(self) -> int: if len(self._parts) <= 3: raise PartialBrowserVersionError() return self._parts[3] @property def is_dev(self) -> bool: return self.is_alpha @property def is_canary(self) -> bool: return self.is_pre_alpha def _channel_name(self, channel: BrowserVersionChannel) -> str: if name := self._CHANNEL_NAME_LOOKUP[channel]: return name raise ValueError(f"Unsupported channel: {channel}") class ChromeDriverVersion(ChromiumVersion): _EMPTY_COMMIT_HASH: Final = "0000000000000000000000000000000000000000" @classmethod def _validate_prefix(cls, prefix: Optional[str]) -> bool: if not prefix: return False return prefix.lower() in ("chromedriver ", "chromedriver-") @classmethod def _parse_default_channel(cls, full_version: str) -> BrowserVersionChannel: if cls._EMPTY_COMMIT_HASH in full_version: return BrowserVersionChannel.PRE_ALPHA return BrowserVersionChannel.STABLE @classmethod def _validate_suffix(cls, suffix: Optional[str]) -> bool: # TODO: extract commit hash / branch info from newer versions return True