aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_configure_tlp_power.py
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-10-05 22:33:44 -0600
committerCraig Jennings <c@cjennings.net>2026-10-05 22:34:54 -0600
commit1e134f5022f626ed36279915369c06fa6df0e76e (patch)
tree378abcba20c1b493435ac79d31156e11611aa6fa /tests/installer-steps/test_configure_tlp_power.py
parent32008ac10409e1a6f8ddb8820c6e8ac30e575db4 (diff)
downloadarchsetup-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.py50
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)