aboutsummaryrefslogtreecommitdiff
path: root/tests/nvidia-preflight
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-24 12:19:04 -0500
committerCraig Jennings <c@cjennings.net>2026-07-24 12:19:04 -0500
commit3becfac66dc569e71d0f6085fb56e92cfa6d626a (patch)
tree1e27bfb5e8135fe3239728aa109cf67448f87cd2 /tests/nvidia-preflight
parent40216e7c8e3c848190cbef1d3d1eebc8b5bc2136 (diff)
downloadarchsetup-3becfac66dc569e71d0f6085fb56e92cfa6d626a.tar.gz
archsetup-3becfac66dc569e71d0f6085fb56e92cfa6d626a.zip
fix(installer): eight fixes from an overnight bug-hunt and its review
I squashed these because the per-bug reasoning lives in todo.org, which this commit carries. Two could cost a machine. configure_initramfs_hook swapped the udev hook for systemd on a LUKS root, leaving a standalone encrypt hook under an init that never runs it. The rebuild succeeds and the installer exits clean, then the root won't unlock at the next boot. trim_firmware ran pacman -Rdd against twelve firmware packages behind a DMI gate reading product_name, where "Framework" never appears. That left it dead on the hardware it targets, and dangerous to fix the obvious way: this machine is a Framework Desktop whose Ryzen iGPU needs the amdgpu firmware. It refuses on PCI modalias evidence now. wipedisk discarded before it checked. blkdiscard ran with -f, which disables the exclusive open, so picking the wrong disk destroyed a live filesystem and then reported that nothing had happened. Four more are smaller. The NVIDIA preflight aborted dwm and headless installs over a driver floor they never need. zfs-replicate exited 0 after every dataset failed. Unattended installs blocked on two prompts, and the first fix for that inherited a [Y/n] default into passwordless console login. A fresh install left the dotfiles repo permanently dirty. The review found a pattern worth more than any single fix. Helpers had thorough tests and none proved they were called. Deleting the call left five suites green, including the guard on that pacman -Rdd. CALL_SITES now pins nine caller/callee pairs. The suite runs 341 tests at exit 0, with no new shellcheck findings. I proved every guard by deleting it and watching the intended test go red.
Diffstat (limited to 'tests/nvidia-preflight')
-rw-r--r--tests/nvidia-preflight/test_nvidia_preflight_gate.py173
1 files changed, 173 insertions, 0 deletions
diff --git a/tests/nvidia-preflight/test_nvidia_preflight_gate.py b/tests/nvidia-preflight/test_nvidia_preflight_gate.py
new file mode 100644
index 0000000..1d76689
--- /dev/null
+++ b/tests/nvidia-preflight/test_nvidia_preflight_gate.py
@@ -0,0 +1,173 @@
+"""Tests for the nvidia_preflight policy wrapper in the archsetup installer.
+
+nvidia_preflight_report (covered by test_nvidia_preflight.py) is the pure
+detection core. nvidia_preflight is the policy around it: it decides whether
+the check applies at all, runs the report, aborts on a too-old driver, and
+confirms an otherwise-supported card with the user.
+
+The check is Wayland-specific -- every line it prints names Wayland/Hyprland,
+and its own failure hint tells the user to install with DESKTOP_ENV=dwm
+instead. So it must not fire on a dwm (X11) or headless (none) install, and it
+must not fire when --no-gpu-drivers / NO_GPU_DRIVERS=yes says the user handles
+the driver themselves -- the same ruling install_gpu_drivers already makes.
+
+These tests exercise the REAL function body, extracted from the `archsetup`
+script at run time (not a copy), with nvidia_preflight_report stubbed to a
+chosen verdict.
+
+Run from repo root:
+ python3 -m unittest tests.nvidia-preflight.test_nvidia_preflight_gate
+"""
+
+import os
+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")
+
+# nvidia_preflight_report's documented return codes.
+RC_NO_NVIDIA = 0
+RC_NVIDIA_OK = 10
+RC_NVIDIA_TOO_OLD = 11
+
+
+class NvidiaPreflightGateHarness(unittest.TestCase):
+ """Source nvidia_preflight out of the real archsetup script."""
+
+ def setUp(self):
+ self.tmp = tempfile.mkdtemp(prefix="nvidia-gate-test-")
+ self.marker = os.path.join(self.tmp, "report-calls")
+ self.wrapper = os.path.join(self.tmp, "run.sh")
+ with open(self.wrapper, "w") as f:
+ f.write(
+ "#!/bin/bash\n"
+ 'ARCHSETUP="$1"; shift\n'
+ "source <(sed -n "
+ "'/^nvidia_preflight() {/,/^}/p' \"$ARCHSETUP\")\n"
+ # Stub the detection core: record the call, answer with $STUB_RC.
+ "nvidia_preflight_report() {\n"
+ ' echo "called" >> "$MARKER"\n'
+ ' return "${STUB_RC:-0}"\n'
+ "}\n"
+ "nvidia_preflight\n"
+ 'echo "PREFLIGHT_RETURNED"\n'
+ # Whatever the prompt did not consume is still on stdin.
+ 'if IFS= read -r leftover; then echo "LEFTOVER:$leftover"; '
+ 'else echo "LEFTOVER:"; fi\n'
+ )
+ os.chmod(self.wrapper, 0o755)
+
+ def tearDown(self):
+ shutil.rmtree(self.tmp, ignore_errors=True)
+
+ def run_gate(self, rc, desktop_env="hyprland", skip_gpu_drivers="false",
+ config_file="", stdin=""):
+ env = dict(os.environ)
+ env["MARKER"] = self.marker
+ env["STUB_RC"] = str(rc)
+ env["desktop_env"] = desktop_env
+ env["skip_gpu_drivers"] = skip_gpu_drivers
+ env["config_file"] = config_file
+ return subprocess.run(
+ ["bash", self.wrapper, ARCHSETUP],
+ input=stdin, capture_output=True, text=True, env=env,
+ )
+
+ def report_calls(self):
+ if not os.path.exists(self.marker):
+ return 0
+ with open(self.marker) as f:
+ return len([line for line in f if line.strip()])
+
+ def assertContinued(self, r):
+ self.assertIn("PREFLIGHT_RETURNED", r.stdout,
+ "preflight aborted; stdout=%r stderr=%r" % (r.stdout, r.stderr))
+ self.assertEqual(r.returncode, 0)
+
+ def assertAborted(self, r):
+ self.assertNotIn("PREFLIGHT_RETURNED", r.stdout)
+ self.assertEqual(r.returncode, 1)
+
+ # ---------------------------------------------------------- normal ----
+ def test_hyprland_no_nvidia_continues_without_prompting(self):
+ r = self.run_gate(RC_NO_NVIDIA, stdin="sentinel\n")
+ self.assertContinued(r)
+ self.assertEqual(self.report_calls(), 1)
+ self.assertIn("LEFTOVER:sentinel", r.stdout)
+
+ def test_hyprland_supported_driver_prompts_and_continues_on_yes(self):
+ # bash emits `read -p`'s prompt only when stdin is a terminal, so the
+ # prompt text is unassertable here. What is assertable -- and is the
+ # thing that matters -- is that the read consumed exactly the answer
+ # line and left the rest of stdin alone.
+ r = self.run_gate(RC_NVIDIA_OK, stdin="y\nsentinel\n")
+ self.assertContinued(r)
+ self.assertIn("Continuing on NVIDIA", r.stdout)
+ self.assertIn("LEFTOVER:sentinel", r.stdout)
+
+ def test_hyprland_old_driver_still_aborts(self):
+ # The check must keep working where it actually applies.
+ r = self.run_gate(RC_NVIDIA_TOO_OLD)
+ self.assertAborted(r)
+ self.assertEqual(self.report_calls(), 1)
+
+ # -------------------------------------------------------- boundary ----
+ def test_dwm_old_driver_does_not_abort(self):
+ # An X11 install does not care about the Wayland driver floor -- and
+ # the abort's own hint tells the user to install with DESKTOP_ENV=dwm.
+ r = self.run_gate(RC_NVIDIA_TOO_OLD, desktop_env="dwm")
+ self.assertContinued(r)
+ self.assertEqual(self.report_calls(), 0)
+
+ def test_none_old_driver_does_not_abort(self):
+ # A headless install never runs a compositor at all.
+ r = self.run_gate(RC_NVIDIA_TOO_OLD, desktop_env="none")
+ self.assertContinued(r)
+ self.assertEqual(self.report_calls(), 0)
+
+ def test_skip_gpu_drivers_old_driver_does_not_abort(self):
+ # --no-gpu-drivers means the user installs the driver themselves, so
+ # the repo's candidate version is not archsetup's call to veto.
+ r = self.run_gate(RC_NVIDIA_TOO_OLD, skip_gpu_drivers="true")
+ self.assertContinued(r)
+ self.assertEqual(self.report_calls(), 0)
+
+ def test_dwm_supported_driver_does_not_prompt(self):
+ r = self.run_gate(RC_NVIDIA_OK, desktop_env="dwm", stdin="sentinel\n")
+ self.assertContinued(r)
+ self.assertIn("LEFTOVER:sentinel", r.stdout)
+
+ def test_unattended_config_file_answers_the_prompt_itself(self):
+ # --config-file is the documented unattended mode. The rc-10 prompt is
+ # advisory (the driver requirement is met), so it must not block on a
+ # read -- the same failure aur_install's --answerdiff/--answerclean
+ # comment already rules out.
+ r = self.run_gate(RC_NVIDIA_OK, config_file="/etc/archsetup.conf",
+ stdin="sentinel\n")
+ self.assertContinued(r)
+ self.assertIn("Continuing on NVIDIA", r.stdout)
+ self.assertIn("LEFTOVER:sentinel", r.stdout)
+
+ # ----------------------------------------------------------- error ----
+ def test_hyprland_supported_driver_aborts_on_no(self):
+ r = self.run_gate(RC_NVIDIA_OK, stdin="n\n")
+ self.assertAborted(r)
+ self.assertIn("Aborted at NVIDIA preflight.", r.stdout)
+
+ def test_hyprland_supported_driver_aborts_on_capital_no(self):
+ r = self.run_gate(RC_NVIDIA_OK, stdin="No\n")
+ self.assertAborted(r)
+
+ def test_unattended_old_driver_still_aborts(self):
+ # Unattended does not mean "never fail" -- a driver below the floor is
+ # still a hard stop on a hyprland install.
+ r = self.run_gate(RC_NVIDIA_TOO_OLD, config_file="/etc/archsetup.conf")
+ self.assertAborted(r)
+
+
+if __name__ == "__main__":
+ unittest.main()