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