aboutsummaryrefslogtreecommitdiff
path: root/tests/normalize-notify/test_normalize_notify_sounds.py
blob: ce3084bdf16c033621280b80bb1cadc475816824 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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()