aboutsummaryrefslogtreecommitdiff
path: root/tests/normalize-notify/test_normalize_notify_sounds.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/normalize-notify/test_normalize_notify_sounds.py')
-rw-r--r--tests/normalize-notify/test_normalize_notify_sounds.py117
1 files changed, 117 insertions, 0 deletions
diff --git a/tests/normalize-notify/test_normalize_notify_sounds.py b/tests/normalize-notify/test_normalize_notify_sounds.py
new file mode 100644
index 0000000..ce3084b
--- /dev/null
+++ b/tests/normalize-notify/test_normalize_notify_sounds.py
@@ -0,0 +1,117 @@
+"""Tests for normalize-notify-sounds.sh temp handling and atomic write.
+
+The script re-encodes each .ogg in place. SOUND_DIR is often the stow-symlinked
+~/.local copy, so the write must land on the real repo file and preserve the
+symlink. Two bugs the fix addresses:
+
+- A failed or empty re-encode used to truncate the target with `cat "$tmp" >
+ "$f"`, corrupting a repo-tracked sound to zero bytes.
+- The mktemp had no EXIT trap, so an interrupted encode leaked a temp file.
+
+ffmpeg/ffprobe are faked via stubs on PATH (this directory) so the encode
+result is controllable without real audio work.
+
+Run from repo root:
+ python3 -m unittest tests.normalize-notify.test_normalize_notify_sounds
+"""
+
+import glob
+import os
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
+SCRIPT = os.path.join(REPO_ROOT, "scripts", "normalize-notify-sounds.sh")
+
+
+class NormalizeNotifySounds(unittest.TestCase):
+ def setUp(self):
+ self.tmp = tempfile.mkdtemp(prefix="norm-notify-test-")
+ self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
+ self.bindir = os.path.join(self.tmp, "bin")
+ os.mkdir(self.bindir)
+ for tool in ("ffmpeg", "ffprobe"):
+ fake = os.path.join(HERE, "fake-" + tool)
+ if not os.path.exists(fake):
+ fake = os.path.join(HERE, "fake-ffmpeg") # ffprobe reuses stub
+ dst = os.path.join(self.bindir, tool)
+ shutil.copy(fake, dst)
+ os.chmod(dst, 0o755)
+ # sound dir with one .ogg, plus a "repo" the sound symlinks into
+ self.repo = os.path.join(self.tmp, "repo")
+ os.mkdir(self.repo)
+ self.sounddir = os.path.join(self.tmp, "sounds")
+ os.mkdir(self.sounddir)
+
+ def run_script(self, env_extra=None):
+ env = dict(os.environ)
+ env["PATH"] = self.bindir + os.pathsep + env["PATH"]
+ if env_extra:
+ env.update(env_extra)
+ return subprocess.run(
+ ["bash", SCRIPT, self.sounddir],
+ capture_output=True, text=True, timeout=20, env=env,
+ )
+
+ def make_sound(self, name="chime.ogg", body="ORIGINAL\n", symlink=False):
+ if symlink:
+ real = os.path.join(self.repo, name)
+ with open(real, "w") as f:
+ f.write(body)
+ link = os.path.join(self.sounddir, name)
+ os.symlink(real, link)
+ return link, real
+ path = os.path.join(self.sounddir, name)
+ with open(path, "w") as f:
+ f.write(body)
+ return path, path
+
+ def leftover_temps(self, directory):
+ return glob.glob(os.path.join(directory, "*.ogg.*")) + \
+ [p for p in glob.glob(os.path.join(directory, "tmp*")) if p.endswith(".ogg")]
+
+ # --- Normal ----------------------------------------------------------
+
+ def test_reencodes_regular_file(self):
+ path, _ = self.make_sound()
+ r = self.run_script()
+ self.assertEqual(r.returncode, 0, r.stderr)
+ with open(path) as f:
+ self.assertEqual(f.read(), "ENCODED\n")
+
+ def test_preserves_symlink_and_updates_target(self):
+ link, real = self.make_sound(symlink=True)
+ r = self.run_script()
+ self.assertEqual(r.returncode, 0, r.stderr)
+ self.assertTrue(os.path.islink(link), "the stow symlink must survive")
+ with open(real) as f:
+ self.assertEqual(f.read(), "ENCODED\n",
+ "the re-encode must land on the real repo file")
+
+ # --- Error / corruption guard ---------------------------------------
+
+ def test_empty_reencode_does_not_corrupt_target(self):
+ path, _ = self.make_sound(body="ORIGINAL\n")
+ self.run_script(env_extra={"FAKE_FFMPEG_EMPTY": "1"})
+ with open(path) as f:
+ self.assertEqual(f.read(), "ORIGINAL\n",
+ "an empty re-encode must not truncate the tracked file")
+
+ def test_failed_encode_leaves_no_temp_in_target_dir(self):
+ self.make_sound(symlink=True)
+ self.run_script(env_extra={"FAKE_FFMPEG_FAIL": "1"})
+ self.assertEqual(self.leftover_temps(self.repo), [],
+ "a failed encode must not leak a temp file")
+
+ def test_failed_encode_does_not_corrupt_target(self):
+ _, real = self.make_sound(symlink=True, body="ORIGINAL\n")
+ self.run_script(env_extra={"FAKE_FFMPEG_FAIL": "1"})
+ with open(real) as f:
+ self.assertEqual(f.read(), "ORIGINAL\n")
+
+
+if __name__ == "__main__":
+ unittest.main()