aboutsummaryrefslogtreecommitdiff
path: root/tests/airplane-mode
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-21 17:48:47 -0400
committerCraig Jennings <c@cjennings.net>2026-05-21 17:48:47 -0400
commit09f4d205fe463faf676f95e798d08e8bf498be96 (patch)
treeac60b2aa4d8350d5ab3c0e6f76361daf70d1d702 /tests/airplane-mode
parenteee30be993c6ff79a5e7fa5f37d6ba368dc0c3d9 (diff)
downloadarchsetup-09f4d205fe463faf676f95e798d08e8bf498be96.tar.gz
archsetup-09f4d205fe463faf676f95e798d08e8bf498be96.zip
feat(hyprland): add airplane-mode waybar toggle
I added a laptop-only waybar button that drops the machine into a low-power state and restores it on a second click. Engaging turns wifi off, sets the CPU energy-performance preference to power, dims the backlight to 35%, and stops network-only services (tailscale, proton-vpn, avahi, cups, wsdd, geoclue, sshd, fail2ban, syncthing). Bluetooth is left alone so earbuds keep working. Disengaging replays the state recorded when airplane mode was engaged rather than writing hardcoded defaults. A lever already in its low-power position is left untouched: wifi that was already off stays off, and a service that was already stopped isn't restarted. The indicator hides itself on machines with no battery, so desktops never show the button. State lives in $XDG_RUNTIME_DIR/airplane-state, and the bar refreshes the moment the toggle fires via a realtime signal.
Diffstat (limited to 'tests/airplane-mode')
-rw-r--r--tests/airplane-mode/test_airplane_mode.py324
1 files changed, 324 insertions, 0 deletions
diff --git a/tests/airplane-mode/test_airplane_mode.py b/tests/airplane-mode/test_airplane_mode.py
new file mode 100644
index 0000000..5db0ed1
--- /dev/null
+++ b/tests/airplane-mode/test_airplane_mode.py
@@ -0,0 +1,324 @@
+"""Tests for dotfiles/hyprland/.local/bin/airplane-mode.
+
+airplane-mode is a stateful toggle. On engage it RECORDS the current state of
+each lever (wifi on/off, CPU EPP value, brightness, which services were
+running) to a state file, then applies the low-power settings. On disengage it
+reads that file and RESTORES exactly what was recorded — so a lever that was
+already in its low-power position before engaging is left untouched on
+disengage. That save-and-replay logic is what these tests pin down.
+
+The real script runs against command stubs (sudo / nmcli / brightnessctl /
+systemctl / notify / pkill) placed on PATH, plus fake EPP sysfs files in a
+temp dir. The stubs log every invocation and report state driven by STUB_*
+env vars, so the test controls "what was running" without touching the host.
+No reimplementation of the script — the production body executes.
+
+Run from repo root:
+ python3 -m unittest tests.airplane-mode.test_airplane_mode
+"""
+
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+
+REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+SCRIPT = os.path.join(REPO_ROOT, "dotfiles/hyprland/.local/bin/airplane-mode")
+
+SYSTEM_SERVICES = "svc-a.service svc-b.service svc-c.service"
+USER_SERVICES = "svc-user.service"
+
+
+class AirplaneModeHarness(unittest.TestCase):
+
+ def setUp(self):
+ self.tmp = tempfile.mkdtemp(prefix="airplane-mode-test-")
+ self.state_file = os.path.join(self.tmp, "airplane-state")
+ self.stub_log = os.path.join(self.tmp, "stub.log")
+
+ # Fake EPP sysfs files for two CPUs, pre-set to the normal value.
+ self.epp_dir = os.path.join(self.tmp, "epp")
+ self.epp_files = []
+ for cpu in ("cpu0", "cpu1"):
+ d = os.path.join(self.epp_dir, cpu)
+ os.makedirs(d)
+ f = os.path.join(d, "energy_performance_preference")
+ self._write(f, "balance_performance\n")
+ self.epp_files.append(f)
+ self.epp_glob = os.path.join(self.epp_dir, "cpu*", "energy_performance_preference")
+
+ self.stub_dir = os.path.join(self.tmp, "stubs")
+ os.makedirs(self.stub_dir)
+ self._make_stubs()
+
+ def tearDown(self):
+ shutil.rmtree(self.tmp, ignore_errors=True)
+
+ # -- helpers --------------------------------------------------------------
+
+ def _write(self, path, contents):
+ with open(path, "w") as f:
+ f.write(contents)
+
+ def _stub(self, name, body):
+ path = os.path.join(self.stub_dir, name)
+ self._write(path, "#!/bin/sh\n" + body)
+ os.chmod(path, 0o755)
+
+ def _make_stubs(self):
+ # sudo just runs the rest of the command line (no privilege needed in
+ # the test; the fake EPP files are writable by the test user).
+ self._stub("sudo", 'exec "$@"\n')
+
+ self._stub("nmcli", (
+ 'echo "nmcli $*" >> "$STUB_LOG"\n'
+ 'if [ "$1" = radio ] && [ "$2" = wifi ] && [ -z "$3" ]; then\n'
+ ' echo "${STUB_WIFI:-enabled}"\n'
+ 'fi\n'
+ 'exit 0\n'
+ ))
+
+ self._stub("brightnessctl", (
+ 'echo "brightnessctl $*" >> "$STUB_LOG"\n'
+ 'case "$1" in\n'
+ ' get) echo "${STUB_BRIGHTNESS:-96000}" ;;\n'
+ ' max) echo 96000 ;;\n'
+ 'esac\n'
+ 'exit 0\n'
+ ))
+
+ self._stub("systemctl", (
+ 'echo "systemctl $*" >> "$STUB_LOG"\n'
+ 'user=0; sub=""; svc=""\n'
+ 'for a in "$@"; do\n'
+ ' case "$a" in\n'
+ ' --user) user=1 ;;\n'
+ ' --quiet) ;;\n'
+ ' is-active|stop|start) sub="$a" ;;\n'
+ ' *) svc="$a" ;;\n'
+ ' esac\n'
+ 'done\n'
+ 'if [ "$sub" = is-active ]; then\n'
+ ' if [ "$user" = 1 ]; then list="$STUB_ACTIVE_USER"; else list="$STUB_ACTIVE_SYSTEM"; fi\n'
+ ' case " $list " in *" $svc "*) exit 0 ;; *) exit 3 ;; esac\n'
+ 'fi\n'
+ 'exit 0\n'
+ ))
+
+ self._stub("notify", 'echo "notify $*" >> "$STUB_LOG"\nexit 0\n')
+ self._stub("pkill", 'echo "pkill $*" >> "$STUB_LOG"\nexit 0\n')
+
+ def run_toggle(self, wifi="enabled", brightness="96000",
+ active_system=SYSTEM_SERVICES, active_user=USER_SERVICES):
+ env = os.environ.copy()
+ env["PATH"] = self.stub_dir + os.pathsep + env["PATH"]
+ env["XDG_RUNTIME_DIR"] = self.tmp
+ env["STUB_LOG"] = self.stub_log
+ env["STUB_WIFI"] = wifi
+ env["STUB_BRIGHTNESS"] = brightness
+ env["STUB_ACTIVE_SYSTEM"] = active_system
+ env["STUB_ACTIVE_USER"] = active_user
+ env["AIRPLANE_EPP_GLOB"] = self.epp_glob
+ env["AIRPLANE_SYSTEM_SERVICES"] = SYSTEM_SERVICES
+ env["AIRPLANE_USER_SERVICES"] = USER_SERVICES
+ env["AIRPLANE_BRIGHTNESS_LOW"] = "35%"
+ return subprocess.run(
+ [SCRIPT], env=env, capture_output=True, text=True, timeout=15,
+ )
+
+ def state(self):
+ out = {}
+ with open(self.state_file) as f:
+ for line in f:
+ if "=" in line:
+ k, v = line.rstrip("\n").split("=", 1)
+ out[k] = v
+ return out
+
+ def log(self):
+ try:
+ with open(self.stub_log) as f:
+ return f.read()
+ except FileNotFoundError:
+ return ""
+
+ def epp_values(self):
+ vals = []
+ for f in self.epp_files:
+ with open(f) as fh:
+ vals.append(fh.read().strip())
+ return vals
+
+
+# -----------------------------------------------------------------------------
+# Normal cases — engage from a clean (everything-on) state
+# -----------------------------------------------------------------------------
+
+class TestEngage(AirplaneModeHarness):
+
+ def test_engage_writes_mode_on(self):
+ r = self.run_toggle()
+ self.assertEqual(r.returncode, 0, msg=r.stderr)
+ self.assertEqual(self.state()["mode"], "on")
+
+ def test_engage_turns_wifi_off(self):
+ self.run_toggle(wifi="enabled")
+ self.assertIn("nmcli radio wifi off", self.log())
+
+ def test_engage_records_prior_wifi_state(self):
+ self.run_toggle(wifi="enabled")
+ self.assertEqual(self.state()["wifi"], "enabled")
+
+ def test_engage_sets_epp_to_power_on_all_cpus(self):
+ self.run_toggle()
+ self.assertEqual(self.epp_values(), ["power", "power"])
+
+ def test_engage_records_prior_epp(self):
+ self.run_toggle()
+ self.assertEqual(self.state()["epp"], "balance_performance")
+
+ def test_engage_dims_brightness_and_saves_prior(self):
+ self.run_toggle(brightness="96000")
+ self.assertIn("brightnessctl set 35%", self.log())
+ self.assertEqual(self.state()["brightness"], "96000")
+
+ def test_engage_stops_active_services_and_records_them(self):
+ self.run_toggle(active_system="svc-a.service svc-c.service",
+ active_user="svc-user.service")
+ log = self.log()
+ self.assertIn("systemctl stop svc-a.service", log)
+ self.assertIn("systemctl stop svc-c.service", log)
+ self.assertIn("systemctl --user stop svc-user.service", log)
+ self.assertIn("svc-a.service", self.state()["stopped_system"])
+ self.assertIn("svc-c.service", self.state()["stopped_system"])
+ self.assertIn("svc-user.service", self.state()["stopped_user"])
+
+ def test_engage_does_not_stop_already_inactive_service(self):
+ # svc-b is not in the active list → never stopped, never recorded.
+ self.run_toggle(active_system="svc-a.service", active_user="")
+ log = self.log()
+ self.assertNotIn("systemctl stop svc-b.service", log)
+ self.assertNotIn("svc-b.service", self.state().get("stopped_system", ""))
+
+ def test_engage_refreshes_waybar(self):
+ self.run_toggle()
+ self.assertIn("pkill -RTMIN+10 waybar", self.log())
+
+
+# -----------------------------------------------------------------------------
+# Normal cases — disengage restores recorded state
+# -----------------------------------------------------------------------------
+
+class TestDisengage(AirplaneModeHarness):
+
+ def seed_on(self, wifi="enabled", epp="balance_performance",
+ brightness="96000", stopped_system="svc-a.service svc-c.service",
+ stopped_user="svc-user.service"):
+ self._write(self.state_file, (
+ f"mode=on\nwifi={wifi}\nepp={epp}\nbrightness={brightness}\n"
+ f"stopped_system={stopped_system}\nstopped_user={stopped_user}\n"
+ ))
+ # EPP files are in low-power state while engaged.
+ for f in self.epp_files:
+ self._write(f, "power\n")
+
+ def test_disengage_writes_mode_off(self):
+ self.seed_on()
+ r = self.run_toggle()
+ self.assertEqual(r.returncode, 0, msg=r.stderr)
+ self.assertEqual(self.state()["mode"], "off")
+
+ def test_disengage_restores_wifi_when_it_was_on(self):
+ self.seed_on(wifi="enabled")
+ self.run_toggle()
+ self.assertIn("nmcli radio wifi on", self.log())
+
+ def test_disengage_restores_epp(self):
+ self.seed_on(epp="balance_performance")
+ self.run_toggle()
+ self.assertEqual(self.epp_values(), ["balance_performance", "balance_performance"])
+
+ def test_disengage_restores_brightness_to_saved_value(self):
+ self.seed_on(brightness="80000")
+ self.run_toggle()
+ self.assertIn("brightnessctl set 80000", self.log())
+
+ def test_disengage_restarts_recorded_services(self):
+ self.seed_on(stopped_system="svc-a.service svc-c.service",
+ stopped_user="svc-user.service")
+ log = self.log() # before
+ self.run_toggle()
+ log = self.log()
+ self.assertIn("systemctl start svc-a.service", log)
+ self.assertIn("systemctl start svc-c.service", log)
+ self.assertIn("systemctl --user start svc-user.service", log)
+
+
+# -----------------------------------------------------------------------------
+# Boundary — "leave it as it was" cases
+# -----------------------------------------------------------------------------
+
+class TestPreserveExistingState(AirplaneModeHarness):
+
+ def test_engage_with_wifi_already_off_records_disabled(self):
+ self.run_toggle(wifi="disabled")
+ self.assertEqual(self.state()["wifi"], "disabled")
+
+ def test_disengage_does_not_reenable_wifi_that_was_already_off(self):
+ # Seed an engaged state where wifi was already off before engaging.
+ self._write(self.state_file, (
+ "mode=on\nwifi=disabled\nepp=balance_performance\n"
+ "brightness=96000\nstopped_system=\nstopped_user=\n"
+ ))
+ for f in self.epp_files:
+ self._write(f, "power\n")
+ self.run_toggle()
+ self.assertNotIn("nmcli radio wifi on", self.log())
+
+ def test_disengage_only_restarts_recorded_services_not_all_known(self):
+ # Only svc-a was recorded as stopped → svc-b/svc-c must not be started.
+ self._write(self.state_file, (
+ "mode=on\nwifi=enabled\nepp=balance_performance\n"
+ "brightness=96000\nstopped_system=svc-a.service\nstopped_user=\n"
+ ))
+ for f in self.epp_files:
+ self._write(f, "power\n")
+ self.run_toggle()
+ log = self.log()
+ self.assertIn("systemctl start svc-a.service", log)
+ self.assertNotIn("systemctl start svc-b.service", log)
+ self.assertNotIn("systemctl start svc-c.service", log)
+
+ def test_disengage_with_no_services_recorded_starts_nothing(self):
+ self._write(self.state_file, (
+ "mode=on\nwifi=enabled\nepp=balance_performance\n"
+ "brightness=96000\nstopped_system=\nstopped_user=\n"
+ ))
+ for f in self.epp_files:
+ self._write(f, "power\n")
+ self.run_toggle()
+ self.assertNotIn("systemctl start", self.log())
+
+
+# -----------------------------------------------------------------------------
+# Boundary — toggle dispatch
+# -----------------------------------------------------------------------------
+
+class TestToggleDispatch(AirplaneModeHarness):
+
+ def test_missing_state_file_engages(self):
+ # No state file → not engaged → first run turns airplane mode ON.
+ self.assertFalse(os.path.exists(self.state_file))
+ self.run_toggle()
+ self.assertEqual(self.state()["mode"], "on")
+
+ def test_mode_off_file_engages(self):
+ self._write(self.state_file, "mode=off\n")
+ self.run_toggle()
+ self.assertEqual(self.state()["mode"], "on")
+
+
+if __name__ == "__main__":
+ unittest.main()