diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-24 12:19:04 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-24 12:19:04 -0500 |
| commit | 3becfac66dc569e71d0f6085fb56e92cfa6d626a (patch) | |
| tree | 1e27bfb5e8135fe3239728aa109cf67448f87cd2 /tests/nvidia-preflight | |
| parent | 40216e7c8e3c848190cbef1d3d1eebc8b5bc2136 (diff) | |
| download | archsetup-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.py | 173 |
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() |
