aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_configure_tlp_power.py
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-08-20 11:52:14 -0500
committerCraig Jennings <c@cjennings.net>2026-08-20 11:52:14 -0500
commit1c504d4d6a7a944de2ca000660d4c4461b5f1779 (patch)
tree922fcdba6d8aef1c42d6d69bccac3de4fc077a4b /tests/installer-steps/test_configure_tlp_power.py
parentfb13378559cdc6a9ef3b23883bcbda2937dc1195 (diff)
parentc588b0841df440e1a5290c7150ee245f90d1788e (diff)
downloadarchsetup-1c504d4d6a7a944de2ca000660d4c4461b5f1779.tar.gz
archsetup-1c504d4d6a7a944de2ca000660d4c4461b5f1779.zip
Merge origin/main: reconcile two parallel sessions' velox work
Both sides worked velox independently while this branch was open, so the overlaps needed settling by hand rather than by whichever side landed last. The settings-persistence bug had been recovered twice under different headings. Kept the upstream wording and dropped my duplicate — one bug, one task. The six tasks I archived had graduated from the Resolved section into archive/task-archive.org; upstream still listed them under Resolved. Removed them there so each lives in exactly one place. The ribbon task is the real disagreement. I verified on 08-15 that the reseat happened and the touchpad came back — the interrupt on amd_gpio pin 8 went from zero counts to 1795 and the i2c-HID reset timeout disappeared. The 08-17 entry concluded the opposite from the absence of a /dev/input/by-path/ node, which an i2c-HID touchpad often lacks even when working. Rather than close it over that entry, I left the task open and recorded both readings with the check that settles it, because velox was refusing ssh and I could not re-verify. Carried across my checks 6 and 7 on the post-rebuild task — the per-install Proton Bridge cert and password, neither of which can be restored from backup — and the restore-versus-re-derive split they belong to.
Diffstat (limited to 'tests/installer-steps/test_configure_tlp_power.py')
-rw-r--r--tests/installer-steps/test_configure_tlp_power.py87
1 files changed, 84 insertions, 3 deletions
diff --git a/tests/installer-steps/test_configure_tlp_power.py b/tests/installer-steps/test_configure_tlp_power.py
index c88e0c2..1ddff72 100644
--- a/tests/installer-steps/test_configure_tlp_power.py
+++ b/tests/installer-steps/test_configure_tlp_power.py
@@ -1,4 +1,4 @@
-"""Test configure_tlp_power's radio-enable line and laptop gating.
+"""Test configure_tlp_power's radio-enable line, daemon masking, and laptop gating.
systemd-rfkill is masked on laptops because it fights TLP's radio handling —
which means nothing restores radio state at boot unless TLP is told to. The
@@ -6,6 +6,20 @@ velox 2026-04-10 setup found wifi and bluetooth soft-blocked on first boot for
exactly this reason. The conf written here must carry
DEVICES_TO_ENABLE_ON_STARTUP so a fresh install comes up with radios on.
+power-profiles-daemon is masked and stopped on laptops for the same class of
+reason. power-profiles-daemon.service declares "Conflicts=tuned.service
+tlp.service auto-cpufreq.service ..." — the line is in ppd's unit, not tlp's —
+so systemd TERMs TLP the instant ppd starts. Leaving ppd merely disabled does
+not prevent that: ppd ships D-Bus activation files, and the desktop-settings
+panel's own powerprofilesctl call activates it on demand. Velox ran that way
+from its 2026-08-13 rebuild until 2026-08-16, with TLP failing at every boot and
+none of its battery policy applied, while the machine looked correctly
+configured. Masking blocks D-Bus activation too, which both keeps TLP alive and
+makes the panel's power control read as unavailable, the behavior the
+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.
+
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.
@@ -25,7 +39,7 @@ 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):
+def run(battery=True, bat_name="BAT0", unwritable_tlpd=False, systemctl_fails=False):
with tempfile.TemporaryDirectory() as d:
psdir = os.path.join(d, "power_supply")
os.makedirs(psdir)
@@ -37,13 +51,14 @@ def run(battery=True, bat_name="BAT0", unwritable_tlpd=False):
os.chmod(tlpd, stat.S_IRUSR | stat.S_IXUSR)
# 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
script = textwrap.dedent(f"""\
logfile=/dev/null
action=""
display() {{ :; }}
pacman_install() {{ echo "INSTALL: $1"; }}
run_task() {{ echo "TASK: $1"; }}
- systemctl() {{ echo "SYSTEMCTL: $*" >> "{d}/systemctl.log"; }}
+ systemctl() {{ echo "SYSTEMCTL: $*" >> "{d}/systemctl.log"; return {sysrc}; }}
error_warn() {{ echo "WARN: $1"; return 1; }}
source <(sed -n '/^configure_tlp_power() {{/,/^}}/p' "{ARCHSETUP}")
configure_tlp_power "{tlpd}" "{psdir}"
@@ -74,6 +89,44 @@ class ConfigureTlpPower(unittest.TestCase):
r.stdout)
self.assertIn("TASK: enabling TLP service", r.stdout)
+ def test_laptop_masks_power_profiles_daemon(self):
+ r = run(battery=True)
+ self.assertIn("SYSTEMCTL: mask power-profiles-daemon.service", r.stdout,
+ "ppd's unit declares Conflicts=...tlp.service..., so ppd "
+ "must be masked or it TERMs TLP whenever it is activated")
+
+ def test_laptop_stops_running_power_profiles_daemon(self):
+ """Masking alone leaves an already-running ppd running.
+
+ The installer runs on a booted system, so a repair re-run would
+ otherwise mask ppd, leave it live, and let it keep TLP dead until the
+ next reboot with nothing reporting it.
+ """
+ r = run(battery=True)
+ self.assertIn("SYSTEMCTL: stop power-profiles-daemon.service", r.stdout)
+
+ def test_ppd_is_masked_before_it_is_stopped(self):
+ """Order matters: stopping first leaves a window to re-activate in."""
+ calls = [line for line in run(battery=True).stdout.splitlines()
+ if line.startswith("SYSTEMCTL:") and "power-profiles-daemon" in line]
+ verbs = [line.split()[1] for line in calls]
+ self.assertEqual(verbs, ["mask", "stop"])
+
+ def test_power_profiles_daemon_is_masked_not_merely_disabled(self):
+ """Disabling ppd is not enough — D-Bus activation ignores it.
+
+ This is the whole point of the mask, so assert the verb directly. A
+ `disable` here would pass a naive "ppd is handled" check while leaving
+ the panel's powerprofilesctl call free to start ppd and kill TLP.
+ """
+ r = run(battery=True)
+ ppd_calls = [line for line in r.stdout.splitlines()
+ if line.startswith("SYSTEMCTL:") and "power-profiles-daemon" in line]
+ self.assertTrue(ppd_calls, "configure_tlp_power must act on ppd at all")
+ for line in ppd_calls:
+ self.assertNotIn(" disable ", line,
+ "disable leaves D-Bus activation live; only mask blocks it")
+
def test_radio_line_is_active_not_commented(self):
r = run(battery=True)
conf = r.stdout.split("CONF:[")[1].split("]")[0]
@@ -97,7 +150,35 @@ class ConfigureTlpPower(unittest.TestCase):
self.assertIn("INSTALL: tlp", r.stdout)
self.assertIn('DEVICES_TO_ENABLE_ON_STARTUP', r.stdout)
+ def test_desktop_keeps_power_profiles_daemon(self):
+ """A batteryless machine must NOT get ppd masked.
+
+ There is no TLP on a desktop to conflict with it, and the package-install
+ site enables ppd precisely so the settings panel's three-way power
+ control works there. Masking it here would break that control for no gain.
+ """
+ r = run(battery=False)
+ self.assertNotIn("power-profiles-daemon", r.stdout)
+
# ------------------------------------------------------------- error ----
+ def test_failed_ppd_mask_warns_and_does_not_crash(self):
+ """A masking failure must surface, not pass silently.
+
+ Silence is the exact failure mode being fixed: velox looked configured
+ while TLP was dead. If the mask cannot be applied, say so.
+
+ Assert on the harness's own RC= line, not on r.returncode. The harness
+ script ends in a literal `exit 0`, so r.returncode is 0 no matter what
+ configure_tlp_power does — asserting it can never fail, which would make
+ this test the same silent no-op it exists to catch.
+ """
+ r = run(battery=True, systemctl_fails=True)
+ self.assertIn("WARN: masking power-profiles-daemon for TLP", r.stdout)
+ self.assertIn("WARN: stopping power-profiles-daemon for TLP", r.stdout)
+ self.assertIn("RC=", r.stdout,
+ "the function must return so the install continues, "
+ "not exit and take the script down with it")
+
@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)