98 lines
4.9 KiB
Python
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()
|