diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/python/mozilla-version/mozilla_version | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/python/mozilla-version/mozilla_version')
12 files changed, 1769 insertions, 0 deletions
diff --git a/third_party/python/mozilla-version/mozilla_version/__init__.py b/third_party/python/mozilla-version/mozilla_version/__init__.py new file mode 100644 index 0000000000..ba46ee264d --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/__init__.py @@ -0,0 +1 @@ +"""Defines characteristics of Mozilla's version numbers.""" diff --git a/third_party/python/mozilla-version/mozilla_version/balrog.py b/third_party/python/mozilla-version/mozilla_version/balrog.py new file mode 100644 index 0000000000..0860d5698a --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/balrog.py @@ -0,0 +1,142 @@ +"""Defines characteristics of a Balrog release name. + +Balrog is the server that delivers Firefox and Thunderbird updates. Release names follow +the pattern "{product}-{version}-build{build_number}" + +Examples: + .. code-block:: python + + from mozilla_version.balrog import BalrogReleaseName + + balrog_release = BalrogReleaseName.parse('firefox-60.0.1-build1') + + balrog_release.product # firefox + balrog_release.version.major_number # 60 + str(balrog_release) # 'firefox-60.0.1-build1' + + previous_release = BalrogReleaseName.parse('firefox-60.0-build2') + previous_release < balrog_release # True + + invalid = BalrogReleaseName.parse('60.0.1') # raises PatternNotMatchedError + invalid = BalrogReleaseName.parse('firefox-60.0.1') # raises PatternNotMatchedError + + # Releases can be built thanks to version classes like FirefoxVersion + BalrogReleaseName('firefox', FirefoxVersion(60, 0, 1, 1)) # 'firefox-60.0.1-build1' + +""" + +import attr +import re + +from mozilla_version.errors import PatternNotMatchedError +from mozilla_version.parser import get_value_matched_by_regex +from mozilla_version.gecko import ( + GeckoVersion, FirefoxVersion, DeveditionVersion, FennecVersion, ThunderbirdVersion +) + + +_VALID_ENOUGH_BALROG_RELEASE_PATTERN = re.compile( + r"^(?P<product>[a-z]+)-(?P<version>.+)$", re.IGNORECASE +) + + +_SUPPORTED_PRODUCTS = { + 'firefox': FirefoxVersion, + 'devedition': DeveditionVersion, + 'fennec': FennecVersion, + 'thunderbird': ThunderbirdVersion, +} + + +def _supported_product(string): + product = string.lower() + if product not in _SUPPORTED_PRODUCTS: + raise PatternNotMatchedError(string, pattern='unknown product') + return product + + +def _products_must_be_identical(method): + def checker(this, other): + if this.product != other.product: + raise ValueError('Cannot compare "{}" and "{}"'.format(this.product, other.product)) + return method(this, other) + return checker + + +@attr.s(frozen=True, cmp=False, hash=True) +class BalrogReleaseName(object): + """Class that validates and handles Balrog release names. + + Raises: + PatternNotMatchedError: if a parsed string doesn't match the pattern of a valid release + MissingFieldError: if a mandatory field is missing in the string. Mandatory fields are + `product`, `major_number`, `minor_number`, and `build_number` + ValueError: if an integer can't be cast or is not (strictly) positive + TooManyTypesError: if the string matches more than 1 `VersionType` + NoVersionTypeError: if the string matches none. + + """ + + product = attr.ib(type=str, converter=_supported_product) + version = attr.ib(type=GeckoVersion) + + def __attrs_post_init__(self): + """Ensure attributes are sane all together.""" + if self.version.build_number is None: + raise PatternNotMatchedError(self, pattern='build_number must exist') + + @classmethod + def parse(cls, release_string): + """Construct an object representing a valid Firefox version number.""" + regex_matches = _VALID_ENOUGH_BALROG_RELEASE_PATTERN.match(release_string) + if regex_matches is None: + raise PatternNotMatchedError(release_string, _VALID_ENOUGH_BALROG_RELEASE_PATTERN) + + product = get_value_matched_by_regex('product', regex_matches, release_string) + try: + VersionClass = _SUPPORTED_PRODUCTS[product.lower()] + except KeyError: + raise PatternNotMatchedError(release_string, pattern='unknown product') + + version_string = get_value_matched_by_regex('version', regex_matches, release_string) + version = VersionClass.parse(version_string) + + return cls(product, version) + + def __str__(self): + """Implement string representation. + + Computes a new string based on the given attributes. + """ + version_string = str(self.version).replace('build', '-build') + return '{}-{}'.format(self.product, version_string) + + @_products_must_be_identical + def __eq__(self, other): + """Implement `==` operator.""" + return self.version == other.version + + @_products_must_be_identical + def __ne__(self, other): + """Implement `!=` operator.""" + return self.version != other.version + + @_products_must_be_identical + def __lt__(self, other): + """Implement `<` operator.""" + return self.version < other.version + + @_products_must_be_identical + def __le__(self, other): + """Implement `<=` operator.""" + return self.version <= other.version + + @_products_must_be_identical + def __gt__(self, other): + """Implement `>` operator.""" + return self.version > other.version + + @_products_must_be_identical + def __ge__(self, other): + """Implement `>=` operator.""" + return self.version >= other.version diff --git a/third_party/python/mozilla-version/mozilla_version/errors.py b/third_party/python/mozilla-version/mozilla_version/errors.py new file mode 100644 index 0000000000..84cd2169f5 --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/errors.py @@ -0,0 +1,64 @@ +"""Defines all errors reported by mozilla-version.""" + + +class PatternNotMatchedError(ValueError): + """Error when a string doesn't match an expected pattern. + + Args: + string (str): The string it was unable to match. + pattern (str): The pattern used it tried to match. + """ + + def __init__(self, string, pattern): + """Constructor.""" + super(PatternNotMatchedError, self).__init__( + '"{}" does not match the pattern: {}'.format(string, pattern) + ) + + +class NoVersionTypeError(ValueError): + """Error when `version_string` matched the pattern, but was unable to find its type. + + Args: + version_string (str): The string it was unable to guess the type. + """ + + def __init__(self, version_string): + """Constructor.""" + super(NoVersionTypeError, self).__init__( + 'Version "{}" matched the pattern of a valid version, but it is unable to find what type it is. \ +This is likely a bug in mozilla-version'.format(version_string) + ) + + +class MissingFieldError(ValueError): + """Error when `version_string` lacks an expected field. + + Args: + version_string (str): The string it was unable to extract a given field. + field_name (str): The name of the missing field. + """ + + def __init__(self, version_string, field_name): + """Constructor.""" + super(MissingFieldError, self).__init__( + 'Release "{}" does not contain a valid {}'.format(version_string, field_name) + ) + + +class TooManyTypesError(ValueError): + """Error when `version_string` has too many types.""" + + def __init__(self, version_string, first_matched_type, second_matched_type): + """Constructor. + + Args: + version_string (str): The string that gave too many types. + first_matched_type (str): The name of the first detected type. + second_matched_type (str): The name of the second detected type + """ + super(TooManyTypesError, self).__init__( + 'Release "{}" cannot match types "{}" and "{}"'.format( + version_string, first_matched_type, second_matched_type + ) + ) diff --git a/third_party/python/mozilla-version/mozilla_version/gecko.py b/third_party/python/mozilla-version/mozilla_version/gecko.py new file mode 100644 index 0000000000..dd14dda6b0 --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/gecko.py @@ -0,0 +1,435 @@ +"""Defines characteristics of a Gecko version number, including Firefox. + +Examples: + .. code-block:: python + + from mozilla_version.gecko import FirefoxVersion + + version = FirefoxVersion.parse('60.0.1') + + version.major_number # 60 + version.minor_number # 0 + version.patch_number # 1 + + version.is_release # True + version.is_beta # False + version.is_nightly # False + + str(version) # '60.0.1' + + previous_version = FirefoxVersion.parse('60.0b14') + previous_version < version # True + + previous_version.beta_number # 14 + previous_version.major_number # 60 + previous_version.minor_number # 0 + previous_version.patch_number # raises AttributeError + + previous_version.is_beta # True + previous_version.is_release # False + previous_version.is_nightly # False + + invalid_version = FirefoxVersion.parse('60.1') # raises PatternNotMatchedError + invalid_version = FirefoxVersion.parse('60.0.0') # raises PatternNotMatchedError + version = FirefoxVersion.parse('60.0') # valid + + # Versions can be built by raw values + FirefoxVersion(60, 0)) # '60.0' + FirefoxVersion(60, 0, 1)) # '60.0.1' + FirefoxVersion(60, 1, 0)) # '60.1.0' + FirefoxVersion(60, 0, 1, 1)) # '60.0.1build1' + FirefoxVersion(60, 0, beta_number=1)) # '60.0b1' + FirefoxVersion(60, 0, is_nightly=True)) # '60.0a1' + FirefoxVersion(60, 0, is_aurora_or_devedition=True)) # '60.0a2' + FirefoxVersion(60, 0, is_esr=True)) # '60.0esr' + FirefoxVersion(60, 0, 1, is_esr=True)) # '60.0.1esr' + +""" + +import attr +import re + +from mozilla_version.errors import ( + PatternNotMatchedError, TooManyTypesError, NoVersionTypeError +) +from mozilla_version.parser import strictly_positive_int_or_none +from mozilla_version.version import BaseVersion, VersionType + + +def _find_type(version): + version_type = None + + def ensure_version_type_is_not_already_defined(previous_type, candidate_type): + if previous_type is not None: + raise TooManyTypesError( + str(version), previous_type, candidate_type + ) + + if version.is_nightly: + version_type = VersionType.NIGHTLY + if version.is_aurora_or_devedition: + ensure_version_type_is_not_already_defined( + version_type, VersionType.AURORA_OR_DEVEDITION + ) + version_type = VersionType.AURORA_OR_DEVEDITION + if version.is_beta: + ensure_version_type_is_not_already_defined(version_type, VersionType.BETA) + version_type = VersionType.BETA + if version.is_esr: + ensure_version_type_is_not_already_defined(version_type, VersionType.ESR) + version_type = VersionType.ESR + if version.is_release: + ensure_version_type_is_not_already_defined(version_type, VersionType.RELEASE) + version_type = VersionType.RELEASE + + if version_type is None: + raise NoVersionTypeError(str(version)) + + return version_type + + +@attr.s(frozen=True, cmp=False, hash=True) +class GeckoVersion(BaseVersion): + """Class that validates and handles version numbers for Gecko-based products. + + You may want to use specific classes like FirefoxVersion. These classes define edge cases + that were shipped. + + Raises: + PatternNotMatchedError: if the string doesn't match the pattern of a valid version number + MissingFieldError: if a mandatory field is missing in the string. Mandatory fields are + `major_number` and `minor_number` + ValueError: if an integer can't be cast or is not (strictly) positive + TooManyTypesError: if the string matches more than 1 `VersionType` + NoVersionTypeError: if the string matches none. + + """ + + # XXX This pattern doesn't catch all subtleties of a Firefox version (like 32.5 isn't valid). + # This regex is intended to assign numbers. Then checks are done by attrs and + # __attrs_post_init__() + _VALID_ENOUGH_VERSION_PATTERN = re.compile(r""" + ^(?P<major_number>\d+) + \.(?P<minor_number>\d+) + (\.(?P<patch_number>\d+))? + ( + (?P<is_nightly>a1) + |(?P<is_aurora_or_devedition>a2) + |b(?P<beta_number>\d+) + |(?P<is_esr>esr) + )? + -?(build(?P<build_number>\d+))?$""", re.VERBOSE) + + _ALL_VERSION_NUMBERS_TYPES = ( + 'major_number', 'minor_number', 'patch_number', 'beta_number', + ) + + _OPTIONAL_NUMBERS = BaseVersion._OPTIONAL_NUMBERS + ('beta_number', 'build_number') + + build_number = attr.ib(type=int, converter=strictly_positive_int_or_none, default=None) + beta_number = attr.ib(type=int, converter=strictly_positive_int_or_none, default=None) + is_nightly = attr.ib(type=bool, default=False) + is_aurora_or_devedition = attr.ib(type=bool, default=False) + is_esr = attr.ib(type=bool, default=False) + version_type = attr.ib(init=False, default=attr.Factory(_find_type, takes_self=True)) + + def __attrs_post_init__(self): + """Ensure attributes are sane all together.""" + if self.minor_number == 0 and self.patch_number == 0: + raise PatternNotMatchedError( + self, pattern='Minor number and patch number cannot be both equal to 0' + ) + + if self.minor_number != 0 and self.patch_number is None: + raise PatternNotMatchedError( + self, pattern='Patch number cannot be undefined if minor number is greater than 0' + ) + + if self.beta_number is not None and self.patch_number is not None: + raise PatternNotMatchedError( + self, pattern='Beta number and patch number cannot be both defined' + ) + + if self.patch_number is not None and self.is_nightly: + raise PatternNotMatchedError( + self, pattern='Patch number cannot be defined on a nightly version' + ) + + if self.patch_number is not None and self.is_aurora_or_devedition: + raise PatternNotMatchedError( + self, pattern='Patch number cannot be defined on an aurora version' + ) + + @classmethod + def parse(cls, version_string): + """Construct an object representing a valid Firefox version number.""" + return super(GeckoVersion, cls).parse( + version_string, regex_groups=('is_nightly', 'is_aurora_or_devedition', 'is_esr') + ) + + @property + def is_beta(self): + """Return `True` if `FirefoxVersion` was built with a string matching a beta version.""" + return self.beta_number is not None + + @property + def is_release(self): + """Return `True` if `FirefoxVersion` was built with a string matching a release version.""" + return not (self.is_nightly or self.is_aurora_or_devedition or self.is_beta or self.is_esr) + + def __str__(self): + """Implement string representation. + + Computes a new string based on the given attributes. + """ + string = super(GeckoVersion, self).__str__() + + if self.is_nightly: + string = '{}a1'.format(string) + elif self.is_aurora_or_devedition: + string = '{}a2'.format(string) + elif self.is_beta: + string = '{}b{}'.format(string, self.beta_number) + elif self.is_esr: + string = '{}esr'.format(string) + + if self.build_number is not None: + string = '{}build{}'.format(string, self.build_number) + + return string + + def __eq__(self, other): + """Implement `==` operator. + + A version is considered equal to another if all numbers match and if they are of the same + `VersionType`. Like said in `VersionType`, release and ESR are considered equal (if they + share the same numbers). If a version contains a build number but not the other, the build + number won't be considered in the comparison. + + Examples: + .. code-block:: python + + assert GeckoVersion.parse('60.0') == GeckoVersion.parse('60.0') + assert GeckoVersion.parse('60.0') == GeckoVersion.parse('60.0esr') + assert GeckoVersion.parse('60.0') == GeckoVersion.parse('60.0build1') + assert GeckoVersion.parse('60.0build1') == GeckoVersion.parse('60.0build1') + + assert GeckoVersion.parse('60.0') != GeckoVersion.parse('61.0') + assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.1.0') + assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.0.1') + assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.0a1') + assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.0a2') + assert GeckoVersion.parse('60.0') != GeckoVersion.parse('60.0b1') + assert GeckoVersion.parse('60.0build1') != GeckoVersion.parse('60.0build2') + + """ + return super(GeckoVersion, self).__eq__(other) + + def _compare(self, other): + """Compare this release with another. + + Returns: + 0 if equal + < 0 is this precedes the other + > 0 if the other precedes this + + """ + if isinstance(other, str): + other = GeckoVersion.parse(other) + elif not isinstance(other, GeckoVersion): + raise ValueError('Cannot compare "{}", type not supported!'.format(other)) + + difference = super(GeckoVersion, self)._compare(other) + if difference != 0: + return difference + + channel_difference = self._compare_version_type(other) + if channel_difference != 0: + return channel_difference + + if self.is_beta and other.is_beta: + beta_difference = self.beta_number - other.beta_number + if beta_difference != 0: + return beta_difference + + # Build numbers are a special case. We might compare a regular version number + # (like "32.0b8") versus a release build (as in "32.0b8build1"). As a consequence, + # we only compare build_numbers when we both have them. + try: + return self.build_number - other.build_number + except TypeError: + pass + + return 0 + + def _compare_version_type(self, other): + return self.version_type.compare(other.version_type) + + +class _VersionWithEdgeCases(GeckoVersion): + def __attrs_post_init__(self): + for edge_case in self._RELEASED_EDGE_CASES: + if all( + getattr(self, number_type) == edge_case.get(number_type, None) + for number_type in self._ALL_VERSION_NUMBERS_TYPES + ): + if self.build_number is None: + return + elif self.build_number == edge_case.get('build_number', None): + return + + super(_VersionWithEdgeCases, self).__attrs_post_init__() + + +class FirefoxVersion(_VersionWithEdgeCases): + """Class that validates and handles Firefox version numbers.""" + + _RELEASED_EDGE_CASES = ({ + 'major_number': 33, + 'minor_number': 1, + 'build_number': 1, + }, { + 'major_number': 33, + 'minor_number': 1, + 'build_number': 2, + }, { + 'major_number': 33, + 'minor_number': 1, + 'build_number': 3, + }, { + 'major_number': 38, + 'minor_number': 0, + 'patch_number': 5, + 'beta_number': 1, + 'build_number': 1, + }, { + 'major_number': 38, + 'minor_number': 0, + 'patch_number': 5, + 'beta_number': 1, + 'build_number': 2, + }, { + 'major_number': 38, + 'minor_number': 0, + 'patch_number': 5, + 'beta_number': 2, + 'build_number': 1, + }, { + 'major_number': 38, + 'minor_number': 0, + 'patch_number': 5, + 'beta_number': 3, + 'build_number': 1, + }) + + +class DeveditionVersion(GeckoVersion): + """Class that validates and handles Devedition after it became an equivalent to beta.""" + + # No edge case were shipped + + def __attrs_post_init__(self): + """Ensure attributes are sane all together.""" + if ( + (not self.is_beta) or + (self.major_number < 54) or + (self.major_number == 54 and self.beta_number < 11) + ): + raise PatternNotMatchedError( + self, pattern='Devedition as a product must be a beta >= 54.0b11' + ) + + +class FennecVersion(_VersionWithEdgeCases): + """Class that validates and handles Fennec (Firefox for Android) version numbers.""" + + _RELEASED_EDGE_CASES = ({ + 'major_number': 33, + 'minor_number': 1, + 'build_number': 1, + }, { + 'major_number': 33, + 'minor_number': 1, + 'build_number': 2, + }, { + 'major_number': 38, + 'minor_number': 0, + 'patch_number': 5, + 'beta_number': 4, + 'build_number': 1, + }) + + def __attrs_post_init__(self): + """Ensure attributes are sane all together.""" + # Versions matching 68.Xa1, 68.XbN, or simply 68.X are expected since bug 1523402. The + # latter is needed because of the version.txt of beta + if ( + self.major_number == 68 and + self.minor_number > 0 and + self.patch_number is None + ): + return + + if self.major_number >= 69: + raise PatternNotMatchedError(self, pattern='Last Fennec version is 68') + + super(FennecVersion, self).__attrs_post_init__() + + +class ThunderbirdVersion(_VersionWithEdgeCases): + """Class that validates and handles Thunderbird version numbers.""" + + _RELEASED_EDGE_CASES = ({ + 'major_number': 45, + 'minor_number': 1, + 'beta_number': 1, + 'build_number': 1, + }, { + 'major_number': 45, + 'minor_number': 2, + 'build_number': 1, + }, { + 'major_number': 45, + 'minor_number': 2, + 'build_number': 2, + }, { + 'major_number': 45, + 'minor_number': 2, + 'beta_number': 1, + 'build_number': 2, + }) + + +class GeckoSnapVersion(GeckoVersion): + """Class that validates and handles Gecko's Snap version numbers. + + Snap is a Linux packaging format developped by Canonical. Valid numbers are like "63.0b7-1", + "1" stands for "build1". Release Engineering set this scheme at the beginning of Snap and now + we can't rename published snap to the regular pattern like "63.0b7-build1". + """ + + # Our Snaps are recent enough to not list any edge case, yet. + + # Differences between this regex and the one in GeckoVersion: + # * no a2 + # * no "build" + # * but mandatory dash and build number. + # Example: 63.0b7-1 + _VALID_ENOUGH_VERSION_PATTERN = re.compile(r""" + ^(?P<major_number>\d+) + \.(?P<minor_number>\d+) + (\.(?P<patch_number>\d+))? + ( + (?P<is_nightly>a1) + |b(?P<beta_number>\d+) + |(?P<is_esr>esr) + )? + -(?P<build_number>\d+)$""", re.VERBOSE) + + def __str__(self): + """Implement string representation. + + Returns format like "63.0b7-1" + """ + string = super(GeckoSnapVersion, self).__str__() + return string.replace('build', '-') diff --git a/third_party/python/mozilla-version/mozilla_version/maven.py b/third_party/python/mozilla-version/mozilla_version/maven.py new file mode 100644 index 0000000000..91cecb3431 --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/maven.py @@ -0,0 +1,56 @@ +"""Defines characteristics of a Maven version at Mozilla.""" + +import attr +import re + +from mozilla_version.version import BaseVersion + + +@attr.s(frozen=True, cmp=False, hash=True) +class MavenVersion(BaseVersion): + """Class that validates and handles Maven version numbers. + + At Mozilla, Maven packages are used in projects like "GeckoView" or "Android-Components". + """ + + is_snapshot = attr.ib(type=bool, default=False) + + _VALID_ENOUGH_VERSION_PATTERN = re.compile(r""" + ^(?P<major_number>\d+) + \.(?P<minor_number>\d+) + (\.(?P<patch_number>\d+))? + (?P<is_snapshot>-SNAPSHOT)?$""", re.VERBOSE) + + @classmethod + def parse(cls, version_string): + """Construct an object representing a valid Maven version number.""" + return super(MavenVersion, cls).parse(version_string, regex_groups=('is_snapshot', )) + + def __str__(self): + """Implement string representation. + + Computes a new string based on the given attributes. + """ + string = super(MavenVersion, self).__str__() + + if self.is_snapshot: + string = '{}-SNAPSHOT'.format(string) + + return string + + def _compare(self, other): + if isinstance(other, str): + other = MavenVersion.parse(other) + elif not isinstance(other, MavenVersion): + raise ValueError('Cannot compare "{}", type not supported!'.format(other)) + + difference = super(MavenVersion, self)._compare(other) + if difference != 0: + return difference + + if not self.is_snapshot and other.is_snapshot: + return 1 + elif self.is_snapshot and not other.is_snapshot: + return -1 + else: + return 0 diff --git a/third_party/python/mozilla-version/mozilla_version/parser.py b/third_party/python/mozilla-version/mozilla_version/parser.py new file mode 100644 index 0000000000..d3a6ef6bb6 --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/parser.py @@ -0,0 +1,48 @@ +"""Defines parser helpers.""" + +from mozilla_version.errors import MissingFieldError + + +def get_value_matched_by_regex(field_name, regex_matches, string): + """Ensure value stored in regex group exists.""" + try: + value = regex_matches.group(field_name) + if value is not None: + return value + except IndexError: + pass + + raise MissingFieldError(string, field_name) + + +def does_regex_have_group(regex_matches, group_name): + """Return a boolean depending on whether a regex group is matched.""" + try: + return regex_matches.group(group_name) is not None + except IndexError: + return False + + +def positive_int(val): + """Parse `val` into a positive integer.""" + if isinstance(val, float): + raise ValueError('"{}" must not be a float'.format(val)) + val = int(val) + if val >= 0: + return val + raise ValueError('"{}" must be positive'.format(val)) + + +def positive_int_or_none(val): + """Parse `val` into either `None` or a positive integer.""" + if val is None: + return val + return positive_int(val) + + +def strictly_positive_int_or_none(val): + """Parse `val` into either `None` or a strictly positive integer.""" + val = positive_int_or_none(val) + if val is None or val > 0: + return val + raise ValueError('"{}" must be strictly positive'.format(val)) diff --git a/third_party/python/mozilla-version/mozilla_version/test/__init__.py b/third_party/python/mozilla-version/mozilla_version/test/__init__.py new file mode 100644 index 0000000000..f094a83248 --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/test/__init__.py @@ -0,0 +1,5 @@ +from contextlib import contextmanager + +@contextmanager +def does_not_raise(): + yield diff --git a/third_party/python/mozilla-version/mozilla_version/test/test_balrog.py b/third_party/python/mozilla-version/mozilla_version/test/test_balrog.py new file mode 100644 index 0000000000..dd9dc24d3c --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/test/test_balrog.py @@ -0,0 +1,172 @@ +import pytest + +from mozilla_version.balrog import BalrogReleaseName +from mozilla_version.errors import PatternNotMatchedError +from mozilla_version.gecko import FirefoxVersion + + +@pytest.mark.parametrize( + 'product, major_number, minor_number, patch_number, beta_number, build_number, is_nightly, \ +is_aurora_or_devedition, is_esr, expected_output_string', (( + 'firefox', 32, 0, None, None, 1, False, False, False, 'firefox-32.0-build1' +), ( + 'firefox', 32, 0, 1, None, 2, False, False, False, 'firefox-32.0.1-build2' +), ( + 'firefox', 32, 0, None, 3, 4, False, False, False, 'firefox-32.0b3-build4' +), ( + 'firefox', 32, 0, None, None, 5, True, False, False, 'firefox-32.0a1-build5' +), ( + 'firefox', 32, 0, None, None, 6, False, True, False, 'firefox-32.0a2-build6' +), ( + 'firefox', 32, 0, None, None, 7, False, False, True, 'firefox-32.0esr-build7' +), ( + 'firefox', 32, 0, 1, None, 8, False, False, True, 'firefox-32.0.1esr-build8' +), ( + 'devedition', 54, 0, None, 12, 1, False, False, False, 'devedition-54.0b12-build1' +), ( + 'fennec', 32, 0, None, None, 1, False, False, False, 'fennec-32.0-build1' +), ( + 'thunderbird', 32, 0, None, None, 1, False, False, False, 'thunderbird-32.0-build1' +))) +def test_balrog_release_name_constructor_and_str( + product, major_number, minor_number, patch_number, beta_number, build_number, is_nightly, + is_aurora_or_devedition, is_esr, expected_output_string +): + assert str(BalrogReleaseName(product, FirefoxVersion( + major_number=major_number, + minor_number=minor_number, + patch_number=patch_number, + build_number=build_number, + beta_number=beta_number, + is_nightly=is_nightly, + is_aurora_or_devedition=is_aurora_or_devedition, + is_esr=is_esr + ))) == expected_output_string + + +@pytest.mark.parametrize('product, major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, ExpectedErrorType', (( + ('nonexistingproduct', 32, 0, None, None, 1, False, False, False, PatternNotMatchedError), + ('firefox', 32, 0, None, None, None, False, False, False, PatternNotMatchedError), +))) +def test_fail_balrog_release_constructor(product, major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, ExpectedErrorType): + with pytest.raises(ExpectedErrorType): + BalrogReleaseName(product, FirefoxVersion( + major_number=major_number, + minor_number=minor_number, + patch_number=patch_number, + beta_number=beta_number, + build_number=build_number, + is_nightly=is_nightly, + is_aurora_or_devedition=is_aurora_or_devedition, + is_esr=is_esr + )) + + +@pytest.mark.parametrize('string, expected_string', (( + ('firefox-32.0-build1', 'firefox-32.0-build1'), + ('firefox-32.0.1-build2', 'firefox-32.0.1-build2'), + ('firefox-32.0b3-build4', 'firefox-32.0b3-build4'), + ('firefox-32.0a1-build5', 'firefox-32.0a1-build5'), + ('firefox-32.0a2-build6', 'firefox-32.0a2-build6'), + ('firefox-32.0esr-build7', 'firefox-32.0esr-build7'), + ('firefox-32.0.1esr-build8', 'firefox-32.0.1esr-build8'), + + ('firefox-32.0build1', 'firefox-32.0-build1'), +))) +def test_balrog_release_name_parse(string, expected_string): + assert str(BalrogReleaseName.parse(string)) == expected_string + + +@pytest.mark.parametrize('string, ExpectedErrorType', ( + ('firefox-32.0', PatternNotMatchedError), + + ('firefox32.0-build1', PatternNotMatchedError), + ('firefox32.0build1', PatternNotMatchedError), + ('firefox-32.0--build1', PatternNotMatchedError), + ('firefox-build1', PatternNotMatchedError), + ('nonexistingproduct-32.0-build1', PatternNotMatchedError), + + ('firefox-32-build1', PatternNotMatchedError), + ('firefox-32.b2-build1', PatternNotMatchedError), + ('firefox-.1-build1', PatternNotMatchedError), + ('firefox-32.0.0-build1', PatternNotMatchedError), + ('firefox-32.2-build1', PatternNotMatchedError), + ('firefox-32.02-build1', PatternNotMatchedError), + ('firefox-32.0a0-build1', ValueError), + ('firefox-32.0b0-build1', ValueError), + ('firefox-32.0.1a1-build1', PatternNotMatchedError), + ('firefox-32.0.1a2-build1', PatternNotMatchedError), + ('firefox-32.0.1b2-build1', PatternNotMatchedError), + ('firefox-32.0-build0', ValueError), + ('firefox-32.0a1a2-build1', PatternNotMatchedError), + ('firefox-32.0a1b2-build1', PatternNotMatchedError), + ('firefox-32.0b2esr-build1', PatternNotMatchedError), + ('firefox-32.0esrb2-build1', PatternNotMatchedError), +)) +def test_firefox_version_raises_when_invalid_version_is_given(string, ExpectedErrorType): + with pytest.raises(ExpectedErrorType): + BalrogReleaseName.parse(string) + + +@pytest.mark.parametrize('previous, next', ( + ('firefox-32.0-build1', 'firefox-33.0-build1'), + ('firefox-32.0-build1', 'firefox-32.1.0-build1'), + ('firefox-32.0-build1', 'firefox-32.0.1-build1'), + ('firefox-32.0-build1', 'firefox-32.0-build2'), + + ('firefox-32.0a1-build1', 'firefox-32.0-build1'), + ('firefox-32.0a2-build1', 'firefox-32.0-build1'), + ('firefox-32.0b1-build1', 'firefox-32.0-build1'), + + ('firefox-32.0.1-build1', 'firefox-33.0-build1'), + ('firefox-32.0.1-build1', 'firefox-32.1.0-build1'), + ('firefox-32.0.1-build1', 'firefox-32.0.2-build1'), + ('firefox-32.0.1-build1', 'firefox-32.0.1-build2'), + + ('firefox-32.1.0-build1', 'firefox-33.0-build1'), + ('firefox-32.1.0-build1', 'firefox-32.2.0-build1'), + ('firefox-32.1.0-build1', 'firefox-32.1.1-build1'), + ('firefox-32.1.0-build1', 'firefox-32.1.0-build2'), + + ('firefox-32.0b1-build1', 'firefox-33.0b1-build1'), + ('firefox-32.0b1-build1', 'firefox-32.0b2-build1'), + ('firefox-32.0b1-build1', 'firefox-32.0b1-build2'), + + ('firefox-2.0-build1', 'firefox-10.0-build1'), + ('firefox-10.2.0-build1', 'firefox-10.10.0-build1'), + ('firefox-10.0.2-build1', 'firefox-10.0.10-build1'), + ('firefox-10.10.1-build1', 'firefox-10.10.10-build1'), + ('firefox-10.0-build2', 'firefox-10.0-build10'), + ('firefox-10.0b2-build1', 'firefox-10.0b10-build1'), +)) +def test_balrog_release_implements_lt_operator(previous, next): + assert BalrogReleaseName.parse(previous) < BalrogReleaseName.parse(next) + + +def test_fail_balrog_release_lt_operator(): + with pytest.raises(ValueError): + assert BalrogReleaseName.parse('thunderbird-32.0-build1') < BalrogReleaseName.parse('Firefox-32.0-build2') + + +def test_balrog_release_implements_remaining_comparision_operators(): + assert BalrogReleaseName.parse('firefox-32.0-build1') == BalrogReleaseName.parse('firefox-32.0-build1') + assert BalrogReleaseName.parse('firefox-32.0-build1') != BalrogReleaseName.parse('firefox-33.0-build1') + + assert BalrogReleaseName.parse('firefox-32.0-build1') <= BalrogReleaseName.parse('firefox-32.0-build1') + assert BalrogReleaseName.parse('firefox-32.0-build1') <= BalrogReleaseName.parse('firefox-33.0-build1') + + assert BalrogReleaseName.parse('firefox-33.0-build1') >= BalrogReleaseName.parse('firefox-32.0-build1') + assert BalrogReleaseName.parse('firefox-33.0-build1') >= BalrogReleaseName.parse('firefox-33.0-build1') + + assert BalrogReleaseName.parse('firefox-33.0-build1') > BalrogReleaseName.parse('firefox-32.0-build1') + assert not BalrogReleaseName.parse('firefox-33.0-build1') > BalrogReleaseName.parse('firefox-33.0-build1') + + assert not BalrogReleaseName.parse('firefox-32.0-build1') < BalrogReleaseName.parse('firefox-32.0-build1') + + assert BalrogReleaseName.parse('firefox-33.0-build1') != BalrogReleaseName.parse('firefox-32.0-build1') + +def test_balrog_release_hashable(): + """ + It is possible to hash `BalrogReleaseNmae`. + """ + hash(BalrogReleaseName.parse('firefox-63.0-build1')) diff --git a/third_party/python/mozilla-version/mozilla_version/test/test_gecko.py b/third_party/python/mozilla-version/mozilla_version/test/test_gecko.py new file mode 100644 index 0000000000..020e959e81 --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/test/test_gecko.py @@ -0,0 +1,411 @@ +import pytest +import re + +from distutils.version import StrictVersion, LooseVersion + +import mozilla_version.gecko + +from mozilla_version.errors import PatternNotMatchedError, TooManyTypesError, NoVersionTypeError +from mozilla_version.gecko import ( + GeckoVersion, FirefoxVersion, DeveditionVersion, + ThunderbirdVersion, FennecVersion, GeckoSnapVersion, +) +from mozilla_version.test import does_not_raise + + +VALID_VERSIONS = { + '32.0a1': 'nightly', + '32.0a2': 'aurora_or_devedition', + '32.0b2': 'beta', + '32.0b10': 'beta', + '32.0': 'release', + '32.0.1': 'release', + '32.0esr': 'esr', + '32.0.1esr': 'esr', +} + + +@pytest.mark.parametrize('major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, expected_output_string', (( + 32, 0, None, None, None, False, False, False, '32.0' +), ( + 32, 0, 1, None, None, False, False, False, '32.0.1' +), ( + 32, 0, None, 3, None, False, False, False, '32.0b3' +), ( + 32, 0, None, None, 10, False, False, False, '32.0build10' +), ( + 32, 0, None, None, None, True, False, False, '32.0a1' +), ( + 32, 0, None, None, None, False, True, False, '32.0a2' +), ( + 32, 0, None, None, None, False, False, True, '32.0esr' +), ( + 32, 0, 1, None, None, False, False, True, '32.0.1esr' +))) +def test_firefox_version_constructor_and_str(major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, expected_output_string): + assert str(FirefoxVersion( + major_number=major_number, + minor_number=minor_number, + patch_number=patch_number, + beta_number=beta_number, + build_number=build_number, + is_nightly=is_nightly, + is_aurora_or_devedition=is_aurora_or_devedition, + is_esr=is_esr + )) == expected_output_string + + +@pytest.mark.parametrize('major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, ExpectedErrorType', (( + 32, 0, None, 1, None, True, False, False, TooManyTypesError +), ( + 32, 0, None, 1, None, False, True, False, TooManyTypesError +), ( + 32, 0, None, 1, None, False, False, True, TooManyTypesError +), ( + 32, 0, None, None, None, True, True, False, TooManyTypesError +), ( + 32, 0, None, None, None, True, False, True, TooManyTypesError +), ( + 32, 0, None, None, None, False, True, True, TooManyTypesError +), ( + 32, 0, None, None, None, True, True, True, TooManyTypesError +), ( + 32, 0, 0, None, None, False, False, False, PatternNotMatchedError +), ( + 32, 0, None, 0, None, False, False, False, ValueError +), ( + 32, 0, None, None, 0, False, False, False, ValueError +), ( + 32, 0, 1, 1, None, False, False, False, PatternNotMatchedError +), ( + 32, 0, 1, None, None, True, False, False, PatternNotMatchedError +), ( + 32, 0, 1, None, None, False, True, False, PatternNotMatchedError +), ( + -1, 0, None, None, None, False, False, False, ValueError +), ( + 32, -1, None, None, None, False, False, False, ValueError +), ( + 32, 0, -1, None, None, False, False, False, ValueError +), ( + 2.2, 0, 0, None, None, False, False, False, ValueError +), ( + 'some string', 0, 0, None, None, False, False, False, ValueError +))) +def test_fail_firefox_version_constructor(major_number, minor_number, patch_number, beta_number, build_number, is_nightly, is_aurora_or_devedition, is_esr, ExpectedErrorType): + with pytest.raises(ExpectedErrorType): + FirefoxVersion( + major_number=major_number, + minor_number=minor_number, + patch_number=patch_number, + beta_number=beta_number, + build_number=build_number, + is_nightly=is_nightly, + is_aurora_or_devedition=is_aurora_or_devedition, + is_esr=is_esr + ) + + +def test_firefox_version_constructor_minimum_kwargs(): + assert str(FirefoxVersion(32, 0)) == '32.0' + assert str(FirefoxVersion(32, 0, 1)) == '32.0.1' + assert str(FirefoxVersion(32, 1, 0)) == '32.1.0' + assert str(FirefoxVersion(32, 0, 1, 1)) == '32.0.1build1' + assert str(FirefoxVersion(32, 0, beta_number=1)) == '32.0b1' + assert str(FirefoxVersion(32, 0, is_nightly=True)) == '32.0a1' + assert str(FirefoxVersion(32, 0, is_aurora_or_devedition=True)) == '32.0a2' + assert str(FirefoxVersion(32, 0, is_esr=True)) == '32.0esr' + assert str(FirefoxVersion(32, 0, 1, is_esr=True)) == '32.0.1esr' + + +@pytest.mark.parametrize('version_string, ExpectedErrorType', ( + ('32', PatternNotMatchedError), + ('32.b2', PatternNotMatchedError), + ('.1', PatternNotMatchedError), + ('32.0.0', PatternNotMatchedError), + ('32.2', PatternNotMatchedError), + ('32.02', PatternNotMatchedError), + ('32.0a0', ValueError), + ('32.0b0', ValueError), + ('32.0.1a1', PatternNotMatchedError), + ('32.0.1a2', PatternNotMatchedError), + ('32.0.1b2', PatternNotMatchedError), + ('32.0build0', ValueError), + ('32.0a1a2', PatternNotMatchedError), + ('32.0a1b2', PatternNotMatchedError), + ('32.0b2esr', PatternNotMatchedError), + ('32.0esrb2', PatternNotMatchedError), +)) +def test_firefox_version_raises_when_invalid_version_is_given(version_string, ExpectedErrorType): + with pytest.raises(ExpectedErrorType): + FirefoxVersion.parse(version_string) + + +@pytest.mark.parametrize('version_string, expected_type', VALID_VERSIONS.items()) +def test_firefox_version_is_of_a_defined_type(version_string, expected_type): + release = FirefoxVersion.parse(version_string) + assert getattr(release, 'is_{}'.format(expected_type)) + + +@pytest.mark.parametrize('previous, next', ( + ('32.0', '33.0'), + ('32.0', '32.1.0'), + ('32.0', '32.0.1'), + ('32.0build1', '32.0build2'), + + ('32.0.1', '33.0'), + ('32.0.1', '32.1.0'), + ('32.0.1', '32.0.2'), + ('32.0.1build1', '32.0.1build2'), + + ('32.1.0', '33.0'), + ('32.1.0', '32.2.0'), + ('32.1.0', '32.1.1'), + ('32.1.0build1', '32.1.0build2'), + + ('32.0b1', '33.0b1'), + ('32.0b1', '32.0b2'), + ('32.0b1build1', '32.0b1build2'), + + ('32.0a1', '32.0a2'), + ('32.0a1', '32.0b1'), + ('32.0a1', '32.0'), + ('32.0a1', '32.0esr'), + + ('32.0a2', '32.0b1'), + ('32.0a2', '32.0'), + ('32.0a2', '32.0esr'), + + ('32.0b1', '32.0'), + ('32.0b1', '32.0esr'), + + ('32.0', '32.0esr'), + + ('2.0', '10.0'), + ('10.2.0', '10.10.0'), + ('10.0.2', '10.0.10'), + ('10.10.1', '10.10.10'), + ('10.0build2', '10.0build10'), + ('10.0b2', '10.0b10'), +)) +def test_firefox_version_implements_lt_operator(previous, next): + assert FirefoxVersion.parse(previous) < FirefoxVersion.parse(next) + + +@pytest.mark.parametrize('equivalent_version_string', ( + '32.0', '032.0', '32.0build1', '32.0build01', '32.0-build1', '32.0build2', +)) +def test_firefox_version_implements_eq_operator(equivalent_version_string): + assert FirefoxVersion.parse('32.0') == FirefoxVersion.parse(equivalent_version_string) + # raw strings are also converted + assert FirefoxVersion.parse('32.0') == equivalent_version_string + + +@pytest.mark.parametrize('wrong_type', ( + 32, + 32.0, + ('32', '0', '1'), + ['32', '0', '1'], + LooseVersion('32.0'), + StrictVersion('32.0'), +)) +def test_firefox_version_raises_eq_operator(wrong_type): + with pytest.raises(ValueError): + assert FirefoxVersion.parse('32.0') == wrong_type + # AttributeError is raised by LooseVersion and StrictVersion + with pytest.raises((ValueError, AttributeError)): + assert wrong_type == FirefoxVersion.parse('32.0') + + +def test_firefox_version_implements_remaining_comparision_operators(): + assert FirefoxVersion.parse('32.0') <= FirefoxVersion.parse('32.0') + assert FirefoxVersion.parse('32.0') <= FirefoxVersion.parse('33.0') + + assert FirefoxVersion.parse('33.0') >= FirefoxVersion.parse('32.0') + assert FirefoxVersion.parse('33.0') >= FirefoxVersion.parse('33.0') + + assert FirefoxVersion.parse('33.0') > FirefoxVersion.parse('32.0') + assert not FirefoxVersion.parse('33.0') > FirefoxVersion.parse('33.0') + + assert not FirefoxVersion.parse('32.0') < FirefoxVersion.parse('32.0') + + assert FirefoxVersion.parse('33.0') != FirefoxVersion.parse('32.0') + + +@pytest.mark.parametrize('version_string, expected_output', ( + ('32.0', '32.0'), + ('032.0', '32.0'), + ('32.0build1', '32.0build1'), + ('32.0build01', '32.0build1'), + ('32.0.1', '32.0.1'), + ('32.0a1', '32.0a1'), + ('32.0a2', '32.0a2'), + ('32.0b1', '32.0b1'), + ('32.0b01', '32.0b1'), + ('32.0esr', '32.0esr'), + ('32.0.1esr', '32.0.1esr'), +)) +def test_firefox_version_implements_str_operator(version_string, expected_output): + assert str(FirefoxVersion.parse(version_string)) == expected_output + + +_SUPER_PERMISSIVE_PATTERN = re.compile(r""" +(?P<major_number>\d+)\.(?P<minor_number>\d+)(\.(\d+))* +(?P<is_nightly>a1)?(?P<is_aurora_or_devedition>a2)?(b(?P<beta_number>\d+))? +(?P<is_esr>esr)? +""", re.VERBOSE) + + +@pytest.mark.parametrize('version_string', ( + '32.0a1a2', '32.0a1b2', '32.0b2esr' +)) +def test_firefox_version_ensures_it_does_not_have_multiple_type(monkeypatch, version_string): + # Let's make sure the sanity checks detect a broken regular expression + original_pattern = FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN + FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN = _SUPER_PERMISSIVE_PATTERN + + with pytest.raises(TooManyTypesError): + FirefoxVersion.parse(version_string) + + FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN = original_pattern + + +def test_firefox_version_ensures_a_new_added_release_type_is_caught(monkeypatch): + # Let's make sure the sanity checks detect a broken regular expression + original_pattern = FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN + FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN = _SUPER_PERMISSIVE_PATTERN + + # And a broken type detection + original_is_release = FirefoxVersion.is_release + FirefoxVersion.is_release = False + + with pytest.raises(NoVersionTypeError): + mozilla_version.gecko.FirefoxVersion.parse('32.0.0.0') + + FirefoxVersion.is_release = original_is_release + FirefoxVersion._VALID_ENOUGH_VERSION_PATTERN = original_pattern + + +@pytest.mark.parametrize('version_string', ( + '33.1', '33.1build1', '33.1build2', '33.1build3', + '38.0.5b1', '38.0.5b1build1', '38.0.5b1build2', + '38.0.5b2', '38.0.5b2build1', + '38.0.5b3', '38.0.5b3build1', +)) +def test_firefox_version_supports_released_edge_cases(version_string): + assert str(FirefoxVersion.parse(version_string)) == version_string + for Class in (DeveditionVersion, FennecVersion, ThunderbirdVersion): + if Class == FennecVersion and version_string in ('33.1', '33.1build1', '33.1build2'): + # These edge cases also exist in Fennec + continue + with pytest.raises(PatternNotMatchedError): + Class.parse(version_string) + + +@pytest.mark.parametrize('version_string', ( + '54.0b11', '54.0b12', '55.0b1' +)) +def test_devedition_version(version_string): + DeveditionVersion.parse(version_string) + + +@pytest.mark.parametrize('version_string', ( + '53.0a1', '53.0b1', '54.0b10', '55.0', '55.0a1', '60.0esr' +)) +def test_devedition_version_bails_on_wrong_version(version_string): + with pytest.raises(PatternNotMatchedError): + DeveditionVersion.parse(version_string) + + +@pytest.mark.parametrize('version_string', ( + '33.1', '33.1build1', '33.1build2', + '38.0.5b4', '38.0.5b4build1' +)) +def test_fennec_version_supports_released_edge_cases(version_string): + assert str(FennecVersion.parse(version_string)) == version_string + for Class in (FirefoxVersion, DeveditionVersion, ThunderbirdVersion): + if Class == FirefoxVersion and version_string in ('33.1', '33.1build1', '33.1build2'): + # These edge cases also exist in Firefox + continue + with pytest.raises(PatternNotMatchedError): + Class.parse(version_string) + + +@pytest.mark.parametrize('version_string, expectation', ( + ('68.0a1', does_not_raise()), + ('68.0b3', does_not_raise()), + ('68.0b17', does_not_raise()), + ('68.0', does_not_raise()), + ('68.0.1', does_not_raise()), + ('68.1a1', does_not_raise()), + ('68.1b2', does_not_raise()), + ('68.1.0', does_not_raise()), + ('68.1', does_not_raise()), + ('68.1b3', does_not_raise()), + ('68.1.1', does_not_raise()), + ('68.2a1', does_not_raise()), + ('68.2b1', does_not_raise()), + ('68.2', does_not_raise()), + + ('67.1', pytest.raises(PatternNotMatchedError)), + ('68.0.1a1', pytest.raises(PatternNotMatchedError)), + ('68.1a1b1', pytest.raises(PatternNotMatchedError)), + ('68.0.1b1', pytest.raises(PatternNotMatchedError)), + ('68.1.0a1', pytest.raises(PatternNotMatchedError)), + ('68.1.0b1', pytest.raises(PatternNotMatchedError)), + ('68.1.1a1', pytest.raises(PatternNotMatchedError)), + ('68.1.1b2', pytest.raises(PatternNotMatchedError)), + + ('69.0a1', pytest.raises(PatternNotMatchedError)), + ('69.0b3', pytest.raises(PatternNotMatchedError)), + ('69.0', pytest.raises(PatternNotMatchedError)), + ('69.0.1', pytest.raises(PatternNotMatchedError)), + ('69.1', pytest.raises(PatternNotMatchedError)), + + ('70.0', pytest.raises(PatternNotMatchedError)), +)) +def test_fennec_version_ends_at_68(version_string, expectation): + with expectation: + FennecVersion.parse(version_string) + + +@pytest.mark.parametrize('version_string', ( + '45.1b1', '45.1b1build1', + '45.2', '45.2build1', '45.2build2', + '45.2b1', '45.2b1build2', +)) +def test_thunderbird_version_supports_released_edge_cases(version_string): + assert str(ThunderbirdVersion.parse(version_string)) == version_string + for Class in (FirefoxVersion, DeveditionVersion, FennecVersion): + with pytest.raises(PatternNotMatchedError): + Class.parse(version_string) + + +@pytest.mark.parametrize('version_string', ( + '63.0b7-1', '63.0b7-2', + '62.0-1', '62.0-2', + '60.2.1esr-1', '60.2.0esr-2', + '60.0esr-1', '60.0esr-13', + # TODO Bug 1451694: Figure out what nightlies version numbers looks like +)) +def test_gecko_snap_version(version_string): + GeckoSnapVersion.parse(version_string) + + +@pytest.mark.parametrize('version_string', ( + '32.0a2', '32.0esr1', '32.0-build1', +)) +def test_gecko_snap_version_bails_on_wrong_version(version_string): + with pytest.raises(PatternNotMatchedError): + GeckoSnapVersion.parse(version_string) + + +def test_gecko_snap_version_implements_its_own_string(): + assert str(GeckoSnapVersion.parse('63.0b7-1')) == '63.0b7-1' + + +def test_gecko_version_hashable(): + """ + It is possible to hash `GeckoVersion`. + """ + hash(GeckoVersion.parse('63.0')) diff --git a/third_party/python/mozilla-version/mozilla_version/test/test_maven.py b/third_party/python/mozilla-version/mozilla_version/test/test_maven.py new file mode 100644 index 0000000000..f8d539872d --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/test/test_maven.py @@ -0,0 +1,87 @@ +import pytest + +from distutils.version import LooseVersion, StrictVersion + +from mozilla_version.errors import PatternNotMatchedError +from mozilla_version.maven import MavenVersion + +@pytest.mark.parametrize('major_number, minor_number, patch_number, is_snapshot, expected_output_string', (( + 32, 0, None, False, '32.0' +), ( + 32, 0, 1, False, '32.0.1' +), ( + 32, 0, None, True, '32.0-SNAPSHOT' +), ( + 32, 0, 1, True, '32.0.1-SNAPSHOT' +))) +def test_maven_version_constructor_and_str(major_number, minor_number, patch_number, is_snapshot, expected_output_string): + assert str(MavenVersion( + major_number=major_number, + minor_number=minor_number, + patch_number=patch_number, + is_snapshot=is_snapshot, + )) == expected_output_string + + +def test_maven_version_constructor_minimum_kwargs(): + assert str(MavenVersion(32, 0)) == '32.0' + assert str(MavenVersion(32, 0, 1)) == '32.0.1' + assert str(MavenVersion(32, 1, 0)) == '32.1.0' + assert str(MavenVersion(32, 1, 0, False)) == '32.1.0' + assert str(MavenVersion(32, 1, 0, True)) == '32.1.0-SNAPSHOT' + + +@pytest.mark.parametrize('version_string, ExpectedErrorType', ( + ('32.0SNAPSHOT', PatternNotMatchedError), + ('32.1.0SNAPSHOT', PatternNotMatchedError), +)) +def test_maven_version_raises_when_invalid_version_is_given(version_string, ExpectedErrorType): + with pytest.raises(ExpectedErrorType): + MavenVersion.parse(version_string) + + +@pytest.mark.parametrize('previous, next', ( + ('32.0-SNAPSHOT', '32.0'), + ('31.0', '32.0-SNAPSHOT'), + ('32.0', '32.0.1-SNAPSHOT'), + ('32.0.1-SNAPSHOT', '32.1.0'), + ('32.0.1-SNAPSHOT', '33.0'), +)) +def test_maven_version_implements_lt_operator(previous, next): + assert MavenVersion.parse(previous) < MavenVersion.parse(next) + + +@pytest.mark.parametrize('previous, next', ( + ('32.0', '32.0-SNAPSHOT'), + ('32.0-SNAPSHOT', '31.0'), + ('32.0.1-SNAPSHOT', '32.0'), + ('32.1.0', '32.0.1-SNAPSHOT'), +)) +def test_maven_version_implements_gt_operator(previous, next): + assert MavenVersion.parse(previous) > MavenVersion.parse(next) + + +@pytest.mark.parametrize('wrong_type', ( + 32, + 32.0, + ('32', '0', '1'), + ['32', '0', '1'], + LooseVersion('32.0'), + StrictVersion('32.0'), +)) +def test_base_version_raises_eq_operator(wrong_type): + with pytest.raises(ValueError): + assert MavenVersion.parse('32.0') == wrong_type + # AttributeError is raised by LooseVersion and StrictVersion + with pytest.raises((ValueError, AttributeError)): + assert wrong_type == MavenVersion.parse('32.0') + + +def test_maven_version_implements_eq_operator(): + assert MavenVersion.parse('32.0-SNAPSHOT') == MavenVersion.parse('32.0-SNAPSHOT') + # raw strings are also converted + assert MavenVersion.parse('32.0-SNAPSHOT') == '32.0-SNAPSHOT' + + +def test_maven_version_hashable(): + hash(MavenVersion.parse('32.0.1')) diff --git a/third_party/python/mozilla-version/mozilla_version/test/test_version.py b/third_party/python/mozilla-version/mozilla_version/test/test_version.py new file mode 100644 index 0000000000..c74bdb735a --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/test/test_version.py @@ -0,0 +1,171 @@ +import pytest + +from distutils.version import LooseVersion, StrictVersion + +from mozilla_version.errors import PatternNotMatchedError +from mozilla_version.version import BaseVersion, VersionType + + +@pytest.mark.parametrize('major_number, minor_number, patch_number, expected_output_string', (( + 32, 0, None, '32.0' +), ( + 32, 0, 1, '32.0.1' +))) +def test_base_version_constructor_and_str(major_number, minor_number, patch_number, expected_output_string): + assert str(BaseVersion( + major_number=major_number, + minor_number=minor_number, + patch_number=patch_number, + )) == expected_output_string + + +@pytest.mark.parametrize('major_number, minor_number, patch_number, ExpectedErrorType', (( + -1, 0, None, ValueError +), ( + 32, -1, None, ValueError +), ( + 32, 0, -1, ValueError +), ( + 2.2, 0, 0, ValueError +), ( + 'some string', 0, 0, ValueError +))) +def test_fail_base_version_constructor(major_number, minor_number, patch_number, ExpectedErrorType): + with pytest.raises(ExpectedErrorType): + BaseVersion( + major_number=major_number, + minor_number=minor_number, + patch_number=patch_number, + ) + + +def test_base_version_constructor_minimum_kwargs(): + assert str(BaseVersion(32, 0)) == '32.0' + assert str(BaseVersion(32, 0, 1)) == '32.0.1' + assert str(BaseVersion(32, 1, 0)) == '32.1.0' + + +@pytest.mark.parametrize('version_string, ExpectedErrorType', ( + ('32', PatternNotMatchedError), + ('.1', PatternNotMatchedError), +)) +def test_base_version_raises_when_invalid_version_is_given(version_string, ExpectedErrorType): + with pytest.raises(ExpectedErrorType): + BaseVersion.parse(version_string) + + +@pytest.mark.parametrize('previous, next', ( + ('32.0', '33.0'), + ('32.0', '32.1.0'), + ('32.0', '32.0.1'), + + ('32.0.1', '33.0'), + ('32.0.1', '32.1.0'), + ('32.0.1', '32.0.2'), + + ('32.1.0', '33.0'), + ('32.1.0', '32.2.0'), + ('32.1.0', '32.1.1'), + + ('2.0', '10.0'), + ('10.2.0', '10.10.0'), + ('10.0.2', '10.0.10'), + ('10.10.1', '10.10.10'), +)) +def test_base_version_implements_lt_operator(previous, next): + assert BaseVersion.parse(previous) < BaseVersion.parse(next) + + +@pytest.mark.parametrize('equivalent_version_string', ( + '32.0', '032.0', '32.00' +)) +def test_base_version_implements_eq_operator(equivalent_version_string): + assert BaseVersion.parse('32.0') == BaseVersion.parse(equivalent_version_string) + # raw strings are also converted + assert BaseVersion.parse('32.0') == equivalent_version_string + + +@pytest.mark.parametrize('wrong_type', ( + 32, + 32.0, + ('32', '0', '1'), + ['32', '0', '1'], + LooseVersion('32.0'), + StrictVersion('32.0'), +)) +def test_base_version_raises_eq_operator(wrong_type): + with pytest.raises(ValueError): + assert BaseVersion.parse('32.0') == wrong_type + # AttributeError is raised by LooseVersion and StrictVersion + with pytest.raises((ValueError, AttributeError)): + assert wrong_type == BaseVersion.parse('32.0') + + +def test_base_version_implements_remaining_comparision_operators(): + assert BaseVersion.parse('32.0') <= BaseVersion.parse('32.0') + assert BaseVersion.parse('32.0') <= BaseVersion.parse('33.0') + + assert BaseVersion.parse('33.0') >= BaseVersion.parse('32.0') + assert BaseVersion.parse('33.0') >= BaseVersion.parse('33.0') + + assert BaseVersion.parse('33.0') > BaseVersion.parse('32.0') + assert not BaseVersion.parse('33.0') > BaseVersion.parse('33.0') + + assert not BaseVersion.parse('32.0') < BaseVersion.parse('32.0') + + assert BaseVersion.parse('33.0') != BaseVersion.parse('32.0') + + +def test_base_version_hashable(): + hash(BaseVersion.parse('63.0')) + + +@pytest.mark.parametrize('previous, next', ( + (VersionType.NIGHTLY, VersionType.AURORA_OR_DEVEDITION), + (VersionType.NIGHTLY, VersionType.BETA), + (VersionType.NIGHTLY, VersionType.RELEASE), + (VersionType.NIGHTLY, VersionType.ESR), + + (VersionType.AURORA_OR_DEVEDITION, VersionType.BETA), + (VersionType.AURORA_OR_DEVEDITION, VersionType.RELEASE), + (VersionType.AURORA_OR_DEVEDITION, VersionType.ESR), + + (VersionType.BETA, VersionType.RELEASE), + (VersionType.BETA, VersionType.ESR), + + (VersionType.RELEASE, VersionType.ESR), +)) +def test_version_type_implements_lt_operator(previous, next): + assert previous < next + + +@pytest.mark.parametrize('first, second', ( + (VersionType.NIGHTLY, VersionType.NIGHTLY), + (VersionType.AURORA_OR_DEVEDITION, VersionType.AURORA_OR_DEVEDITION), + (VersionType.BETA, VersionType.BETA), + (VersionType.RELEASE, VersionType.RELEASE), + (VersionType.ESR, VersionType.ESR), +)) +def test_version_type_implements_eq_operator(first, second): + assert first == second + + +def test_version_type_implements_remaining_comparision_operators(): + assert VersionType.NIGHTLY <= VersionType.NIGHTLY + assert VersionType.NIGHTLY <= VersionType.BETA + + assert VersionType.NIGHTLY >= VersionType.NIGHTLY + assert VersionType.BETA >= VersionType.NIGHTLY + + assert not VersionType.NIGHTLY > VersionType.NIGHTLY + assert VersionType.BETA > VersionType.NIGHTLY + + assert not VersionType.BETA < VersionType.NIGHTLY + + assert VersionType.NIGHTLY != VersionType.BETA + + +def test_version_type_compare(): + assert VersionType.NIGHTLY.compare(VersionType.NIGHTLY) == 0 + assert VersionType.NIGHTLY.compare(VersionType.BETA) < 0 + assert VersionType.BETA.compare(VersionType.NIGHTLY) > 0 diff --git a/third_party/python/mozilla-version/mozilla_version/version.py b/third_party/python/mozilla-version/mozilla_version/version.py new file mode 100644 index 0000000000..1f78bec590 --- /dev/null +++ b/third_party/python/mozilla-version/mozilla_version/version.py @@ -0,0 +1,177 @@ +"""Defines common characteristics of a version at Mozilla.""" + +import attr +import re + +from enum import Enum + +from mozilla_version.errors import MissingFieldError, PatternNotMatchedError +from mozilla_version.parser import ( + get_value_matched_by_regex, + does_regex_have_group, + positive_int, + positive_int_or_none +) + + +@attr.s(frozen=True, cmp=False, hash=True) +class BaseVersion(object): + """Class that validates and handles general version numbers.""" + + major_number = attr.ib(type=int, converter=positive_int) + minor_number = attr.ib(type=int, converter=positive_int) + patch_number = attr.ib(type=int, converter=positive_int_or_none, default=None) + + _MANDATORY_NUMBERS = ('major_number', 'minor_number') + _OPTIONAL_NUMBERS = ('patch_number', ) + _ALL_NUMBERS = _MANDATORY_NUMBERS + _OPTIONAL_NUMBERS + + _VALID_ENOUGH_VERSION_PATTERN = re.compile(r""" + ^(?P<major_number>\d+) + \.(?P<minor_number>\d+) + (\.(?P<patch_number>\d+))?$""", re.VERBOSE) + + @classmethod + def parse(cls, version_string, regex_groups=()): + """Construct an object representing a valid version number.""" + regex_matches = cls._VALID_ENOUGH_VERSION_PATTERN.match(version_string) + + if regex_matches is None: + raise PatternNotMatchedError(version_string, cls._VALID_ENOUGH_VERSION_PATTERN) + + kwargs = {} + + for field in cls._MANDATORY_NUMBERS: + kwargs[field] = get_value_matched_by_regex(field, regex_matches, version_string) + for field in cls._OPTIONAL_NUMBERS: + try: + kwargs[field] = get_value_matched_by_regex(field, regex_matches, version_string) + except MissingFieldError: + pass + + for regex_group in regex_groups: + kwargs[regex_group] = does_regex_have_group(regex_matches, regex_group) + + return cls(**kwargs) + + def __str__(self): + """Implement string representation. + + Computes a new string based on the given attributes. + """ + semvers = [str(self.major_number), str(self.minor_number)] + if self.patch_number is not None: + semvers.append(str(self.patch_number)) + + return '.'.join(semvers) + + def __eq__(self, other): + """Implement `==` operator.""" + return self._compare(other) == 0 + + def __ne__(self, other): + """Implement `!=` operator.""" + return self._compare(other) != 0 + + def __lt__(self, other): + """Implement `<` operator.""" + return self._compare(other) < 0 + + def __le__(self, other): + """Implement `<=` operator.""" + return self._compare(other) <= 0 + + def __gt__(self, other): + """Implement `>` operator.""" + return self._compare(other) > 0 + + def __ge__(self, other): + """Implement `>=` operator.""" + return self._compare(other) >= 0 + + def _compare(self, other): + """Compare this release with another. + + Returns: + 0 if equal + < 0 is this precedes the other + > 0 if the other precedes this + + """ + if isinstance(other, str): + other = BaseVersion.parse(other) + elif not isinstance(other, BaseVersion): + raise ValueError('Cannot compare "{}", type not supported!'.format(other)) + + for field in ('major_number', 'minor_number', 'patch_number'): + this_number = getattr(self, field) + this_number = 0 if this_number is None else this_number + other_number = getattr(other, field) + other_number = 0 if other_number is None else other_number + + difference = this_number - other_number + + if difference != 0: + return difference + + return 0 + + +class VersionType(Enum): + """Enum that sorts types of versions (e.g.: nightly, beta, release, esr). + + Supports comparison. `ESR` is considered higher than `RELEASE` (even if they technically have + the same codebase). For instance: 60.0.1 < 60.0.1esr but 61.0 > 60.0.1esr. + This choice has a practical use case: if you have a list of Release and ESR version, you can + easily extract one kind or the other thanks to the VersionType. + + Examples: + .. code-block:: python + + assert VersionType.NIGHTLY == VersionType.NIGHTLY + assert VersionType.ESR > VersionType.RELEASE + + """ + + NIGHTLY = 1 + AURORA_OR_DEVEDITION = 2 + BETA = 3 + RELEASE = 4 + ESR = 5 + + def __eq__(self, other): + """Implement `==` operator.""" + return self.compare(other) == 0 + + def __ne__(self, other): + """Implement `!=` operator.""" + return self.compare(other) != 0 + + def __lt__(self, other): + """Implement `<` operator.""" + return self.compare(other) < 0 + + def __le__(self, other): + """Implement `<=` operator.""" + return self.compare(other) <= 0 + + def __gt__(self, other): + """Implement `>` operator.""" + return self.compare(other) > 0 + + def __ge__(self, other): + """Implement `>=` operator.""" + return self.compare(other) >= 0 + + def compare(self, other): + """Compare this `VersionType` with anotherself. + + Returns: + 0 if equal + < 0 is this precedes the other + > 0 if the other precedes this + + """ + return self.value - other.value + + __hash__ = Enum.__hash__ |