aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/selfupdate.py14
-rw-r--r--app/tests/test_hub.py55
-rw-r--r--app/tests/test_selfupdate.py21
-rw-r--r--app/ui/hub.py73
4 files changed, 84 insertions, 79 deletions
diff --git a/app/selfupdate.py b/app/selfupdate.py
index 239db92..95e0ef4 100644
--- a/app/selfupdate.py
+++ b/app/selfupdate.py
@@ -15,8 +15,9 @@ update can never silently overwrite user configuration with upstream's
new version. A kept file whose upstream version changed is reported, so
new upstream options can be merged by hand.
-Run via the hub's Configure Backends > Update Generator action; the
-module is stdlib-only like ``backends.common`` / ``backends.envs``.
+Run via the hub's Configure Backends > Update tts-audiobook-generator
+action; the module is stdlib-only like ``backends.common`` /
+``backends.envs``.
"""
from pathlib import Path
@@ -165,7 +166,8 @@ def update_generator(*, emit=None, cancel=None,
branch = common.origin_default_branch(base)
remote = _rev_parse(base, f"origin/{branch}")
if remote is not None and _rev_parse(base, "HEAD") == remote:
- _say(f"[INFO] The generator is already up to date ({remote}).", emit)
+ _say(f"[INFO] tts-audiobook-generator is already up to date "
+ f"({remote}).", emit)
_restore(snapshot, base)
return 0
@@ -177,4 +179,10 @@ def update_generator(*, emit=None, cancel=None,
for rel in _restore(snapshot, base):
_say(f"[OK] Kept your local {rel} (upstream's changes to it were "
"skipped — merge new options by hand if needed).", emit)
+ if not reset_rc:
+ # The fresh code loads on relaunch (where requirements re-install
+ # runs automatically) — the console says so because the hub no
+ # longer flashes after the task view's own summary.
+ _say("[OK] Restart to apply the update — requirements re-install "
+ "runs automatically on next launch.", emit)
return reset_rc
diff --git a/app/tests/test_hub.py b/app/tests/test_hub.py
index 228a103..8d5edd9 100644
--- a/app/tests/test_hub.py
+++ b/app/tests/test_hub.py
@@ -396,7 +396,8 @@ class SubmenuStatusTableTests(unittest.TestCase):
def setUp(self):
# These tests concern the backend actions; the host's git-checkout
- # state (which gates the "Update Generator" entry) must not leak in.
+ # state (which gates the "Update tts-audiobook-generator" entry)
+ # must not leak in.
patcher = patch.object(hub.selfupdate, "is_git_checkout",
return_value=False)
patcher.start()
@@ -4049,7 +4050,7 @@ class ConfigureBackendsDispatchTests(unittest.TestCase):
self.assertEqual(flashes[-1][1], "err")
self.assertIn("did not complete", flashes[-1][0])
- def test_update_backends_action_flashes_warn_when_cancelled(self):
+ def test_update_backends_action_silent_when_cancelled(self):
infos = [BackendInfo("qwen", "qwen-tts", lambda: None, lambda: 0,
update=lambda **kw: 0)]
statuses = [BackendStatus("qwen", "qwen-tts", installed=True,
@@ -4057,11 +4058,14 @@ class ConfigureBackendsDispatchTests(unittest.TestCase):
patch_flash, flashes = self._capture_flashes()
with patch.object(hub, "REGISTRY", infos), \
patch.object(hub, "detect_all", return_value=statuses), \
- patch.object(hub.taskview, "run_steps", return_value=130), \
+ patch.object(hub.taskview, "run_steps",
+ return_value=130) as mk_run, \
patch_flash:
hub._update_backends_action(None)
- self.assertEqual(flashes[-1][1], "warn")
- self.assertIn("cancelled", flashes[-1][0])
+ # The task view already showed "cancelled" and waited for a key,
+ # so the hub adds no flash on top of it.
+ mk_run.assert_called_once()
+ self.assertEqual(flashes, [])
def test_update_backends_action_without_targets_flashes_a_hint(self):
# An installed backend without an update action (and nothing else
@@ -4129,8 +4133,8 @@ class ConfigureBackendsDispatchTests(unittest.TestCase):
hub._Hub(None).screen_configure()
return [opt[0] for opt in captured["options"]]
- self.assertIn("Update Generator", options_for(True))
- self.assertNotIn("Update Generator", options_for(False))
+ self.assertIn("Update tts-audiobook-generator", options_for(True))
+ self.assertNotIn("Update tts-audiobook-generator", options_for(False))
def test_selecting_update_generator_runs_the_action_and_reshows(self):
titles = []
@@ -4170,8 +4174,6 @@ class ConfigureBackendsDispatchTests(unittest.TestCase):
patch_flash, flashes = self._capture_flashes()
with patch.object(hub.selfupdate, "is_git_checkout",
return_value=True), \
- patch.object(hub.selfupdate, "current_commit",
- return_value="aaa"), \
patch.object(hub.selfupdate, "modified_tracked_files",
return_value=["app/converter/config.py"]), \
patch.object(hub.tui, "confirm",
@@ -4185,12 +4187,10 @@ class ConfigureBackendsDispatchTests(unittest.TestCase):
body = mk_confirm.call_args[1]["body"]
self.assertIn("app/converter/config.py", body)
- def test_update_self_action_runs_step_and_flashes_update(self):
+ def test_update_self_action_runs_step_silently_on_success(self):
patch_flash, flashes = self._capture_flashes()
with patch.object(hub.selfupdate, "is_git_checkout",
return_value=True), \
- patch.object(hub.selfupdate, "current_commit",
- side_effect=["aaa", "bbb"]), \
patch.object(hub.selfupdate, "modified_tracked_files",
return_value=[]), \
patch.object(hub.taskview, "run_steps",
@@ -4201,7 +4201,8 @@ class ConfigureBackendsDispatchTests(unittest.TestCase):
hub._update_self_action(None)
mk_run.assert_called_once()
self.assertEqual(mk_run.call_args[0][0], None)
- self.assertEqual(mk_run.call_args[0][1], "Update Generator")
+ self.assertEqual(mk_run.call_args[0][1],
+ "Update tts-audiobook-generator")
steps = mk_run.call_args[0][2]
self.assertEqual([step.title for step in steps],
["Fetch and reset the checkout"])
@@ -4211,48 +4212,38 @@ class ConfigureBackendsDispatchTests(unittest.TestCase):
steps[0].work(emit, "CANCEL")
mk_update.assert_called_once_with(emit=emit, cancel="CANCEL")
- self.assertEqual(len(flashes), 1)
- text, kind = flashes[0]
- self.assertEqual(kind, "ok")
- self.assertIn("aaa", text)
- self.assertIn("bbb", text)
- self.assertIn("Restart to apply", text)
+ # The task view already showed "completed" and waited for a key;
+ # the restart reminder lives in the run's console output.
+ self.assertEqual(flashes, [])
- def test_update_self_action_flashes_already_up_to_date(self):
+ def test_update_self_action_silent_when_already_up_to_date(self):
patch_flash, flashes = self._capture_flashes()
with patch.object(hub.selfupdate, "is_git_checkout",
return_value=True), \
- patch.object(hub.selfupdate, "current_commit",
- side_effect=["aaa", "aaa"]), \
patch.object(hub.selfupdate, "modified_tracked_files",
return_value=[]), \
patch.object(hub.taskview, "run_steps", return_value=0), \
patch_flash:
hub._update_self_action(None)
- self.assertEqual(flashes,
- [("The generator is already up to date (aaa).",
- "ok")])
+ # The console already said so; no flash after the key press.
+ self.assertEqual(flashes, [])
- def test_update_self_action_flashes_warn_when_cancelled(self):
+ def test_update_self_action_silent_when_cancelled(self):
patch_flash, flashes = self._capture_flashes()
with patch.object(hub.selfupdate, "is_git_checkout",
return_value=True), \
- patch.object(hub.selfupdate, "current_commit",
- side_effect=["aaa", "aaa"]), \
patch.object(hub.selfupdate, "modified_tracked_files",
return_value=[]), \
patch.object(hub.taskview, "run_steps", return_value=130), \
patch_flash:
hub._update_self_action(None)
- self.assertEqual(flashes[-1][1], "warn")
- self.assertIn("cancelled", flashes[-1][0])
+ # The task view already showed "cancelled" and waited for a key.
+ self.assertEqual(flashes, [])
def test_update_self_action_flashes_err_when_failed(self):
patch_flash, flashes = self._capture_flashes()
with patch.object(hub.selfupdate, "is_git_checkout",
return_value=True), \
- patch.object(hub.selfupdate, "current_commit",
- side_effect=["aaa", "aaa"]), \
patch.object(hub.selfupdate, "modified_tracked_files",
return_value=[]), \
patch.object(hub.taskview, "run_steps", return_value=1), \
diff --git a/app/tests/test_selfupdate.py b/app/tests/test_selfupdate.py
index bf2f6b3..b69c6e3 100644
--- a/app/tests/test_selfupdate.py
+++ b/app/tests/test_selfupdate.py
@@ -155,6 +155,9 @@ class UpdateGeneratorTests(unittest.TestCase):
self.assertEqual(self.config.read_bytes(), b"# user settings\n")
self.assertTrue(any("already up to date" in line
for line in self.emitted))
+ # A no-op update needs no restart, so no reminder either.
+ self.assertFalse(any("Restart to apply" in line
+ for line in self.emitted))
def test_fetch_failure_short_circuits(self):
run = self._patch_git(fetch_rc=128)
@@ -200,6 +203,24 @@ class UpdateGeneratorTests(unittest.TestCase):
self.assertEqual(run.call_args_list[1][1]["emit"],
self.emitted.append)
+ def test_successful_reset_prints_the_restart_reminder(self):
+ # The hub no longer flashes after the task view's summary, so the
+ # console output itself must tell the user to restart.
+ self._patch_git()
+ rc = selfupdate.update_generator(root=self.root,
+ emit=self.emitted.append)
+ self.assertEqual(rc, 0)
+ self.assertTrue(any("Restart to apply" in line
+ for line in self.emitted))
+
+ def test_failed_reset_prints_no_restart_reminder(self):
+ self._patch_git(reset_rc=1)
+ rc = selfupdate.update_generator(root=self.root,
+ emit=self.emitted.append)
+ self.assertEqual(rc, 1)
+ self.assertFalse(any("Restart to apply" in line
+ for line in self.emitted))
+
def test_reset_failure_still_restores_the_snapshot(self):
self._patch_git(reset_rc=1, on_reset=lambda: self.config.write_bytes(
b"# upstream settings\n"))
diff --git a/app/ui/hub.py b/app/ui/hub.py
index 67680db..f9151f8 100644
--- a/app/ui/hub.py
+++ b/app/ui/hub.py
@@ -226,7 +226,8 @@ class _Hub:
if any(_updatable(info, by_key) for info in REGISTRY):
options.append(("Update Backends", "update"))
if selfupdate.is_git_checkout():
- options.append(("Update Generator", "update_self"))
+ options.append(("Update tts-audiobook-generator",
+ "update_self"))
if any(_uninstallable(info, by_key) for info in REGISTRY):
options.append(("Uninstall Backend", "uninstall"))
@@ -234,8 +235,8 @@ class _Hub:
self.stdscr, "Configure Backends", options,
back_value=tui.Wizard.BACK,
help_lines=["Install, update, configure, or remove a TTS "
- "backend. 'Update Generator' refreshes this "
- "tool's own checkout instead."],
+ "backend. 'Update tts-audiobook-generator' "
+ "refreshes this tool's own checkout instead."],
table_rows=_status_rows(statuses),
notice_lines=_notice_lines())
if choice is tui.Wizard.BACK:
@@ -687,12 +688,12 @@ def _download_models_action(stdscr) -> None:
Computes the missing models; when they map to install commands it runs
the downloads in the task view (with real byte progress and cancellation)
- instead of dropping to the console — a successful run returns silently
- (the view already shows [OK] and waits for a key), and only a cancelled
- or failed run flashes. When the checkout/server.json is missing, nothing
- is missing, or the models do not map to an install command, it flashes
- an explanatory notice (the latter explaining how to install each model
- by hand).
+ instead of dropping to the console — the view shows the outcome and
+ waits for a key, so a successful or cancelled run returns silently and
+ only a failed run flashes. When the checkout/server.json is missing,
+ nothing is missing, or the models do not map to an install command, it
+ flashes an explanatory notice (the latter explaining how to install
+ each model by hand).
"""
checkout = audiocpp_backend.find_local_checkout()
if checkout is None:
@@ -723,10 +724,7 @@ def _download_models_action(stdscr) -> None:
rc = taskview.run_steps(stdscr, "Download models",
[taskview.TaskStep("Download missing models",
run)])
- if rc == 130:
- tui.flash(stdscr, "Model download cancelled — re-run it any time.",
- "warn")
- elif rc:
+ if rc not in (0, 130):
tui.flash(stdscr, "Some model downloads failed. Re-run 'Download "
"Missing Models' or install them by hand (see the log).",
"err")
@@ -741,9 +739,9 @@ def _update_backends_action(stdscr) -> None:
git fetch + hard reset for the checkouts, with audio.cpp's binary
rebuilt when its checkout moved. A failing backend's step is marked
[FAIL] and the remaining backends still update (the run's exit code
- is the first failure). A successful run returns silently (the view
- already shows [OK] and waits for a key); only a cancelled or failed
- run flashes. The status table re-detects when the menu re-shows.
+ is the first failure). Successful and cancelled runs return silently
+ (the view already shows the outcome and waits for a key); only a
+ failed run flashes. The status table re-detects when the menu re-shows.
"""
statuses = detect_all()
by_key = {st.key: st for st in statuses}
@@ -760,17 +758,14 @@ def _update_backends_action(stdscr) -> None:
steps = [taskview.TaskStep(f"Update {info.label}", make_work(info))
for info in targets]
rc = taskview.run_steps(stdscr, "Update Backends", steps)
- if rc == 130:
- tui.flash(stdscr, "Update cancelled — re-run 'Update Backends' "
- "any time.", "warn")
- elif rc:
+ if rc not in (0, 130):
tui.flash(stdscr, "Some updates did not complete (failed or "
"cancelled) — see the log above. Re-run 'Update "
"Backends' to retry.", "err")
def _update_self_action(stdscr) -> None:
- """Run the "Update Generator" action inside the TUI.
+ """Run the "Update tts-audiobook-generator" action inside the TUI.
Moves the generator's own git checkout to the remote's default-branch
HEAD (fetch + hard reset — the same flow the backend checkouts use).
@@ -781,17 +776,17 @@ def _update_self_action(stdscr) -> None:
and written back afterwards (a kept file whose upstream version
changed is reported for a manual merge). A dirty checkout confirms
first (declining, Esc included, aborts before anything runs); the run
- itself streams in the task view like the other inline actions. After:
- a successful run flashes the moved commit range (or "already up to
- date") plus the restart reminder — the fresh code loads on relaunch,
- where requirements re-install runs automatically — while cancel and
- failure flash their usual warn/err. The menu re-shows either way.
+ itself streams in the task view like the other inline actions, whose
+ outcome summary the user dismisses with a key — so a successful run
+ (including a no-op "already up to date" one) flashes nothing, and the
+ restart reminder is part of the run's own console output (update_generator
+ emits it after the reset). Only a failed run flashes; the menu re-shows
+ either way.
"""
if not selfupdate.is_git_checkout():
tui.flash(stdscr, "This install is not a git checkout — update by "
"re-cloning the repository.", "err")
return
- before = selfupdate.current_commit()
modified = selfupdate.modified_tracked_files()
if modified:
body = ["These locally modified files are kept as they are:",
@@ -799,30 +794,20 @@ def _update_self_action(stdscr) -> None:
body += modified[:6]
if len(modified) > 6:
body.append(f"...and {len(modified) - 6} more")
- if tui.confirm(stdscr, "Update the generator?", body=body,
- default=False, cancel_value=False) is not True:
+ if tui.confirm(stdscr, "Update tts-audiobook-generator?",
+ body=body, default=False,
+ cancel_value=False) is not True:
return
def work(emit, cancel):
return selfupdate.update_generator(emit=emit, cancel=cancel)
rc = taskview.run_steps(
- stdscr, "Update Generator",
+ stdscr, "Update tts-audiobook-generator",
[taskview.TaskStep("Fetch and reset the checkout", work)])
- after = selfupdate.current_commit()
- if rc == 130:
- tui.flash(stdscr, "Update cancelled — re-run 'Update Generator' "
- "any time.", "warn")
- elif rc:
- tui.flash(stdscr, "The generator update did not complete — see the "
- "log above and retry.", "err")
- elif before is not None and before == after:
- tui.flash(stdscr, f"The generator is already up to date ({before}).",
- "ok")
- else:
- tui.flash(stdscr, f"Generator updated {before or '?'} → {after}. "
- "Restart to apply — requirements re-install runs "
- "automatically on next launch.", "ok")
+ if rc not in (0, 130):
+ tui.flash(stdscr, "The tts-audiobook-generator update did not "
+ "complete — see the log above and retry.", "err")
def _status_mark(status: Optional[BackendStatus]) -> Tuple[str, str, str]: