"""Tests for scripts/zz-bluetooth-resume. Two things break bluetooth across a sleep cycle on a TLP laptop, and nothing else on the machine fixes either. The rfkill soft-block is not restored. systemd-rfkill would do it, but it is masked deliberately -- it fights TLP's radio handling, so TLP owns radios instead. TLP's own sleep hook runs `tlp resume`, and its setting is DEVICES_TO_ENABLE_ON_STARTUP: startup, not resume. There is no ON_RESUME in TLP's vocabulary, so the resume edge has no owner at all. WiFi survives only because NetworkManager unblocks itself; bluetooth has no equivalent. The controller also comes back wedged from a hibernate. It reports powered and unblocked while scanning finds nothing whatever -- zero devices where the same room gave seventeen a minute later. bluetoothd logs "Failed to set mode" and "Failed to add device " at the instant of resume. Reloading btusb clears it. Both observed on velox 2026-08-21, on its first suspend-then-hibernate cycle after hibernate was switched back on. The hook re-asserts TLP's own declared intent rather than inventing a policy, so a machine that deliberately blocks bluetooth keeps it blocked. Run from repo root: python3 -m unittest tests.bluetooth-resume.test_bluetooth_resume """ import os import stat import subprocess import tempfile import unittest REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) HOOK = os.path.join(REPO_ROOT, "scripts", "zz-bluetooth-resume") TLP_WANTS_BT = 'DEVICES_TO_ENABLE_ON_STARTUP="bluetooth wifi"\n' TLP_WIFI_ONLY = 'DEVICES_TO_ENABLE_ON_STARTUP="wifi"\n' def run(phase="post", kind="suspend-then-hibernate", tlp_conf=TLP_WANTS_BT, conf_present=True): """Drive the hook with rfkill and modprobe faked, and read back the calls.""" with tempfile.TemporaryDirectory() as d: calls = os.path.join(d, "calls.log") bindir = os.path.join(d, "bin") os.makedirs(bindir) for tool in ("rfkill", "modprobe"): p = os.path.join(bindir, tool) with open(p, "w") as fh: fh.write(f'#!/bin/sh\necho "{tool} $*" >> "{calls}"\nexit 0\n') os.chmod(p, 0o755) conf = os.path.join(d, "tlp.conf") if conf_present: with open(conf, "w") as fh: fh.write(tlp_conf) env = dict(os.environ) env.update({ "BTR_RFKILL": os.path.join(bindir, "rfkill"), "BTR_MODPROBE": os.path.join(bindir, "modprobe"), "BTR_TLP_CONF": conf, "BTR_TLP_CONF_DIR": os.path.join(d, "tlp.d"), "BTR_SETTLE": "0", }) r = subprocess.run(["sh", HOOK, phase, kind], env=env, capture_output=True, text=True, timeout=20) log = "" if os.path.exists(calls): with open(calls) as fh: log = fh.read() return r, log class BluetoothResume(unittest.TestCase): # --- Normal --------------------------------------------------------- def test_hibernate_reloads_the_driver_and_unblocks(self): _, log = run(kind="suspend-then-hibernate") self.assertIn("modprobe -r btusb", log) self.assertIn("modprobe btusb", log) self.assertIn("rfkill unblock bluetooth", log) def test_the_unblock_comes_after_the_reload(self): # A freshly loaded btusb can come up soft-blocked, so unblocking first # would be undone by the reload that follows it. _, log = run() self.assertLess(log.index("modprobe btusb"), log.index("rfkill unblock")) def test_plain_suspend_unblocks_without_reloading(self): # The wedge was seen coming out of hibernate, which reinitialises the # controller from a saved image. A plain suspend restores USB intact, # so reloading there would cost a working adapter for nothing. _, log = run(kind="suspend") self.assertIn("rfkill unblock bluetooth", log) self.assertNotIn("btusb", log) # --- Boundary ------------------------------------------------------- def test_the_pre_phase_does_nothing(self): _, log = run(phase="pre") self.assertEqual(log, "") def test_a_tlp_policy_without_bluetooth_is_left_alone(self): # The hook re-asserts TLP's stated intent. It must not invent one, or # a machine that deliberately keeps bluetooth off gets it turned on at # every wakeup. _, log = run(tlp_conf=TLP_WIFI_ONLY) self.assertEqual(log, "") def test_a_commented_out_policy_does_not_count(self): _, log = run(tlp_conf='#DEVICES_TO_ENABLE_ON_STARTUP="bluetooth"\n') self.assertEqual(log, "") def test_hibernate_proper_also_reloads(self): _, log = run(kind="hibernate") self.assertIn("modprobe -r btusb", log) # --- Error ---------------------------------------------------------- def test_a_missing_tlp_config_is_left_alone(self): # No declared policy means no intent to re-assert. Failing safe here # means doing nothing, not guessing. _, log = run(conf_present=False) self.assertEqual(log, "") def test_the_hook_always_exits_zero(self): # systemd-sleep logs a failing hook and the noise outlives the cause. # Nothing here is worth delaying or alarming a resume over. for kind in ("suspend", "hibernate", "suspend-then-hibernate"): with self.subTest(kind=kind): r, _ = run(kind=kind) self.assertEqual(r.returncode, 0, r.stderr) def test_it_is_executable(self): self.assertTrue(os.stat(HOOK).st_mode & stat.S_IXUSR, "systemd-sleep only runs executables") if __name__ == "__main__": unittest.main()