aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/package-inventory/test_package_inventory.py155
1 files changed, 155 insertions, 0 deletions
diff --git a/tests/package-inventory/test_package_inventory.py b/tests/package-inventory/test_package_inventory.py
new file mode 100644
index 0000000..b48e2ab
--- /dev/null
+++ b/tests/package-inventory/test_package_inventory.py
@@ -0,0 +1,155 @@
+"""Tests for scripts/package-inventory.
+
+package-inventory compares the packages archsetup declares against what is
+installed on the live system. It extracts archsetup's packages — direct
+pacman_install/aur_install calls plus `for software in ...` loop lists,
+skipping variable arguments — then diffs them against pacman's view of the
+system in both directions: declared-but-not-installed, and
+installed-but-not-declared (split into AUR/foreign vs official).
+
+Tests run the real script against a fixture installer snippet and a fake
+pacman serving controlled -Qqe/-Qq/-Qqen/-Qqem output. No real pacman db,
+no network. The seams are env overrides:
+ PKGINV_ARCHSETUP path to the installer script to extract from
+ PKGINV_PACMAN pacman binary to query the system with
+
+Run from repo root:
+ make test-unit
+ (or python3 -m unittest tests.package-inventory.test_package_inventory)
+"""
+
+import os
+import re
+import shutil
+import stat
+import subprocess
+import tempfile
+import unittest
+
+ANSI = re.compile(r"\x1b\[[0-9;]*m") # summary colorizes counts; strip for matching
+
+REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+SCRIPT = os.path.join(REPO_ROOT, "scripts/package-inventory")
+
+# Installer fixture. Declared set after extraction (sorted, unique):
+# pkg-common, pkg-missing, aur-common, loop-a, loop-b, loop-c
+# The bare "$software" lines are loop plumbing, not packages, and drop out.
+FIXTURE = """
+pacman_install pkg-common # installed -> in both
+pacman_install pkg-missing # not installed anywhere
+aur_install aur-common # installed (foreign) -> in both
+for software in loop-a loop-b \\
+ loop-c; do
+ pacman_install "$software"
+done
+aur_install "$software" # variable -> not a package
+"""
+
+# Fake-system state, keyed by the pacman query flag the script uses.
+# -Qqe explicitly installed -Qq all installed (incl deps)
+# -Qqen native (official) -Qqem foreign (AUR/manual)
+# loop-c and pkg-missing are absent from the system; extra-native and
+# extra-foreign are installed but undeclared.
+SYSTEM = {
+ "-Qqe": ["pkg-common", "aur-common", "loop-a", "loop-b",
+ "extra-native", "extra-foreign"],
+ "-Qq": ["pkg-common", "aur-common", "loop-a", "loop-b",
+ "extra-native", "extra-foreign", "dep-x"],
+ "-Qqen": ["pkg-common", "loop-a", "loop-b", "extra-native"],
+ "-Qqem": ["aur-common", "extra-foreign"],
+}
+
+
+class InventoryHarness(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.mkdtemp(prefix="pkg-inv-test-")
+ self.fixture = os.path.join(self.tmp, "installer-snippet")
+ with open(self.fixture, "w") as f:
+ f.write(FIXTURE)
+
+ # Fake pacman: dispatch on the query flag, print the fixed set.
+ cases = "".join(
+ f' {flag}) printf "%s\\n" {" ".join(pkgs)} ;;\n'
+ for flag, pkgs in SYSTEM.items()
+ )
+ self.fake_pacman = os.path.join(self.tmp, "pacman")
+ self._stub(self.fake_pacman, f'case "$1" in\n{cases} *) exit 1 ;;\nesac\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_inv(self, *args):
+ env = os.environ.copy()
+ env["PKGINV_ARCHSETUP"] = self.fixture
+ env["PKGINV_PACMAN"] = self.fake_pacman
+ return subprocess.run(
+ [SCRIPT, *args],
+ env=env, capture_output=True, text=True, timeout=20,
+ )
+
+
+class TestSummary(InventoryHarness):
+ def test_summary_counts_extraction_and_both_diffs(self):
+ r = self.run_inv("--summary")
+ self.assertEqual(r.returncode, 0, msg=r.stderr)
+ out = ANSI.sub("", r.stdout)
+ # 6 declared proves the for-loop list parsed and "$software" dropped.
+ self.assertRegex(out, r"Archsetup declares:\s+6\b")
+ self.assertRegex(out, r"In common:\s+4\b")
+ self.assertRegex(out, r"In archsetup, not system:\s+2\b")
+ self.assertRegex(out, r"On system, not archsetup:\s+2\b")
+
+
+class TestDiffLists(InventoryHarness):
+ def test_archsetup_only_lists_declared_but_uninstalled(self):
+ r = self.run_inv("--archsetup-only")
+ self.assertEqual(r.returncode, 0, msg=r.stderr)
+ got = set(r.stdout.split())
+ # loop-c proves a loop-extracted package flows through the diff.
+ self.assertEqual(got, {"pkg-missing", "loop-c"})
+
+ def test_system_only_lists_installed_but_undeclared(self):
+ r = self.run_inv("--system-only")
+ self.assertEqual(r.returncode, 0, msg=r.stderr)
+ got = set(r.stdout.split())
+ self.assertEqual(got, {"extra-native", "extra-foreign"})
+
+ def test_variable_arg_is_not_treated_as_a_package(self):
+ r = self.run_inv("--archsetup-only")
+ self.assertNotIn("software", r.stdout)
+
+
+class TestFullReport(InventoryHarness):
+ def test_full_report_splits_undeclared_by_source(self):
+ r = self.run_inv()
+ self.assertEqual(r.returncode, 0, msg=r.stderr)
+ self.assertIn("In archsetup but NOT installed", r.stdout)
+ self.assertIn("pkg-missing", r.stdout)
+ self.assertIn("loop-c", r.stdout)
+ self.assertIn("On system but NOT in archsetup", r.stdout)
+ self.assertIn("AUR/foreign", r.stdout)
+ self.assertIn("extra-foreign", r.stdout)
+ self.assertIn("Official repos", r.stdout)
+ self.assertIn("extra-native", r.stdout)
+
+
+class TestCli(InventoryHarness):
+ def test_help_describes_modes(self):
+ r = self.run_inv("--help")
+ self.assertEqual(r.returncode, 0, msg=r.stderr)
+ self.assertIn("Usage", r.stdout)
+ self.assertIn("--archsetup-only", r.stdout)
+
+ def test_unknown_option_errors(self):
+ r = self.run_inv("--bogus")
+ self.assertNotEqual(r.returncode, 0)
+ self.assertIn("Unknown option", r.stderr)
+
+
+if __name__ == "__main__":
+ unittest.main()