diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-21 17:48:47 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-21 17:48:47 -0400 |
| commit | 09f4d205fe463faf676f95e798d08e8bf498be96 (patch) | |
| tree | ac60b2aa4d8350d5ab3c0e6f76361daf70d1d702 /tests | |
| parent | eee30be993c6ff79a5e7fa5f37d6ba368dc0c3d9 (diff) | |
| download | archsetup-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')
| -rw-r--r-- | tests/airplane-mode/test_airplane_mode.py | 324 | ||||
| -rw-r--r-- | tests/waybar-airplane/test_waybar_airplane.py | 156 |
2 files changed, 480 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() diff --git a/tests/waybar-airplane/test_waybar_airplane.py b/tests/waybar-airplane/test_waybar_airplane.py new file mode 100644 index 0000000..b3b9c06 --- /dev/null +++ b/tests/waybar-airplane/test_waybar_airplane.py @@ -0,0 +1,156 @@ +"""Tests for dotfiles/hyprland/.local/bin/waybar-airplane. + +The script emits one JSON line for waybar's custom/airplane module, derived +from the airplane-mode state file that the airplane-mode toggle maintains at +$XDG_RUNTIME_DIR/airplane-state. The file holds key=value lines; the only +key the indicator reads is `mode` (on/off). Tests point XDG_RUNTIME_DIR at a +temp dir and assert on the emitted JSON. No mocking: the real script runs +against a real state file. + +Run from repo root: + python3 -m unittest tests.waybar-airplane.test_waybar_airplane +""" + +import json +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/waybar-airplane") + + +class WaybarAirplaneHarness(unittest.TestCase): + + def setUp(self): + self.tmp = tempfile.mkdtemp(prefix="waybar-airplane-test-") + self.state_file = os.path.join(self.tmp, "airplane-state") + # Fake power-supply dir with a battery → the script treats the host as + # a laptop. Desktop tests point this at a battery-less dir. + self.ps_dir = os.path.join(self.tmp, "power_supply") + os.makedirs(os.path.join(self.ps_dir, "BAT0")) + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def set_state(self, contents): + with open(self.state_file, "w") as f: + f.write(contents) + + def run_script(self): + env = os.environ.copy() + env["XDG_RUNTIME_DIR"] = self.tmp + env["AIRPLANE_POWER_SUPPLY_DIR"] = self.ps_dir + return subprocess.run( + [SCRIPT], env=env, capture_output=True, text=True, timeout=10, + ) + + def emitted_json(self): + result = self.run_script() + self.assertEqual(result.returncode, 0, msg=result.stderr) + return json.loads(result.stdout) + + +# ----------------------------------------------------------------------------- +# Normal cases +# ----------------------------------------------------------------------------- + +class TestWaybarAirplaneNormal(WaybarAirplaneHarness): + + def test_mode_on_emits_active_class_and_plane_icon(self): + # State is carried by class/color, not a slashed glyph: the same clear + # plane shows in both states (gold when active, gray when inactive). + self.set_state("mode=on\nwifi=enabled\n") + data = self.emitted_json() + self.assertEqual(data["class"], "active") + self.assertIn("", data["text"]) # plane + self.assertIn("on", data["tooltip"].lower()) + + def test_mode_off_emits_inactive_class_and_plane_icon(self): + self.set_state("mode=off\n") + data = self.emitted_json() + self.assertEqual(data["class"], "inactive") + self.assertIn("", data["text"]) # plane + self.assertIn("off", data["tooltip"].lower()) + + +# ----------------------------------------------------------------------------- +# Boundary cases +# ----------------------------------------------------------------------------- + +class TestWaybarAirplaneBoundary(WaybarAirplaneHarness): + + def test_missing_state_file_defaults_to_inactive(self): + # No state file → airplane mode is not engaged (radios on = safe default). + data = self.emitted_json() + self.assertEqual(data["class"], "inactive") + + def test_mode_on_with_other_keys_present(self): + # The toggle writes several keys; the indicator only cares about mode. + self.set_state( + "mode=on\nwifi=enabled\nepp=balance_performance\n" + "brightness=96000\nstopped_system=sshd.service tailscaled.service\n" + ) + data = self.emitted_json() + self.assertEqual(data["class"], "active") + + def test_unknown_mode_value_treated_as_inactive(self): + # Anything that isn't "on" reads as off (fail-safe: radios shown as on). + self.set_state("mode=garbage\n") + data = self.emitted_json() + self.assertEqual(data["class"], "inactive") + + def test_empty_state_file_defaults_to_inactive(self): + self.set_state("") + data = self.emitted_json() + self.assertEqual(data["class"], "inactive") + + def test_output_is_a_single_json_object(self): + self.set_state("mode=on\n") + result = self.run_script() + lines = [ln for ln in result.stdout.splitlines() if ln.strip()] + self.assertEqual(len(lines), 1, msg=f"expected one line, got {lines!r}") + + +# ----------------------------------------------------------------------------- +# Laptop gating — the module only shows on machines with a battery +# ----------------------------------------------------------------------------- + +class TestWaybarAirplaneLaptopGating(WaybarAirplaneHarness): + + def run_desktop(self): + # Point the power-supply dir at a battery-less location (a desktop). + env = os.environ.copy() + env["XDG_RUNTIME_DIR"] = self.tmp + empty = os.path.join(self.tmp, "power_supply_desktop") + os.makedirs(empty, exist_ok=True) # AC adapter only, no BAT* + os.makedirs(os.path.join(empty, "ADP1"), exist_ok=True) + env["AIRPLANE_POWER_SUPPLY_DIR"] = empty + return subprocess.run( + [SCRIPT], env=env, capture_output=True, text=True, timeout=10, + ) + + def test_desktop_emits_nothing(self): + # No battery → module hidden → script prints no output. + self.set_state("mode=off\n") + result = self.run_desktop() + self.assertEqual(result.returncode, 0, msg=result.stderr) + self.assertEqual(result.stdout.strip(), "") + + def test_desktop_emits_nothing_even_when_engaged(self): + self.set_state("mode=on\n") + result = self.run_desktop() + self.assertEqual(result.stdout.strip(), "") + + def test_laptop_with_battery_emits_module(self): + # Sanity: the battery-present harness (default) does emit JSON. + self.set_state("mode=off\n") + data = self.emitted_json() + self.assertEqual(data["class"], "inactive") + + +if __name__ == "__main__": + unittest.main() |
