Files

98 lines
4.9 KiB
Python

"""Speech discovery is read-only; all dispatcher connections here are mocked."""
import json
from pathlib import Path
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from talking_clock.config import Config, Settings
from talking_clock.speech import connect, discover, matching_voices, system_language
class SpeechTests(unittest.TestCase):
def test_locale_precedence_encoding_and_c_fallback(self):
self.assertEqual(system_language({"LC_ALL": "de_DE.UTF-8", "LANG": "en_US.UTF-8"}), "de-de")
self.assertEqual(system_language({"LC_ALL": "C.UTF-8", "LANGUAGE": "es_MX:en"}), "es-mx")
self.assertEqual(system_language({"LC_MESSAGES": "fr_CA.UTF-8@modifier", "LANG": "en_US.UTF-8"}), "fr-ca")
self.assertEqual(system_language({"LANG": "pt_BR.UTF-8"}), "pt-br")
self.assertEqual(system_language({"LC_ALL": "POSIX", "LANG": "C"}), "en")
def test_voice_filter_prefers_region_and_falls_back_to_language(self):
voices = (("British", "en-gb", ""), ("American", "en_US", ""),
("French", "fr", ""), ("Unknown", None, None),
("British", "en-gb", "other"), ("English", "EN", ""))
self.assertEqual(matching_voices(voices, "en-us"), ("American",))
self.assertEqual(matching_voices(voices, "en-au"), ("American", "British", "English"))
self.assertEqual(matching_voices(voices, "en"), ("English",))
self.assertEqual(matching_voices(voices, "fr-fr"), ("French",))
self.assertEqual(matching_voices(voices, "es"), ())
def test_discovery_queries_selected_module_and_closes_without_speaking(self):
client = Mock()
client.list_output_modules.return_value = ("rhvoice", "espeak-ng")
client.list_synthesis_voices.return_value = (("American", "en-us", ""), ("French", "fr", ""))
with patch("talking_clock.speech.connect", return_value=client), patch(
"talking_clock.speech.system_language", return_value="en-us"):
catalog = discover("espeak-ng")
self.assertEqual(catalog.modules, ("espeak-ng", "rhvoice"))
self.assertEqual(catalog.voices, ("American",))
client.set_output_module.assert_called_once_with("espeak-ng")
client.close.assert_called_once()
client.speak.assert_not_called()
def test_system_default_module_is_queried_without_overriding_it(self):
client = Mock()
client.list_output_modules.return_value = ("espeak-ng",)
client.get_output_module.return_value = "espeak-ng"
client.list_synthesis_voices.return_value = ()
with patch("talking_clock.speech.connect", return_value=client):
self.assertEqual(discover().module, "espeak-ng")
client.set_output_module.assert_not_called()
client.speak.assert_not_called()
def test_connection_does_not_autostart_dispatcher(self):
module = SimpleNamespace(SSIPClient=Mock())
with patch.dict(sys.modules, {"speechd": module}):
connect()
module.SSIPClient.assert_called_once_with("talking-clock", component="settings", autospawn=False)
def test_unavailable_dispatcher_and_module_are_reported(self):
with patch("talking_clock.speech.connect", side_effect=OSError("offline")):
with self.assertRaisesRegex(RuntimeError, "offline"):
discover()
client = Mock()
client.list_output_modules.return_value = ()
with patch("talking_clock.speech.connect", return_value=client):
with self.assertRaisesRegex(RuntimeError, "unavailable"):
discover("missing")
client.close.assert_called_once()
def test_numeric_limits_and_saved_intermediate_values(self):
for key in ("rate", "pitch", "volume"):
for value in (-100, -37, 0, 37, 100):
settings = Settings(**{key: value})
self.assertIs(settings.validate(), settings)
for value in (-101, 101, "10", True):
with self.assertRaises(ValueError):
Settings(**{key: value}).validate()
def test_old_python_speech_preferences_migrate_to_dispatcher_defaults(self):
with tempfile.TemporaryDirectory() as directory:
config = Config(directory)
config.path.write_text(json.dumps({"voice": "custom", "voice_command": "some old command",
"rhvoice": "alan", "gama_path": "/old", "font_size": 100}))
settings = config.load()
self.assertEqual(settings.module, "")
self.assertEqual(settings.voice, "")
self.assertEqual(settings.font_size, 100)
config.save(settings)
data = json.loads(config.path.read_text())
self.assertFalse(set(data) & {"voice_command", "rhvoice", "gama_path"})
if __name__ == "__main__":
unittest.main()