aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_runview.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-01 03:17:01 -0400
committerhistoria <historiavg@proton.me>2026-09-01 03:17:01 -0400
commit058b19e7a65b40b1024a4fdeb2233062ff273cfd (patch)
treefe3643872cd6b317a88eec950ae6ecc4d81d843d /app/tests/test_runview.py
parent10e72d4960e865acf5346ab8cf518ed5844fe45c (diff)
downloadtts-audiobook-generator-058b19e7a65b40b1024a4fdeb2233062ff273cfd.tar.gz
fix: better errors for generate all models
Diffstat (limited to 'app/tests/test_runview.py')
-rw-r--r--app/tests/test_runview.py132
1 files changed, 132 insertions, 0 deletions
diff --git a/app/tests/test_runview.py b/app/tests/test_runview.py
index f8ab452..4e3f9fe 100644
--- a/app/tests/test_runview.py
+++ b/app/tests/test_runview.py
@@ -118,6 +118,97 @@ class StateTransitionTests(_FakeTui, unittest.TestCase):
self.assertEqual(view.phase, "error")
self.assertTrue(view.error_message)
+ def test_chunk_failure_message_names_the_model_and_reason(self):
+ # An "All" run stamps chunk_failed with the generating model and
+ # the server's error detail; the message carries both, and the
+ # results row keeps the reason (book_done(ok=False) has none).
+ view, _ = self.make_view()
+ view.handle_event({"kind": "book", "index": 1, "total": 3,
+ "name": "b.txt", "model": "m1"})
+ view.handle_event({"kind": "chunk_failed", "chunk": 1, "total": 1,
+ "error": "vocoder backend buffer allocation "
+ "failed", "model": "m1"})
+ self.assertEqual(view.error_message,
+ "m1: chunk 1/1 failed — vocoder backend buffer "
+ "allocation failed")
+ view.handle_event({"kind": "book_done", "name": "b.txt",
+ "ok": False, "model": "m1"})
+ self.assertEqual(view.book_results,
+ [("b.txt", False, [],
+ "vocoder backend buffer allocation failed",
+ "m1")])
+
+ def test_chunk_failure_without_detail_keeps_the_count(self):
+ # No server error detail: the message (and the row reason) still
+ # say which chunk of how many failed.
+ view, _ = self.make_view()
+ view.handle_event({"kind": "book", "index": 1, "total": 1,
+ "name": "b", "model": "m1"})
+ view.handle_event({"kind": "chunk_failed", "chunk": 2, "total": 4,
+ "model": "m1"})
+ self.assertEqual(view.error_message, "m1: chunk 2/4 failed")
+ view.handle_event({"kind": "book_done", "name": "b", "ok": False,
+ "model": "m1"})
+ self.assertEqual(view.book_results[0][3], "chunk 2/4 failed")
+
+ def test_new_book_clears_a_stale_failure_message(self):
+ # The heart of the "Chunk 1/1 failed persisted for every later
+ # model" bug: a failure message from one book of an "All" run must
+ # not linger under the next book's progress.
+ view, _ = self.make_view()
+ view.handle_event({"kind": "book", "index": 1, "total": 2,
+ "name": "b.txt", "model": "m1"})
+ view.handle_event({"kind": "chunk_failed", "chunk": 1, "total": 1,
+ "error": "boom", "model": "m1"})
+ view.handle_event({"kind": "book_done", "name": "b.txt",
+ "ok": False, "model": "m1"})
+ self.assertTrue(view.error_message)
+ view.handle_event({"kind": "book", "index": 2, "total": 2,
+ "name": "b.txt", "model": "m2"})
+ self.assertEqual(view.error_message, "")
+ self.assertEqual(view._book_error, "")
+
+ def test_done_message_lists_the_failed_models(self):
+ # The terminal "done" screen names what failed instead of showing
+ # the last chunk failure (or a bare count).
+ view, _ = self.make_view()
+ for index, (model, ok) in enumerate(
+ [("m1", False), ("m2", True), ("m3", False)], 1):
+ view.handle_event({"kind": "book", "index": index, "total": 3,
+ "name": "b.txt", "model": model})
+ view.handle_event({"kind": "book_done", "name": "b.txt",
+ "ok": ok, "model": model})
+ view.handle_event({"kind": "done", "ok": 1, "total": 3})
+ self.assertEqual(view.phase, "error")
+ self.assertEqual(view.error_message,
+ "2 of 3 book(s) failed: m1, m3")
+
+ def test_done_message_caps_the_failed_model_list(self):
+ # More failures than fit the two detail lines: the list is capped
+ # with the leftover count (the rows carry the full list).
+ view, _ = self.make_view()
+ for index in range(1, 8):
+ view.handle_event({"kind": "book", "index": index, "total": 7,
+ "name": "b.txt", "model": f"m{index}"})
+ view.handle_event({"kind": "book_done", "name": "b.txt",
+ "ok": False, "model": f"m{index}"})
+ view.handle_event({"kind": "done", "ok": 0, "total": 7})
+ self.assertEqual(
+ view.error_message,
+ "7 of 7 book(s) failed: m1, m2, m3, m4, m5, … +2 more")
+
+ def test_done_all_failed_rows_only_names_models_of_failures(self):
+ # A model that cannot even start (audiobook.py's constructor
+ # failure path) reports book_failed without a model field: the
+ # name stands in for the model in the summary.
+ view, _ = self.make_view()
+ view.handle_event({"kind": "book_failed", "name": "m1",
+ "error": "voice 'x' is not available",
+ "files": []})
+ view.handle_event({"kind": "done", "ok": 0, "total": 1})
+ self.assertEqual(view.error_message,
+ "1 of 1 book(s) failed: m1")
+
def test_server_exit_during_boot_is_error(self):
view, _ = self.make_view()
view.handle_event({"kind": "starting", "name": "audiocpp"})
@@ -273,6 +364,47 @@ class RenderTests(_FakeTui, unittest.TestCase):
text = self._strings(screen)
self.assertIn("book.txt — m1", text)
+ def test_summary_shows_failed_rows_first_with_their_reason(self):
+ # With dozens of "All"-run results, the failures must be visible
+ # without scrolling past the successes, and each failed row says
+ # why it failed.
+ view, screen = self.make_view()
+ view.handle_event({"kind": "book", "index": 1, "total": 3,
+ "name": "book.txt", "model": "m1"})
+ view.handle_event({"kind": "book_done", "name": "book.txt",
+ "ok": True, "model": "m1"})
+ view.handle_event({"kind": "book", "index": 2, "total": 3,
+ "name": "book.txt", "model": "m2"})
+ view.handle_event({"kind": "chunk_failed", "chunk": 1, "total": 1,
+ "error": "missing model root: dac",
+ "model": "m2"})
+ view.handle_event({"kind": "book_done", "name": "book.txt",
+ "ok": False, "model": "m2"})
+ view.handle_event({"kind": "book", "index": 3, "total": 3,
+ "name": "book.txt", "model": "m3"})
+ view.handle_event({"kind": "book_done", "name": "book.txt",
+ "ok": True, "model": "m3"})
+ view.handle_event({"kind": "done", "ok": 2, "total": 3})
+ view.render()
+ text = self._strings(screen)
+ fail_pos = text.index("[FAIL]")
+ ok_pos = text.index("[OK]")
+ self.assertLess(fail_pos, ok_pos)
+ self.assertIn("book.txt — m2: missing model root: dac", text)
+
+ def test_progress_line_shows_the_model_of_a_chunk_failure(self):
+ # While the run is live, the message line names the model that
+ # failed (not just the chunk counters).
+ view, screen = self.make_view()
+ view.handle_event({"kind": "book", "index": 4, "total": 30,
+ "name": "book.txt", "model": "DramaBox-GGUF"})
+ view.handle_event({"kind": "chunk_failed", "chunk": 1, "total": 1,
+ "error": "vocoder backend buffer allocation "
+ "failed", "model": "DramaBox-GGUF"})
+ view.render()
+ text = self._strings(screen)
+ self.assertIn("DramaBox-GGUF: chunk 1/1 failed", text)
+
def test_stopping_status_shows_elapsed(self):
view, screen = self.make_view()
view.handle_event({"kind": "book", "index": 1, "total": 1,