#!/usr/bin/python3 # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program 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. """Run: /usr/bin/python3 desktop/modules/updates/test_updatesctl.py Imports updatesctl for vercmp, then drives it as a CLI against a fixture tree under a temp dir (UPDATESCTL_ROOT for system paths, HOME for the user's), never the live system. Plain asserts, no framework. """ import importlib.machinery import importlib.util import json import os import shutil import subprocess import tempfile from pathlib import Path SCRIPT = Path(__file__).resolve().parent / "updatesctl" loader = importlib.machinery.SourceFileLoader("updatesctl", str(SCRIPT)) spec = importlib.util.spec_from_loader("updatesctl", loader) u = importlib.util.module_from_spec(spec) loader.exec_module(u) # --- vercmp --- assert u.vercmp("1.14.10", "1.14.9") > 0 assert u.vercmp("2.1.278", "2.1.267") > 0 assert u.vercmp("2026_10_02", "2026_09_12") > 0 assert u.vercmp("v1.2", "1.2") == 0 assert u.vercmp("1.0", "1.0") == 0 # A pre-release is older than the release, a suffix is never behind. assert u.vercmp("0.12.3_beta.4", "0.12.3") < 0 assert u.vercmp("9.018.00_6.18.41", "9.018.00") > 0 assert u.vercmp("3.1-659215d", "3.1") > 0 assert u.vercmp("1.0.1", "1.0rc1") > 0 # --- fixture tree --- tmp = Path(tempfile.mkdtemp(prefix="updatesctl-test-")) home = tmp / "home" def w(path, text=""): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text) def info(repo_rel, version): w(home / "Programming/GIT/slackbuilds-superrepo" / repo_rel, f'PRGNAM="x"\nVERSION="{version}"\n') def sbo(rel, version): w(tmp / "var/lib/sbopkg/SBo-git" / rel, f'VERSION="{version}"\n') def pkgtxt(entries): return "".join(f"PACKAGE NAME: {full}.txz\nPACKAGE LOCATION: ./{loc}\n\n" for loc, full in entries) installed = [ "kitty-0.49.1-x86_64-1_danix", # built 0.49.2 -> upgrade "gamescope-3.16.30-x86_64-1_danix", # recipe bumped -> build "discord-1.0.159-x86_64-1_danix", # recipe behind -> upstream "claude-code-2.1.278-x86_64-1_SBo", # ahead of SBo -> upstream "nessus-10.12.4-x86_64-1_SBo", # SBo current -> build "neovim-0.12.6-x86_64-1_SBo", # current -> nothing "r8125-9.018.00_6.18.41-x86_64-1_danix", # suffix -> nothing "solvespace-3.1-659215d-x86_64-1_danix", # dash version -> nothing "glibc-2.44-x86_64-3", # mirror has -4 -> slackware upgrade "bash-5.3-x86_64-1", # testing/ newer, blacklisted -> nothing "dhcp-4.4.3-x86_64-1", # removed upstream -> slackware removed "foo-1.0-x86_64-1_SBo", # blacklisted build -> nothing ] for f in installed: w(tmp / "var/log/packages" / f) w(home / ".config/nvchecker/new_ver.json", json.dumps({"version": 2, "data": { "kitty": {"version": "0.49.2"}, "gamescope": {"version": "3.16.31"}, "discord": {"version": "1.0.162"}, "claude-code": {"version": "2.1.300"}, "nessus": {"version": "10.12.5"}, "neovim": {"version": "0.12.6"}, "r8125": {"version": "9.018.00"}, "solvespace": {"version": "3.1"}, "exploitdb": {"version": "2026_10_02"}, # not installed, my recipe behind "obsidian": {"version": "1.14.4"}, # not installed, SBo behind: hidden "Typora": {"version": "1.14.10"}, # AppImage behind "GIMP": {"version": "3.0.6"}, # flatpak behind }})) info("my-slackbuilds/kitty-bin/kitty-bin.info", "0.49.2") info("my-slackbuilds/gamescope/gamescope.info", "3.16.31") info("my-slackbuilds/network/discord/discord.info", "1.0.161") info("my-slackbuilds/r8125/r8125.info", "9.018.00") info("my-slackbuilds/solvespace/solvespace.info", "3.1") info("sbo-slackbuilds/system/exploitdb/exploitdb.info", "2026_09_12") sbo("development/claude-code-bin/claude-code-bin.info", "2.1.267") sbo("network/nessus/nessus.info", "10.12.5") sbo("development/neovim/neovim.info", "0.12.4") sbo("office/obsidian/obsidian.info", "1.13.7") w(home / ".local/state/appimage-updater/installed.json", json.dumps({"Typora": {"version": "1.14.9"}})) w(tmp / "bin/flatpak", "#!/bin/sh\nprintf 'org.gimp.GIMP\\t3.0.4\\n'\n") (tmp / "bin/flatpak").chmod(0o755) w(tmp / "etc/slackpkg/slackpkgplus.conf", "MIRRORPLUS['danix']=dir://repo/\n") w(tmp / "repo/PACKAGES.TXT", pkgtxt([ ("devel/kitty", "kitty-0.49.2-x86_64-1_danix"), ("games/gamescope", "gamescope-3.16.30-x86_64-1_danix"), ])) w(tmp / "etc/slackpkg/mirrors", "# a comment\nfile://mnt/mirror/\n") w(tmp / "etc/slackpkg/blacklist", '"testing/"\n[0-9]+_SBo\n') w(tmp / "mnt/mirror/PACKAGES.TXT", pkgtxt([ ("slackware64/l", "glibc-2.44-x86_64-4"), # testing/ first: the blacklisted entry must lose even when it comes first. ("testing/packages", "bash-5.4-x86_64-1"), ("slackware64/a", "bash-5.3-x86_64-1"), ("slackware64/a", "foo-1.1-x86_64-1"), ("slackware64/l", "libtatsu-1.0.5-x86_64-1"), ])) w(tmp / "mnt/mirror/ChangeLog.txt", """\ Wed Oct 14 21:48:34 UTC 2026 l/glibc-2.44-x86_64-4.txz: Rebuilt. l/libtatsu-1.0.5-x86_64-1.txz: Upgraded. n/dhcp-4.4.3-x86_64-1.txz: Removed. +--------------------------+ Thu Oct 1 10:00:00 UTC 2026 x/oldthing-1.0-x86_64-1.txz: Removed. l/libtatsu-1.0.4-x86_64-1.txz: Added. +--------------------------+ Tue Sep 1 10:00:00 UTC 2026 x/oldthing-1.0-x86_64-1.txz: Added. """) def run(*args, timeout="5"): env = {"HOME": str(home), "UPDATESCTL_ROOT": str(tmp), "UPDATESCTL_NFS_TIMEOUT": timeout, "PATH": f"{tmp}/bin:/usr/bin:/bin"} r = subprocess.run(["/usr/bin/python3", str(SCRIPT), *args], capture_output=True, text=True, env=env, timeout=30) assert r.returncode == 0, r.stderr return json.loads(r.stdout) def rows(section): return {x["name"]: (x["repo"], x["from"], x["to"]) for x in section} d = run() assert d["errors"] == {}, d["errors"] assert d["checked"] > 0 assert rows(d["upgrade"]) == {"kitty": ("personal", "0.49.1", "0.49.2")}, d["upgrade"] assert rows(d["build"]) == { "gamescope": ("personal", "3.16.30", "3.16.31"), "nessus": ("SBo", "10.12.4", "10.12.5"), }, d["build"] assert rows(d["upstream"]) == { "discord": ("personal", "1.0.161", "1.0.162"), "claude-code": ("SBo", "2.1.278", "2.1.300"), "exploitdb": ("my-sbo", "2026_09_12", "2026_10_02"), }, d["upstream"] assert rows(d["apps"]) == { "Typora": ("AppImage", "1.14.9", "1.14.10"), "GIMP": ("flatpak", "3.0.4", "3.0.6"), }, d["apps"] sw = d["slackware"] assert sw["upgrade"] == [{"name": "glibc", "from": "2.44-3", "to": "2.44-4"}], sw assert sw["new"] == [{"name": "libtatsu", "version": "1.0.5-1"}], sw assert sw["removed"] == [{"name": "dhcp", "date": "2026-10-14"}], sw # A fifo with no writer blocks cat at open(), as a hard NFS mount blocks a # read while the NAS is down. import time as _t (tmp / "repo/PACKAGES.TXT").unlink() os.mkfifo(tmp / "repo/PACKAGES.TXT") t0 = _t.monotonic() d = run(timeout="0.5") assert _t.monotonic() - t0 < 5, "a hung read stalled the script" assert d["errors"] == {"repo": "unreachable"}, d["errors"] assert d["upgrade"] == [], d["upgrade"] assert d["slackware"]["upgrade"], "the mirror sections must still fill" shutil.rmtree(tmp) print("ok")