diff options
| -rw-r--r-- | sb_builder/packages.py | 41 | ||||
| -rw-r--r-- | tests/test_gui.py | 15 |
2 files changed, 46 insertions, 10 deletions
diff --git a/sb_builder/packages.py b/sb_builder/packages.py index f1a7224..0a669bb 100644 --- a/sb_builder/packages.py +++ b/sb_builder/packages.py @@ -17,6 +17,7 @@ """Packages tab: search the inventory, read a package's hints, start slackrepo previews.""" import html +import re from PyQt6.QtCore import QAbstractTableModel, QModelIndex, QSortFilterProxyModel, Qt, pyqtSignal from PyQt6.QtGui import QColor, QKeySequence, QShortcut @@ -29,22 +30,48 @@ from .fmt import NA, fmt_time from .ui import SP, action, chip, enable, group_label, search_field OVERLAYS = ("personal", "pentesting") -STATUSES = ("ok", "failed", "never", "outdated") +STATUSES = ("ok", "failed", "never", "outdated", "newer") COLUMNS = ("", "name", "category", "version", "built", "status", "maintainer") -STATUS_COLOR = {"ok": STATUS["ok"], "failed": STATUS["failed"], "outdated": STATUS["aborted"]} +STATUS_COLOR = {"ok": STATUS["ok"], "failed": STATUS["failed"], "outdated": STATUS["aborted"], + "newer": STATUS["bad"]} FIELDS = {"cat": "category", "maint": "maintainer", "req": "requires", "status": "status"} +PRE_RELEASE = ("alpha", "beta", "pre", "rc", "dev") + + +def _runs(v): + return [int(r) if r.isdigit() else r.lower() for r in re.findall(r"\d+|[A-Za-z]+", v)] + + +def vercmp(a, b): + """-1, 0 or 1: a older, equal, newer than b. + + Digit runs compare as numbers, letter runs as text, a number beats a + letter. Past the shorter version, a pre-release word makes the longer one + older (1.0rc1 < 1.0), anything else newer (1.0.1, 4.3.5b > their base). + """ + # ponytail: single-letter pre-releases (1.0b1) count as newer than 1.0; + # snapshots (20240101_ab12cd) order by date, their hash part arbitrarily + ra, rb = _runs(a), _runs(b) + for x, y in zip(ra, rb): + if x != y: + if type(x) is type(y): + return -1 if x < y else 1 + return 1 if isinstance(x, int) else -1 + if len(ra) == len(rb): + return 0 + sign, nxt = (1, ra[len(rb)]) if len(ra) > len(rb) else (-1, rb[len(ra)]) + return -sign if nxt in PRE_RELEASE else sign def status(row): - # ponytail: outdated compares versions only; slackrepo also rebuilds on - # SlackBuild or dependency changes, which its preview shows + # ponytail: compares versions only; slackrepo also rebuilds on SlackBuild + # or dependency changes, which its preview shows if not row["built_version"] and not row["result"]: return "never" if row["result"] in ("failed", "bad", "aborted"): return "failed" - if row["built_version"] != row["version"]: - return "outdated" - return "ok" + c = vercmp(row["built_version"] or "", row["version"] or "") + return "outdated" if c < 0 else "newer" if c > 0 else "ok" def overlay(row): diff --git a/tests/test_gui.py b/tests/test_gui.py index d005447..eef9c5d 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -154,6 +154,7 @@ INVENTORY = {"columns": COLS, "rows": [ inv_row("personal/foo", built="1.0", result="ok", hints={"DELREQUIRES": "x"}, requires=["x", "y"]), inv_row("pentesting/nmap", built="7.9", result="bad"), inv_row("network/curl", maintainer="orphaned - no maintainer"), + inv_row("system/borgbackup", "1.4.2", built="1.4.5", result="ok"), ]} RESULTS = {"rows": [["games/pcsx2", "failed", 1789992366, None, True], # newest first, as the helper ["development/whisper.cpp", "aborted", 1789753147, None, False], @@ -245,7 +246,15 @@ class FilterTest(unittest.TestCase): def test_status(self): self.assertEqual({n: r["status"] for n, r in self.rows.items()}, - {"nuclei": "outdated", "foo": "ok", "nmap": "failed", "curl": "never"}) + {"nuclei": "outdated", "foo": "ok", "nmap": "failed", "curl": "never", + "borgbackup": "newer"}) + + def test_vercmp(self): + for a, b, want in (("1.0", "1.0", 0), ("1.4.10", "1.4.9", 1), ("1.0rc1", "1.0", -1), + ("1.0.1", "1.0", 1), ("4.3.5b", "4.3.5", 1), ("1_0", "1.0", 0), + ("2.0beta", "2.0alpha", 1), ("1.0a", "1.0.1", -1), ("", "1.0", -1)): + self.assertEqual(pk.vercmp(a, b), want, (a, b)) + self.assertEqual(pk.vercmp(b, a), -want, (b, a)) def test_query(self): self.assertEqual(pk.parse_query("Nuc cat:misc bogus:x status:"), @@ -386,7 +395,7 @@ class SmokeTest(unittest.TestCase): w, remote = self.window(**{"job-start": ({"id": "20260101-000001-slackrepo"}, None)}) w.poll_jobs() tab = w.open_packages() - self.assertEqual(tab.proxy.rowCount(), 4) + self.assertEqual(tab.proxy.rowCount(), 5) self.assertFalse(tab.chips["failed"].icon().isNull()) # a dot in the table's color self.assertTrue(tab.chips["personal"].icon().isNull()) self.assertTrue(tab.query.isClearButtonEnabled()) @@ -413,7 +422,7 @@ class SmokeTest(unittest.TestCase): tab.model.checked.clear() select, clear = tab.keys select.activated.emit() - self.assertEqual(len(tab.model.checked), 4) + self.assertEqual(len(tab.model.checked), 5) clear.activated.emit() self.assertEqual(tab.model.checked, set()) tab.model.checked.update({"nuclei", "curl"}) |
