From 1f89523739a575f5dec19616ec44df4143df4866 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 11 Jun 2026 11:29:41 -0500 Subject: feat(scripts): package auditor + fix the four packages it caught MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/audit-packages.sh extracts every pacman_install/aur_install package (loop lists included) and verifies each against its declared source — sync dbs for official, one batched RPC query for AUR — flagging movers in both directions. Unit-tested against fixture installers with fake pacman/curl. First real run over 420 packages found four that vanished from both sources, each now fixed: libva-mesa-driver folded into mesa (line dropped), nvidia-dkms replaced by nvidia-open-dkms (Turing+; legacy cards are the preflight task's problem), swww replaced by awww (its successor, already what both machines run), and libappindicator-gtk3 replaced by libayatana-appindicator. Fifteen AUR entries that graduated to official repos still install fine via yay and are left as-is. --- tests/audit-packages/test_audit_packages.py | 127 ++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/audit-packages/test_audit_packages.py (limited to 'tests') diff --git a/tests/audit-packages/test_audit_packages.py b/tests/audit-packages/test_audit_packages.py new file mode 100644 index 0000000..5adb26d --- /dev/null +++ b/tests/audit-packages/test_audit_packages.py @@ -0,0 +1,127 @@ +"""Tests for scripts/audit-packages.sh. + +The auditor extracts every package archsetup installs — direct +pacman_install/aur_install calls plus `for software in ...` loop lists — +then verifies each against the right source: official packages must exist +in the sync databases, AUR packages in the AUR. Packages that moved +between sources are flagged in both directions. Tests run the real script +against a fixture installer snippet, a fake pacman, and a fake curl that +serves controlled AUR RPC JSON; no network, no real pacman db. + +Run from repo root: + make test-unit (or python3 -m unittest tests.audit-packages.test_audit_packages) +""" + +import json +import os +import shutil +import stat +import subprocess +import tempfile +import unittest + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +SCRIPT = os.path.join(REPO_ROOT, "scripts/audit-packages.sh") + +FIXTURE = """ +pacman_install good-official # exists in repos +pacman_install gone-official # vanished from repos, not in AUR +pacman_install moved-to-aur # left repos, lives in AUR now +aur_install good-aur # exists in AUR +aur_install gone-aur # vanished from AUR +aur_install now-official # promoted into the repos +aur_install "$software" # variable — not a literal package +for software in loop-pkg-a loop-pkg-b \\ + loop-pkg-c; do + pacman_install "$software" +done +""" + + +class AuditHarness(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="audit-pkgs-test-") + self.fixture = os.path.join(self.tmp, "installer-snippet") + with open(self.fixture, "w") as f: + f.write(FIXTURE) + + # Fake pacman: -Si exits 0 only for the packages "in the repos". + official = ["good-official", "now-official", + "loop-pkg-a", "loop-pkg-b", "loop-pkg-c"] + self.fake_pacman = os.path.join(self.tmp, "pacman") + self._stub(self.fake_pacman, ( + 'case "$2" in\n' + + "".join(f" {p}) exit 0 ;;\n" for p in official) + + ' *) exit 1 ;;\nesac\n' + )) + + # Fake curl: serves AUR RPC v5 info JSON for the packages "in the AUR". + aur = ["good-aur", "moved-to-aur"] + rpc = json.dumps({"results": [{"Name": p} for p in aur]}) + self.fake_curl = os.path.join(self.tmp, "curl") + self._stub(self.fake_curl, f"printf '%s' '{rpc}'\nexit 0\n") + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def _stub(self, path, body): + with open(path, "w") as f: + f.write("#!/bin/sh\n" + body) + os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR) + + def run_audit(self, *args): + env = os.environ.copy() + env["AUDIT_PACMAN"] = self.fake_pacman + env["AUDIT_CURL"] = self.fake_curl + return subprocess.run( + [SCRIPT, *args, self.fixture], + env=env, capture_output=True, text=True, timeout=20, + ) + + +class TestParser(AuditHarness): + def test_list_mode_extracts_direct_and_loop_packages(self): + r = self.run_audit("--list") + self.assertEqual(r.returncode, 0, msg=r.stderr) + for p in ("good-official", "gone-official", "moved-to-aur", + "loop-pkg-a", "loop-pkg-b", "loop-pkg-c"): + self.assertIn(p, r.stdout) + for p in ("good-aur", "gone-aur", "now-official"): + self.assertIn(p, r.stdout) + + def test_variable_args_are_not_packages(self): + r = self.run_audit("--list") + self.assertNotIn("$software", r.stdout) + + +class TestAudit(AuditHarness): + def test_clean_packages_pass_silently(self): + r = self.run_audit() + self.assertNotIn("good-official", r.stdout) + self.assertNotIn("good-aur", r.stdout) + + def test_missing_everywhere_reported(self): + r = self.run_audit() + self.assertNotEqual(r.returncode, 0) + self.assertIn("gone-official", r.stdout) + self.assertIn("gone-aur", r.stdout) + + def test_movers_flagged_in_both_directions(self): + r = self.run_audit() + self.assertIn("moved-to-aur", r.stdout) + self.assertIn("now-official", r.stdout) + + def test_clean_fixture_exits_zero(self): + clean = os.path.join(self.tmp, "clean-snippet") + with open(clean, "w") as f: + f.write("pacman_install good-official\naur_install good-aur\n") + env = os.environ.copy() + env["AUDIT_PACMAN"] = self.fake_pacman + env["AUDIT_CURL"] = self.fake_curl + r = subprocess.run([SCRIPT, clean], env=env, + capture_output=True, text=True, timeout=20) + self.assertEqual(r.returncode, 0, msg=r.stdout + r.stderr) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3