[Git][reproducible-builds/diffoscope][master] 10 commits: util: add a version comparison class based on rpm version rules

Chris Lamb (@lamby) gitlab at salsa.debian.org
Tue Sep 14 08:07:45 UTC 2021



Chris Lamb pushed to branch master at Reproducible Builds / diffoscope


Commits:
fce00cf6 by Zbigniew Jędrzejewski-Szmek at 2021-09-14T07:57:32+00:00
util: add a version comparison class based on rpm version rules

This (strives to) implement the same algorithm that rpm uses [1].
All strings are accepted, and the answer is always -1, 0, or +1.
This should be also compatible with Debian rules: Debian doesn't
allow ^ in versions, but otherwise follows similar rules. The addition
of ~ in rpm was modelled after Debian. If Debian adds support for ^
in the future, hopefully it'll be in a way that is compatible.
(rpm and deb have different rules how to split the package name
version parts, but this shouldn't matter for us, since we're only
looking at the version part here.)

For all not overly complicated strings this should generally return the
result that people expect. For some of the corner cases the results
might be nonobvious, but I think it's better to follow one established
standard than to try to come up with something fresh.

[1] So… I should put a link to some docs here. But I don't think
    the are is any reference for this. Oh, rpm :---[
    https://github.com/rpm-software-management/rpm/blob/master/rpmio/rpmvercmp.c#L12

[2] https://www.debian.org/doc/debian-policy/ch-controlfields.html#version

Note on the copyright: I wrote this from scratch based on my
understanding of the rules and examples online and the (scant)
documentation that is online. The example list is copied from
rpm tests, but the way that the examples are structured and invoked
is completely different. The only part that was carried over are
the version strings to test, so I think it's reasonable to treat
this as an indepedent work.

- - - - -
fef2375b by Zbigniew Jędrzejewski-Szmek at 2021-09-14T07:57:32+00:00
Stop using deprecated distutils.spawn.find_executable

distutils is deprecated and slated for removal. Every import of the
module raises a warning with python3.10.

PEP 632 says to use shutil.which instead.

- - - - -
beebe3fd by Zbigniew Jędrzejewski-Szmek at 2021-09-14T07:57:32+00:00
comparators: improve extraction of ssh version

E.g. Fedora has "OpenSSH_8.7p1, OpenSSL 1.1.1l  FIPS 24 Aug 2021", which
would give "8.7p1," as the version.

- - - - -
953a599c by Zbigniew Jędrzejewski-Szmek at 2021-09-14T07:57:32+00:00
Replace distutils.LooseVersion by Version

Every import of distuils now raises a warning, and the module is
slated for removal.

The recommended replacement is packaging.version. It has Version and
LegacyVersion. LegacyVersion documents itself as "This class
implements the previous de facto sorting algorithm used by
setuptools". (Comparisons of packaging.LegacyVersion and
packaging.Version are intentionally borked, so packaging.LegacyVersion
should not be mixed with packaging.Version or packaging.parse(), which
can return packaging.Version.) But use of LegacyVersion also raises
a deprecation warning, with removal slated for the next release.

The runtime requirement on 'distutils' is dropped, since it is not
needed any more.

- - - - -
4c3109cb by Zbigniew Jędrzejewski-Szmek at 2021-09-14T07:57:32+00:00
utils: unbork comparisons of file versions

String comparisons don't really work for version strings.

- - - - -
b2b1f731 by Zbigniew Jędrzejewski-Szmek at 2021-09-14T07:57:32+00:00
Adjust invocations of llvm-objdump

With llvm-13.0.0~rc1, we get the following error:

Command `'llvm-objdump --arch-name x86_64 --section __TEXT,__text --macho --demangle --no-leading-addr --no-show-raw-insn {}'` failed with exit code 1. Standard output:
│┄     llvm-objdump: error: unknown argument '--arch-name'

--arch-name=… works with both the new llvm and older ones.
I thought this would fix #275, but it turns out it's not enough.

- - - - -
258d09a4 by Zbigniew Jędrzejewski-Szmek at 2021-09-14T07:57:32+00:00
tests: adjust test_llmv_diff for llvm-13

It seems the output changes a bit. Fixes #275.

- - - - -
e6024b3b by Chris Lamb at 2021-09-14T09:03:28+01:00
Reformat diffoscope.versions.

- - - - -
a33cf81e by Chris Lamb at 2021-09-14T09:04:29+01:00
Reformat with Black.

- - - - -
2fae7e85 by Chris Lamb at 2021-09-14T09:06:58+01:00
Move diffoscope.versions to diffoscope.tests.utils.versions.

- - - - -


12 changed files:

- debian/control
- diffoscope/comparators/macho.py
- diffoscope/tools.py
- tests/comparators/test_macho.py
- tests/comparators/test_openssh_pub_key.py
- tests/comparators/test_rlib.py
- tests/data/macho_llvm_expected_diff_symbols
- + tests/data/macho_llvm_expected_diff_symbols_llvm_11
- tests/test_progress.py
- + tests/test_versions.py
- tests/utils/tools.py
- + tests/utils/versions.py


Changes:

=====================================
debian/control
=====================================
@@ -100,7 +100,6 @@ Vcs-Browser: https://salsa.debian.org/reproducible-builds/diffoscope
 Package: diffoscope-minimal
 Architecture: all
 Depends:
- python3-distutils,
  python3-pkg-resources,
  ${misc:Depends},
  ${python3:Depends},


=====================================
diffoscope/comparators/macho.py
=====================================
@@ -294,10 +294,8 @@ class LlvmObjdump(Command):
 
     def objdump_options(self):
         return [
-            "--arch-name",
-            self._arch,
-            "--section",
-            self._section,
+            f"--arch-name={self._arch}",
+            f"--section={self._section}",
             "--macho",
             "--demangle",
             "--no-leading-addr",


=====================================
diffoscope/tools.py
=====================================
@@ -19,20 +19,18 @@
 import collections
 import functools
 import platform
+import shutil
 
 try:
     import distro
 except ImportError:
     distro = None
 
-from distutils.spawn import find_executable
-
 from .profiling import profile
 from .external_tools import EXTERNAL_TOOLS, REMAPPED_TOOL_NAMES, GNU_TOOL_NAMES
 
-# Memoize calls to ``distutils.spawn.find_executable`` to avoid excessive stat
-# calls
-find_executable = functools.lru_cache()(find_executable)
+# Memoize calls to ``which`` to avoid excessive stat calls
+find_executable = functools.lru_cache()(shutil.which)
 
 # The output of --help and --list-tools will use the order of this dict.
 # Please keep it alphabetized.


=====================================
tests/comparators/test_macho.py
=====================================
@@ -27,6 +27,7 @@ from diffoscope.comparators.missing_file import MissingFile
 
 from ..utils.data import load_fixture, assert_diff
 from ..utils.tools import skip_unless_tools_exist
+from .test_rlib import llvm_version
 
 
 obj1 = load_fixture("test1.macho")
@@ -93,13 +94,18 @@ def test_llvm_obj_compare_non_existing(monkeypatch, obj1):
 
 @skip_unless_tools_exist("llvm-readobj", "llvm-objdump")
 def test_llvm_diff(obj_differences):
+    if llvm_version() < "13":
+        diff_symbols = "macho_llvm_expected_diff_symbols_llvm_11"
+    else:
+        diff_symbols = "macho_llvm_expected_diff_symbols"
+
     # Headers
     assert len(obj_differences) == 8
     filenames = [
         "macho_llvm_expected_diff_strings",
         "macho_llvm_expected_diff_file_headers",
         "macho_llvm_expected_diff_needed_libs",
-        "macho_llvm_expected_diff_symbols",
+        diff_symbols,
         "macho_llvm_expected_diff_dyn_symbols",
         "macho_llvm_expected_diff_relocations",
         "macho_llvm_expected_diff_dyn_relocations",


=====================================
tests/comparators/test_openssh_pub_key.py
=====================================
@@ -19,6 +19,7 @@
 
 import pytest
 import subprocess
+import re
 
 from diffoscope.config import Config
 from diffoscope.comparators.openssh import PublicKeyFile
@@ -36,7 +37,7 @@ opensshpubkey2 = load_fixture("test_openssh_pub_key2.pub")
 
 def openssh_version():
     out = subprocess.check_output(("ssh", "-V"), stderr=subprocess.STDOUT)
-    return out.decode().split()[0].split("_")[1]
+    return re.match(r"OpenSSH_([a-zA-Z0-9._+-]+)", out.decode()).group(1)
 
 
 def test_identification(opensshpubkey1):


=====================================
tests/comparators/test_rlib.py
=====================================
@@ -22,7 +22,6 @@ import pytest
 import subprocess
 
 from diffoscope.config import Config
-from distutils.version import LooseVersion
 from diffoscope.comparators.ar import ArFile
 
 from ..utils import diff_ignore_line_numbers
@@ -33,6 +32,7 @@ from ..utils.tools import (
     skip_if_binutils_does_not_support_x86,
 )
 from ..utils.nonexisting import assert_non_existing
+from ..utils.versions import Version
 
 
 rlib1 = load_fixture("test1.rlib")
@@ -46,7 +46,7 @@ def init_tests(request, monkeypatch):
 
 
 def llvm_version():
-    return (
+    return Version(
         subprocess.check_output(["llvm-config", "--version"])
         .decode("utf-8")
         .strip()
@@ -71,16 +71,16 @@ def differences(rlib1, rlib2):
 def rlib_dis_expected_diff():
     actual_ver = llvm_version()
 
-    if LooseVersion(str(actual_ver)) >= LooseVersion("3.8"):
+    if actual_ver >= "3.8":
         diff_file = "rlib_llvm_dis_expected_diff"
 
-    if LooseVersion(str(actual_ver)) >= LooseVersion("5.0"):
+    if actual_ver >= "5.0":
         diff_file = "rlib_llvm_dis_expected_diff_5"
 
-    if LooseVersion(str(actual_ver)) >= LooseVersion("7.0"):
+    if actual_ver >= "7.0":
         diff_file = "rlib_llvm_dis_expected_diff_7"
 
-    if LooseVersion(str(actual_ver)) >= LooseVersion("10.0"):
+    if actual_ver >= "10.0":
         diff_file = "rlib_llvm_dis_expected_diff_10"
 
     return get_data(diff_file)


=====================================
tests/data/macho_llvm_expected_diff_symbols
=====================================
@@ -66,7 +66,7 @@
    Symbol {
      Name: __mh_execute_header (2)
      Extern
-@@ -70,49 +11,37 @@
+@@ -70,49 +11,39 @@
      RefType: UndefinedNonLazy (0x0)
      Flags [ (0x10)
        ReferencedDynamically (0x10)
@@ -105,6 +105,7 @@
 -    Flags [ (0x200)
 -      AltEntry (0x200)
 +    Flags [ (0x100)
++      SymbolResolver (0x100)
      ]
      Value: 0x0
    }
@@ -118,6 +119,7 @@
 -    Flags [ (0x200)
 -      AltEntry (0x200)
 +    Flags [ (0x100)
++      SymbolResolver (0x100)
      ]
      Value: 0x0
    }


=====================================
tests/data/macho_llvm_expected_diff_symbols_llvm_11
=====================================
@@ -0,0 +1,124 @@
+@@ -1,67 +1,8 @@
+ 
+-Format: Mach-O 32-bit i386
+-Arch: i386
+-AddressSize: 32bit
+-Symbols [
+-  Symbol {
+-    Name: __mh_execute_header (2)
+-    Extern
+-    Type: Section (0xE)
+-    Section: __text (0x1)
+-    RefType: UndefinedNonLazy (0x0)
+-    Flags [ (0x10)
+-      ReferencedDynamically (0x10)
+-    ]
+-    Value: 0x1000
+-  }
+-  Symbol {
+-    Name: _i (22)
+-    Extern
+-    Type: Section (0xE)
+-    Section: __data (0x8)
+-    RefType: UndefinedNonLazy (0x0)
+-    Flags [ (0x0)
+-    ]
+-    Value: 0x200C
+-  }
+-  Symbol {
+-    Name: _main (25)
+-    Extern
+-    Type: Section (0xE)
+-    Section: __text (0x1)
+-    RefType: UndefinedNonLazy (0x0)
+-    Flags [ (0x0)
+-    ]
+-    Value: 0x1F30
+-  }
+-  Symbol {
+-    Name: _printf (31)
+-    Extern
+-    Type: Undef (0x0)
+-    Section:  (0x0)
+-    RefType: UndefinedNonLazy (0x0)
+-    Flags [ (0x200)
+-      AltEntry (0x200)
+-    ]
+-    Value: 0x0
+-  }
+-  Symbol {
+-    Name: dyld_stub_binder (39)
+-    Extern
+-    Type: Undef (0x0)
+-    Section:  (0x0)
+-    RefType: UndefinedNonLazy (0x0)
+-    Flags [ (0x200)
+-      AltEntry (0x200)
+-    ]
+-    Value: 0x0
+-  }
+-]
+-
+ Format: Mach-O 64-bit x86-64
+ Arch: x86_64
+ AddressSize: 64bit
+ Symbols [
+   Symbol {
+     Name: __mh_execute_header (2)
+     Extern
+@@ -70,49 +11,37 @@
+     RefType: UndefinedNonLazy (0x0)
+     Flags [ (0x10)
+       ReferencedDynamically (0x10)
+     ]
+     Value: 0x100000000
+   }
+   Symbol {
+-    Name: _i (22)
+-    Extern
+-    Type: Section (0xE)
+-    Section: __data (0x9)
+-    RefType: UndefinedNonLazy (0x0)
+-    Flags [ (0x0)
+-    ]
+-    Value: 0x100001018
+-  }
+-  Symbol {
+-    Name: _main (25)
++    Name: _main (22)
+     Extern
+     Type: Section (0xE)
+     Section: __text (0x1)
+     RefType: UndefinedNonLazy (0x0)
+     Flags [ (0x0)
+     ]
+-    Value: 0x100000F20
++    Value: 0x100000F40
+   }
+   Symbol {
+-    Name: _printf (31)
++    Name: _printf (28)
+     Extern
+     Type: Undef (0x0)
+     Section:  (0x0)
+     RefType: UndefinedNonLazy (0x0)
+-    Flags [ (0x200)
+-      AltEntry (0x200)
++    Flags [ (0x100)
+     ]
+     Value: 0x0
+   }
+   Symbol {
+-    Name: dyld_stub_binder (39)
++    Name: dyld_stub_binder (36)
+     Extern
+     Type: Undef (0x0)
+     Section:  (0x0)
+     RefType: UndefinedNonLazy (0x0)
+-    Flags [ (0x200)
+-      AltEntry (0x200)
++    Flags [ (0x100)
+     ]
+     Value: 0x0
+   }
+ ]


=====================================
tests/test_progress.py
=====================================
@@ -21,12 +21,12 @@ import sys
 import json
 import pytest
 
-from distutils.version import LooseVersion
-
 from diffoscope.main import main
 from diffoscope.progress import ProgressManager, StatusFD
 
 from .utils.tools import skip_unless_module_exists
+from .utils.versions import Version
+
 
 TEST_TAR1_PATH = os.path.join(os.path.dirname(__file__), "data", "test1.tar")
 TEST_TAR2_PATH = os.path.join(os.path.dirname(__file__), "data", "test2.tar")
@@ -56,7 +56,7 @@ def progressbar_err():
     actual_ver = progressbar_version()
 
     for k, v in expected_err.items():
-        if LooseVersion(actual_ver) < LooseVersion(k):
+        if Version(actual_ver) < Version(k):
             return v
 
     return ""


=====================================
tests/test_versions.py
=====================================
@@ -0,0 +1,176 @@
+#
+# diffoscope: in-depth comparison of files, archives, and directories
+#
+# Copyright © 2021      Zbigniew Jędrzejewski-Szmek <zbyszek at in.waw.pl>
+#
+# diffoscope is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# diffoscope is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with diffoscope.  If not, see <https://www.gnu.org/licenses/>.
+
+from .utils.versions import Version
+
+import pytest
+
+cases = [
+    ("1.0", "1.0", 0),
+    ("1.0", "2.0", -1),
+    ("2.0", "1.0", 1),
+    ("2.0.1", "2.0.1", 0),
+    ("2.0", "2.0.1", -1),
+    ("2.0.1", "2.0", 1),
+    ("2.0.1a", "2.0.1a", 0),
+    ("2.0.1a", "2.0.1", 1),
+    ("2.0.1", "2.0.1a", -1),
+    ("5.5p1", "5.5p1", 0),
+    ("5.5p1", "5.5p2", -1),
+    ("5.5p2", "5.5p1", 1),
+    ("5.5p10", "5.5p10", 0),
+    ("5.5p1", "5.5p10", -1),
+    ("5.5p10", "5.5p1", 1),
+    ("10xyz", "10.1xyz", -1),
+    ("10.1xyz", "10xyz", 1),
+    ("xyz10", "xyz10", 0),
+    ("xyz10", "xyz10.1", -1),
+    ("xyz10.1", "xyz10", 1),
+    ("xyz.4", "xyz.4", 0),
+    ("xyz.4", "8", -1),
+    ("8", "xyz.4", 1),
+    ("xyz.4", "2", -1),
+    ("2", "xyz.4", 1),
+    ("5.5p2", "5.6p1", -1),
+    ("5.6p1", "5.5p2", 1),
+    ("5.6p1", "6.5p1", -1),
+    ("6.5p1", "5.6p1", 1),
+    ("6.0.rc1", "6.0", 1),
+    ("6.0", "6.0.rc1", -1),
+    ("10b2", "10a1", 1),
+    ("10a2", "10b2", -1),
+    ("1.0aa", "1.0aa", 0),
+    ("1.0a", "1.0aa", -1),
+    ("1.0aa", "1.0a", 1),
+    ("10.0001", "10.0001", 0),
+    ("10.0001", "10.1", 0),
+    ("10.1", "10.0001", 0),
+    ("10.0001", "10.0039", -1),
+    ("10.0039", "10.0001", 1),
+    ("4.999.9", "5.0", -1),
+    ("5.0", "4.999.9", 1),
+    ("20101121", "20101121", 0),
+    ("20101121", "20101122", -1),
+    ("20101122", "20101121", 1),
+    ("2_0", "2_0", 0),
+    ("2.0", "2_0", 0),
+    ("2_0", "2.0", 0),
+    # Additional nastiness
+    ("2_0.", "2_0", 0),
+    ("2..0", "2_0__", 0),
+    ("2_0", "__2.0", 0),
+    ("2_1.", "2_0", +1),
+    ("2..1", "2_0__", +1),
+    ("2_1", "__2.0", +1),
+    ("2_1.", "2_2", -1),
+    ("2..1", "2_2__", -1),
+    ("2_1", "__2.2", -1),
+    # https://bugzilla.redhat.com/178798
+    ("a", "a", 0),
+    ("a+", "a+", 0),
+    ("a+", "a_", 0),
+    ("a_", "a+", 0),
+    ("+a", "+a", 0),
+    ("+a", "_a", 0),
+    ("_a", "+a", 0),
+    ("+_", "+_", 0),
+    ("_+", "+_", 0),
+    ("_+", "_+", 0),
+    ("+", "_", 0),
+    ("_", "+", 0),
+    # Basic testcases for tilde sorting
+    ("1.0~rc1", "1.0~rc1", 0),
+    ("1.0~rc1", "1.0", -1),
+    ("1.0", "1.0~rc1", 1),
+    ("1.0~rc1", "1.0~rc2", -1),
+    ("1.0~rc2", "1.0~rc1", 1),
+    ("1.0~rc1~git123", "1.0~rc1~git123", 0),
+    ("1.0~rc1~git123", "1.0~rc1", -1),
+    ("1.0~rc1", "1.0~rc1~git123", 1),
+    ("a", "a", 0),
+    ("a~", "a", -1),
+    ("a~~", "a", -1),
+    ("a~~~", "a", -1),
+    ("a~~~^", "a", -1),
+    ("a^", "a", +1),
+    ("a^", "a", +1),
+    ("a^", "a^", 0),
+    ("a^", "a^^", -1),
+    ("a^b", "a^^", +1),
+    # Basic testcases for caret sorting
+    ("1.0^", "1.0^", 0),
+    ("1.0^", "1.0", 1),
+    ("1.0", "1.0^", -1),
+    ("1.0^git1", "1.0^git1", 0),
+    ("1.0^git1", "1.0", 1),
+    ("1.0", "1.0^git1", -1),
+    ("1.0^git1", "1.0^git2", -1),
+    ("1.0^git2", "1.0^git1", 1),
+    ("1.0^git1", "1.01", -1),
+    ("1.01", "1.0^git1", 1),
+    ("1.0^20160101", "1.0^20160101", 0),
+    ("1.0^20160101", "1.0.1", -1),
+    ("1.0.1", "1.0^20160101", 1),
+    ("1.0^20160101^git1", "1.0^20160101^git1", 0),
+    ("1.0^20160102", "1.0^20160101^git1", 1),
+    ("1.0^20160101^git1", "1.0^20160102", -1),
+    # Basic testcases for tilde and caret sorting
+    ("1.0~rc1^git1", "1.0~rc1^git1", 0),
+    ("1.0~rc1^git1", "1.0~rc1", 1),
+    ("1.0~rc1", "1.0~rc1^git1", -1),
+    ("1.0^git1~pre", "1.0^git1~pre", 0),
+    ("1.0^git1", "1.0^git1~pre", 1),
+    ("1.0^git1~pre", "1.0^git1", -1),
+    # Additional testing for zeroes
+    ("0", "0", 0),
+    ("0", "00", 0),
+    ("0", "000", 0),
+    ("00", "000", 0),
+    ("000", "000", 0),
+    ("00", "0", 0),
+    ("000", "0", 0),
+    ("0", "0.0", -1),
+    ("0.0", "0", +1),
+    ("0~", "0", -1),
+    ("0^", "0", +1),
+    ("0^", "0^", 0),
+    ("0^", "0^~", 1),
+]
+
+
+ at pytest.mark.parametrize("a,b,expected", cases)
+def test_version_comparisons(a, b, expected):
+    assert Version(a)._cmp(b) == expected
+
+
+ at pytest.mark.parametrize("a,b", [c[:2] for c in cases if c[2] < 0])
+def test_version_lt(a, b):
+    assert Version(a) < b
+    assert Version(a) < Version(b)
+
+
+ at pytest.mark.parametrize("a,b", [c[:2] for c in cases if c[2] > 0])
+def test_version_gt(a, b):
+    assert Version(a) > b
+    assert Version(a) > Version(b)
+
+
+ at pytest.mark.parametrize("a,b", [c[:2] for c in cases if c[2] == 0])
+def test_version_gt(a, b):
+    assert Version(a) == b
+    assert Version(a) == Version(b)


=====================================
tests/utils/tools.py
=====================================
@@ -24,10 +24,9 @@ import functools
 import importlib.util
 import subprocess
 
-from distutils.spawn import find_executable
-from distutils.version import LooseVersion
+from diffoscope.tools import get_package_provider, find_executable
 
-from diffoscope.tools import get_package_provider
+from ..utils.versions import Version
 
 
 def file_version():
@@ -40,11 +39,11 @@ def file_version():
 
 
 def file_version_is_lt(version):
-    return file_version() < version
+    return Version(file_version()) < Version(version)
 
 
 def file_version_is_ge(version):
-    return file_version() >= version
+    return Version(file_version()) >= Version(version)
 
 
 def tools_missing(*required):
@@ -105,7 +104,7 @@ def skip_unless_tools_exist(*required):
     )
 
 
-def skip_if_tool_version_is(tool, actual_ver, target_ver, vcls=LooseVersion):
+def skip_if_tool_version_is(tool, actual_ver, target_ver, vcls=Version):
     if tools_missing(tool):
         return skipif(True, reason=reason(tool), tools=(tool,))
     if callable(actual_ver):
@@ -119,7 +118,7 @@ def skip_if_tool_version_is(tool, actual_ver, target_ver, vcls=LooseVersion):
     )
 
 
-def skip_unless_tool_is_at_least(tool, actual_ver, min_ver, vcls=LooseVersion):
+def skip_unless_tool_is_at_least(tool, actual_ver, min_ver, vcls=Version):
     if tools_missing(tool) and module_is_not_importable(tool):
         return skipif(True, reason=reason(tool), tools=(tool,))
     if callable(actual_ver):
@@ -133,7 +132,7 @@ def skip_unless_tool_is_at_least(tool, actual_ver, min_ver, vcls=LooseVersion):
     )
 
 
-def skip_unless_tool_is_at_most(tool, actual_ver, max_ver, vcls=LooseVersion):
+def skip_unless_tool_is_at_most(tool, actual_ver, max_ver, vcls=Version):
     if tools_missing(tool) and module_is_not_importable(tool):
         return skipif(True, reason=reason(tool), tools=(tool,))
     if callable(actual_ver):
@@ -148,7 +147,7 @@ def skip_unless_tool_is_at_most(tool, actual_ver, max_ver, vcls=LooseVersion):
 
 
 def skip_unless_tool_is_between(
-    tool, actual_ver, min_ver, max_ver, vcls=LooseVersion
+    tool, actual_ver, min_ver, max_ver, vcls=Version
 ):
     if tools_missing(tool):
         return skipif(True, reason=reason(tool), tools=(tool,))


=====================================
tests/utils/versions.py
=====================================
@@ -0,0 +1,103 @@
+#
+# diffoscope: in-depth comparison of files, archives, and directories
+#
+# Copyright © 2021 Zbigniew Jędrzejewski-Szmek <zbyszek at in.waw.pl>
+#
+# diffoscope is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# diffoscope is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with diffoscope.  If not, see <https://www.gnu.org/licenses/>.
+
+import itertools
+import functools
+import string
+
+
+ at functools.total_ordering
+class Version(str):
+    CHARS = string.ascii_letters + string.digits + "~^"
+    _ALPHA_ORD = {
+        "~": -1,
+        "^": +1,
+        None: 0,
+        **{c: ord(c) for c in string.ascii_letters},
+    }
+
+    @staticmethod
+    def _len_prefix(s, chars, matching=True):
+        for i in range(len(s)):
+            if (s[i] in chars) != matching:
+                return i
+        return len(s)
+
+    @classmethod
+    def _cmp_alphabetical(cls, string_a, string_b):
+        len_a = cls._len_prefix(string_a, string.ascii_letters + "~^")
+        len_b = cls._len_prefix(string_b, string.ascii_letters + "~^")
+
+        for i in range(max(len_a, len_b)):
+            a = string_a[i] if i < len_a else None
+            b = string_b[i] if i < len_b else None
+            c = cls._ALPHA_ORD[a] - cls._ALPHA_ORD[b]
+            if c:
+                return i, c
+
+        return len_a, 0
+
+    def _cmp(self, other):
+        I, J = len(self), len(other)
+        i = j = 0
+
+        k = 20
+
+        while i < I or j < J:
+            # print(f'{i=} {j=} {self[i:]=!r} {other[j:]=!r}')
+            i += self._len_prefix(self[i:], self.CHARS, matching=False)
+            j += self._len_prefix(other[j:], self.CHARS, matching=False)
+
+            # print(f'{i=} {j=} {self[i:]=!r} {other[j:]=!r}')
+
+            if (i < I and self[i] in string.digits) or (
+                j < J and other[j] in string.digits
+            ):
+                ii = i + self._len_prefix(self[i:], string.digits)
+                jj = j + self._len_prefix(other[j:], string.digits)
+
+                c = (ii == i) - (jj == j)
+                # print(f'digits: {c=}')
+                if c:
+                    return -c
+
+                c = int(self[i:ii]) - int(other[j:jj])
+                # print(f'digits: {c=}')
+
+                i, j = ii, jj
+
+            else:
+                skip, c = self._cmp_alphabetical(self[i:], other[j:])
+                # print(f'alphas: {c=}')
+
+                i += skip
+                j += skip
+
+            if c:
+                return -1 if c < 0 else +1
+
+            k -= 1
+            assert k >= 0
+
+        return 0
+
+    def __eq__(self, other):
+        return self._cmp(other) == 0
+
+    def __lt__(self, other):
+        return self._cmp(other) < 0



View it on GitLab: https://salsa.debian.org/reproducible-builds/diffoscope/-/compare/d5707121e6a967377c1e5aaae78037c24df73be7...2fae7e85506f3fdff8f2117dd7a4c3340b7c47ef

-- 
View it on GitLab: https://salsa.debian.org/reproducible-builds/diffoscope/-/compare/d5707121e6a967377c1e5aaae78037c24df73be7...2fae7e85506f3fdff8f2117dd7a4c3340b7c47ef
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.reproducible-builds.org/pipermail/rb-commits/attachments/20210914/a7ffea0a/attachment.htm>


More information about the rb-commits mailing list