aboutsummaryrefslogtreecommitdiff
path: root/app/tests
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-25 18:10:08 -0400
committerhistoria <historiavg@proton.me>2026-08-25 18:10:08 -0400
commit4b49797b4d57c2d2cf63472636622a7a6280a38e (patch)
treee3687c734de5d6159cdad288e025057ed6cd3a16 /app/tests
parentbcac6c42eaf9e004716d72960bddefb1db68a93c (diff)
downloadtts-audiobook-generator-4b49797b4d57c2d2cf63472636622a7a6280a38e.tar.gz
feat: no console drop when uninstalling backends
Diffstat (limited to 'app/tests')
-rw-r--r--app/tests/test_backends.py37
-rw-r--r--app/tests/test_backends_audiocpp.py29
-rw-r--r--app/tests/test_backends_envs.py45
-rw-r--r--app/tests/test_backends_faster.py58
-rw-r--r--app/tests/test_hub.py79
5 files changed, 244 insertions, 4 deletions
diff --git a/app/tests/test_backends.py b/app/tests/test_backends.py
index 0d3be37..8ea6a3f 100644
--- a/app/tests/test_backends.py
+++ b/app/tests/test_backends.py
@@ -325,3 +325,40 @@ class QwenSetupScreenTests(unittest.TestCase):
self.assertIs(mk_steps.call_args[0][0], settings)
mk_run.assert_called_once()
self.assertEqual(mk_run.call_args[0][2], steps)
+
+
+class QwenUninstallTests(unittest.TestCase):
+ """qwen.uninstall: stop both servers, then pip-uninstall the package."""
+
+ def test_stops_servers_and_pips(self):
+ from backends import qwen
+ with patch.object(qwen.servers, "stop") as mk_stop, \
+ patch.object(qwen.common, "pip_uninstall",
+ return_value=0) as mk_pip:
+ rc = qwen.uninstall(emit="EMIT")
+ self.assertEqual(rc, 0)
+ self.assertEqual([c.args[0] for c in mk_stop.call_args_list],
+ ["qwen-custom", "qwen-clone"])
+ # The task view's emit is forwarded so pip never touches the terminal.
+ mk_pip.assert_called_once_with([qwen.QWEN_PIP_PKG], emit="EMIT")
+
+ def test_cancel_before_pip_skips_uninstall(self):
+ import threading
+
+ from backends import qwen
+ cancel = threading.Event()
+ cancel.set()
+ with patch.object(qwen.servers, "stop") as mk_stop, \
+ patch.object(qwen.common, "pip_uninstall") as mk_pip:
+ rc = qwen.uninstall(cancel=cancel)
+ self.assertEqual(rc, 130)
+ self.assertEqual(mk_stop.call_count, 2)
+ mk_pip.assert_not_called()
+
+ def test_pip_failure_warns_but_still_succeeds(self):
+ from backends import qwen
+ with patch.object(qwen.servers, "stop"), \
+ patch.object(qwen.common, "pip_uninstall",
+ return_value=1):
+ rc = qwen.uninstall()
+ self.assertEqual(rc, 0)
diff --git a/app/tests/test_backends_audiocpp.py b/app/tests/test_backends_audiocpp.py
index e206ddd..665dbf0 100644
--- a/app/tests/test_backends_audiocpp.py
+++ b/app/tests/test_backends_audiocpp.py
@@ -1836,6 +1836,35 @@ class UninstallTests(unittest.TestCase):
self.assertEqual(rc, 0)
mk_stop.assert_called_once_with("audiocpp")
+ def test_accepts_task_view_kwargs_for_registry_symmetry(self):
+ # The hub calls uninstall(emit=..., cancel=...); emit is unused here
+ # (no subprocess phase) and cancel=None behaves like the plain call.
+ with tempfile.TemporaryDirectory() as td:
+ checkout = Path(td) / "audio.cpp"
+ checkout.mkdir()
+ with patch.object(make_server, "find_local_checkout",
+ return_value=checkout), \
+ patch.object(make_server.servers, "stop"):
+ rc = make_server.uninstall(emit=lambda line: None,
+ cancel=None)
+ self.assertEqual(rc, 0)
+ self.assertFalse(checkout.exists())
+
+ def test_cancel_before_delete_keeps_checkout(self):
+ # Cancel is honored between phases only: once the server is stopped
+ # and cancellation is pending, the checkout deletion never starts.
+ with tempfile.TemporaryDirectory() as td:
+ checkout = Path(td) / "audio.cpp"
+ checkout.mkdir()
+ cancel = threading.Event()
+ cancel.set()
+ with patch.object(make_server, "find_local_checkout",
+ return_value=checkout), \
+ patch.object(make_server.servers, "stop"):
+ rc = make_server.uninstall(cancel=cancel)
+ self.assertEqual(rc, 130)
+ self.assertTrue(checkout.exists())
+
if __name__ == "__main__":
unittest.main()
diff --git a/app/tests/test_backends_envs.py b/app/tests/test_backends_envs.py
index bf27f60..82d903a 100644
--- a/app/tests/test_backends_envs.py
+++ b/app/tests/test_backends_envs.py
@@ -106,6 +106,51 @@ class PipInstallTests(unittest.TestCase):
run.assert_not_called()
+class PipUninstallTests(unittest.TestCase):
+ def test_missing_env_is_success_without_running_pip(self):
+ with patch.object(envs, "env_exists", return_value=False), \
+ patch.object(envs.common, "run_console_subprocess") as run:
+ rc = envs.pip_uninstall(["qwen-tts"])
+ self.assertEqual(rc, 0)
+ run.assert_not_called()
+
+ def test_runs_pip_uninstall_against_the_venv_python(self):
+ calls = []
+
+ def fake_run(argv, **kwargs):
+ calls.append(list(argv))
+ return 0
+
+ with patch.object(envs, "env_exists", return_value=True), \
+ patch.object(envs.common, "run_console_subprocess",
+ side_effect=fake_run):
+ rc = envs.pip_uninstall(["qwen-tts"])
+ self.assertEqual(rc, 0)
+ # The uninstall targets the venv's python.
+ self.assertEqual(calls[0][0], str(envs.env_python()))
+ self.assertIn("uninstall", calls[0])
+ self.assertIn("-y", calls[0])
+ self.assertIn("qwen-tts", calls[0])
+
+ def test_streams_to_emit_when_given(self):
+ with patch.object(envs, "env_exists", return_value=True), \
+ patch.object(envs.common, "run_console_subprocess",
+ return_value=0) as run:
+ rc = envs.pip_uninstall(["qwen-tts"], emit="EMIT")
+ self.assertEqual(rc, 0)
+ # The task view's emit is forwarded so pip never touches the
+ # terminal behind curses.
+ self.assertEqual(run.call_args.kwargs.get("emit"), "EMIT")
+
+ def test_console_path_passes_no_emit(self):
+ with patch.object(envs, "env_exists", return_value=True), \
+ patch.object(envs.common, "run_console_subprocess",
+ return_value=0) as run:
+ rc = envs.pip_uninstall(["qwen-tts"])
+ self.assertEqual(rc, 0)
+ self.assertIsNone(run.call_args.kwargs.get("emit"))
+
+
class ModuleAvailableTests(unittest.TestCase):
def test_false_when_env_missing(self):
with patch.object(envs, "env_exists", return_value=False):
diff --git a/app/tests/test_backends_faster.py b/app/tests/test_backends_faster.py
index eb1fa28..7e27b47 100644
--- a/app/tests/test_backends_faster.py
+++ b/app/tests/test_backends_faster.py
@@ -3,6 +3,7 @@
import json
import sys
import tempfile
+import threading
import unittest
import contextlib
from pathlib import Path
@@ -276,3 +277,60 @@ class SetupScreenTests(unittest.TestCase):
self.assertIs(mk_steps.call_args[0][0], settings)
mk_run.assert_called_once()
self.assertEqual(mk_run.call_args[0][2], steps)
+
+
+class UninstallTests(unittest.TestCase):
+ """uninstall: stop the server, pip-uninstall, delete the checkout."""
+
+ def test_pips_and_removes_checkout(self):
+ with tempfile.TemporaryDirectory() as td:
+ checkout = Path(td) / "faster-qwen3-tts"
+ checkout.mkdir()
+ with patch.object(make_voices, "_checkout",
+ return_value=checkout), \
+ patch.object(make_voices.servers, "stop") as mk_stop, \
+ patch.object(make_voices.common, "pip_uninstall",
+ return_value=0) as mk_pip:
+ rc = make_voices.uninstall(emit="EMIT")
+ self.assertEqual(rc, 0)
+ mk_stop.assert_called_once_with("faster")
+ # The task view's emit is forwarded so pip never touches the terminal.
+ mk_pip.assert_called_once_with(["faster-qwen3-tts"], emit="EMIT")
+ self.assertFalse(checkout.exists())
+
+ def test_no_checkout_still_uninstalls_the_package(self):
+ with patch.object(make_voices, "_checkout",
+ return_value=Path("/no/such/dir")), \
+ patch.object(make_voices.servers, "stop"), \
+ patch.object(make_voices.common, "pip_uninstall",
+ return_value=0) as mk_pip:
+ rc = make_voices.uninstall()
+ self.assertEqual(rc, 0)
+ mk_pip.assert_called_once_with(["faster-qwen3-tts"], emit=None)
+
+ def test_cancel_before_pip_skips_everything_after_stopping(self):
+ cancel = threading.Event()
+ cancel.set()
+ with patch.object(make_voices.servers, "stop") as mk_stop, \
+ patch.object(make_voices.common, "pip_uninstall") as mk_pip:
+ rc = make_voices.uninstall(cancel=cancel)
+ self.assertEqual(rc, 130)
+ mk_stop.assert_called_once_with("faster")
+ mk_pip.assert_not_called()
+
+ def test_cancel_before_delete_keeps_checkout(self):
+ # Cancel between phases: pip runs to completion, but a pending
+ # cancellation stops the checkout deletion from starting.
+ with tempfile.TemporaryDirectory() as td:
+ checkout = Path(td) / "faster-qwen3-tts"
+ checkout.mkdir()
+ cancel = threading.Event()
+ cancel.set()
+ with patch.object(make_voices, "_checkout",
+ return_value=checkout), \
+ patch.object(make_voices.servers, "stop"), \
+ patch.object(make_voices.common, "pip_uninstall",
+ return_value=0):
+ rc = make_voices.uninstall(cancel=cancel)
+ self.assertEqual(rc, 130)
+ self.assertTrue(checkout.exists())
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py
index 965cbaa..ec079f5 100644
--- a/app/tests/test_hub.py
+++ b/app/tests/test_hub.py
@@ -1730,14 +1730,85 @@ class ConfigureBackendsDispatchTests(unittest.TestCase):
self.assertIs(result, tui.Wizard.BACK)
self.assertEqual(flashes, ["kaboom"])
- def test_screen_uninstall_runs_uninstall_and_goes_back(self):
- import contextlib
+ def test_screen_uninstall_runs_in_task_view_and_goes_back(self):
info = BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0)
with patch.object(hub._Hub, "_pick_backend", return_value=info), \
+ patch.object(hub.tui, "confirm", return_value=True), \
+ patch.object(hub.taskview, "run_steps",
+ return_value=0) as mk_run, \
patch.object(info, "uninstall") as mk_uninstall, \
- patch.object(hub.tui, "suspend", contextlib.nullcontext):
+ patch.object(hub.tui, "flash"):
result = hub._Hub(None).screen_uninstall()
- mk_uninstall.assert_called_once_with()
+ # The uninstall runs as one task-view step on the session (no
+ # suspend); executing the step forwards emit/cancel to uninstall.
+ mk_run.assert_called_once()
+ self.assertEqual(mk_run.call_args[0][0], None)
+ steps = mk_run.call_args[0][2]
+ self.assertEqual([step.title for step in steps],
+ ["Uninstall qwen-tts"])
+ self.assertFalse(mk_run.call_args.kwargs["wait_on_finish"])
+ emit = lambda line: None
+ steps[0].work(emit, None)
+ mk_uninstall.assert_called_once_with(emit=emit, cancel=None)
+ self.assertIs(result, tui.Wizard.BACK)
+
+ def test_screen_uninstall_success_flashes_ok(self):
+ flashes = []
+
+ def fake_flash(scr, text, kind="warn"):
+ flashes.append((text, kind))
+
+ info = BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0)
+ with patch.object(hub._Hub, "_pick_backend", return_value=info), \
+ patch.object(hub.tui, "confirm", return_value=True), \
+ patch.object(hub.taskview, "run_steps", return_value=0), \
+ patch.object(info, "uninstall"), \
+ patch.object(hub.tui, "flash", fake_flash):
+ result = hub._Hub(None).screen_uninstall()
+ self.assertIs(result, tui.Wizard.BACK)
+ self.assertEqual(flashes[-1], ("qwen-tts uninstalled.", "ok"))
+
+ def test_screen_uninstall_failure_flashes_error(self):
+ flashes = []
+
+ def fake_flash(scr, text, kind="warn"):
+ flashes.append((text, kind))
+
+ info = BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0)
+ with patch.object(hub._Hub, "_pick_backend", return_value=info), \
+ patch.object(hub.tui, "confirm", return_value=True), \
+ patch.object(hub.taskview, "run_steps", return_value=1), \
+ patch.object(info, "uninstall"), \
+ patch.object(hub.tui, "flash", fake_flash):
+ result = hub._Hub(None).screen_uninstall()
+ self.assertIs(result, tui.Wizard.BACK)
+ self.assertEqual(flashes[-1][1], "err")
+ self.assertIn("Could not fully uninstall", flashes[-1][0])
+
+ def test_screen_uninstall_confirm_declined_skips_uninstall(self):
+ info = BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0)
+ with patch.object(hub._Hub, "_pick_backend", return_value=info), \
+ patch.object(hub.tui, "confirm", return_value=False) \
+ as mk_confirm, \
+ patch.object(hub.taskview, "run_steps") as mk_run, \
+ patch.object(info, "uninstall") as mk_uninstall:
+ result = hub._Hub(None).screen_uninstall()
+ mk_run.assert_not_called()
+ mk_uninstall.assert_not_called()
+ self.assertIs(result, tui.Wizard.BACK)
+ # The confirm names the backend and is Esc-able (cancel_value set).
+ self.assertIn("Uninstall qwen-tts?", mk_confirm.call_args[0][1])
+
+ def test_screen_uninstall_esc_on_confirm_backs_out(self):
+ info = BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0)
+ with patch.object(hub._Hub, "_pick_backend", return_value=info), \
+ patch.object(hub.tui, "confirm",
+ return_value=tui.Wizard.BACK), \
+ patch.object(hub.taskview, "run_steps") as mk_run, \
+ patch.object(info, "uninstall") as mk_uninstall:
+ result = hub._Hub(None).screen_uninstall()
+ mk_run.assert_not_called()
+ mk_uninstall.assert_not_called()
self.assertIs(result, tui.Wizard.BACK)
def _capture_flashes(self):