diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-21 20:16:34 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-21 20:16:34 -0400 |
| commit | f7079db3aa3e0073df6ce5409d4b6de0a431e26f (patch) | |
| tree | e6ed90fdbd40c0122c4a2cd6c4ac25b34cb02be6 /tests | |
| parent | d6fa23bb592ce4184c3a8b62de5cb6826f874ee2 (diff) | |
| download | archsetup-f7079db3aa3e0073df6ce5409d4b6de0a431e26f.tar.gz archsetup-f7079db3aa3e0073df6ce5409d4b6de0a431e26f.zip | |
feat(notify): add --silent flag, volume knob, and level sound files
The touchpad toggle's notification was too loud, and the eight notify sounds varied by ~13 dB in RMS loudness — bug and fail came out two to three times louder than info or security.
I added a --silent flag to notify (shows the popup, plays no sound) and a NOTIFY_VOLUME knob (paplay scale, default 65536) so the master level can drop without re-encoding. toggle-touchpad now passes --silent on both enable and disable. normalize-notify-sounds.sh measures each .ogg and shifts it to a uniform -31 dB mean. It writes through the file instead of mv-ing over it, so the stow symlinks survive when the script runs against the live sound dir. I re-encoded all eight sounds to the new level.
Tests: a new tests/notify suite (12 tests) covers --silent, the volume knob, flag composition, and the error paths.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/notify/test_notify.py | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/tests/notify/test_notify.py b/tests/notify/test_notify.py new file mode 100644 index 0000000..c49af57 --- /dev/null +++ b/tests/notify/test_notify.py @@ -0,0 +1,186 @@ +"""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() |
