aboutsummaryrefslogtreecommitdiff
path: root/tests/test_hub.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_hub.py')
-rw-r--r--tests/test_hub.py204
1 files changed, 193 insertions, 11 deletions
diff --git a/tests/test_hub.py b/tests/test_hub.py
index 21f795e..b3a17e8 100644
--- a/tests/test_hub.py
+++ b/tests/test_hub.py
@@ -71,14 +71,14 @@ class HubMenuTests(unittest.TestCase):
return BackendStatus(key, label, installed=False, configured=False)
def test_quit_returns_none_when_no_backend(self):
- # No backends installed/running: menu is [Set up, Quit]. Quit is the
- # 2nd option (Down once) then Enter.
- screen = FakeScreen(keys=[FakeCurses.KEY_DOWN, 10])
+ # No backends installed/running: menu is [Set up, Settings, Quit].
+ # Quit is the 3rd option (Down twice) then Enter.
+ screen = FakeScreen(keys=[FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN, 10])
with patch.object(hub, "detect_all", return_value=[]):
result = hub._hub_menu(screen)
self.assertIsNone(result)
- def test_menu_has_only_setup_and_quit_without_backends(self):
+ def test_menu_has_only_setup_settings_and_quit_without_backends(self):
# Capture the options handed to tui.menu: with nothing installed or
# running, Convert/Configure must be absent.
captured = {}
@@ -92,9 +92,10 @@ class HubMenuTests(unittest.TestCase):
patch.object(hub, "detect_all", return_value=[]):
hub._hub_menu(screen)
labels = [label for label, _ in captured["options"]]
- self.assertEqual(labels, ["Set up a backend...", "Quit"])
+ self.assertEqual(labels, ["Set up a backend...", "Settings...",
+ "Quit"])
- def test_menu_has_all_five_when_one_installed(self):
+ def test_menu_has_all_six_when_one_installed(self):
captured = {}
def fake_menu(stdscr, title, options, **kwargs):
@@ -112,7 +113,7 @@ class HubMenuTests(unittest.TestCase):
self.assertEqual(
labels,
["Convert books...", "Set up a backend...",
- "Configure a backend...", "Server...", "Quit"])
+ "Configure a backend...", "Server...", "Settings...", "Quit"])
# The status table is passed through, one row per backend.
self.assertEqual(captured["rows"],
[("qwen-tts", "installed", "warn", "body")])
@@ -138,7 +139,7 @@ class HubMenuTests(unittest.TestCase):
[("audio.cpp", "unavailable", "err", "dim"),
("qwen-tts", "running", "ok", "body")])
- def test_menu_has_all_five_when_one_running_only(self):
+ def test_menu_has_all_six_when_one_running_only(self):
# Running but not installed (an external server) still unlocks the
# Convert/Configure/Server entries.
captured = {}
@@ -157,7 +158,7 @@ class HubMenuTests(unittest.TestCase):
self.assertEqual(
labels,
["Convert books...", "Set up a backend...",
- "Configure a backend...", "Server...", "Quit"])
+ "Configure a backend...", "Server...", "Settings...", "Quit"])
def test_convert_with_no_available_backend_offers_setup(self):
# One installed-but-not-ready backend → Convert is offered. The
@@ -182,11 +183,12 @@ class HubMenuTests(unittest.TestCase):
with patch.object(hub, "detect_all", return_value=statuses), \
patch.object(hub, "REGISTRY", infos):
# Convert(Enter), setup-entry(Enter), Esc on setup menu,
- # back at main menu -> Down x4 -> Enter (Quit).
+ # back at main menu -> Down x5 -> Enter (Quit; Settings sits
+ # just before it).
screen = FakeScreen(keys=[10, 10, 27,
FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN,
FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN,
- 10])
+ FakeCurses.KEY_DOWN, 10])
result = hub._hub_menu(screen)
self.assertIsNone(result)
@@ -316,5 +318,185 @@ class AddAutostartTests(unittest.TestCase):
self.assertNotIn("autostart", cmd[2])
+class SettingsTests(unittest.TestCase):
+ """Settings menu: field collection, validation, config.py writing."""
+
+ def test_write_config_preserves_comments_and_other_lines(self):
+ import tempfile
+ with tempfile.TemporaryDirectory() as td:
+ path = Path(td) / "config.py"
+ path.write_text(
+ "# Default output options\n"
+ 'AUDIO_FORMAT = "m4b"\n'
+ 'AUDIO_BITRATE = "128k"\n'
+ 'LANGUAGE = "English"\n'
+ "\n"
+ "CHUNK_SIZE = 250 # words per request\n",
+ encoding="utf-8")
+ with patch.object(hub.config, "__file__", str(path)):
+ hub._write_config({"AUDIO_FORMAT": "mp3",
+ "AUDIO_BITRATE": "192k",
+ "LANGUAGE": "Japanese",
+ "CHUNK_SIZE": 300})
+ text = path.read_text(encoding="utf-8")
+ self.assertEqual(
+ text,
+ "# Default output options\n"
+ 'AUDIO_FORMAT = "mp3"\n'
+ 'AUDIO_BITRATE = "192k"\n'
+ 'LANGUAGE = "Japanese"\n'
+ "\n"
+ "CHUNK_SIZE = 300 # words per request\n")
+
+ def test_write_config_missing_key_raises(self):
+ import tempfile
+ with tempfile.TemporaryDirectory() as td:
+ path = Path(td) / "config.py"
+ path.write_text("X = 1\n", encoding="utf-8")
+ with patch.object(hub.config, "__file__", str(path)):
+ with self.assertRaises(ValueError):
+ hub._write_config({"AUDIO_FORMAT": "mp3"})
+
+ def test_apply_settings_writes_and_reloads_in_memory(self):
+ written = {}
+
+ def fake_write(updates):
+ written.update(updates)
+
+ original = {name: getattr(hub.config, name) for name in
+ ("AUDIO_FORMAT", "AUDIO_BITRATE", "LANGUAGE",
+ "CHUNK_SIZE")}
+ self.addCleanup(lambda: [setattr(hub.config, name, value)
+ for name, value in original.items()])
+ values = {"audio_format": "ogg", "audio_bitrate": " 192k ",
+ "language": "en", "chunk_size": "300"}
+ with patch.object(hub, "_write_config", fake_write):
+ hub._apply_settings(values)
+ # Values are trimmed and language normalized to a display name.
+ self.assertEqual(written, {"AUDIO_FORMAT": "ogg",
+ "AUDIO_BITRATE": "192k",
+ "LANGUAGE": "English",
+ "CHUNK_SIZE": 300})
+ # In-memory config is reloaded so this session sees the change.
+ self.assertEqual(hub.config.AUDIO_FORMAT, "ogg")
+ self.assertEqual(hub.config.AUDIO_BITRATE, "192k")
+ self.assertEqual(hub.config.LANGUAGE, "English")
+ self.assertEqual(hub.config.CHUNK_SIZE, 300)
+
+ def test_apply_settings_rejects_bad_values(self):
+ original = {name: getattr(hub.config, name) for name in
+ ("AUDIO_FORMAT", "AUDIO_BITRATE", "LANGUAGE",
+ "CHUNK_SIZE")}
+ self.addCleanup(lambda: [setattr(hub.config, name, value)
+ for name, value in original.items()])
+ with patch.object(hub, "_write_config") as mk_write:
+ with self.assertRaises(ValueError):
+ hub._apply_settings({"audio_format": "m4b",
+ "audio_bitrate": "128k",
+ "language": "Klingon",
+ "chunk_size": "250"})
+ with self.assertRaises(ValueError):
+ hub._apply_settings({"audio_format": "m4b",
+ "audio_bitrate": "128k",
+ "language": "English",
+ "chunk_size": "0"})
+ mk_write.assert_not_called()
+
+ def test_field_validators(self):
+ self.assertIsNone(hub._validate_bitrate("128k"))
+ self.assertIsNotNone(hub._validate_bitrate(" "))
+ self.assertIsNone(hub._validate_language("English"))
+ self.assertIsNone(hub._validate_language("en"))
+ self.assertIsNotNone(hub._validate_language("Klingon"))
+ self.assertIsNone(hub._validate_chunk_size("250"))
+ self.assertIsNotNone(hub._validate_chunk_size("abc"))
+ self.assertIsNotNone(hub._validate_chunk_size("0"))
+
+ def test_settings_menu_builds_form_and_saves(self):
+ captured = {}
+
+ def fake_form(stdscr, title, fields, back_value=None):
+ captured["fields"] = fields
+ return {"audio_format": "ogg", "audio_bitrate": "192k",
+ "language": "English", "chunk_size": "300"}
+
+ applied = []
+
+ def fake_apply(values):
+ applied.append(values)
+
+ def fake_flash(stdscr, text, kind="warn"):
+ captured["flash"] = (text, kind)
+
+ with patch.object(hub.tui, "form", fake_form), \
+ patch.object(hub, "_apply_settings", fake_apply), \
+ patch.object(hub.tui, "flash", fake_flash):
+ hub._settings_menu(None)
+ self.assertEqual([f["key"] for f in captured["fields"]],
+ ["audio_format", "audio_bitrate", "language",
+ "chunk_size"])
+ kinds = {f["key"]: f["kind"] for f in captured["fields"]}
+ self.assertEqual(kinds["audio_format"], "choice")
+ self.assertEqual(kinds["audio_bitrate"], "text")
+ self.assertEqual(applied, [{"audio_format": "ogg",
+ "audio_bitrate": "192k",
+ "language": "English",
+ "chunk_size": "300"}])
+ self.assertEqual(captured["flash"], ("Settings saved.", "ok"))
+
+ def test_settings_menu_cancel_does_not_apply(self):
+ def fake_form(stdscr, title, fields, back_value=None):
+ return back_value # user pressed Cancel
+
+ applied = []
+
+ def fake_apply(values):
+ applied.append(values)
+
+ with patch.object(hub.tui, "form", fake_form), \
+ patch.object(hub, "_apply_settings", fake_apply):
+ hub._settings_menu(None)
+ self.assertEqual(applied, [])
+
+ def test_settings_menu_writes_config_end_to_end(self):
+ import tempfile
+ tui._THEME.clear()
+ self.addCleanup(tui._THEME.clear)
+ curses = FakeCurses()
+ patcher = patch.dict("sys.modules", {"curses": curses})
+ patcher.start()
+ self.addCleanup(patcher.stop)
+
+ original = {name: getattr(hub.config, name) for name in
+ ("AUDIO_FORMAT", "AUDIO_BITRATE", "LANGUAGE",
+ "CHUNK_SIZE")}
+ self.addCleanup(lambda: [setattr(hub.config, name, value)
+ for name, value in original.items()])
+
+ with tempfile.TemporaryDirectory() as td:
+ path = Path(td) / "config.py"
+ path.write_text(
+ "# Default output options\n"
+ 'AUDIO_FORMAT = "m4b"\n'
+ 'AUDIO_BITRATE = "128k"\n'
+ 'LANGUAGE = "English"\n'
+ "\n"
+ "CHUNK_SIZE = 250\n",
+ encoding="utf-8")
+ with patch.object(hub.config, "__file__", str(path)):
+ # Down to Chunk size, Enter -> editor, Ctrl-U + '300',
+ # Enter; Tab -> Save, Enter; a key dismisses the flash.
+ screen = FakeScreen(keys=[
+ FakeCurses.KEY_DOWN, FakeCurses.KEY_DOWN,
+ FakeCurses.KEY_DOWN, 10, 21, ord("3"), ord("0"),
+ ord("0"), 10, 9, 10, 10])
+ hub._settings_menu(screen)
+ text = path.read_text(encoding="utf-8")
+ self.assertIn('AUDIO_FORMAT = "m4b"', text)
+ self.assertIn("CHUNK_SIZE = 300", text)
+ # The running session also picked up the change in-memory.
+ self.assertEqual(hub.config.CHUNK_SIZE, 300)
+
+
if __name__ == "__main__":
unittest.main()