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