From: Daan De Meyer Date: Sun, 6 Aug 2023 12:17:27 +0000 (+0200) Subject: Split out GenericVersion into its own module X-Git-Tag: v15~26^2~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d0ddf5472791ccc367fc866a8b5b143550933a81;p=thirdparty%2Fmkosi.git Split out GenericVersion into its own module --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index a927a6a4c..f847a5b36 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -23,7 +23,6 @@ from mkosi.archive import extract_tar, make_cpio, make_tar from mkosi.config import ( Compression, ConfigFeature, - GenericVersion, ManifestFormat, MkosiArgs, MkosiConfig, @@ -56,6 +55,7 @@ from mkosi.util import ( try_import, umask, ) +from mkosi.versioncomp import GenericVersion @contextlib.contextmanager diff --git a/mkosi/config.py b/mkosi/config.py index 6050d5b68..f0ea7aa8c 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -19,7 +19,6 @@ import subprocess import sys import textwrap from collections.abc import Iterable, Sequence -from itertools import takewhile from pathlib import Path from typing import Any, Callable, Optional, Type, Union, cast @@ -37,6 +36,7 @@ from mkosi.util import ( qemu_check_kvm_support, qemu_check_vsock_support, ) +from mkosi.versioncomp import GenericVersion __version__ = "14" @@ -1836,171 +1836,6 @@ class MkosiConfigParser: return args, tuple(load_config(ns) for ns in presets) -class GenericVersion: - # These constants follow the convention of the return value of rpmdev-vercmp that are followe - # by systemd-analyze compare-versions when called with only two arguments (without a comparison - # operator), recreated in the compare_versions method. - _EQUAL = 0 - _RIGHT_SMALLER = 1 - _LEFT_SMALLER = -1 - - def __init__(self, version: str): - self._version = version - - @classmethod - def compare_versions(cls, v1: str, v2: str) -> int: - """Implements comparison according to UAPI Group Version Format Specification""" - def rstrip_invalid_version_chars(s: str) -> str: - valid_version_chars = {*string.ascii_letters, *string.digits, "~", "-", "^", "."} - for i, c in enumerate(s): - if c in valid_version_chars: - return s[i:] - return "" - - def digit_prefix(s: str) -> str: - return "".join(takewhile(lambda c: c in string.digits, s)) - - def letter_prefix(s: str) -> str: - return "".join(takewhile(lambda c: c in string.ascii_letters, s)) - - while True: - # Any characters which are outside of the set of listed above (a-z, A-Z, 0-9, -, ., ~, - # ^) are skipped in both strings. In particular, this means that non-ASCII characters - # that are Unicode digits or letters are skipped too. - v1 = rstrip_invalid_version_chars(v1) - v2 = rstrip_invalid_version_chars(v2) - - # If the remaining part of one of strings starts with "~": if other remaining part does - # not start with ~, the string with ~ compares lower. Otherwise, both tilde characters - # are skipped. - - if v1.startswith("~") and v2.startswith("~"): - v1 = v1.removeprefix("~") - v2 = v2.removeprefix("~") - elif v1.startswith("~"): - return cls._LEFT_SMALLER - elif v2.startswith("~"): - return cls._RIGHT_SMALLER - - # If one of the strings has ended: if the other string hasn’t, the string that has - # remaining characters compares higher. Otherwise, the strings compare equal. - - if not v1 and not v2: - return cls._EQUAL - elif not v1 and v2: - return cls._LEFT_SMALLER - elif v1 and not v2: - return cls._RIGHT_SMALLER - - # If the remaining part of one of strings starts with "-": if the other remaining part - # does not start with -, the string with - compares lower. Otherwise, both minus - # characters are skipped. - - if v1.startswith("-") and v2.startswith("-"): - v1 = v1.removeprefix("-") - v2 = v2.removeprefix("-") - elif v1.startswith("-"): - return cls._LEFT_SMALLER - elif v2.startswith("-"): - return cls._RIGHT_SMALLER - - # If the remaining part of one of strings starts with "^": if the other remaining part - # does not start with ^, the string with ^ compares higher. Otherwise, both caret - # characters are skipped. - - if v1.startswith("^") and v2.startswith("^"): - v1 = v1.removeprefix("^") - v2 = v2.removeprefix("^") - elif v1.startswith("^"): - # TODO: bug? - return cls._LEFT_SMALLER #cls._RIGHT_SMALLER - elif v2.startswith("^"): - return cls._RIGHT_SMALLER #cls._LEFT_SMALLER - - # If the remaining part of one of strings starts with ".": if the other remaining part - # does not start with ., the string with . compares lower. Otherwise, both dot - # characters are skipped. - - if v1.startswith(".") and v2.startswith("."): - v1 = v1.removeprefix(".") - v2 = v2.removeprefix(".") - elif v1.startswith("."): - return cls._LEFT_SMALLER - elif v2.startswith("."): - return cls._RIGHT_SMALLER - - # If either of the remaining parts starts with a digit: numerical prefixes are compared - # numerically. Any leading zeroes are skipped. The numerical prefixes (until the first - # non-digit character) are evaluated as numbers. If one of the prefixes is empty, it - # evaluates as 0. If the numbers are different, the string with the bigger number - # compares higher. Otherwise, the comparison continues at the following characters at - # point 1. - - v1_digit_prefix = digit_prefix(v1) - v2_digit_prefix = digit_prefix(v2) - - if v1_digit_prefix or v2_digit_prefix: - v1_digits = int(v1_digit_prefix) if v1_digit_prefix else 0 - v2_digits = int(v2_digit_prefix) if v2_digit_prefix else 0 - - if v1_digits < v2_digits: - return cls._LEFT_SMALLER - elif v1_digits > v2_digits: - return cls._RIGHT_SMALLER - - v1 = v1.removeprefix(v1_digit_prefix) - v2 = v2.removeprefix(v2_digit_prefix) - continue - - # Leading alphabetical prefixes are compared alphabetically. The substrings are - # compared letter-by-letter. If both letters are the same, the comparison continues - # with the next letter. Capital letters compare lower than lower-case letters (A < - # a). When the end of one substring has been reached (a non-letter character or the end - # of the whole string), if the other substring has remaining letters, it compares - # higher. Otherwise, the comparison continues at the following characters at point 1. - - v1_letter_prefix = letter_prefix(v1) - v2_letter_prefix = letter_prefix(v2) - - if v1_letter_prefix < v2_letter_prefix: - return cls._LEFT_SMALLER - elif v1_letter_prefix > v2_letter_prefix: - return cls._RIGHT_SMALLER - - v1 = v1.removeprefix(v1_letter_prefix) - v2 = v2.removeprefix(v2_letter_prefix) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, GenericVersion): - return False - return self.compare_versions(self._version, other._version) == self._EQUAL - - def __ne__(self, other: object) -> bool: - if not isinstance(other, GenericVersion): - return False - return self.compare_versions(self._version, other._version) != self._EQUAL - - def __lt__(self, other: object) -> bool: - if not isinstance(other, GenericVersion): - return False - return self.compare_versions(self._version, other._version) == self._LEFT_SMALLER - - def __le__(self, other: object) -> bool: - if not isinstance(other, GenericVersion): - return False - return self.compare_versions(self._version, other._version) in (self._EQUAL, self._LEFT_SMALLER) - - def __gt__(self, other: object) -> bool: - if not isinstance(other, GenericVersion): - return False - return self.compare_versions(self._version, other._version) == self._RIGHT_SMALLER - - def __ge__(self, other: object) -> bool: - if not isinstance(other, GenericVersion): - return False - return self.compare_versions(self._version, other._version) in (self._EQUAL, self._RIGHT_SMALLER) - - def load_credentials(args: argparse.Namespace) -> dict[str, str]: creds = {} diff --git a/mkosi/mounts.py b/mkosi/mounts.py index fbf287825..cfb03d6aa 100644 --- a/mkosi/mounts.py +++ b/mkosi/mounts.py @@ -9,11 +9,11 @@ from collections.abc import Iterator, Sequence from pathlib import Path from typing import Optional, TypeVar -from mkosi.config import GenericVersion from mkosi.log import complete_step from mkosi.run import run from mkosi.types import PathString from mkosi.util import umask +from mkosi.versioncomp import GenericVersion T = TypeVar("T") diff --git a/mkosi/versioncomp.py b/mkosi/versioncomp.py new file mode 100644 index 000000000..3079f92c9 --- /dev/null +++ b/mkosi/versioncomp.py @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +import string +from itertools import takewhile + + +class GenericVersion: + # These constants follow the convention of the return value of rpmdev-vercmp that are followe + # by systemd-analyze compare-versions when called with only two arguments (without a comparison + # operator), recreated in the compare_versions method. + _EQUAL = 0 + _RIGHT_SMALLER = 1 + _LEFT_SMALLER = -1 + + def __init__(self, version: str): + self._version = version + + @classmethod + def compare_versions(cls, v1: str, v2: str) -> int: + """Implements comparison according to UAPI Group Version Format Specification""" + def rstrip_invalid_version_chars(s: str) -> str: + valid_version_chars = {*string.ascii_letters, *string.digits, "~", "-", "^", "."} + for i, c in enumerate(s): + if c in valid_version_chars: + return s[i:] + return "" + + def digit_prefix(s: str) -> str: + return "".join(takewhile(lambda c: c in string.digits, s)) + + def letter_prefix(s: str) -> str: + return "".join(takewhile(lambda c: c in string.ascii_letters, s)) + + while True: + # Any characters which are outside of the set of listed above (a-z, A-Z, 0-9, -, ., ~, + # ^) are skipped in both strings. In particular, this means that non-ASCII characters + # that are Unicode digits or letters are skipped too. + v1 = rstrip_invalid_version_chars(v1) + v2 = rstrip_invalid_version_chars(v2) + + # If the remaining part of one of strings starts with "~": if other remaining part does + # not start with ~, the string with ~ compares lower. Otherwise, both tilde characters + # are skipped. + + if v1.startswith("~") and v2.startswith("~"): + v1 = v1.removeprefix("~") + v2 = v2.removeprefix("~") + elif v1.startswith("~"): + return cls._LEFT_SMALLER + elif v2.startswith("~"): + return cls._RIGHT_SMALLER + + # If one of the strings has ended: if the other string hasn’t, the string that has + # remaining characters compares higher. Otherwise, the strings compare equal. + + if not v1 and not v2: + return cls._EQUAL + elif not v1 and v2: + return cls._LEFT_SMALLER + elif v1 and not v2: + return cls._RIGHT_SMALLER + + # If the remaining part of one of strings starts with "-": if the other remaining part + # does not start with -, the string with - compares lower. Otherwise, both minus + # characters are skipped. + + if v1.startswith("-") and v2.startswith("-"): + v1 = v1.removeprefix("-") + v2 = v2.removeprefix("-") + elif v1.startswith("-"): + return cls._LEFT_SMALLER + elif v2.startswith("-"): + return cls._RIGHT_SMALLER + + # If the remaining part of one of strings starts with "^": if the other remaining part + # does not start with ^, the string with ^ compares higher. Otherwise, both caret + # characters are skipped. + + if v1.startswith("^") and v2.startswith("^"): + v1 = v1.removeprefix("^") + v2 = v2.removeprefix("^") + elif v1.startswith("^"): + # TODO: bug? + return cls._LEFT_SMALLER #cls._RIGHT_SMALLER + elif v2.startswith("^"): + return cls._RIGHT_SMALLER #cls._LEFT_SMALLER + + # If the remaining part of one of strings starts with ".": if the other remaining part + # does not start with ., the string with . compares lower. Otherwise, both dot + # characters are skipped. + + if v1.startswith(".") and v2.startswith("."): + v1 = v1.removeprefix(".") + v2 = v2.removeprefix(".") + elif v1.startswith("."): + return cls._LEFT_SMALLER + elif v2.startswith("."): + return cls._RIGHT_SMALLER + + # If either of the remaining parts starts with a digit: numerical prefixes are compared + # numerically. Any leading zeroes are skipped. The numerical prefixes (until the first + # non-digit character) are evaluated as numbers. If one of the prefixes is empty, it + # evaluates as 0. If the numbers are different, the string with the bigger number + # compares higher. Otherwise, the comparison continues at the following characters at + # point 1. + + v1_digit_prefix = digit_prefix(v1) + v2_digit_prefix = digit_prefix(v2) + + if v1_digit_prefix or v2_digit_prefix: + v1_digits = int(v1_digit_prefix) if v1_digit_prefix else 0 + v2_digits = int(v2_digit_prefix) if v2_digit_prefix else 0 + + if v1_digits < v2_digits: + return cls._LEFT_SMALLER + elif v1_digits > v2_digits: + return cls._RIGHT_SMALLER + + v1 = v1.removeprefix(v1_digit_prefix) + v2 = v2.removeprefix(v2_digit_prefix) + continue + + # Leading alphabetical prefixes are compared alphabetically. The substrings are + # compared letter-by-letter. If both letters are the same, the comparison continues + # with the next letter. Capital letters compare lower than lower-case letters (A < + # a). When the end of one substring has been reached (a non-letter character or the end + # of the whole string), if the other substring has remaining letters, it compares + # higher. Otherwise, the comparison continues at the following characters at point 1. + + v1_letter_prefix = letter_prefix(v1) + v2_letter_prefix = letter_prefix(v2) + + if v1_letter_prefix < v2_letter_prefix: + return cls._LEFT_SMALLER + elif v1_letter_prefix > v2_letter_prefix: + return cls._RIGHT_SMALLER + + v1 = v1.removeprefix(v1_letter_prefix) + v2 = v2.removeprefix(v2_letter_prefix) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GenericVersion): + return False + return self.compare_versions(self._version, other._version) == self._EQUAL + + def __ne__(self, other: object) -> bool: + if not isinstance(other, GenericVersion): + return False + return self.compare_versions(self._version, other._version) != self._EQUAL + + def __lt__(self, other: object) -> bool: + if not isinstance(other, GenericVersion): + return False + return self.compare_versions(self._version, other._version) == self._LEFT_SMALLER + + def __le__(self, other: object) -> bool: + if not isinstance(other, GenericVersion): + return False + return self.compare_versions(self._version, other._version) in (self._EQUAL, self._LEFT_SMALLER) + + def __gt__(self, other: object) -> bool: + if not isinstance(other, GenericVersion): + return False + return self.compare_versions(self._version, other._version) == self._RIGHT_SMALLER + + def __ge__(self, other: object) -> bool: + if not isinstance(other, GenericVersion): + return False + return self.compare_versions(self._version, other._version) in (self._EQUAL, self._RIGHT_SMALLER) diff --git a/tests/test_config.py b/tests/test_config.py index 441f052c1..d61c93744 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,10 +1,6 @@ # SPDX-License-Identifier: LGPL-2.1+ -import itertools - -import pytest - -from mkosi.config import Compression, GenericVersion +from mkosi.config import Compression def test_compression_enum_creation() -> None: @@ -35,222 +31,3 @@ def test_compression_enum_str() -> None: assert str(Compression.gz) == "gz" assert str(Compression.lz4) == "lz4" assert str(Compression.lzma) == "lzma" - - -def test_generic_version_systemd() -> None: - """Same as the first block of systemd/test/test-compare-versions.sh""" - assert GenericVersion("1") < GenericVersion("2") - assert GenericVersion("1") <= GenericVersion("2") - assert GenericVersion("1") != GenericVersion("2") - assert not (GenericVersion("1") > GenericVersion("2")) - assert not (GenericVersion("1") == GenericVersion("2")) - assert not (GenericVersion("1") >= GenericVersion("2")) - assert GenericVersion.compare_versions("1", "2") == -1 - assert GenericVersion.compare_versions("2", "2") == 0 - assert GenericVersion.compare_versions("2", "1") == 1 - - -def test_generic_version_spec() -> None: - """Examples from the uapi group version format spec""" - assert GenericVersion("11") == GenericVersion("11") - assert GenericVersion("systemd-123") == GenericVersion("systemd-123") - assert GenericVersion("bar-123") < GenericVersion("foo-123") - assert GenericVersion("123a") > GenericVersion("123") - assert GenericVersion("123.a") > GenericVersion("123") - assert GenericVersion("123.a") < GenericVersion("123.b") - assert GenericVersion("123a") > GenericVersion("123.a") - assert GenericVersion("11α") == GenericVersion("11β") - assert GenericVersion("A") < GenericVersion("a") - assert GenericVersion("") < GenericVersion("0") - assert GenericVersion("0.") > GenericVersion("0") - assert GenericVersion("0.0") > GenericVersion("0") - assert GenericVersion("0") > GenericVersion("~") - assert GenericVersion("") > GenericVersion("~") - assert GenericVersion("1_") == GenericVersion("1") - assert GenericVersion("_1") == GenericVersion("1") - assert GenericVersion("1_") < GenericVersion("1.2") - assert GenericVersion("1_2_3") > GenericVersion("1.3.3") - assert GenericVersion("1+") == GenericVersion("1") - assert GenericVersion("+1") == GenericVersion("1") - assert GenericVersion("1+") < GenericVersion("1.2") - assert GenericVersion("1+2+3") > GenericVersion("1.3.3") - - -@pytest.mark.parametrize( - "s1,s2", - itertools.combinations_with_replacement( - enumerate( - [ - GenericVersion("122.1"), - GenericVersion("123~rc1-1"), - GenericVersion("123"), - GenericVersion("123-a"), - GenericVersion("123-a.1"), - GenericVersion("123-1"), - GenericVersion("123-1.1"), - GenericVersion("123^post1"), - GenericVersion("123.a-1"), - GenericVersion("123.1-1"), - GenericVersion("123a-1"), - GenericVersion("124-1"), - ], - ), - 2 - ) -) -def test_generic_version_strverscmp_improved_doc(s1: tuple[int, GenericVersion], s2: tuple[int, GenericVersion]) -> None: - """Example from the doc string of strverscmp_improved in systemd/src/fundamental/string-util-fundamental.c""" - i1, v1 = s1 - i2, v2 = s2 - assert (v1 == v2) == (i1 == i2) - assert (v1 < v2) == (i1 < i2) - assert (v1 <= v2) == (i1 <= i2) - assert (v1 > v2) == (i1 > i2) - assert (v1 >= v2) == (i1 >= i2) - assert (v1 != v2) == (i1 != i2) - - -def RPMVERCMP(a: str, b: str, expected: int) -> None: - assert (GenericVersion(a) > GenericVersion(b)) - (GenericVersion(a) < GenericVersion(b)) == expected - - -def test_generic_version_rpmvercmp() -> None: - # Tests copied from rpm's rpmio test suite, under the LGPL license: - # https://github.com/rpm-software-management/rpm/blob/master/tests/rpmvercmp.at. - # The original form is retained as much as possible for easy comparisons and updates. - - RPMVERCMP("1.0", "1.0", 0) - RPMVERCMP("1.0", "2.0", -1) - RPMVERCMP("2.0", "1.0", 1) - - RPMVERCMP("2.0.1", "2.0.1", 0) - RPMVERCMP("2.0", "2.0.1", -1) - RPMVERCMP("2.0.1", "2.0", 1) - - RPMVERCMP("2.0.1a", "2.0.1a", 0) - RPMVERCMP("2.0.1a", "2.0.1", 1) - RPMVERCMP("2.0.1", "2.0.1a", -1) - - RPMVERCMP("5.5p1", "5.5p1", 0) - RPMVERCMP("5.5p1", "5.5p2", -1) - RPMVERCMP("5.5p2", "5.5p1", 1) - - RPMVERCMP("5.5p10", "5.5p10", 0) - RPMVERCMP("5.5p1", "5.5p10", -1) - RPMVERCMP("5.5p10", "5.5p1", 1) - - RPMVERCMP("10xyz", "10.1xyz", 1) # Note: this is reversed from rpm's vercmp */ - RPMVERCMP("10.1xyz", "10xyz", -1) # Note: this is reversed from rpm's vercmp */ - - RPMVERCMP("xyz10", "xyz10", 0) - RPMVERCMP("xyz10", "xyz10.1", -1) - RPMVERCMP("xyz10.1", "xyz10", 1) - - RPMVERCMP("xyz.4", "xyz.4", 0) - RPMVERCMP("xyz.4", "8", -1) - RPMVERCMP("8", "xyz.4", 1) - RPMVERCMP("xyz.4", "2", -1) - RPMVERCMP("2", "xyz.4", 1) - - RPMVERCMP("5.5p2", "5.6p1", -1) - RPMVERCMP("5.6p1", "5.5p2", 1) - - RPMVERCMP("5.6p1", "6.5p1", -1) - RPMVERCMP("6.5p1", "5.6p1", 1) - - RPMVERCMP("6.0.rc1", "6.0", 1) - RPMVERCMP("6.0", "6.0.rc1", -1) - - RPMVERCMP("10b2", "10a1", 1) - RPMVERCMP("10a2", "10b2", -1) - - RPMVERCMP("1.0aa", "1.0aa", 0) - RPMVERCMP("1.0a", "1.0aa", -1) - RPMVERCMP("1.0aa", "1.0a", 1) - - RPMVERCMP("10.0001", "10.0001", 0) - RPMVERCMP("10.0001", "10.1", 0) - RPMVERCMP("10.1", "10.0001", 0) - RPMVERCMP("10.0001", "10.0039", -1) - RPMVERCMP("10.0039", "10.0001", 1) - - RPMVERCMP("4.999.9", "5.0", -1) - RPMVERCMP("5.0", "4.999.9", 1) - - RPMVERCMP("20101121", "20101121", 0) - RPMVERCMP("20101121", "20101122", -1) - RPMVERCMP("20101122", "20101121", 1) - - RPMVERCMP("2_0", "2_0", 0) - RPMVERCMP("2.0", "2_0", -1) # Note: in rpm those compare equal - RPMVERCMP("2_0", "2.0", 1) # Note: in rpm those compare equal - - # RhBug:178798 case */ - RPMVERCMP("a", "a", 0) - RPMVERCMP("a+", "a+", 0) - RPMVERCMP("a+", "a_", 0) - RPMVERCMP("a_", "a+", 0) - RPMVERCMP("+a", "+a", 0) - RPMVERCMP("+a", "_a", 0) - RPMVERCMP("_a", "+a", 0) - RPMVERCMP("+_", "+_", 0) - RPMVERCMP("_+", "+_", 0) - RPMVERCMP("_+", "_+", 0) - RPMVERCMP("+", "_", 0) - RPMVERCMP("_", "+", 0) - - # Basic testcases for tilde sorting - RPMVERCMP("1.0~rc1", "1.0~rc1", 0) - RPMVERCMP("1.0~rc1", "1.0", -1) - RPMVERCMP("1.0", "1.0~rc1", 1) - RPMVERCMP("1.0~rc1", "1.0~rc2", -1) - RPMVERCMP("1.0~rc2", "1.0~rc1", 1) - RPMVERCMP("1.0~rc1~git123", "1.0~rc1~git123", 0) - RPMVERCMP("1.0~rc1~git123", "1.0~rc1", -1) - RPMVERCMP("1.0~rc1", "1.0~rc1~git123", 1) - - # Basic testcases for caret sorting - RPMVERCMP("1.0^", "1.0^", 0) - RPMVERCMP("1.0^", "1.0", 1) - RPMVERCMP("1.0", "1.0^", -1) - RPMVERCMP("1.0^git1", "1.0^git1", 0) - RPMVERCMP("1.0^git1", "1.0", 1) - RPMVERCMP("1.0", "1.0^git1", -1) - RPMVERCMP("1.0^git1", "1.0^git2", -1) - RPMVERCMP("1.0^git2", "1.0^git1", 1) - RPMVERCMP("1.0^git1", "1.01", -1) - RPMVERCMP("1.01", "1.0^git1", 1) - RPMVERCMP("1.0^20160101", "1.0^20160101", 0) - RPMVERCMP("1.0^20160101", "1.0.1", -1) - RPMVERCMP("1.0.1", "1.0^20160101", 1) - RPMVERCMP("1.0^20160101^git1", "1.0^20160101^git1", 0) - RPMVERCMP("1.0^20160102", "1.0^20160101^git1", 1) - RPMVERCMP("1.0^20160101^git1", "1.0^20160102", -1) - - # Basic testcases for tilde and caret sorting */ - RPMVERCMP("1.0~rc1^git1", "1.0~rc1^git1", 0) - RPMVERCMP("1.0~rc1^git1", "1.0~rc1", 1) - RPMVERCMP("1.0~rc1", "1.0~rc1^git1", -1) - RPMVERCMP("1.0^git1~pre", "1.0^git1~pre", 0) - RPMVERCMP("1.0^git1", "1.0^git1~pre", 1) - RPMVERCMP("1.0^git1~pre", "1.0^git1", -1) - - # These are included here to document current, arguably buggy behaviors - # for reference purposes and for easy checking against unintended - # behavior changes. */ - print("/* RPM version comparison oddities */") - # RhBug:811992 case - RPMVERCMP("1b.fc17", "1b.fc17", 0) - RPMVERCMP("1b.fc17", "1.fc17", 1) # Note: this is reversed from rpm's vercmp, WAT! */ - RPMVERCMP("1.fc17", "1b.fc17", -1) - RPMVERCMP("1g.fc17", "1g.fc17", 0) - RPMVERCMP("1g.fc17", "1.fc17", 1) - RPMVERCMP("1.fc17", "1g.fc17", -1) - - # Non-ascii characters are considered equal so these are all the same, eh… */ - RPMVERCMP("1.1.α", "1.1.α", 0) - RPMVERCMP("1.1.α", "1.1.β", 0) - RPMVERCMP("1.1.β", "1.1.α", 0) - RPMVERCMP("1.1.αα", "1.1.α", 0) - RPMVERCMP("1.1.α", "1.1.ββ", 0) - RPMVERCMP("1.1.ββ", "1.1.αα", 0) diff --git a/tests/test_versioncomp.py b/tests/test_versioncomp.py new file mode 100644 index 000000000..b6f8e8c94 --- /dev/null +++ b/tests/test_versioncomp.py @@ -0,0 +1,225 @@ +# SPDX-License-Identifier: LGPL-2.1+ +import itertools + +import pytest + +from mkosi.versioncomp import GenericVersion + + +def test_generic_version_systemd() -> None: + """Same as the first block of systemd/test/test-compare-versions.sh""" + assert GenericVersion("1") < GenericVersion("2") + assert GenericVersion("1") <= GenericVersion("2") + assert GenericVersion("1") != GenericVersion("2") + assert not (GenericVersion("1") > GenericVersion("2")) + assert not (GenericVersion("1") == GenericVersion("2")) + assert not (GenericVersion("1") >= GenericVersion("2")) + assert GenericVersion.compare_versions("1", "2") == -1 + assert GenericVersion.compare_versions("2", "2") == 0 + assert GenericVersion.compare_versions("2", "1") == 1 + + +def test_generic_version_spec() -> None: + """Examples from the uapi group version format spec""" + assert GenericVersion("11") == GenericVersion("11") + assert GenericVersion("systemd-123") == GenericVersion("systemd-123") + assert GenericVersion("bar-123") < GenericVersion("foo-123") + assert GenericVersion("123a") > GenericVersion("123") + assert GenericVersion("123.a") > GenericVersion("123") + assert GenericVersion("123.a") < GenericVersion("123.b") + assert GenericVersion("123a") > GenericVersion("123.a") + assert GenericVersion("11α") == GenericVersion("11β") + assert GenericVersion("A") < GenericVersion("a") + assert GenericVersion("") < GenericVersion("0") + assert GenericVersion("0.") > GenericVersion("0") + assert GenericVersion("0.0") > GenericVersion("0") + assert GenericVersion("0") > GenericVersion("~") + assert GenericVersion("") > GenericVersion("~") + assert GenericVersion("1_") == GenericVersion("1") + assert GenericVersion("_1") == GenericVersion("1") + assert GenericVersion("1_") < GenericVersion("1.2") + assert GenericVersion("1_2_3") > GenericVersion("1.3.3") + assert GenericVersion("1+") == GenericVersion("1") + assert GenericVersion("+1") == GenericVersion("1") + assert GenericVersion("1+") < GenericVersion("1.2") + assert GenericVersion("1+2+3") > GenericVersion("1.3.3") + + +@pytest.mark.parametrize( + "s1,s2", + itertools.combinations_with_replacement( + enumerate( + [ + GenericVersion("122.1"), + GenericVersion("123~rc1-1"), + GenericVersion("123"), + GenericVersion("123-a"), + GenericVersion("123-a.1"), + GenericVersion("123-1"), + GenericVersion("123-1.1"), + GenericVersion("123^post1"), + GenericVersion("123.a-1"), + GenericVersion("123.1-1"), + GenericVersion("123a-1"), + GenericVersion("124-1"), + ], + ), + 2 + ) +) +def test_generic_version_strverscmp_improved_doc(s1: tuple[int, GenericVersion], s2: tuple[int, GenericVersion]) -> None: + """Example from the doc string of strverscmp_improved in systemd/src/fundamental/string-util-fundamental.c""" + i1, v1 = s1 + i2, v2 = s2 + assert (v1 == v2) == (i1 == i2) + assert (v1 < v2) == (i1 < i2) + assert (v1 <= v2) == (i1 <= i2) + assert (v1 > v2) == (i1 > i2) + assert (v1 >= v2) == (i1 >= i2) + assert (v1 != v2) == (i1 != i2) + + +def RPMVERCMP(a: str, b: str, expected: int) -> None: + assert (GenericVersion(a) > GenericVersion(b)) - (GenericVersion(a) < GenericVersion(b)) == expected + + +def test_generic_version_rpmvercmp() -> None: + # Tests copied from rpm's rpmio test suite, under the LGPL license: + # https://github.com/rpm-software-management/rpm/blob/master/tests/rpmvercmp.at. + # The original form is retained as much as possible for easy comparisons and updates. + + RPMVERCMP("1.0", "1.0", 0) + RPMVERCMP("1.0", "2.0", -1) + RPMVERCMP("2.0", "1.0", 1) + + RPMVERCMP("2.0.1", "2.0.1", 0) + RPMVERCMP("2.0", "2.0.1", -1) + RPMVERCMP("2.0.1", "2.0", 1) + + RPMVERCMP("2.0.1a", "2.0.1a", 0) + RPMVERCMP("2.0.1a", "2.0.1", 1) + RPMVERCMP("2.0.1", "2.0.1a", -1) + + RPMVERCMP("5.5p1", "5.5p1", 0) + RPMVERCMP("5.5p1", "5.5p2", -1) + RPMVERCMP("5.5p2", "5.5p1", 1) + + RPMVERCMP("5.5p10", "5.5p10", 0) + RPMVERCMP("5.5p1", "5.5p10", -1) + RPMVERCMP("5.5p10", "5.5p1", 1) + + RPMVERCMP("10xyz", "10.1xyz", 1) # Note: this is reversed from rpm's vercmp */ + RPMVERCMP("10.1xyz", "10xyz", -1) # Note: this is reversed from rpm's vercmp */ + + RPMVERCMP("xyz10", "xyz10", 0) + RPMVERCMP("xyz10", "xyz10.1", -1) + RPMVERCMP("xyz10.1", "xyz10", 1) + + RPMVERCMP("xyz.4", "xyz.4", 0) + RPMVERCMP("xyz.4", "8", -1) + RPMVERCMP("8", "xyz.4", 1) + RPMVERCMP("xyz.4", "2", -1) + RPMVERCMP("2", "xyz.4", 1) + + RPMVERCMP("5.5p2", "5.6p1", -1) + RPMVERCMP("5.6p1", "5.5p2", 1) + + RPMVERCMP("5.6p1", "6.5p1", -1) + RPMVERCMP("6.5p1", "5.6p1", 1) + + RPMVERCMP("6.0.rc1", "6.0", 1) + RPMVERCMP("6.0", "6.0.rc1", -1) + + RPMVERCMP("10b2", "10a1", 1) + RPMVERCMP("10a2", "10b2", -1) + + RPMVERCMP("1.0aa", "1.0aa", 0) + RPMVERCMP("1.0a", "1.0aa", -1) + RPMVERCMP("1.0aa", "1.0a", 1) + + RPMVERCMP("10.0001", "10.0001", 0) + RPMVERCMP("10.0001", "10.1", 0) + RPMVERCMP("10.1", "10.0001", 0) + RPMVERCMP("10.0001", "10.0039", -1) + RPMVERCMP("10.0039", "10.0001", 1) + + RPMVERCMP("4.999.9", "5.0", -1) + RPMVERCMP("5.0", "4.999.9", 1) + + RPMVERCMP("20101121", "20101121", 0) + RPMVERCMP("20101121", "20101122", -1) + RPMVERCMP("20101122", "20101121", 1) + + RPMVERCMP("2_0", "2_0", 0) + RPMVERCMP("2.0", "2_0", -1) # Note: in rpm those compare equal + RPMVERCMP("2_0", "2.0", 1) # Note: in rpm those compare equal + + # RhBug:178798 case */ + RPMVERCMP("a", "a", 0) + RPMVERCMP("a+", "a+", 0) + RPMVERCMP("a+", "a_", 0) + RPMVERCMP("a_", "a+", 0) + RPMVERCMP("+a", "+a", 0) + RPMVERCMP("+a", "_a", 0) + RPMVERCMP("_a", "+a", 0) + RPMVERCMP("+_", "+_", 0) + RPMVERCMP("_+", "+_", 0) + RPMVERCMP("_+", "_+", 0) + RPMVERCMP("+", "_", 0) + RPMVERCMP("_", "+", 0) + + # Basic testcases for tilde sorting + RPMVERCMP("1.0~rc1", "1.0~rc1", 0) + RPMVERCMP("1.0~rc1", "1.0", -1) + RPMVERCMP("1.0", "1.0~rc1", 1) + RPMVERCMP("1.0~rc1", "1.0~rc2", -1) + RPMVERCMP("1.0~rc2", "1.0~rc1", 1) + RPMVERCMP("1.0~rc1~git123", "1.0~rc1~git123", 0) + RPMVERCMP("1.0~rc1~git123", "1.0~rc1", -1) + RPMVERCMP("1.0~rc1", "1.0~rc1~git123", 1) + + # Basic testcases for caret sorting + RPMVERCMP("1.0^", "1.0^", 0) + RPMVERCMP("1.0^", "1.0", 1) + RPMVERCMP("1.0", "1.0^", -1) + RPMVERCMP("1.0^git1", "1.0^git1", 0) + RPMVERCMP("1.0^git1", "1.0", 1) + RPMVERCMP("1.0", "1.0^git1", -1) + RPMVERCMP("1.0^git1", "1.0^git2", -1) + RPMVERCMP("1.0^git2", "1.0^git1", 1) + RPMVERCMP("1.0^git1", "1.01", -1) + RPMVERCMP("1.01", "1.0^git1", 1) + RPMVERCMP("1.0^20160101", "1.0^20160101", 0) + RPMVERCMP("1.0^20160101", "1.0.1", -1) + RPMVERCMP("1.0.1", "1.0^20160101", 1) + RPMVERCMP("1.0^20160101^git1", "1.0^20160101^git1", 0) + RPMVERCMP("1.0^20160102", "1.0^20160101^git1", 1) + RPMVERCMP("1.0^20160101^git1", "1.0^20160102", -1) + + # Basic testcases for tilde and caret sorting */ + RPMVERCMP("1.0~rc1^git1", "1.0~rc1^git1", 0) + RPMVERCMP("1.0~rc1^git1", "1.0~rc1", 1) + RPMVERCMP("1.0~rc1", "1.0~rc1^git1", -1) + RPMVERCMP("1.0^git1~pre", "1.0^git1~pre", 0) + RPMVERCMP("1.0^git1", "1.0^git1~pre", 1) + RPMVERCMP("1.0^git1~pre", "1.0^git1", -1) + + # These are included here to document current, arguably buggy behaviors + # for reference purposes and for easy checking against unintended + # behavior changes. */ + print("/* RPM version comparison oddities */") + # RhBug:811992 case + RPMVERCMP("1b.fc17", "1b.fc17", 0) + RPMVERCMP("1b.fc17", "1.fc17", 1) # Note: this is reversed from rpm's vercmp, WAT! */ + RPMVERCMP("1.fc17", "1b.fc17", -1) + RPMVERCMP("1g.fc17", "1g.fc17", 0) + RPMVERCMP("1g.fc17", "1.fc17", 1) + RPMVERCMP("1.fc17", "1g.fc17", -1) + + # Non-ascii characters are considered equal so these are all the same, eh… */ + RPMVERCMP("1.1.α", "1.1.α", 0) + RPMVERCMP("1.1.α", "1.1.β", 0) + RPMVERCMP("1.1.β", "1.1.α", 0) + RPMVERCMP("1.1.αα", "1.1.α", 0) + RPMVERCMP("1.1.α", "1.1.ββ", 0) + RPMVERCMP("1.1.ββ", "1.1.αα", 0)