aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sb_builder/packages.py41
-rw-r--r--tests/test_gui.py15
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"})