diff options
| author | Craig Jennings <c@cjennings.net> | 2026-10-05 22:33:44 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-10-05 22:34:54 -0600 |
| commit | 1e134f5022f626ed36279915369c06fa6df0e76e (patch) | |
| tree | 378abcba20c1b493435ac79d31156e11611aa6fa /tests/installer-steps/test_configure_tlp_power.py | |
| parent | 32008ac10409e1a6f8ddb8820c6e8ac30e575db4 (diff) | |
| download | archsetup-1e134f5022f626ed36279915369c06fa6df0e76e.tar.gz archsetup-1e134f5022f626ed36279915369c06fa6df0e76e.zip | |
fix(tlp): install the bluetooth resume hook where systemd-sleep looks
systemd-sleep scans only /usr/lib/systemd/system-sleep/. The step created /etc/systemd/system-sleep/ and installed the hook there, so it never ran. Velox came out of about fifteen hibernate cycles since 08-21 with bluetooth rfkilled, and TLP's own resume hook carried the block forward every time. I dropped the mkdir, since the systemd package owns the real directory. The step's test now asserts the full install list, so a copy under /etc fails it.
Diffstat (limited to 'tests/installer-steps/test_configure_tlp_power.py')
| -rw-r--r-- | tests/installer-steps/test_configure_tlp_power.py | 50 |
1 files changed, 49 insertions, 1 deletions
diff --git a/tests/installer-steps/test_configure_tlp_power.py b/tests/installer-steps/test_configure_tlp_power.py index 1ddff72..dc46c93 100644 --- a/tests/installer-steps/test_configure_tlp_power.py +++ b/tests/installer-steps/test_configure_tlp_power.py @@ -20,6 +20,15 @@ package-install site in `archsetup` already documents as intended. The stop is what makes a repair re-run take effect on a booted machine, where a mask alone would leave a running ppd running. +The bluetooth resume hook installed here must land in +/usr/lib/systemd/system-sleep/. That is the only directory systemd-sleep scans +(systemd 262's binary and its man page both name just the one), so a hook under +/etc/systemd/system-sleep/ is silently dead. Velox ran about fifteen hibernate +cycles between 2026-08-21 and 2026-10-05 with the hook installed there and it +never fired once; bluetooth came back rfkilled every time, and TLP's own hook +(which lives in /usr/lib and does run) carried the block across every later +cycle. + Method: sed-extract configure_tlp_power from the real `archsetup`, point it at a temp tlp.d dir and a temp power-supply dir, and fake pacman_install / run_task / display / error_warn / systemctl. @@ -39,7 +48,8 @@ REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") -def run(battery=True, bat_name="BAT0", unwritable_tlpd=False, systemctl_fails=False): +def run(battery=True, bat_name="BAT0", unwritable_tlpd=False, systemctl_fails=False, + install_fails=False): with tempfile.TemporaryDirectory() as d: psdir = os.path.join(d, "power_supply") os.makedirs(psdir) @@ -52,6 +62,9 @@ def run(battery=True, bat_name="BAT0", unwritable_tlpd=False, systemctl_fails=Fa # The real mask call redirects stdout into $logfile, so the fake # systemctl records to a side file the test reads back instead. sysrc = 1 if systemctl_fails else 0 + # The real install_executable warns on its own; the fake only reports + # the return code so the test can see whether the step keeps going. + instrc = 1 if install_fails else 0 script = textwrap.dedent(f"""\ logfile=/dev/null action="" @@ -60,6 +73,8 @@ def run(battery=True, bat_name="BAT0", unwritable_tlpd=False, systemctl_fails=Fa run_task() {{ echo "TASK: $1"; }} systemctl() {{ echo "SYSTEMCTL: $*" >> "{d}/systemctl.log"; return {sysrc}; }} error_warn() {{ echo "WARN: $1"; return 1; }} + user_archsetup_dir=/src + install_executable() {{ echo "INSTALL_EXEC: $1 -> $2"; return {instrc}; }} source <(sed -n '/^configure_tlp_power() {{/,/^}}/p' "{ARCHSETUP}") configure_tlp_power "{tlpd}" "{psdir}" echo "RC=$?" @@ -138,7 +153,29 @@ class ConfigureTlpPower(unittest.TestCase): else: self.fail("DEVICES_TO_ENABLE_ON_STARTUP line missing from conf") + def test_laptop_installs_the_resume_hook_where_systemd_sleep_scans(self): + """The hook goes to /usr/lib/systemd/system-sleep/, the only scanned dir. + + Assert the full install list, not a substring: a second copy under + /etc/ would pass an assertIn and still be the dead hook this test + exists to catch. + """ + r = run(battery=True) + installs = [line for line in r.stdout.splitlines() + if line.startswith("INSTALL_EXEC:")] + self.assertEqual( + installs, + ["INSTALL_EXEC: /src/scripts/zz-bluetooth-resume -> " + "/usr/lib/systemd/system-sleep/zz-bluetooth-resume"], + "systemd-sleep never scans /etc/systemd/system-sleep/; a hook " + "there installs cleanly and never runs") + # ---------------------------------------------------------- boundary ---- + def test_desktop_installs_no_resume_hook(self): + """No TLP on a desktop means no masked rfkill and no gap to fill.""" + r = run(battery=False) + self.assertNotIn("INSTALL_EXEC:", r.stdout) + def test_desktop_without_battery_is_a_no_op(self): r = run(battery=False) self.assertNotIn("INSTALL:", r.stdout) @@ -179,6 +216,17 @@ class ConfigureTlpPower(unittest.TestCase): "the function must return so the install continues, " "not exit and take the script down with it") + def test_failed_hook_install_does_not_stop_the_ppd_mask(self): + """install_executable warns for itself, so the step must carry on. + + The mask that follows is what keeps TLP alive; a hook copy that fails + (source missing on a partial checkout) must not take it with it. + """ + r = run(battery=True, install_fails=True) + self.assertIn("INSTALL_EXEC:", r.stdout) + self.assertIn("SYSTEMCTL: mask power-profiles-daemon.service", r.stdout) + self.assertIn("RC=", r.stdout) + @unittest.skipUnless(os.geteuid() != 0, "root ignores directory write bits") def test_unwritable_tlpd_warns_and_does_not_crash(self): r = run(battery=True, unwritable_tlpd=True) |
