aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_logging_kit.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests/test_logging_kit.py')
-rw-r--r--app/tests/test_logging_kit.py118
1 files changed, 118 insertions, 0 deletions
diff --git a/app/tests/test_logging_kit.py b/app/tests/test_logging_kit.py
new file mode 100644
index 0000000..1945e5e
--- /dev/null
+++ b/app/tests/test_logging_kit.py
@@ -0,0 +1,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()