diff options
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) |
