aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_logging_kit.py
blob: 1945e5efcfe153dae8678340b99361011ff1da59 (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
118
"""Tests for app/logging_kit.py — log naming, teeing, and retention."""

import io
import os
import tempfile
import time
import unittest
from pathlib import Path

import logging_kit


class _TempDir(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.dir = Path(self.tmp.name)


class DayStreamTests(_TempDir):
    def test_creates_dated_stream_and_appends(self):
        handle = logging_kit.day_stream("tui", log_dir=self.dir)
        self.assertIsNotNone(handle)
        logging_kit.write_line(handle, "first")
        logging_kit.write_line(handle, "second")
        handle.close()
        files = list(self.dir.glob("tui_*.log"))
        self.assertEqual(len(files), 1)
        self.assertRegex(files[0].name, r"^tui_\d{8}\.log$")
        self.assertEqual(files[0].read_text(encoding="utf-8"),
                         "first\nsecond\n")

    def test_unwritable_dir_returns_none(self):
        blocker = self.dir / "file"   # a file where the directory would be
        blocker.write_text("x", encoding="utf-8")
        self.assertIsNone(
            logging_kit.day_stream("tui", log_dir=blocker))


class RunArtifactTests(_TempDir):
    def test_creates_timestamped_artifact(self):
        path, handle = logging_kit.run_artifact("audiocpp_build",
                                                log_dir=self.dir)
        self.assertIsNotNone(handle)
        self.assertRegex(path.name, r"^audiocpp_build_\d{8}_\d{6}\.log$")
        logging_kit.write_line(handle, "out")
        handle.close()
        self.assertEqual(path.read_text(encoding="utf-8"), "out\n")

    def test_returns_path_even_when_unwritable(self):
        blocker = self.dir / "file"
        blocker.write_text("x", encoding="utf-8")
        path, handle = logging_kit.run_artifact("x", log_dir=blocker)
        self.assertIsNone(handle)
        self.assertEqual(path.parent, blocker)


class WriteLineTests(unittest.TestCase):
    def test_none_handle_is_a_noop(self):
        logging_kit.write_line(None, "x")   # must not raise

    def test_broken_handle_is_swallowed(self):
        class Broken:
            def write(self, _):
                raise OSError("nope")

            def flush(self):
                raise OSError("nope")

        logging_kit.write_line(Broken(), "x")   # must not raise


class TeeWriterTests(unittest.TestCase):
    def test_mirrors_to_both_sides(self):
        logf, inner = io.StringIO(), io.StringIO()
        writer = logging_kit.TeeWriter(logf, inner)
        self.assertEqual(writer.write("hello"), 5)
        writer.flush()
        self.assertEqual(logf.getvalue(), "hello")
        self.assertEqual(inner.getvalue(), "hello")

    def test_empty_write_returns_zero(self):
        self.assertEqual(logging_kit.TeeWriter(io.StringIO()).write(""), 0)

    def test_none_sides_and_errors_are_swallowed(self):
        writer = logging_kit.TeeWriter(None, None)
        self.assertEqual(writer.write("x"), 1)   # must not raise
        writer.flush()
        self.assertFalse(writer.isatty())


class PruneTests(_TempDir):
    def test_deletes_old_logs_keeps_new_server_logs_and_pid_files(self):
        old = self.dir / "tui_20200101.log"
        old.write_text("x", encoding="utf-8")
        new = self.dir / "audiobook_20990101.log"
        new.write_text("x", encoding="utf-8")
        server = self.dir / "audiocpp-server.log"
        server.write_text("x", encoding="utf-8")
        pid = self.dir / "audiocpp-server.pid"
        pid.write_text("1", encoding="utf-8")
        ancient = time.time() - 100 * 86400
        os.utime(old, (ancient, ancient))
        os.utime(server, (ancient, ancient))

        logging_kit.prune_logs(log_dir=self.dir)

        self.assertFalse(old.exists())
        self.assertTrue(new.exists())
        self.assertTrue(server.exists())   # owned by a server process
        self.assertTrue(pid.exists())      # not a log

    def test_missing_dir_is_a_noop(self):
        logging_kit.prune_logs(log_dir=self.dir / "nope")   # must not raise


if __name__ == "__main__":
    unittest.main()