Repository: RedHatInsights/insights-core

Issue:
get_max(package_name) returns TypeError for some package lists
When parsing installed package lists from various Insights archives, using get_max function to get newest package version of given package name throws often this error:

```
2019-04-15 20:08:51,852:ERROR:__main__:Unable to parse archive: 'Traceback (most recent call last):
File "/listener/listener/upload_listener.py", line 100, in process_upload\n    parser.parse()\n
File "/listener/listener/archive_parser.py", line 62, in parse\n    pkg = pkglist.get_max(pkg_name)\n
File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 211, in get_max\n   return max(self.packages[package_name])\n
File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 599, in __gt__\n    return isinstance(other, InstalledRpm) and other.__lt__(self)\n
File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 584, in __lt__\n    return self_v < other_v\nTypeError: \'<\' not supported between instances of \'int\' and \'str\''|
```


Discussion:
@jdobes 

Do you have an example installed package lists that triggered this error?
This package entry is causing issues
```
{"name":"gpg-pubkey","epoch":"(none)","version":"2fa658e0","release":"45700c69","arch":"(none)","installtime":"Thu Apr 25 08:21:00 2019","buildtime":"1556194860","vendor":"(none)","buildhost":"localhost","sigpgp":"(none)"}
```

when get_max is called

```
broker[Installed].get_max("gpg-pubkey")
```
`gpg-pubkey` is special one. It is rpm name style, but it is not version and release. 
Do not compare for it.

Example:  gpg-pubkey-f5282ee4-58ac9

gpg-pubkey is a valid rpm, collected by the insights-client. If insights-core is the preferred library to work with the archives, I'd expect it to support this case as well. Besides that it's not clear, whether gpg-pubkey is the only rpm, that causes the traceback, or whether it's just a example of many.

$ rpm -qi gpg-pubkey-2fa658e0-45700c69 | head -n 15
Name        : gpg-pubkey
Version     : 2fa658e0
Release     : 45700c69
Architecture: (none)
Install Date: Fri 07 Oct 2016 07:29:38 AM EDT
Group       : Public Keys
Size        : 0
License     : pubkey
Signature   : (none)
Source RPM  : (none)
Build Date  : Fri 01 Dec 2006 06:05:13 AM EST
Build Host  : localhost
Relocations : (not relocatable)
Packager    : Red Hat, Inc. (auxiliary key) <security@redhat.com>
Summary     : gpg(Red Hat, Inc. (auxiliary key) <security@redhat.com>)

I think this is a problem of insights-core. Yes, `gpg-pubkey` is special package and yes, getting newest version of this package doesn't make sense. But since insights-core parses this package into `InstalledRpm` object, it should also make sure that it's `__lt__` method doesn't crash. Or make sure that such packages are ignored by parser.
Another package that fails in get_max:

2019-05-06 15:03:37,946:ERROR:__main__:Unable to parse archive: 'Traceback (most recent call last):\n  File "/listener/listener/upload_listener.py", line 141, in parse_archive\n    parser.parse()\n  File "/listener/listener/archive_parser.py", line 65, in parse\n    pkg = pkglist.get_max(pkg_name)\n  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 211, in get_max\n    return max(self.packages[package_name])\n  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 599, in __gt__\n    return isinstance(other, InstalledRpm) and other.__lt__(self)\n  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 575, in __lt__\n    if self == other:\n  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 555, in __eq__\n    .format(self.package, other.package))\nValueError: Cannot compare packages that one has distribution while the other does not nagios-plugins-disk-2.2.1-16.20180725git3429dad.el7 != nagios-plugins-disk-2.2.1-9git5c7eb5b9.el7'|

Red Hat NEVRAs can be kind of filthy - code that claims to be able to do NEVRA-comparisons, has to Not Barf in the presence of suboptimal NEVRAs. get_max() has *got* to be more robust, you can't push the problem off on the users of the library.
And another:


`2019-05-06 14:48:42,470:ERROR:__main__:Unable to parse archive: 'Traceback (most recent call last):\n  File "/listener/listener/upload_listener.py", line 141, in parse_archive\n    parser.parse()\n  File "/listener/listener/archive_parser.py", line 65, in parse\n    pkg = pkglist.get_max(pkg_name)\n  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 211, in get_max\n    return max(self.packages[package_name])\n  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 599, in __gt__\n    return isinstance(other, InstalledRpm) and other.__lt__(self)\n  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 575, in __lt__\n    if self == other:\n  File "/opt/rh/rh-python36/root/usr/lib/python3.6/site-packages/insights/parsers/installed_rpms.py", line 555, in __eq__\n    .format(self.package, other.package))\nValueError: Cannot compare packages that one has distribution while the other does not xz-5.2.2-1.el7 != xz-5.1.2-12alpha.el7'\|`




@jhjaggars can you please help? get_max tracebacks cause us errors and processing failures what results in not processing incoming archives.
(We temporarily blacklisted gpg-pubkey package, but it looks like there're much more packages that insights-core has troubles to process.)
Failures vulnerability-engine-listener, upload_archive.py, attached

[get_max_failures-2019-05-04-20:08:53_to_2019-05-06-15:51:30.gz](https://github.com/RedHatInsights/insights-core/files/3148848/get_max_failures-2019-05-04-20.08.53_to_2019-05-06-15.51.30.gz)

@ggainey thanks! I strongly suspect these are coming from a machine or machines that are using an old version of the client pinned to the old uploader.json that tells it what to collect. We changed the rpm invocation a long time ago to disambiguate the different rpm fields in the output, but the old spec that's out there supporting one of the first clients still has the old command.
Current code can not handle below two compare.  it will raise Value error "Cannot compare packages that one has distribution while the other does not"

nagios-plugins-disk-2.2.1-16.20180725git3429dad.el7
 nagios-plugins-disk-2.2.1-9git5c7eb5b9.el7

xz-5.2.2-1.el7 
 xz-5.1.2-12alpha.el7

Root cause is the distribution is None for  xz-5.1.2-12alpha.el7 and nagios-plugins-disk-2.2.1-9git5c7eb5b9.el7

Code fragment:

~~~
        def _start_of_distribution(rest_split):
            """
            The start of distribution field: from the right, the last non-digit part
            - bash-4.2.39-3.el7_2.2
              distribution: el7_2.2
            - kernel-rt-debug-3.10.0-327.rt56.204.el7
              distribution: el7
            """
            nondigit_flag = False
            for i, r in enumerate(reversed(rest_split)):
                if not r.isdigit():
                    nondigit_flag = True
                elif nondigit_flag and r.isdigit():
                    return len(rest_split) - i

        self._release_sep = self.release
        self._distribution = None
        rl_split = self._release_sep.split('.') if self._release_sep else None
~~~

~~~
        if (not self._distribution) != (not other._distribution):
            raise ValueError('Cannot compare packages that one has distribution while the other does not {0} != {1}'
                             .format(self.package, other.package))
~~~



Fix ValueError: Cannot compare packages that one has distribution while the other does not xz-5.2.2-1.el7 != xz-5.1.2-12alpha.el7 by below PR

https://github.com/RedHatInsights/insights-core/pull/1913
A complete fix for rpm comparison: #1914
I'm working on a PR that doesn't require external dependencies and passes all of the [rpm tests](https://raw.githubusercontent.com/rpm-software-management/rpm/master/tests/rpmvercmp.at).

Resolving patch:
diff --git a/insights/parsers/installed_rpms.py b/insights/parsers/installed_rpms.py
--- a/insights/parsers/installed_rpms.py
+++ b/insights/parsers/installed_rpms.py
@@ -76,6 +76,7 @@
 
 from ..util import rsplit
 from .. import parser, get_active_lines, CommandParser
+from .rpm_vercmp import rpm_version_compare
 from insights.specs import Specs
 
 # This list of architectures is taken from PDC (Product Definition Center):
@@ -341,30 +342,6 @@ def __init__(self, data):
             setattr(self, k, v)
         self.epoch = data['epoch'] if 'epoch' in data and data['epoch'] != '(none)' else '0'
 
-        """Below is only for version comparison"""
-        def _start_of_distribution(rest_split):
-            """
-            The start of distribution field: from the right, the last non-digit part
-            - bash-4.2.39-3.el7_2.2
-              distribution: el7_2.2
-            - kernel-rt-debug-3.10.0-327.rt56.204.el7
-              distribution: el7
-            """
-            nondigit_flag = False
-            for i, r in enumerate(reversed(rest_split)):
-                if not r.isdigit():
-                    nondigit_flag = True
-                elif nondigit_flag and r.isdigit():
-                    return len(rest_split) - i
-
-        self._release_sep = self.release
-        self._distribution = None
-        rl_split = self._release_sep.split('.') if self._release_sep else None
-        idx = _start_of_distribution(rl_split) if rl_split else None
-        if idx:
-            self._release_sep = '.'.join(rl_split[:idx])
-            self._distribution = '.'.join(rl_split[idx:])
-
     @classmethod
     def from_package(cls, package_string):
         """
@@ -550,23 +527,8 @@ def __eq__(self, other):
         if self.name != other.name:
             raise ValueError('Cannot compare packages with differing names {0} != {1}'
                              .format(self.name, other.name))
-        if (not self._distribution) != (not other._distribution):
-            raise ValueError('Cannot compare packages that one has distribution while the other does not {0} != {1}'
-                             .format(self.package, other.package))
-
-        self_ep, other_ep = pad_version(self.epoch, other.epoch)
-        self_v, other_v = pad_version(self.version, other.version)
-        self_rl, other_rl = pad_version(self.release, other.release)
-        eq_ret = (type(self) == type(other) and
-                  self_ep == other_ep and
-                  self_v == other_v and
-                  self_rl == other_rl)
-
-        if self._distribution:
-            self_d, other_d = pad_version(self._distribution, other._distribution)
-            return eq_ret and self_d == other_d
-        else:
-            return eq_ret
+
+        return rpm_version_compare(self, other) == 0
 
     def __lt__(self, other):
         if not isinstance(other, InstalledRpm):
@@ -575,22 +537,7 @@ def __lt__(self, other):
         if self == other:
             return False
 
-        self_ep, other_ep = pad_version(self.epoch, other.epoch)
-        if self_ep != other_ep:
-            return self_ep < other_ep
-
-        self_v, other_v = pad_version(self.version, other.version)
-        if self_v != other_v:
-            return self_v < other_v
-
-        self_rl, other_rl = pad_version(self._release_sep, other._release_sep)
-        if self_rl != other_rl:
-            return self_rl < other_rl
-
-        # If we reach this point, the self == other test has determined that
-        # we have a _distribution, so we rely on that.
-        self_d, other_d = pad_version(self._distribution, other._distribution)
-        return self_d < other_d
+        return rpm_version_compare(self, other) < 0
 
     def __ne__(self, other):
         return not self == other
diff --git a/insights/parsers/rpm_vercmp.py b/insights/parsers/rpm_vercmp.py
new file mode 100644
--- /dev/null
+++ b/insights/parsers/rpm_vercmp.py
@@ -0,0 +1,139 @@
+"""
+Nearly direct translation of rpm comparison code in the rpm project at
+https://github.com/rpm-software-management/rpm/blob/master/lib/rpmvercmp.c
+
+Handles all of the cases in the rpm test file, including the "buggy" tests
+and non-ascii characters.
+
+https://raw.githubusercontent.com/rpm-software-management/rpm/master/tests/rpmvercmp.at
+"""
+
+from collections import deque
+from itertools import takewhile
+
+
+def _rpm_vercmp(a, b):
+    if a == b:
+        return 0
+
+    # ord check required for consistent unicode handling
+    # empty string appended to simplify other checks below
+    a = deque([c if ord(c) < 128 else "." for c in a])
+    a.append("")
+
+    b = deque([c if ord(c) < 128 else "." for c in b])
+    b.append("")
+
+    while a[0] or b[0]:
+        for x in [a, b]:
+            while x[0] and not x[0].isalnum() and x[0] not in "~^":
+                x.popleft()
+
+        # handle the tilde separator, it sorts before everything else
+        if a[0] == "~" or b[0] == "~":
+            if a[0] != "~":
+                return 1
+            if b[0] != "~":
+                return -1
+            a.popleft()
+            b.popleft()
+            continue
+
+        # Handle caret separator. Concept is the same as tilde,
+        # except that if one of the strings ends (base version),
+        # the other is considered as higher version.
+        if a[0] == "^" or b[0] == "^":
+            if not a[0]:
+                return -1
+            if not b[0]:
+                return 1
+            if a[0] != "^":
+                return 1
+            if b[0] != "^":
+                return -1
+            a.popleft()
+            b.popleft()
+            continue
+
+        # If we ran to the end of either, we are finished with the loop
+        if not a[0] or not b[0]:
+            break
+
+        # grab first completely alpha or completely numeric segment
+        if a[0].isdigit():
+            l = deque(takewhile(lambda v: v.isdigit(), a))
+            r = deque(takewhile(lambda v: v.isdigit(), b))
+            isnum = True
+        else:
+            l = deque(takewhile(lambda v: v.isalpha(), a))
+            r = deque(takewhile(lambda v: v.isalpha(), b))
+            isnum = False
+
+        # this cannot happen, as we previously tested to make sure that
+        # the first string has a non-null segment
+        if not l:
+            return -1
+
+        # take care of the case where the two version segments are
+        # different types: one numeric, the other alpha (i.e. empty)
+        # numeric segments are always newer than alpha segments
+        # XXX See details from bugzilla #50977.
+        if not r:
+            return 1 if isnum else -1
+
+        ll = len(l)
+        lr = len(r)
+        if isnum:
+            # throw away any leading zeros - it's a number, right?
+            for x in [l, r]:
+                while x and x[0] == '0':
+                    x.popleft()
+
+            # whichever number has more digits wins
+            lenl = len(l)
+            lenr = len(r)
+            if lenl > lenr:
+                return 1
+
+            if lenr > lenl:
+                return -1
+
+        if l > r:
+            return 1
+
+        if l < r:
+            return -1
+
+        for _ in range(ll):
+            a.popleft()
+
+        for _ in range(lr):
+            b.popleft()
+
+    # this catches the case where all numeric and alpha segments have
+    # compared identically but the segment separating characters were
+    # different
+    if not a[0] and not b[0]:
+        return 0
+
+    # whichever version still has characters left over wins
+    if not a[0]:
+        return -1
+    return 1
+
+
+def rpm_version_compare(left, right):
+    if left is right:
+        return 0
+
+    le, re = int(left.epoch), int(right.epoch)
+    if le < re:
+        return -1
+    elif le > re:
+        return 1
+
+    rc = _rpm_vercmp(left.version, right.version)
+    if rc != 0:
+        return rc
+
+    return _rpm_vercmp(left.release, right.release)


Test patch:
diff --git a/insights/parsers/tests/test_installed_rpms.py b/insights/parsers/tests/test_installed_rpms.py
--- a/insights/parsers/tests/test_installed_rpms.py
+++ b/insights/parsers/tests/test_installed_rpms.py
@@ -109,6 +109,49 @@
 openssl-1.0.0-27.el6.x86_64
 '''.strip()
 
+RPMS_GPG_PUBKEY = '''
+{"name":"gpg-pubkey","epoch":"(none)","version":"2fa658e0","release":"45700c69","arch":"(none)","installtime":"Thu Apr 25 08:21:00 2019","buildtime":"1556194860","vendor":"(none)","buildhost":"localhost","sigpgp":"(none)"}
+'''.strip()
+
+RPMS_VMAAS = '''
+compat-libstdc++-296-2.96-138
+compat-libstdc++-296-2.96-144.el6
+compat-libstdc++-33-3.2.3-47.3
+compat-libstdc++-33-3.2.3-61
+compat-libstdc++-33-3.2.3-69.el6
+compat-libstdc++-33-3.2.3-72.el7
+flash-plugin-11.2.202.635-release
+flash-plugin-31.0.0.148-1.el6_10
+flash-plugin-32.0.0.156-release
+flash-plugin-32.0.0.171-1.el6_10
+glibc-2.12-1.149.el6_6.5
+glibc-2.5-81
+infinipath-psm-3.0.1-115.1015_open.2.el6
+infinipath-psm-3.3-1.7.git05f6f14_open.el6
+infinipath-psm-3.3-19_g67c0807_open.el6
+kmod-fnic-1.5.0.45-1.el6
+kmod-fnic-1.6.0.12b-rhel6u6.el6
+krb5-libs-1.10.3-42z1.el6_7
+krb5-libs-1.10.3-65.el6
+libaio-devel-0.3.106-5
+libaio-devel-0.3.107-10.el6
+mdadm-4.0-13.el7
+mdadm-4.1-rc1_2.el7
+nagios-plugins-disk-2.2.1-16.20180725git3429dad.el7
+nagios-plugins-disk-2.2.1-9git5c7eb5b9.el7
+TaniumClient-6.0.314.1321-1
+TaniumClient-6.0.314.1579_1.rhe7-1
+TaniumClient-7.2.314.2962_1.oel6-1
+TaniumClient-7.2.314.2962-1.rhe6
+TaniumClient-7.2.314.3211-1.rhe7
+xymon-4.3.11-11.1
+xymon-4.3.25-1.el6
+xz-5.1.2-12alpha.el7
+xz-5.2.2-1.el7
+xz-libs-5.1.2-12alpha.el7
+xz-libs-5.2.2-1.el7
+'''.strip()
+
 
 def test_from_package():
     rpms = InstalledRpms(context_wrap(RPMS_PACKAGE))
@@ -201,21 +244,6 @@ def test_release_compare():
     rpm7 = InstalledRpm.from_package('kernel-3.10.0-327.1')
     rpm8 = InstalledRpm.from_package('kernel-3.10.0-327.x86_64')
     rpm9 = InstalledRpm.from_package('kernel-3.10.0-327.1.x86_64')
-    with pytest.raises(ValueError) as ve:
-        rpm4 < rpm6
-    assert "the other does not" in str(ve)
-    with pytest.raises(ValueError) as ve:
-        rpm4 != rpm6
-    assert "the other does not" in str(ve)
-    with pytest.raises(ValueError) as ve:
-        rpm4 == rpm6
-    assert "the other does not" in str(ve)
-    with pytest.raises(ValueError) as ve:
-        rpm4 >= rpm6
-    assert "the other does not" in str(ve)
-    with pytest.raises(ValueError) as ve:
-        rpm4 < rpm8
-    assert "the other does not" in str(ve)
     with pytest.raises(ValueError) as ve:
         rpm1 > rpm7
     assert "differing names" in str(ve)
@@ -399,3 +427,83 @@ def test_rpm_object_hashing():
     rpm2 = InstalledRpm.from_package('yum-3.4.3-132.el7.ppc64le')
     assert rpm2.arch == 'ppc64le'
     assert rpm1.__hash__() != rpm2.__hash__()
+
+
+def test_gpgkey():
+    rpms = InstalledRpms(context_wrap(RPMS_GPG_PUBKEY))
+    assert isinstance(rpms.get_max("gpg-pubkey"), InstalledRpm)
+    assert len(rpms.packages) == 1
+    assert rpms.get_max("gpg-pubkey").name == "gpg-pubkey"
+
+
+def test_vmaas():
+    rpms = InstalledRpms(context_wrap(RPMS_VMAAS))
+    rpm = rpms.get_max("compat-libstdc++-296")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "2.96"
+    assert rpm.release == "144.el6"
+
+    rpm = rpms.get_max("compat-libstdc++-33")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "3.2.3"
+    assert rpm.release == "72.el7"
+
+    rpm = rpms.get_max("flash-plugin")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "32.0.0.171"
+    assert rpm.release == "1.el6_10"
+
+    rpm = rpms.get_max("glibc")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "2.12"
+    assert rpm.release == "1.149.el6_6.5"
+
+    rpm = rpms.get_max("infinipath-psm")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "3.3"
+    assert rpm.release == "19_g67c0807_open.el6"
+
+    rpm = rpms.get_max("kmod-fnic")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "1.6.0.12b"
+    assert rpm.release == "rhel6u6.el6"
+
+    rpm = rpms.get_max("krb5-libs")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "1.10.3"
+    assert rpm.release == "65.el6"
+
+    rpm = rpms.get_max("libaio-devel")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "0.3.107"
+    assert rpm.release == "10.el6"
+
+    rpm = rpms.get_max("mdadm")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "4.1"
+    assert rpm.release == "rc1_2.el7"
+
+    rpm = rpms.get_max("nagios-plugins-disk")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "2.2.1"
+    assert rpm.release == "16.20180725git3429dad.el7"
+
+    rpm = rpms.get_max("TaniumClient")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "7.2.314.3211"
+    assert rpm.release == "1.rhe7"
+
+    rpm = rpms.get_max("xymon")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "4.3.25"
+    assert rpm.release == "1.el6"
+
+    rpm = rpms.get_max("xz")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "5.2.2"
+    assert rpm.release == "1.el7"
+
+    rpm = rpms.get_max("xz-libs")
+    assert isinstance(rpm, InstalledRpm)
+    assert rpm.version == "5.2.2"
+    assert rpm.release == "1.el7"
diff --git a/insights/parsers/tests/test_rpm_vercmp.py b/insights/parsers/tests/test_rpm_vercmp.py
new file mode 100644
--- /dev/null
+++ b/insights/parsers/tests/test_rpm_vercmp.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+import pytest
+from insights.parsers.rpm_vercmp import _rpm_vercmp
+
+
+# data copied from
+# https://raw.githubusercontent.com/rpm-software-management/rpm/master/tests/rpmvercmp.at
+
+DEFAULT_DATA = """
+m4_define([RPMVERCMP],[
+AT_SETUP([rpmvercmp($1, $2) = $3])
+AT_KEYWORDS([vercmp])
+AT_CHECK([
+AT_SKIP_IF([$LUA_DISABLED])
+runroot rpm --eval '%{lua: print(rpm.vercmp("$1", "$2"))}'], [0], [$3
+], [])
+AT_CLEANUP
+])
+
+AT_BANNER([RPM version comparison])
+
+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)
+RPMVERCMP(10.1xyz, 10xyz, 1)
+
+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, 0)
+RPMVERCMP(2_0, 2.0, 0)
+
+dnl 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)
+
+dnl 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)
+
+dnl 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)
+
+dnl 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)
+
+dnl These are included here to document current, arguably buggy behaviors
+dnl for reference purposes and for easy checking against  unintended
+dnl behavior changes.
+dnl
+dnl AT_BANNER([RPM version comparison oddities])
+dnl RhBug:811992 case
+dnl RPMVERCMP(1b.fc17, 1b.fc17, 0)
+dnl RPMVERCMP(1b.fc17, 1.fc17, -1)
+dnl RPMVERCMP(1.fc17, 1b.fc17, 1)
+dnl RPMVERCMP(1g.fc17, 1g.fc17, 0)
+dnl RPMVERCMP(1g.fc17, 1.fc17, 1)
+dnl RPMVERCMP(1.fc17, 1g.fc17, -1)
+
+dnl Non-ascii characters are considered equal so these are all the same, eh...
+dnl RPMVERCMP(1.1.α, 1.1.α, 0)
+dnl RPMVERCMP(1.1.α, 1.1.β, 0)
+dnl RPMVERCMP(1.1.β, 1.1.α, 0)
+dnl RPMVERCMP(1.1.αα, 1.1.α, 0)
+dnl RPMVERCMP(1.1.α, 1.1.ββ, 0)
+dnl RPMVERCMP(1.1.ββ, 1.1.αα, 0)
+"""
+
+
+def convert(data):
+    f = "RPMVERCMP("
+    lines = [l for l in data.splitlines() if f in l]
+    tuples = [tuple(c.strip() for c in l[len(f) + l.find(f):].rstrip(")").split(",")) for l in lines]
+    return [(l, r, int(i)) for (l, r, i) in tuples]
+
+
+@pytest.fixture
+def rpm_data():
+    return convert(DEFAULT_DATA)
+
+
+def test_rpm_vercmp(rpm_data):
+    for l, r, expected in rpm_data:
+        actual = _rpm_vercmp(l, r)
+        assert actual == expected, (l, r, actual, expected)