aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-02 12:16:38 -0500
committerCraig Jennings <c@cjennings.net>2026-06-02 12:16:38 -0500
commitb10cba594db836c0747066addad48bda4d30cd02 (patch)
tree063119a623fa3f7139feda4ef302896d8f5f934c /tests
parent49c2ba9c4510bf6e1acd306687473bc8ba9ad8dd (diff)
downloadarchsetup-b10cba594db836c0747066addad48bda4d30cd02.tar.gz
archsetup-b10cba594db836c0747066addad48bda4d30cd02.zip
refactor: drop in-repo dotfiles/, move stow tooling to the dotfiles repo
Since the installer clones DOTFILES_REPO into ~/.dotfiles and stows from there, the in-repo dotfiles/ tree was dead weight. Nothing reads it at install time. I removed it (831 files) now that both machines are migrated. The Makefile's stow / restow / reset / unstow / import targets and the dotfile-script unit suites moved to the dotfiles repo. They sit alongside the scripts they manage and run standalone (cd ~/.dotfiles && make ...). This Makefile keeps the VM-integration targets and the installer-helper suite (safe-rm-rf). I updated CLAUDE.md and README.md so stow operations run from ~/.dotfiles, and the dotfile-management, theme, and unit-test sections point at the standalone repo. The README was already describing the old in-repo model from before the installer switched to cloning. This brings it in line.
Diffstat (limited to 'tests')
-rw-r--r--tests/airplane-mode/test_airplane_mode.py324
-rwxr-xr-xtests/layout-navigate/fake-hyprctl48
-rw-r--r--tests/layout-navigate/test_layout_navigate.py219
-rw-r--r--tests/notify/test_notify.py186
-rwxr-xr-xtests/tmux-util/fake-fzf30
-rwxr-xr-xtests/tmux-util/fake-kill11
-rwxr-xr-xtests/tmux-util/fake-sleep4
-rwxr-xr-xtests/tmux-util/fake-tmux205
-rw-r--r--tests/tmux-util/test_tmux_util.py712
-rw-r--r--tests/waybar-airplane/test_waybar_airplane.py156
-rw-r--r--tests/waybar-touchpad/test_waybar_touchpad.py104
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()