aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/installer-steps/test_install_gnome_keyring_fix.py209
1 files changed, 209 insertions, 0 deletions
diff --git a/tests/installer-steps/test_install_gnome_keyring_fix.py b/tests/installer-steps/test_install_gnome_keyring_fix.py
new file mode 100644
index 0000000..1a4115a
--- /dev/null
+++ b/tests/installer-steps/test_install_gnome_keyring_fix.py
@@ -0,0 +1,209 @@
+"""Tests for install_gnome_keyring_fix in the archsetup installer.
+
+gnome-keyring 51.1 (and 48 through 50) aborts when a Secret Service client's
+first OpenSession is dispatched before the daemon has created its per-client
+record. Every abort loses the unlocked state, so the next request pops the
+keyring password dialog; on ratio this fired three times a day once the
+ChatGPT desktop app's codex app-server started bursting keyring reads. The
+fix is upstream merge request !112 plus an alias-lifetime correction, carried
+as a patched rebuild of Arch's own package under packages/gnome-keyring in
+this repo (pkgrel 1.1). install_gnome_keyring_fix builds and installs it with
+makepkg as the user when the stock 1:51.1-1 is what is installed.
+
+Versioning is the whole contract. The rebuild sorts above stock 1:51.1-1 and
+below any future 1:51.1-2 or 51.2, so a normal pacman upgrade replaces it the
+moment Arch or upstream ships a fix, and the step must never pin or hold the
+package. When something newer than the rebuild is already installed, the fix
+is assumed upstream and the step skips with a warning so the assumption is
+visible rather than silent. When the rebuild itself is installed, the step is
+a no-op.
+
+These tests exercise the REAL function body, extracted from the `archsetup`
+script at run time. pacman, vercmp, makepkg and sudo are replaced by
+recorders driven by environment switches; the package directory is a temp
+copy of the real packages/gnome-keyring tree.
+
+Run from repo root:
+ python3 -m unittest tests.installer-steps.test_install_gnome_keyring_fix
+"""
+
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")
+PACKAGE_DIR = os.path.join(REPO_ROOT, "packages", "gnome-keyring")
+
+STUBS = r"""
+logfile=/dev/null
+action=""
+username=tester
+display() { echo "DISPLAY: $*"; }
+pacman_install() { echo "PACMAN_INSTALL: $1" >> "$MARKER"; }
+error_warn() { echo "WARN:$1"; return 1; }
+# pacman -Q prints the installed version, or fails when INSTALLED is empty.
+pacman() {
+ echo "PACMAN: $*" >> "$MARKER"
+ case "$1" in
+ -Q) [ -n "$INSTALLED" ] && echo "gnome-keyring $INSTALLED" ;;
+ *) return 0 ;;
+ esac
+}
+# The real vercmp is used so the ordering under test is pacman's own.
+makepkg() {
+ echo "MAKEPKG: $* (pwd=$PWD)" >> "$MARKER"
+ [ "$MAKEPKG_FAILS" = 1 ] && return 2
+ return 0
+}
+# sudo -u user cmd... runs cmd in place; the recorder notes the user.
+sudo() {
+ echo "SUDO: $*" >> "$MARKER"
+ shift 2
+ "$@"
+}
+safe_rm_rf() { rm -rf "$1"; }
+# chown needs root; the tests only care that the copy was staged.
+chown() { echo "CHOWN: $*" >> "$MARKER"; }
+"""
+
+
+def run(pkg_dir, installed, makepkg_fails=False, source_dir=None):
+ marker = tempfile.NamedTemporaryFile(prefix="keyring-fix-", delete=False)
+ marker.close()
+ source_dir = source_dir or tempfile.mkdtemp(prefix="keyring-src-")
+ script = (
+ STUBS
+ + "source <(sed -n '/^install_gnome_keyring_fix() {/,/^}/p' \"%s\")\n"
+ + 'user_archsetup_dir="%s"\n'
+ + 'source_dir="%s"\n'
+ + "install_gnome_keyring_fix\n"
+ + 'echo "RC=$?"\n'
+ ) % (ARCHSETUP, pkg_dir, source_dir)
+ env = dict(os.environ, MARKER=marker.name, INSTALLED=installed,
+ MAKEPKG_FAILS="1" if makepkg_fails else "0")
+ result = subprocess.run(["bash", "-c", script], capture_output=True,
+ text=True, timeout=30, env=env)
+ with open(marker.name) as f:
+ calls = f.read().splitlines()
+ os.remove(marker.name)
+ return result, calls, source_dir
+
+
+class InstallGnomeKeyringFix(unittest.TestCase):
+ def setUp(self):
+ # A fake archsetup checkout holding a copy of the real package tree.
+ self.checkout = tempfile.mkdtemp(prefix="archsetup-checkout-")
+ self.addCleanup(shutil.rmtree, self.checkout, ignore_errors=True)
+ shutil.copytree(PACKAGE_DIR, os.path.join(self.checkout, "packages", "gnome-keyring"))
+ self.source_dir = tempfile.mkdtemp(prefix="keyring-src-")
+ self.addCleanup(shutil.rmtree, self.source_dir, ignore_errors=True)
+
+ def _run(self, installed, **kw):
+ return run(self.checkout, installed, source_dir=self.source_dir, **kw)
+
+ @staticmethod
+ def _makepkg_calls(calls):
+ return [c for c in calls if c.startswith("MAKEPKG:")]
+
+ # ---------------------------------------------------------- normal ----
+ def test_stock_51_1_gets_the_rebuild(self):
+ result, calls, _ = self._run("1:51.1-1")
+ self.assertEqual(len(self._makepkg_calls(calls)), 1, calls)
+ self.assertIn("RC=0", result.stdout)
+ self.assertNotIn("WARN:", result.stdout)
+
+ def test_builds_as_the_user_with_syncdeps_and_install(self):
+ _, calls, _ = self._run("1:51.1-1")
+ self.assertTrue(any(c.startswith("SUDO: -u tester makepkg") for c in calls), calls)
+ makepkg = self._makepkg_calls(calls)[0]
+ self.assertIn("--noconfirm", makepkg)
+ # -s (sync makedepends) and -i (install), spelled separately or combined.
+ short = "".join(t[1:] for t in makepkg.split() if re.fullmatch(r"-[a-zA-Z]+", t))
+ self.assertIn("s", short, makepkg)
+ self.assertIn("i", short, makepkg)
+
+ def test_builds_from_a_user_owned_copy_not_the_checkout(self):
+ # makepkg writes into its directory; the checkout must stay clean.
+ _, calls, _ = self._run("1:51.1-1")
+ makepkg = self._makepkg_calls(calls)[0]
+ self.assertIn("pwd=" + self.source_dir, makepkg)
+ self.assertNotIn("pwd=" + self.checkout, makepkg)
+ self.assertTrue(os.path.exists(os.path.join(self.source_dir, "gnome-keyring", "PKGBUILD")))
+
+ def test_older_stock_releases_also_get_the_rebuild(self):
+ # 50.0 carries the same bug; the rebuild is 51.1 so it is an upgrade.
+ _, calls, _ = self._run("1:50.0-1")
+ self.assertEqual(len(self._makepkg_calls(calls)), 1)
+
+ # -------------------------------------------------------- boundary ----
+ def test_rebuild_already_installed_is_a_noop(self):
+ result, calls, _ = self._run("1:51.1-1.1")
+ self.assertEqual(self._makepkg_calls(calls), [])
+ self.assertIn("RC=0", result.stdout)
+ self.assertNotIn("WARN:", result.stdout)
+
+ def test_newer_package_skips_visibly_without_counting_an_error(self):
+ for newer in ("1:51.1-2", "1:51.2-1", "1:52.0-1"):
+ with self.subTest(installed=newer):
+ result, calls, _ = self._run(newer)
+ self.assertEqual(self._makepkg_calls(calls), [], newer)
+ self.assertIn("newer than the rebuild", result.stdout,
+ "a newer package means the fix is assumed upstream; say so")
+ self.assertNotIn("WARN:", result.stdout,
+ "a deliberate skip must not count as an installer error")
+ self.assertIn("RC=0", result.stdout)
+
+ def test_rerun_after_success_does_not_rebuild(self):
+ _, calls, _ = self._run("1:51.1-1")
+ self.assertEqual(len(self._makepkg_calls(calls)), 1)
+ _, calls, _ = self._run("1:51.1-1.1")
+ self.assertEqual(self._makepkg_calls(calls), [])
+
+ def test_missing_package_installs_stock_first_then_builds(self):
+ # With no gnome-keyring present, stock is installed as the baseline
+ # and the rebuild then upgrades it; both happen in that order.
+ result, calls, _ = self._run("")
+ self.assertEqual(len(self._makepkg_calls(calls)), 1)
+ stock = [i for i, c in enumerate(calls) if c.startswith("PACMAN_INSTALL: gnome-keyring")]
+ build = [i for i, c in enumerate(calls) if c.startswith("MAKEPKG:")]
+ self.assertEqual(len(stock), 1, calls)
+ self.assertLess(stock[0], build[0], "stock install must precede the build")
+ self.assertIn("RC=0", result.stdout)
+
+ def test_rerun_with_rebuild_installed_never_touches_stock(self):
+ # pacman's --needed only skips an equal version, so a stock install
+ # ahead of the rebuild would downgrade it on every re-run. The step
+ # must not call the stock install at all once anything is installed.
+ for installed in ("1:51.1-1.1", "1:51.1-1", "1:51.2-1"):
+ with self.subTest(installed=installed):
+ _, calls, _ = self._run(installed)
+ self.assertFalse(any(c.startswith("PACMAN_INSTALL:") for c in calls), calls)
+
+ # ----------------------------------------------------------- error ----
+ def test_failed_build_warns_and_leaves_stock_in_place(self):
+ result, calls, _ = self._run("1:51.1-1", makepkg_fails=True)
+ self.assertIn("WARN:", result.stdout)
+ self.assertNotIn("RC=0", result.stdout)
+ self.assertFalse(any(c.startswith("PACMAN: -U") for c in calls),
+ "nothing may be installed by hand after a failed build")
+
+ def test_missing_recipe_warns_instead_of_building(self):
+ shutil.rmtree(os.path.join(self.checkout, "packages", "gnome-keyring"))
+ result, calls, _ = self._run("1:51.1-1")
+ self.assertEqual(self._makepkg_calls(calls), [])
+ self.assertIn("WARN:", result.stdout)
+
+ def test_never_pins_or_holds_the_package(self):
+ # No IgnorePkg edit and no pacman -D hold: the next upstream fix must
+ # replace the rebuild on a normal upgrade.
+ _, calls, _ = self._run("1:51.1-1")
+ self.assertFalse(any("IgnorePkg" in c for c in calls))
+ self.assertFalse(any(c.startswith("PACMAN: -D") for c in calls))
+
+
+if __name__ == "__main__":
+ unittest.main()