diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/airplane-mode/test_airplane_mode.py | 324 | ||||
| -rwxr-xr-x | tests/layout-navigate/fake-hyprctl | 48 | ||||
| -rw-r--r-- | tests/layout-navigate/test_layout_navigate.py | 219 | ||||
| -rw-r--r-- | tests/notify/test_notify.py | 186 | ||||
| -rwxr-xr-x | tests/tmux-util/fake-fzf | 30 | ||||
| -rwxr-xr-x | tests/tmux-util/fake-kill | 11 | ||||
| -rwxr-xr-x | tests/tmux-util/fake-sleep | 4 | ||||
| -rwxr-xr-x | tests/tmux-util/fake-tmux | 205 | ||||
| -rw-r--r-- | tests/tmux-util/test_tmux_util.py | 712 | ||||
| -rw-r--r-- | tests/waybar-airplane/test_waybar_airplane.py | 156 | ||||
| -rw-r--r-- | tests/waybar-touchpad/test_waybar_touchpad.py | 104 |
11 files changed, 0 insertions, 1999 deletions
diff --git a/tests/airplane-mode/test_airplane_mode.py b/tests/airplane-mode/test_airplane_mode.py deleted file mode 100644 index 5db0ed1..0000000 --- a/tests/airplane-mode/test_airplane_mode.py +++ /dev/null @@ -1,324 +0,0 @@ -"""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/layout-navigate/fake-hyprctl b/tests/layout-navigate/fake-hyprctl deleted file mode 100755 index 701f397..0000000 --- a/tests/layout-navigate/fake-hyprctl +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/sh -# Fake hyprctl for testing layout-navigate. -# -# State files live in $FAKE_HYPR_DIR: -# activewindow.json - first activewindow call returns this -# activewindow.1.json - second call (after togglespecialworkspace) returns this, if present -# layout.json - getoption general:layout returns this -# dispatch.log - every "dispatch" invocation appended here (one line) -# call-count - internal counter for activewindow calls - -: "${FAKE_HYPR_DIR:?FAKE_HYPR_DIR must be set}" - -cmd="$1" -shift - -case "$cmd" in - activewindow) - # Count calls so tests can provide a post-toggle state - count_file="$FAKE_HYPR_DIR/call-count" - count=$(cat "$count_file" 2>/dev/null || echo 0) - next=$((count + 1)) - echo "$next" > "$count_file" - - if [ "$count" -eq 0 ]; then - cat "$FAKE_HYPR_DIR/activewindow.json" - else - # Try numbered file; fall back to original - numbered="$FAKE_HYPR_DIR/activewindow.$count.json" - if [ -f "$numbered" ]; then - cat "$numbered" - else - cat "$FAKE_HYPR_DIR/activewindow.json" - fi - fi - ;; - getoption) - cat "$FAKE_HYPR_DIR/layout.json" - ;; - dispatch) - # Log the entire dispatch invocation as one line - echo "dispatch $*" >> "$FAKE_HYPR_DIR/dispatch.log" - echo "ok" - ;; - *) - echo "fake-hyprctl: unknown command '$cmd'" >&2 - exit 1 - ;; -esac diff --git a/tests/layout-navigate/test_layout_navigate.py b/tests/layout-navigate/test_layout_navigate.py deleted file mode 100644 index 41294b7..0000000 --- a/tests/layout-navigate/test_layout_navigate.py +++ /dev/null @@ -1,219 +0,0 @@ -"""Tests for dotfiles/hyprland/.local/bin/layout-navigate. - -The script is a sh wrapper around `hyprctl` that chooses the right dispatch -command for the active layout (master/dwindle/scrolling) and for the active -window's state (floating vs tiled, regular workspace vs special overlay). - -Tests invoke the real script with a faked `hyprctl` on PATH. The fake reads -canned JSON for activewindow/getoption queries and records each dispatch -call to a log file. Assertions compare the dispatch log to the expected -sequence for the given scenario — we test behavior (what hyprctl calls -the script emits), not implementation. -""" - -import json -import os -import stat -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/layout-navigate") -FAKE_HYPRCTL = os.path.join(os.path.dirname(__file__), "fake-hyprctl") - - -def make_activewindow(floating=False, workspace_name="1", workspace_id=1): - return { - "address": "0xabc", - "floating": bool(floating), - "workspace": {"id": workspace_id, "name": workspace_name}, - "class": "test", - "title": "test", - } - - -def make_layout(name): - return {"str": name} - - -class LayoutNavigateHarness(unittest.TestCase): - """Shared harness: run layout-navigate with fake hyprctl, read dispatch log.""" - - def setUp(self): - self.tmp = tempfile.mkdtemp(prefix="layout-navigate-test-") - # Ensure the fake hyprctl is executable - os.chmod(FAKE_HYPRCTL, os.stat(FAKE_HYPRCTL).st_mode | stat.S_IEXEC) - - # Create a bin dir with a symlink to fake-hyprctl named "hyprctl" - self.bin_dir = os.path.join(self.tmp, "bin") - os.makedirs(self.bin_dir) - os.symlink(FAKE_HYPRCTL, os.path.join(self.bin_dir, "hyprctl")) - - # Initialize dispatch log - self.dispatch_log = os.path.join(self.tmp, "dispatch.log") - open(self.dispatch_log, "w").close() - - def tearDown(self): - import shutil - shutil.rmtree(self.tmp, ignore_errors=True) - - def set_state(self, activewindow, layout, next_activewindow=None): - with open(os.path.join(self.tmp, "activewindow.json"), "w") as f: - json.dump(activewindow, f) - with open(os.path.join(self.tmp, "layout.json"), "w") as f: - json.dump(layout, f) - if next_activewindow is not None: - with open(os.path.join(self.tmp, "activewindow.1.json"), "w") as f: - json.dump(next_activewindow, f) - - def run_script(self, *args): - # Preserve current PATH so jq, sh, etc. are reachable - env = os.environ.copy() - env["PATH"] = self.bin_dir + os.pathsep + env.get("PATH", "") - env["FAKE_HYPR_DIR"] = self.tmp - result = subprocess.run( - [SCRIPT] + list(args), - env=env, - capture_output=True, - text=True, - timeout=10, - ) - return result - - def dispatches(self): - with open(self.dispatch_log) as f: - return [line.rstrip("\n") for line in f if line.strip()] - - -class TestTiledMasterLayout(LayoutNavigateHarness): - """Characterization: existing behavior for master/dwindle layout on a regular workspace.""" - - def test_layout_navigate_master_tiled_next_focus_emits_cyclenext(self): - self.set_state(make_activewindow(), make_layout("master")) - self.run_script("next") - self.assertEqual(self.dispatches(), ["dispatch layoutmsg cyclenext"]) - - def test_layout_navigate_master_tiled_prev_focus_emits_cycleprev(self): - self.set_state(make_activewindow(), make_layout("master")) - self.run_script("prev") - self.assertEqual(self.dispatches(), ["dispatch layoutmsg cycleprev"]) - - def test_layout_navigate_master_tiled_next_move_emits_swapnext(self): - self.set_state(make_activewindow(), make_layout("master")) - self.run_script("next", "move") - self.assertEqual(self.dispatches(), ["dispatch layoutmsg swapnext"]) - - def test_layout_navigate_master_tiled_prev_move_emits_swapprev(self): - self.set_state(make_activewindow(), make_layout("master")) - self.run_script("prev", "move") - self.assertEqual(self.dispatches(), ["dispatch layoutmsg swapprev"]) - - -class TestScrollingLayout(LayoutNavigateHarness): - """Characterization: existing behavior for scrolling layout on a regular workspace.""" - - def test_layout_navigate_scrolling_next_focus_emits_focus_l(self): - self.set_state(make_activewindow(), make_layout("scrolling")) - self.run_script("next") - self.assertEqual(self.dispatches(), ["dispatch layoutmsg focus l"]) - - def test_layout_navigate_scrolling_next_move_emits_swapwindow_l(self): - self.set_state(make_activewindow(), make_layout("scrolling")) - self.run_script("next", "move") - self.assertEqual(self.dispatches(), ["dispatch swapwindow l"]) - - -class TestFloatingOnRegularWorkspace(LayoutNavigateHarness): - """Characterization: floating window on a regular workspace short-circuits to cyclenext tiled.""" - - def test_layout_navigate_floating_regular_next_focus_emits_cyclenext_tiled(self): - self.set_state(make_activewindow(floating=True), make_layout("master")) - self.run_script("next") - self.assertEqual(self.dispatches(), ["dispatch cyclenext tiled"]) - - def test_layout_navigate_floating_regular_prev_focus_emits_cyclenext_prev_tiled(self): - self.set_state(make_activewindow(floating=True), make_layout("master")) - self.run_script("prev") - self.assertEqual(self.dispatches(), ["dispatch cyclenext prev tiled"]) - - -class TestTiledInSpecialWorkspace(LayoutNavigateHarness): - """New behavior: tiled window in a special workspace toggles overlay off, then cycles. - - The special:stash overlay (or any special workspace) hides the underlying regular - workspace. Cycling with layoutmsg only operates within the current workspace, so - without toggling first, $mod+J gets trapped inside the overlay. Fix: hide the - overlay, then dispatch the normal cycle — one keypress does both. - """ - - def test_layout_navigate_tiled_special_next_focus_toggles_then_cyclenext(self): - active = make_activewindow(workspace_name="special:stash", workspace_id=-92) - # Post-toggle, focus lands on a tiled window on regular ws 1 - post = make_activewindow(workspace_name="1", workspace_id=1) - self.set_state(active, make_layout("master"), next_activewindow=post) - self.run_script("next") - self.assertEqual( - self.dispatches(), - [ - "dispatch togglespecialworkspace stash", - "dispatch layoutmsg cyclenext", - ], - ) - - def test_layout_navigate_tiled_special_prev_focus_toggles_then_cycleprev(self): - active = make_activewindow(workspace_name="special:stash", workspace_id=-92) - post = make_activewindow(workspace_name="1", workspace_id=1) - self.set_state(active, make_layout("master"), next_activewindow=post) - self.run_script("prev") - self.assertEqual( - self.dispatches(), - [ - "dispatch togglespecialworkspace stash", - "dispatch layoutmsg cycleprev", - ], - ) - - def test_layout_navigate_tiled_special_scrolling_toggles_then_focus_l(self): - """Toggle-then-cycle must honor the active layout, not hard-code master.""" - active = make_activewindow(workspace_name="special:stash", workspace_id=-92) - post = make_activewindow(workspace_name="1", workspace_id=1) - self.set_state(active, make_layout("scrolling"), next_activewindow=post) - self.run_script("next") - self.assertEqual( - self.dispatches(), - [ - "dispatch togglespecialworkspace stash", - "dispatch layoutmsg focus l", - ], - ) - - def test_layout_navigate_tiled_special_next_move_does_not_toggle(self): - """MOVE variant should NOT auto-toggle — moving a window out of a scratchpad - is a separate UX we don't want triggered by the common navigate key.""" - active = make_activewindow(workspace_name="special:stash", workspace_id=-92) - self.set_state(active, make_layout("master")) - self.run_script("next", "move") - self.assertEqual(self.dispatches(), ["dispatch layoutmsg swapnext"]) - - def test_layout_navigate_floating_special_next_focus_toggles_first(self): - """Floating scratchpad (e.g. special:S-term foot) should also toggle off. - After the toggle, the re-read state determines whether to take the - floating branch or fall through to the layout branch.""" - active = make_activewindow(floating=True, workspace_name="special:S-term", workspace_id=-98) - # After toggling S-term off, focus lands on a tiled window on ws 1 - post = make_activewindow(floating=False, workspace_name="1", workspace_id=1) - self.set_state(active, make_layout("master"), next_activewindow=post) - self.run_script("next") - self.assertEqual( - self.dispatches(), - [ - "dispatch togglespecialworkspace S-term", - "dispatch layoutmsg cyclenext", - ], - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/notify/test_notify.py b/tests/notify/test_notify.py deleted file mode 100644 index c49af57..0000000 --- a/tests/notify/test_notify.py +++ /dev/null @@ -1,186 +0,0 @@ -"""Tests for dotfiles/common/.local/bin/notify. - -notify wraps notify-send (the visual popup) and paplay (the sound). The tests -run the real script with HOME pointed at a temp dir holding fake icon/sound -assets, and with fake `notify-send` and `paplay` executables on PATH that log -their arguments. Assertions are made on those logs: - - - the popup always fires (notify-send logged) - - the sound fires only when not --silent, at NOTIFY_VOLUME - - --persist and --silent compose in any order - - bad type / too few args / unknown flag fail - -No real audio plays and no real notification is sent. - -Run from repo root: - python3 -m unittest tests.notify.test_notify -""" - -import os -import shutil -import subprocess -import tempfile -import time -import unittest - - -REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) -SCRIPT = os.path.join(REPO_ROOT, "dotfiles/common/.local/bin/notify") - -SOUND_TYPES = ["success", "fail", "alert", "question", "alarm", "info", "security", "bug"] - - -class NotifyHarness(unittest.TestCase): - - def setUp(self): - self.tmp = tempfile.mkdtemp(prefix="notify-test-") - self.home = os.path.join(self.tmp, "home") - self.bin = os.path.join(self.tmp, "bin") - self.sound_dir = os.path.join(self.home, ".local/share/sounds/notify") - self.icon_dir = os.path.join(self.home, ".local/share/icons/notify") - os.makedirs(self.bin) - os.makedirs(self.sound_dir) - os.makedirs(self.icon_dir) - - # Fake assets so the script's existence checks pass. - for t in SOUND_TYPES: - open(os.path.join(self.sound_dir, f"{t}.ogg"), "w").close() - open(os.path.join(self.icon_dir, f"{t}.png"), "w").close() - - # Fake binaries that log their argv to a file under tmp. - self.notify_log = os.path.join(self.tmp, "notify-send.log") - self.paplay_log = os.path.join(self.tmp, "paplay.log") - self._make_stub("notify-send", self.notify_log) - self._make_stub("paplay", self.paplay_log) - - def tearDown(self): - shutil.rmtree(self.tmp, ignore_errors=True) - - def _make_stub(self, name, logfile): - path = os.path.join(self.bin, name) - with open(path, "w") as f: - f.write("#!/bin/bash\n") - f.write('printf "%s\\n" "$*" >> "' + logfile + '"\n') - os.chmod(path, 0o755) - - def run_notify(self, *args, env_extra=None): - env = os.environ.copy() - env["HOME"] = self.home - env["PATH"] = self.bin + os.pathsep + env.get("PATH", "") - if env_extra: - env.update(env_extra) - return subprocess.run( - [SCRIPT, *args], env=env, capture_output=True, text=True, timeout=10, - ) - - def read_log(self, path, wait=False): - """Return log contents. If wait, poll briefly (the sound is backgrounded).""" - if wait: - deadline = time.time() + 2.0 - while time.time() < deadline and not os.path.exists(path): - time.sleep(0.02) - if not os.path.exists(path): - return "" - with open(path) as f: - return f.read() - - def paplay_called(self): - # Silent path never spawns paplay, so a short settle is enough to be sure. - time.sleep(0.3) - return os.path.exists(self.paplay_log) - - -# ----------------------------------------------------------------------------- -# Normal cases -# ----------------------------------------------------------------------------- - -class TestNotifyNormal(NotifyHarness): - - def test_default_shows_popup_and_plays_sound(self): - result = self.run_notify("info", "Title", "Body") - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("Title", self.read_log(self.notify_log)) - self.assertIn("info.ogg", self.read_log(self.paplay_log, wait=True)) - - def test_sound_played_at_default_volume(self): - self.run_notify("success", "T", "B") - self.assertIn("--volume=65536", self.read_log(self.paplay_log, wait=True)) - - def test_each_type_selects_its_own_sound(self): - for t in SOUND_TYPES: - with self.subTest(type=t): - # Fresh log per type. - if os.path.exists(self.paplay_log): - os.remove(self.paplay_log) - self.run_notify(t, "T", "B") - self.assertIn(f"{t}.ogg", self.read_log(self.paplay_log, wait=True)) - - -# ----------------------------------------------------------------------------- -# --silent -# ----------------------------------------------------------------------------- - -class TestNotifySilent(NotifyHarness): - - def test_silent_shows_popup_but_no_sound(self): - result = self.run_notify("info", "T", "B", "--silent") - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("T", self.read_log(self.notify_log)) - self.assertFalse(self.paplay_called(), "paplay should not run under --silent") - - def test_silent_then_persist_both_apply(self): - result = self.run_notify("info", "T", "B", "--silent", "--persist") - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("--expire-time=0", self.read_log(self.notify_log)) - self.assertFalse(self.paplay_called()) - - def test_persist_then_silent_order_independent(self): - result = self.run_notify("info", "T", "B", "--persist", "--silent") - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("--expire-time=0", self.read_log(self.notify_log)) - self.assertFalse(self.paplay_called()) - - -# ----------------------------------------------------------------------------- -# Volume knob + persist -# ----------------------------------------------------------------------------- - -class TestNotifyVolumeAndPersist(NotifyHarness): - - def test_notify_volume_env_overrides_playback_volume(self): - self.run_notify("info", "T", "B", env_extra={"NOTIFY_VOLUME": "30000"}) - self.assertIn("--volume=30000", self.read_log(self.paplay_log, wait=True)) - - def test_persist_adds_expire_time_zero(self): - self.run_notify("info", "T", "B", "--persist") - self.assertIn("--expire-time=0", self.read_log(self.notify_log)) - - def test_no_persist_has_no_expire_time(self): - self.run_notify("info", "T", "B") - self.assertNotIn("--expire-time", self.read_log(self.notify_log)) - - -# ----------------------------------------------------------------------------- -# Error cases -# ----------------------------------------------------------------------------- - -class TestNotifyErrors(NotifyHarness): - - def test_unknown_type_exits_nonzero(self): - result = self.run_notify("bogus", "T", "B") - self.assertNotEqual(result.returncode, 0) - self.assertIn("Unknown type", result.stderr) - - def test_too_few_args_shows_usage(self): - result = self.run_notify("info", "OnlyTitle") - self.assertNotEqual(result.returncode, 0) - self.assertIn("Usage", result.stderr + result.stdout) - - def test_unknown_flag_exits_nonzero(self): - result = self.run_notify("info", "T", "B", "--bogus") - self.assertNotEqual(result.returncode, 0) - self.assertIn("Unknown option", result.stderr) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/tmux-util/fake-fzf b/tests/tmux-util/fake-fzf deleted file mode 100755 index 476bf7f..0000000 --- a/tests/tmux-util/fake-fzf +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# Fake fzf for testing tmux-util. -# -# Control via env vars: -# FAKE_FZF_CHOICE_LINE — 1-based line index into stdin; that line is -# printed as the selection and exit 0. -# FAKE_FZF_CHOICE — print this literal string as the selection and -# exit 0 (ignored if FAKE_FZF_CHOICE_LINE is set). -# (neither set) — exit 130 (user cancelled). -# -# Every invocation is logged to $FAKE_TMUX_DIR/calls.log as one line. - -: "${FAKE_TMUX_DIR:?FAKE_TMUX_DIR must be set}" -printf 'fzf %s\n' "$*" >> "$FAKE_TMUX_DIR/calls.log" - -if [ -n "${FAKE_FZF_CHOICE_LINE:-}" ]; then - awk -v n="$FAKE_FZF_CHOICE_LINE" 'NR == n { print; exit }' - exit 0 -fi - -if [ -n "${FAKE_FZF_CHOICE:-}" ]; then - # Drain stdin so the pipe upstream doesn't SIGPIPE - cat >/dev/null - printf '%s\n' "$FAKE_FZF_CHOICE" - exit 0 -fi - -# Cancelled -cat >/dev/null -exit 130 diff --git a/tests/tmux-util/fake-kill b/tests/tmux-util/fake-kill deleted file mode 100755 index a157e32..0000000 --- a/tests/tmux-util/fake-kill +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# Fake kill for testing tmux-util. -# -# Records every invocation to $FAKE_TMUX_DIR/kill.log as one line per call. -# Format: kill <args> -# Exits 0 unconditionally — tests assert on the log, not the actual signaling. - -: "${FAKE_TMUX_DIR:?FAKE_TMUX_DIR must be set}" - -printf 'kill %s\n' "$*" >> "$FAKE_TMUX_DIR/kill.log" -exit 0 diff --git a/tests/tmux-util/fake-sleep b/tests/tmux-util/fake-sleep deleted file mode 100755 index 2b8a549..0000000 --- a/tests/tmux-util/fake-sleep +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -# Fake sleep — no-op so tests don't actually wait. Tests assert on behavior, -# not timing. -exit 0 diff --git a/tests/tmux-util/fake-tmux b/tests/tmux-util/fake-tmux deleted file mode 100755 index 1b84956..0000000 --- a/tests/tmux-util/fake-tmux +++ /dev/null @@ -1,205 +0,0 @@ -#!/bin/bash -# Fake tmux for testing tmux-util. -# -# State file: $FAKE_TMUX_DIR/sessions.txt -# One line per session, space-separated: -# <name> <attached> <pids_csv> [<activity_epoch> [<windows> [<cwd>]]] -# pids_csv is a comma-separated list of pane PIDs (or '-' for none) -# cwd cannot contain spaces in test data. -# -# Log file: $FAKE_TMUX_DIR/calls.log -# Each invocation appended as a single line: tmux <args> - -: "${FAKE_TMUX_DIR:?FAKE_TMUX_DIR must be set}" - -STATE="$FAKE_TMUX_DIR/sessions.txt" -LOG="$FAKE_TMUX_DIR/calls.log" - -# Log every invocation -printf 'tmux %s\n' "$*" >> "$LOG" - -cmd="$1" -shift - -read_state() { - [ -f "$STATE" ] || return 0 - grep -v '^[[:space:]]*$' "$STATE" || true -} - -# Render a tmux format string against one session's fields. -# Args: format, name, attached, activity, windows, cwd -render_format() { - local out="$1" name="$2" attached="$3" activity="$4" windows="$5" cwd="$6" - out="${out//\#\{session_name\}/$name}" - out="${out//\#\{session_attached\}/$attached}" - out="${out//\#\{session_activity\}/$activity}" - out="${out//\#\{session_windows\}/$windows}" - out="${out//\#\{pane_current_path\}/$cwd}" - echo "$out" -} - -# Render a tmux format string against one pane's fields. -# Args: format, name, pid, cmd, idx, cwd -render_pane_format() { - local out="$1" name="$2" pid="$3" cmd="$4" idx="$5" cwd="$6" - out="${out//\#\{pane_pid\}/$pid}" - out="${out//\#\{pane_current_command\}/$cmd}" - out="${out//\#\{pane_current_path\}/$cwd}" - out="${out//\#\{session_name\}/$name}" - out="${out//\#\{window_index\}/0}" - out="${out//\#\{pane_index\}/$idx}" - echo "$out" -} - -case "$cmd" in - list-sessions) - fmt="" - while [ "$#" -gt 0 ]; do - case "$1" in - -F) shift; fmt="${1:-}"; shift ;; - *) shift ;; - esac - done - [ -n "$fmt" ] || fmt='#{session_name} #{session_attached}' - read_state | while IFS=' ' read -r name attached pids activity windows cwd; do - [ -n "$name" ] || continue - render_format "$fmt" "$name" "$attached" "${activity:-0}" "${windows:-1}" "${cwd:-/tmp}" - done - ;; - list-panes) - all=0 - fmt="" - session="" - while [ "$#" -gt 0 ]; do - case "$1" in - -t) shift; session="$1"; shift ;; - -F) shift; fmt="${1:-}"; shift ;; - -s) shift ;; - -a) all=1; shift ;; - *) shift ;; - esac - done - [ -n "$fmt" ] || fmt='#{pane_pid}' - read_state | while IFS=' ' read -r name attached pids activity windows cwd; do - [ -n "$name" ] || continue - if [ "$all" -eq 0 ] && [ "$name" != "$session" ]; then - continue - fi - [ "$pids" = "-" ] && continue - idx=0 - for entry in $(echo "$pids" | tr ',' ' '); do - pid="${entry%%:*}" - cmd="${entry##*:}" - [ "$cmd" = "$entry" ] && cmd="shell" - render_pane_format "$fmt" "$name" "$pid" "$cmd" "$idx" "${cwd:-/tmp}" - idx=$((idx + 1)) - done - done - ;; - display) - fmt="" - target="" - while [ "$#" -gt 0 ]; do - case "$1" in - -p) shift ;; - -t) shift; target="$1"; shift ;; - *) fmt="$1"; shift ;; - esac - done - while IFS=' ' read -r name attached pids activity windows cwd; do - [ -n "$name" ] || continue - if [ "$name" = "$target" ]; then - render_format "$fmt" "$name" "$attached" "${activity:-0}" "${windows:-1}" "${cwd:-/tmp}" - exit 0 - fi - done < "$STATE" - exit 1 - ;; - has-session) - session="" - while [ "$#" -gt 0 ]; do - case "$1" in - -t) shift; session="$1"; shift ;; - *) shift ;; - esac - done - # tmux accepts a `=name` form to force exact match; strip the prefix. - session="${session#=}" - while IFS=' ' read -r name attached pids _rest; do - if [ "$name" = "$session" ]; then - exit 0 - fi - done < "$STATE" - exit 1 - ;; - new-session) - detached=0 - name="" - cwd="" - while [ "$#" -gt 0 ]; do - case "$1" in - -s) shift; name="$1"; shift ;; - -c) shift; cwd="$1"; shift ;; - -d) detached=1; shift ;; - *) shift ;; - esac - done - attached=1 - [ "$detached" -eq 1 ] && attached=0 - printf '%s %s - 0 1 %s\n' "$name" "$attached" "${cwd:-/tmp}" >> "$STATE" - ;; - attach-session|switch-client) - # No state mutation needed — the call log already records intent. - ;; - rename-session) - # Forms: rename-session -t <old> <new> OR rename-session <new> - old="" - new="" - while [ "$#" -gt 0 ]; do - case "$1" in - -t) shift; old="$1"; shift ;; - *) new="$1"; shift ;; - esac - done - if [ -z "$old" ] || [ -z "$new" ]; then - echo "fake-tmux rename-session: need both -t <old> and <new>" >&2 - exit 1 - fi - tmp="$STATE.tmp" - : > "$tmp" - while IFS= read -r line; do - [ -n "$line" ] || continue - first="${line%% *}" - rest="${line#* }" - if [ "$first" = "$old" ]; then - printf '%s %s\n' "$new" "$rest" >> "$tmp" - else - printf '%s\n' "$line" >> "$tmp" - fi - done < "$STATE" - mv "$tmp" "$STATE" - ;; - kill-session) - session="" - while [ "$#" -gt 0 ]; do - case "$1" in - -t) shift; session="$1"; shift ;; - *) shift ;; - esac - done - tmp="$STATE.tmp" - : > "$tmp" - while IFS= read -r line; do - [ -n "$line" ] || continue - first="${line%% *}" - if [ "$first" != "$session" ]; then - printf '%s\n' "$line" >> "$tmp" - fi - done < "$STATE" - mv "$tmp" "$STATE" - ;; - *) - echo "fake-tmux: unknown command '$cmd'" >&2 - exit 1 - ;; -esac diff --git a/tests/tmux-util/test_tmux_util.py b/tests/tmux-util/test_tmux_util.py deleted file mode 100644 index b3eab8d..0000000 --- a/tests/tmux-util/test_tmux_util.py +++ /dev/null @@ -1,712 +0,0 @@ -"""Tests for dotfiles/common/.local/bin/tmux-util. - -The script is a bash wrapper around `tmux` with subcommand dispatch. Tests -invoke the real script with a faked `tmux`, `kill`, and `sleep` on PATH. The -fakes read canned state from a temp dir and append a one-line record to a -call log on each invocation. Assertions compare the call log to the expected -sequence for the scenario — we test behavior (what the script causes tmux / -kill to do), not implementation. - -Run from repo root: - python3 -m unittest tests.tmux-util.test_tmux_util -""" - -import os -import shutil -import stat -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/common/.local/bin/tmux-util") -FAKES_DIR = os.path.dirname(__file__) - - -class TmuxUtilHarness(unittest.TestCase): - """Shared harness: run tmux-util with faked tmux/kill/sleep on PATH.""" - - def setUp(self): - self.tmp = tempfile.mkdtemp(prefix="tmux-util-test-") - - # bin dir with the fakes symlinked under their canonical names - self.bin_dir = os.path.join(self.tmp, "bin") - os.makedirs(self.bin_dir) - for name in ("tmux", "kill", "sleep", "fzf"): - fake = os.path.join(FAKES_DIR, f"fake-{name}") - os.chmod(fake, os.stat(fake).st_mode | stat.S_IEXEC) - os.symlink(fake, os.path.join(self.bin_dir, name)) - - # Pre-create empty state + log files so the fakes never have to - self.state_file = os.path.join(self.tmp, "sessions.txt") - self.calls_log = os.path.join(self.tmp, "calls.log") - self.kill_log = os.path.join(self.tmp, "kill.log") - open(self.state_file, "w").close() - open(self.calls_log, "w").close() - open(self.kill_log, "w").close() - - def tearDown(self): - shutil.rmtree(self.tmp, ignore_errors=True) - - def set_sessions(self, sessions): - """sessions: list of tuples (name, attached, pids[, activity, windows, cwd]). - - Defaults for trailing fields: activity=0, windows=1, cwd='/tmp'. - cwd must not contain spaces (the fake's state file is space-delimited). - """ - with open(self.state_file, "w") as f: - for s in sessions: - name = s[0] - attached = s[1] - pids = s[2] - activity = s[3] if len(s) > 3 else 0 - windows = s[4] if len(s) > 4 else 1 - cwd = s[5] if len(s) > 5 else "/tmp" - pids_csv = ",".join(str(p) for p in pids) if pids else "-" - f.write(f"{name} {attached} {pids_csv} {activity} {windows} {cwd}\n") - - def run_script(self, *args, env_extra=None, stdin=None): - env = os.environ.copy() - # Prepend the bin dir so the fakes win - env["PATH"] = self.bin_dir + os.pathsep + env.get("PATH", "") - env["FAKE_TMUX_DIR"] = self.tmp - if env_extra: - env.update(env_extra) - return subprocess.run( - [SCRIPT] + list(args), - env=env, - input=stdin, - capture_output=True, - text=True, - timeout=10, - ) - - def tmux_calls(self): - with open(self.calls_log) as f: - return [line.rstrip("\n") for line in f if line.strip()] - - def kill_calls(self): - with open(self.kill_log) as f: - return [line.rstrip("\n") for line in f if line.strip()] - - def remaining_sessions(self): - with open(self.state_file) as f: - return [line.split()[0] for line in f if line.strip()] - - -# ----------------------------------------------------------------------------- -# Dispatch + usage -# ----------------------------------------------------------------------------- - -class TestDispatch(TmuxUtilHarness): - - def test_no_args_prints_usage_to_stdout_and_exits_zero(self): - result = self.run_script() - self.assertEqual(result.returncode, 0) - self.assertIn("Usage: tmux-util", result.stdout) - self.assertIn("reap", result.stdout) - - def test_dash_h_prints_usage(self): - result = self.run_script("-h") - self.assertEqual(result.returncode, 0) - self.assertIn("Usage: tmux-util", result.stdout) - - def test_double_dash_help_prints_usage(self): - result = self.run_script("--help") - self.assertEqual(result.returncode, 0) - self.assertIn("Usage: tmux-util", result.stdout) - - def test_help_subcommand_prints_usage(self): - result = self.run_script("help") - self.assertEqual(result.returncode, 0) - self.assertIn("Usage: tmux-util", result.stdout) - - def test_unknown_subcommand_exits_nonzero_and_prints_usage_to_stderr(self): - result = self.run_script("frobnicate") - self.assertNotEqual(result.returncode, 0) - self.assertIn("unknown subcommand", result.stderr) - self.assertIn("Usage: tmux-util", result.stderr) - - -# ----------------------------------------------------------------------------- -# Reap — Normal cases -# ----------------------------------------------------------------------------- - -class TestReapNormal(TmuxUtilHarness): - - def test_reap_unattached_sends_sighup_then_kill_session(self): - # bar is unattached, foo is attached. Only bar should be reaped. - self.set_sessions([ - ("foo", 1, [101, 102]), - ("bar", 0, [201]), - ]) - result = self.run_script("reap") - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("Reaping: bar", result.stdout) - self.assertNotIn("Reaping: foo", result.stdout) - # SIGHUP went to bar's pid only, never to foo's - self.assertIn("kill -HUP 201", self.kill_calls()) - self.assertNotIn("kill -HUP 101", self.kill_calls()) - self.assertNotIn("kill -HUP 102", self.kill_calls()) - # Fallback kill-session ran (our fake-kill is a no-op, so the session - # stays "alive" and the force-kill branch fires) - tmux = self.tmux_calls() - self.assertTrue( - any("kill-session -t bar" in c for c in tmux), - f"expected kill-session -t bar in {tmux!r}", - ) - - def test_reap_skips_attached_sessions(self): - self.set_sessions([ - ("foo", 1, [101]), - ("bar", 1, [201]), - ]) - result = self.run_script("reap") - self.assertEqual(result.returncode, 0) - self.assertIn("No unattached sessions", result.stdout) - self.assertEqual(self.kill_calls(), []) - - def test_reap_skips_aiv_prefix_by_default(self): - self.set_sessions([ - ("aiv-claude", 0, [301]), - ("aiv-gemini", 0, [302]), - ("worker", 0, [401]), - ]) - result = self.run_script("reap") - self.assertEqual(result.returncode, 0) - self.assertIn("Reaping: worker", result.stdout) - self.assertNotIn("Reaping: aiv-claude", result.stdout) - self.assertNotIn("Reaping: aiv-gemini", result.stdout) - self.assertIn("kill -HUP 401", self.kill_calls()) - self.assertNotIn("kill -HUP 301", self.kill_calls()) - self.assertNotIn("kill -HUP 302", self.kill_calls()) - - def test_reap_skip_pattern_overridable_via_env(self): - # Override to match nothing → aiv-claude should now be reaped. - self.set_sessions([ - ("aiv-claude", 0, [301]), - ]) - result = self.run_script("reap", env_extra={"TMUX_UTIL_REAP_SKIP": "^never-matches-anything$"}) - self.assertEqual(result.returncode, 0) - self.assertIn("Reaping: aiv-claude", result.stdout) - self.assertIn("kill -HUP 301", self.kill_calls()) - - def test_reap_session_with_multiple_panes_sighups_each(self): - self.set_sessions([ - ("multi", 0, [501, 502, 503]), - ]) - result = self.run_script("reap") - self.assertEqual(result.returncode, 0) - # xargs may collapse into one call; verify each PID is mentioned - kc = "\n".join(self.kill_calls()) - for pid in ("501", "502", "503"): - self.assertIn(pid, kc, f"expected pid {pid} in kill calls: {kc!r}") - - -# ----------------------------------------------------------------------------- -# Reap — Boundary cases -# ----------------------------------------------------------------------------- - -class TestReapBoundary(TmuxUtilHarness): - - def test_reap_no_sessions_at_all(self): - self.set_sessions([]) - result = self.run_script("reap") - self.assertEqual(result.returncode, 0) - self.assertIn("No unattached sessions", result.stdout) - self.assertEqual(self.kill_calls(), []) - - def test_reap_session_with_no_panes_does_not_invoke_kill(self): - # A session listed but with no PIDs — should still get kill-session. - self.set_sessions([ - ("ghost", 0, []), - ]) - result = self.run_script("reap") - self.assertEqual(result.returncode, 0) - self.assertIn("Reaping: ghost", result.stdout) - # No kill -HUP because no PIDs - self.assertEqual(self.kill_calls(), []) - # But kill-session still ran (force kill, since fake-tmux didn't - # auto-remove the session on its own) - tmux = self.tmux_calls() - self.assertTrue( - any("kill-session -t ghost" in c for c in tmux), - f"expected kill-session -t ghost in {tmux!r}", - ) - - def test_reap_only_attached_and_skipped_sessions(self): - # Mix that leaves no candidates after filtering. - self.set_sessions([ - ("foo", 1, [101]), # attached - ("aiv-bar", 0, [201]), # skipped by pattern - ]) - result = self.run_script("reap") - self.assertEqual(result.returncode, 0) - self.assertIn("No unattached sessions", result.stdout) - self.assertEqual(self.kill_calls(), []) - - -# ----------------------------------------------------------------------------- -# ls — Normal cases -# ----------------------------------------------------------------------------- - -class TestLsNormal(TmuxUtilHarness): - - def test_ls_prints_header_and_columns_for_attached_session(self): - # activity=950, now=1000 → idle 50s. cwd=/tmp/work → unchanged (not under HOME). - self.set_sessions([ - ("work", 1, [101], 950, 3, "/tmp/work"), - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) - self.assertEqual(result.returncode, 0, msg=result.stderr) - out = result.stdout - self.assertIn("STATE", out) - self.assertIn("NAME", out) - self.assertIn("IDLE", out) - self.assertIn("WINDOWS", out) - self.assertIn("CWD", out) - self.assertIn("attached", out) - self.assertIn("work", out) - self.assertIn("50s", out) - self.assertIn("3", out) - self.assertIn("/tmp/work", out) - - def test_ls_shows_detached_state_for_unattached_session(self): - self.set_sessions([ - ("idle", 0, [201], 900, 1, "/tmp/idle"), - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) - self.assertEqual(result.returncode, 0) - self.assertIn("detached", result.stdout) - self.assertIn("idle", result.stdout) - - def test_ls_lists_multiple_sessions(self): - self.set_sessions([ - ("foo", 1, [101], 990, 2, "/tmp/foo"), - ("bar", 0, [201], 900, 1, "/tmp/bar"), - ("baz", 0, [301], 800, 4, "/tmp/baz"), - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) - self.assertEqual(result.returncode, 0) - for name in ("foo", "bar", "baz"): - self.assertIn(name, result.stdout) - - -# ----------------------------------------------------------------------------- -# ls — Boundary cases -# ----------------------------------------------------------------------------- - -class TestLsBoundary(TmuxUtilHarness): - - def test_ls_no_sessions_prints_message(self): - self.set_sessions([]) - result = self.run_script("ls") - self.assertEqual(result.returncode, 0) - self.assertIn("No tmux sessions", result.stdout) - - def test_ls_idle_zero_seconds(self): - self.set_sessions([ - ("now", 1, [101], 1000, 1, "/tmp/now"), - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) - self.assertEqual(result.returncode, 0) - self.assertIn("0s", result.stdout) - - def test_ls_idle_under_minute_shows_seconds(self): - self.set_sessions([ - ("s59", 0, [201], 941, 1, "/tmp/s"), # 59s ago - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) - self.assertIn("59s", result.stdout) - - def test_ls_idle_exactly_one_minute_shows_minutes(self): - self.set_sessions([ - ("m1", 0, [201], 940, 1, "/tmp/m"), # 60s ago - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) - self.assertIn("1m", result.stdout) - - def test_ls_idle_under_hour_shows_minutes(self): - self.set_sessions([ - ("m45", 0, [201], 0, 1, "/tmp/m"), # 2700s = 45m - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "2700"}) - self.assertIn("45m", result.stdout) - - def test_ls_idle_exactly_one_hour_shows_hours(self): - self.set_sessions([ - ("h1", 0, [201], 0, 1, "/tmp/h"), # 3600s = 1h - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "3600"}) - self.assertIn("1h", result.stdout) - - def test_ls_idle_under_day_shows_hours(self): - self.set_sessions([ - ("h23", 0, [201], 0, 1, "/tmp/h"), # 23 * 3600 = 82800s - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "82800"}) - self.assertIn("23h", result.stdout) - - def test_ls_idle_exactly_one_day_shows_days(self): - self.set_sessions([ - ("d1", 0, [201], 0, 1, "/tmp/d"), # 86400s = 1d - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "86400"}) - self.assertIn("1d", result.stdout) - - def test_ls_tilde_fies_home_paths(self): - # cwd inside $HOME should render as ~/... - home = os.environ.get("HOME", "/home/cjennings") - cwd_under_home = home + "/code/foo" - self.set_sessions([ - ("h", 1, [101], 990, 1, cwd_under_home), - ]) - result = self.run_script("ls", env_extra={"TMUX_UTIL_NOW": "1000"}) - self.assertEqual(result.returncode, 0) - self.assertIn("~/code/foo", result.stdout) - # The absolute path under HOME should NOT appear in raw form - self.assertNotIn(cwd_under_home, result.stdout) - - -# ----------------------------------------------------------------------------- -# go — Normal cases -# ----------------------------------------------------------------------------- - -class TestGoNormal(TmuxUtilHarness): - - def test_go_existing_session_outside_tmux_attaches(self): - self.set_sessions([ - ("work", 0, [201], 900, 1, "/tmp/work"), - ]) - # Outside tmux: TMUX env should be unset. - env = {k: v for k, v in os.environ.items() if k != "TMUX"} - result = subprocess.run( - [SCRIPT, "go", "work"], - env={**env, "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""), - "FAKE_TMUX_DIR": self.tmp}, - capture_output=True, text=True, timeout=10, - ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - calls = self.tmux_calls() - # has-session should be the first existence check (exact-match form) - self.assertTrue(any("has-session" in c and "work" in c for c in calls), - f"expected has-session probe in {calls!r}") - self.assertTrue(any("attach-session -t work" in c for c in calls), - f"expected attach-session -t work in {calls!r}") - # Must NOT create a new session - self.assertFalse(any("new-session" in c for c in calls), - f"unexpected new-session in {calls!r}") - - def test_go_existing_session_inside_tmux_switches_client(self): - self.set_sessions([ - ("work", 0, [201], 900, 1, "/tmp/work"), - ]) - result = self.run_script("go", "work", env_extra={"TMUX": "/tmp/fake-tmux-socket,1234,0"}) - self.assertEqual(result.returncode, 0, msg=result.stderr) - calls = self.tmux_calls() - self.assertTrue(any("switch-client -t work" in c for c in calls), - f"expected switch-client in {calls!r}") - # attach-session shouldn't be called inside tmux - self.assertFalse(any("attach-session" in c for c in calls), - f"unexpected attach-session in {calls!r}") - - def test_go_missing_session_outside_tmux_creates(self): - self.set_sessions([]) - env = {k: v for k, v in os.environ.items() if k != "TMUX"} - result = subprocess.run( - [SCRIPT, "go", "fresh"], - env={**env, "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""), - "FAKE_TMUX_DIR": self.tmp}, - cwd="/tmp", - capture_output=True, text=True, timeout=10, - ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - calls = self.tmux_calls() - # new-session with -s and -c pointing at the subprocess's cwd - self.assertTrue( - any("new-session" in c and "-s fresh" in c and "-c /tmp" in c - for c in calls), - f"expected new-session -s fresh -c /tmp in {calls!r}", - ) - - def test_go_missing_session_inside_tmux_creates_detached_then_switches(self): - self.set_sessions([]) - env = os.environ.copy() - env.update({ - "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""), - "FAKE_TMUX_DIR": self.tmp, - "TMUX": "/tmp/fake-tmux-socket,1234,0", - }) - result = subprocess.run( - [SCRIPT, "go", "fresh"], - env=env, - cwd="/tmp", - capture_output=True, text=True, timeout=10, - ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - calls = self.tmux_calls() - self.assertTrue( - any("new-session" in c and "-d" in c and "-s fresh" in c and "-c /tmp" in c - for c in calls), - f"expected detached new-session in {calls!r}", - ) - self.assertTrue(any("switch-client -t fresh" in c for c in calls), - f"expected switch-client after create in {calls!r}") - - -# ----------------------------------------------------------------------------- -# go — Error / Boundary cases -# ----------------------------------------------------------------------------- - -class TestGoErrors(TmuxUtilHarness): - - def test_go_with_no_name_exits_nonzero(self): - result = self.run_script("go") - self.assertNotEqual(result.returncode, 0) - self.assertIn("missing session name", result.stderr) - - def test_go_with_empty_string_name_exits_nonzero(self): - result = self.run_script("go", "") - self.assertNotEqual(result.returncode, 0) - self.assertIn("missing session name", result.stderr) - - -# ----------------------------------------------------------------------------- -# find — Normal cases -# ----------------------------------------------------------------------------- - -class TestFindNormal(TmuxUtilHarness): - - def test_find_matches_pane_running_named_command(self): - # Two sessions, one has a pane running vim; find should locate it. - self.set_sessions([ - ("work", 1, ["101:zsh", "102:vim"], 950, 2, "/tmp/work"), - ("idle", 0, ["201:zsh"], 900, 1, "/tmp/idle"), - ]) - result = self.run_script("find", "vim") - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("work:", result.stdout) - self.assertIn("vim", result.stdout) - # idle session pane shouldn't be in the output - self.assertNotIn("idle:", result.stdout) - - def test_find_matches_multiple_panes(self): - self.set_sessions([ - ("a", 1, ["101:zsh", "102:vim"], 950, 1, "/tmp/a"), - ("b", 1, ["201:vim"], 900, 1, "/tmp/b"), - ]) - result = self.run_script("find", "vim") - self.assertEqual(result.returncode, 0) - self.assertIn("a:", result.stdout) - self.assertIn("b:", result.stdout) - # Two matches, two output lines - lines = [ln for ln in result.stdout.strip().split("\n") if ln] - self.assertEqual(len(lines), 2, msg=f"expected 2 matching lines, got {lines!r}") - - def test_find_output_includes_session_window_pane_location(self): - self.set_sessions([ - ("work", 1, ["101:zsh", "102:vim"], 950, 1, "/tmp/work"), - ]) - result = self.run_script("find", "vim") - self.assertEqual(result.returncode, 0) - # Expected format: work:0.1 vim (window 0, pane index 1) - self.assertRegex(result.stdout, r"work:0\.1\s+vim") - - -# ----------------------------------------------------------------------------- -# find — Boundary / Error cases -# ----------------------------------------------------------------------------- - -class TestFindBoundary(TmuxUtilHarness): - - def test_find_no_matches_exits_nonzero(self): - self.set_sessions([ - ("work", 1, ["101:zsh"], 950, 1, "/tmp/work"), - ]) - result = self.run_script("find", "nothing-matches-this") - self.assertNotEqual(result.returncode, 0) - self.assertEqual(result.stdout.strip(), "") - - def test_find_no_sessions_exits_nonzero(self): - self.set_sessions([]) - result = self.run_script("find", "vim") - self.assertNotEqual(result.returncode, 0) - - def test_find_no_pattern_exits_nonzero(self): - result = self.run_script("find") - self.assertNotEqual(result.returncode, 0) - self.assertIn("missing pattern", result.stderr) - - def test_find_empty_pattern_exits_nonzero(self): - result = self.run_script("find", "") - self.assertNotEqual(result.returncode, 0) - self.assertIn("missing pattern", result.stderr) - - -# ----------------------------------------------------------------------------- -# pick — Normal cases -# ----------------------------------------------------------------------------- - -class TestPickNormal(TmuxUtilHarness): - - def test_pick_attaches_chosen_session_outside_tmux(self): - self.set_sessions([ - ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"), - ("bar", 0, ["201:zsh"], 900, 1, "/tmp/bar"), - ]) - # Pick the second line (bar) - env = {k: v for k, v in os.environ.items() if k != "TMUX"} - result = subprocess.run( - [SCRIPT, "pick"], - env={**env, "PATH": self.bin_dir + os.pathsep + env.get("PATH", ""), - "FAKE_TMUX_DIR": self.tmp, "FAKE_FZF_CHOICE_LINE": "2"}, - capture_output=True, text=True, timeout=10, - ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - calls = self.tmux_calls() - self.assertTrue(any("attach-session -t bar" in c for c in calls), - f"expected attach-session -t bar in {calls!r}") - - def test_pick_switches_client_inside_tmux(self): - self.set_sessions([ - ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"), - ("bar", 0, ["201:zsh"], 900, 1, "/tmp/bar"), - ]) - result = self.run_script("pick", env_extra={ - "TMUX": "/tmp/fake-tmux-socket,1234,0", - "FAKE_FZF_CHOICE_LINE": "1", # first line → foo - }) - self.assertEqual(result.returncode, 0, msg=result.stderr) - calls = self.tmux_calls() - self.assertTrue(any("switch-client -t foo" in c for c in calls), - f"expected switch-client -t foo in {calls!r}") - self.assertFalse(any("attach-session" in c for c in calls), - f"unexpected attach-session in {calls!r}") - - -# ----------------------------------------------------------------------------- -# pick — Boundary cases -# ----------------------------------------------------------------------------- - -class TestPickBoundary(TmuxUtilHarness): - - def test_pick_no_sessions_prints_message(self): - self.set_sessions([]) - result = self.run_script("pick") - self.assertEqual(result.returncode, 0) - self.assertIn("No tmux sessions", result.stdout) - # fzf should never have been invoked - self.assertFalse(any(c.startswith("fzf ") for c in self.tmux_calls()), - f"unexpected fzf call in {self.tmux_calls()!r}") - - def test_pick_user_cancels_fzf_returns_zero_no_attach(self): - self.set_sessions([ - ("foo", 1, ["101:zsh"], 950, 1, "/tmp/foo"), - ]) - # No FAKE_FZF_CHOICE / FAKE_FZF_CHOICE_LINE → fzf exits 130 (cancelled) - result = self.run_script("pick") - self.assertEqual(result.returncode, 0, msg=result.stderr) - calls = self.tmux_calls() - self.assertFalse(any("attach-session" in c or "switch-client" in c for c in calls), - f"unexpected attach/switch in {calls!r}") - - -# ----------------------------------------------------------------------------- -# rename — Normal cases -# ----------------------------------------------------------------------------- - -class TestRenameNormal(TmuxUtilHarness): - - def test_rename_picks_session_and_renames_it(self): - self.set_sessions([ - ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), - ("other", 0, ["201:zsh"], 900, 1, "/tmp/other"), - ]) - result = self.run_script( - "rename", - env_extra={"FAKE_FZF_CHOICE_LINE": "1"}, # picks "old" - stdin="new\n", - ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - calls = self.tmux_calls() - self.assertTrue( - any("rename-session -t old new" in c for c in calls), - f"expected rename-session -t old new in {calls!r}", - ) - # State should reflect the rename - names = self.remaining_sessions() - self.assertIn("new", names) - self.assertIn("other", names) - self.assertNotIn("old", names) - self.assertIn("Renamed: old", result.stdout) - - -# ----------------------------------------------------------------------------- -# rename — Boundary / Error cases -# ----------------------------------------------------------------------------- - -class TestRenameBoundary(TmuxUtilHarness): - - def test_rename_no_sessions_prints_message(self): - self.set_sessions([]) - result = self.run_script("rename") - self.assertEqual(result.returncode, 0) - self.assertIn("No tmux sessions", result.stdout) - # fzf should never have been invoked - self.assertFalse(any(c.startswith("fzf ") for c in self.tmux_calls())) - - def test_rename_user_cancels_fzf_no_action(self): - self.set_sessions([ - ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), - ]) - # No FAKE_FZF_CHOICE → fzf exits 130 - result = self.run_script("rename") - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertFalse(any("rename-session" in c for c in self.tmux_calls())) - # State unchanged - self.assertEqual(self.remaining_sessions(), ["old"]) - - def test_rename_empty_new_name_exits_nonzero(self): - self.set_sessions([ - ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), - ]) - result = self.run_script( - "rename", - env_extra={"FAKE_FZF_CHOICE_LINE": "1"}, - stdin="\n", # empty new name - ) - self.assertNotEqual(result.returncode, 0) - self.assertIn("empty new name", result.stderr) - self.assertFalse(any("rename-session" in c for c in self.tmux_calls())) - - def test_rename_same_name_is_noop(self): - self.set_sessions([ - ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), - ]) - result = self.run_script( - "rename", - env_extra={"FAKE_FZF_CHOICE_LINE": "1"}, - stdin="old\n", # same as current name - ) - self.assertEqual(result.returncode, 0, msg=result.stderr) - self.assertIn("same as old", result.stdout) - self.assertFalse(any("rename-session" in c for c in self.tmux_calls())) - - def test_rename_conflict_with_existing_exits_nonzero(self): - self.set_sessions([ - ("old", 0, ["101:zsh"], 950, 1, "/tmp/old"), - ("taken", 0, ["201:zsh"], 900, 1, "/tmp/taken"), - ]) - result = self.run_script( - "rename", - env_extra={"FAKE_FZF_CHOICE_LINE": "1"}, # picks "old" - stdin="taken\n", # collides with existing - ) - self.assertNotEqual(result.returncode, 0) - self.assertIn("already exists", result.stderr) - self.assertFalse(any("rename-session" in c for c in self.tmux_calls())) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/waybar-airplane/test_waybar_airplane.py b/tests/waybar-airplane/test_waybar_airplane.py deleted file mode 100644 index b3b9c06..0000000 --- a/tests/waybar-airplane/test_waybar_airplane.py +++ /dev/null @@ -1,156 +0,0 @@ -"""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() diff --git a/tests/waybar-touchpad/test_waybar_touchpad.py b/tests/waybar-touchpad/test_waybar_touchpad.py deleted file mode 100644 index 3133bb4..0000000 --- a/tests/waybar-touchpad/test_waybar_touchpad.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Tests for dotfiles/hyprland/.local/bin/waybar-touchpad. - -The script emits one JSON line for waybar's custom/touchpad module, derived -from the touchpad state file that toggle-touchpad / touchpad-auto maintain -at $XDG_RUNTIME_DIR/touchpad-state. Tests point XDG_RUNTIME_DIR at a temp -dir and assert on the emitted JSON for each state — enabled, disabled, and -the missing-file default. No mocking: the real script runs against a real -state file. - -Run from repo root: - python3 -m unittest tests.waybar-touchpad.test_waybar_touchpad -""" - -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-touchpad") - - -class WaybarTouchpadHarness(unittest.TestCase): - - def setUp(self): - self.tmp = tempfile.mkdtemp(prefix="waybar-touchpad-test-") - self.state_file = os.path.join(self.tmp, "touchpad-state") - - def tearDown(self): - shutil.rmtree(self.tmp, ignore_errors=True) - - def set_state(self, value): - with open(self.state_file, "w") as f: - f.write(value) - - def run_script(self): - env = os.environ.copy() - env["XDG_RUNTIME_DIR"] = self.tmp - 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 TestWaybarTouchpadNormal(WaybarTouchpadHarness): - - def test_enabled_state_emits_enabled_class_and_mouse_icon(self): - self.set_state("enabled") - data = self.emitted_json() - self.assertEqual(data["class"], "enabled") - self.assertIn("\U000f037d", data["text"]) # mouse - self.assertIn("enabled", data["tooltip"].lower()) - - def test_disabled_state_emits_disabled_class_and_mouse_off_icon(self): - self.set_state("disabled") - data = self.emitted_json() - self.assertEqual(data["class"], "disabled") - self.assertIn("\U000f037e", data["text"]) # mouse-off - self.assertIn("disabled", data["tooltip"].lower()) - - -# ----------------------------------------------------------------------------- -# Boundary cases -# ----------------------------------------------------------------------------- - -class TestWaybarTouchpadBoundary(WaybarTouchpadHarness): - - def test_missing_state_file_defaults_to_enabled(self): - # No state file written → script should treat the touchpad as enabled. - data = self.emitted_json() - self.assertEqual(data["class"], "enabled") - - def test_state_with_trailing_newline_is_handled(self): - self.set_state("disabled\n") - data = self.emitted_json() - self.assertEqual(data["class"], "disabled") - - def test_unknown_state_value_treated_as_enabled(self): - # Anything that isn't "disabled" reads as enabled (fail-safe: the - # pointer is usable rather than hidden). - self.set_state("garbage") - data = self.emitted_json() - self.assertEqual(data["class"], "enabled") - - def test_output_is_a_single_json_object(self): - self.set_state("enabled") - 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}") - - -if __name__ == "__main__": - unittest.main() |
