Files
voculinux/tests/test_download_error_reporting.py
T
Jatin K MalikandGitHub d8ff7052cd fix(settings): stop Test Dictation calling a missing model "no speech" (#702)
* fix(settings): stop Test Dictation calling a missing model "no speech"

start_recognition now returns whether listening actually began. Test
Dictation checks that before starting its timer, so a missing or
auto-paused model gets a real error instead of "No speech detected".

* fix(settings): reconfigure Test Dictation against the live engine

UI matching config.json is not enough when speech_engine is still on
another engine or model size. Compare those too before the listen timer.

* fix(tests): keep settings_dialog lookups on one module

_load_settings_dialog() restored sys.modules but left
vocalinux.ui.settings_dialog pointing at the private reload.
Python 3.9/3.10 mock follows that attribute, so
test_whisper_list_and_delete patched a different copy than it
imported and the expanduser stub hid the real cache.

Point the package attribute back at the restored module, and
patch the imported module object in the deletion test.
2026-08-18 17:59:57 -07:00

110 lines
3.9 KiB
Python

"""Tests for reporting failed model downloads instead of swallowing them."""
import importlib
import sys
from unittest.mock import MagicMock, Mock, patch
import pytest
def _load_settings_dialog():
"""Import settings_dialog with real base classes for its GTK subclasses.
conftest swaps gi for a MagicMock, which leaves every ``class X(Gtk.Y)`` in
the module as a mock and makes its methods unreachable. Handing the three
bases the module subclasses a real class keeps the classes intact, while
the rest of GTK stays mocked.
Restore both sys.modules and the vocalinux.ui.settings_dialog attribute so
3.9/3.10 mock and later imports see the same module object.
"""
import vocalinux.ui as ui_pkg
repository = sys.modules["gi.repository"]
bases = {name: type(name, (), {}) for name in ("Box", "ListBoxRow", "Dialog")}
saved = sys.modules.pop("vocalinux.ui.settings_dialog", None)
try:
with patch.object(repository, "Gtk", MagicMock(**bases)):
module = importlib.import_module("vocalinux.ui.settings_dialog")
finally:
sys.modules.pop("vocalinux.ui.settings_dialog", None)
if saved is not None:
sys.modules["vocalinux.ui.settings_dialog"] = saved
ui_pkg.settings_dialog = saved
else:
ui_pkg.__dict__.pop("settings_dialog", None)
return module
settings_dialog = _load_settings_dialog()
SettingsDialog = settings_dialog.SettingsDialog
def _dialog_stub():
dialog = Mock()
dialog.speech_engine.state = "idle"
return dialog
def test_download_threads_get_the_failure():
"""With raise_errors the caller's handler sees the exception.
The download threads rely on this: their except blocks turn the failure
into set_complete(False, reason) on the progress dialog. Without it they
reported "Complete!" for downloads that were cancelled or never finished.
"""
dialog = _dialog_stub()
dialog.speech_engine.reconfigure.side_effect = RuntimeError("Download cancelled")
with pytest.raises(RuntimeError, match="Download cancelled"):
SettingsDialog._apply_settings_internal(dialog, {"engine": "vosk"}, raise_errors=True)
def test_no_gtk_dialog_is_built_when_errors_are_raised():
"""The failing thread path must not touch Gtk — that is the caller's job.
Building and running a Gtk.MessageDialog off the main loop is what the
swallowed-error path used to do from the download thread.
"""
dialog = _dialog_stub()
dialog.speech_engine.reconfigure.side_effect = RuntimeError("boom")
with patch.object(settings_dialog, "Gtk") as gtk:
with pytest.raises(RuntimeError):
SettingsDialog._apply_settings_internal(dialog, {"engine": "vosk"}, raise_errors=True)
gtk.MessageDialog.assert_not_called()
def test_main_loop_callers_keep_the_dialog_and_the_false_return():
"""Without the flag the old contract stays: swallow, show, return False."""
dialog = _dialog_stub()
dialog.speech_engine.reconfigure.side_effect = RuntimeError("boom")
with patch.object(settings_dialog, "Gtk") as gtk:
result = SettingsDialog._apply_settings_internal(dialog, {"engine": "vosk"})
assert result is False
gtk.MessageDialog.assert_called_once()
def test_success_still_returns_true():
dialog = _dialog_stub()
assert SettingsDialog._apply_settings_internal(dialog, {"engine": "vosk"}) is True
assert (
SettingsDialog._apply_settings_internal(dialog, {"engine": "vosk"}, raise_errors=True)
is True
)
def test_both_download_threads_ask_for_the_failure():
"""Source guard: every _apply_settings_internal call made off the main loop
passes raise_errors=True, so a regression cannot silently reintroduce the
swallowed-error path in a download thread."""
import inspect
source = inspect.getsource(settings_dialog)
thread_calls = source.count("_apply_settings_internal(settings, raise_errors=True)")
assert thread_calls == 2