"""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()