From 93f106aac2d6411c80a911adac62cd12f80e58be Mon Sep 17 00:00:00 2001 From: historia Date: Sun, 30 Aug 2026 18:49:32 -0400 Subject: feat: output log path when audiobook generation fails --- app/tests/test_audiobook_cli.py | 34 ++++++++++++++++++++++++++++++++-- app/tests/test_runview.py | 29 +++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 6 deletions(-) (limited to 'app/tests') diff --git a/app/tests/test_audiobook_cli.py b/app/tests/test_audiobook_cli.py index f13cf9e..a3f0b90 100644 --- a/app/tests/test_audiobook_cli.py +++ b/app/tests/test_audiobook_cli.py @@ -26,6 +26,7 @@ if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) import audiobook # noqa: E402 +import logging_kit # noqa: E402 from converter import config # noqa: E402 from converter import converter as converter_mod # noqa: E402 from converter.converter import AudiobookConverter # noqa: E402 @@ -565,6 +566,10 @@ class FatalErrorReportingTests(unittest.TestCase): (log_path,) = self.log_dir.glob("audiobook_*.log") return log_path.read_text(encoding="utf-8") + def _log_path(self) -> str: + """The dated log file's full path, as the console reports it.""" + return str(logging_kit.stream_path("audiobook", self.log_dir)) + def test_expected_failure_shows_one_friendly_line(self): message = ("The audio.cpp model 'Qwen3-TTS-12Hz-1.7B-Base-GGUF' " "(family 'qwen3_tts') has no built-in speakers " @@ -573,6 +578,8 @@ class FatalErrorReportingTests(unittest.TestCase): self.assertEqual(code, 1) self.assertEqual(out.count("[FATAL]"), 1) self.assertIn(f"[FATAL] Fatal error: {message}", out) + self.assertIn(f"[INFO] Full details in the log file: " + f"{self._log_path()}", out) self.assertNotIn("Traceback (most recent call last)", out) self.assertNotIn("Traceback (most recent call last)", err) log_text = self._log_text() @@ -581,11 +588,14 @@ class FatalErrorReportingTests(unittest.TestCase): def test_expected_failure_reports_error_event(self): events = [] - code, _, _ = self._convert(ValueError("bad input"), - progress=events.append) + code, out, _ = self._convert(ValueError("bad input"), + progress=events.append) self.assertEqual(code, 1) self.assertEqual(events, [{"kind": "error", "message": "bad input"}]) + # The TUI run view owns the console and points failures at the log + # itself; no console pointer on this path. + self.assertNotIn("Full details in the log file", out) def test_unexpected_crash_also_shows_traceback(self): code, out, err = self._convert(TypeError("boom")) @@ -594,6 +604,26 @@ class FatalErrorReportingTests(unittest.TestCase): self.assertIn("Traceback (most recent call last)", err) self.assertIn("TypeError: boom", err) self.assertIn("Traceback (most recent call last)", self._log_text()) + self.assertIn(f"[INFO] Full details in the log file: " + f"{self._log_path()}", out) + + def test_failed_run_prints_the_log_path(self): + # A run that fails without raising (a book aborted the rest) ends + # with the log file path too. + preflight = MagicMock( + return_value=([self.book], [(self.book, "dune")])) + fake_class = MagicMock() + fake_class.preflight_overwrites = preflight + fake_class.return_value.run.return_value = False + out, err = io.StringIO(), io.StringIO() + with patch.object(audiobook, "setup_directories"), \ + contextlib.redirect_stdout(out), \ + contextlib.redirect_stderr(err), \ + patch.object(audiobook, "AudiobookConverter", fake_class): + code = audiobook.convert(backend="audiocpp") + self.assertEqual(code, 1) + self.assertIn(f"[INFO] Full details in the log file: " + f"{self._log_path()}", out.getvalue()) if __name__ == "__main__": diff --git a/app/tests/test_runview.py b/app/tests/test_runview.py index 3f77ed0..5c0dc38 100644 --- a/app/tests/test_runview.py +++ b/app/tests/test_runview.py @@ -333,13 +333,15 @@ class RunLoopTests(_FakeTui, unittest.TestCase): def test_stop_and_exit_stops_server_and_quits_without_keys(self): # Toggle on: the moment the run ends the server is stopped and the - # view reports quit — no key press and no prompt anywhere. + # view reports quit — no key press and no prompt anywhere. A + # successful run's summary does not point at the log file. with patch.object(runview.servers, "alive", return_value=True), \ patch.object(runview.servers, "stop") as mk_stop, \ patch.object(runview.common, "record_post_tui_notice") as mk_notice: view, screen = self.make_view([], autostart_spec="SPEC", - stop_and_exit=True) + stop_and_exit=True, + log_path="/tmp/runs/a.log") view.started_server = True view._queue.put({"kind": "book", "index": 1, "total": 1, "name": "book.txt"}) @@ -355,6 +357,7 @@ class RunLoopTests(_FakeTui, unittest.TestCase): self.assertIn("[OK] book.txt: book_test_michael.mp3", text) self.assertIn("1 of 1 book(s) generated successfully", text) self.assertIn("Elapsed time:", text) + self.assertNotIn("Full details in the log file", text) def test_stop_and_exit_leaves_external_servers_alone(self): # A server this run did not start is never stopped; the TUI still @@ -374,13 +377,15 @@ class RunLoopTests(_FakeTui, unittest.TestCase): def test_stop_and_exit_failure_summary_lists_the_error(self): # A failed ending also auto-exits; the failing book's detail line - # lands in the post-TUI summary. + # lands in the post-TUI summary, which ends with the converter's + # log file path where the full details live. with patch.object(runview.servers, "alive", return_value=True), \ patch.object(runview.servers, "stop"), \ patch.object(runview.common, "record_post_tui_notice") as mk_notice: view, screen = self.make_view([], autostart_spec="SPEC", - stop_and_exit=True) + stop_and_exit=True, + log_path="/tmp/runs/a.log") view.started_server = True view._queue.put({"kind": "book_failed", "name": "bad.txt", "error": "chunk 3 failed", @@ -392,6 +397,22 @@ class RunLoopTests(_FakeTui, unittest.TestCase): self.assertIn("[FAIL] bad.txt: bad_x.mp3", text) self.assertIn("chunk 3 failed", text) self.assertIn("0 of 1 book(s) generated successfully", text) + self.assertIn("Full details in the log file: /tmp/runs/a.log", text) + + def test_stop_and_exit_empty_failure_summary_lists_the_log(self): + # A run that errors before any book result (worker crash) still + # points the post-TUI summary at the log file. + with patch.object(runview.servers, "stop"), \ + patch.object(runview.common, + "record_post_tui_notice") as mk_notice: + view, screen = self.make_view([], stop_and_exit=True, + log_path="/tmp/runs/a.log") + view._queue.put({"kind": "error", "message": "boom"}) + view.run() + self.assertIn("No books were converted", + mk_notice.call_args[0][0]) + self.assertIn("Full details in the log file: /tmp/runs/a.log", + mk_notice.call_args[0][0]) class WorkerTests(_FakeTui, unittest.TestCase): -- cgit v1.2.3