import sys import time import unittest from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) from cthulhu import speechdispatcherfactory from cthulhu import speechserver import speechd class SpeechDispatcherInterruptRegressionTests(unittest.TestCase): def _make_server(self): server = speechdispatcherfactory.SpeechServer.__new__(speechdispatcherfactory.SpeechServer) server._lastKeyEchoTime = None server._cancel = mock.Mock() server._apply_acss = mock.Mock() server._send_command = mock.Mock() server._speak = mock.Mock() server._client = mock.Mock() server._client.char = mock.Mock() server._say_all_generation = 0 server._cancelled_say_all_generation = None server._CALLBACK_TYPE_MAP = { speechd.CallbackType.BEGIN: speechserver.SayAllContext.PROGRESS, speechd.CallbackType.CANCEL: speechserver.SayAllContext.INTERRUPTED, speechd.CallbackType.END: speechserver.SayAllContext.COMPLETED, speechd.CallbackType.INDEX_MARK: speechserver.SayAllContext.PROGRESS, } return server def _make_say_all_context(self, utterance): return speechserver.SayAllContext( mock.Mock(), utterance, 0, len(utterance), ) def test_interrupting_string_speech_cancels_active_output_first(self): server = self._make_server() server.speak("long utterance", interrupt=True) server._cancel.assert_called_once_with() server._speak.assert_called_once_with("long utterance", None) def test_string_speech_queues_by_default(self): server = self._make_server() server.speak("next") server._cancel.assert_not_called() server._speak.assert_called_once_with("next", None) def test_recent_key_echo_suppresses_backend_cancel(self): server = self._make_server() server._lastKeyEchoTime = time.time() server.speak("next", interrupt=True) server._cancel.assert_not_called() server._speak.assert_called_once_with("next", None) def test_stop_invalidates_pending_say_all_continuation(self): server = self._make_server() first = self._make_say_all_context("first") second = self._make_say_all_context("second") callback = mock.Mock() iterator = iter([[first, None], [second, None]]) server.sayAll(iterator, callback) server._say_all(iterator, callback, 1) speechd_callback = server._speak.call_args.kwargs["callback"] server.stop() speechd_callback(speechd.CallbackType.END) self.assertEqual(1, server._speak.call_count) callback.assert_not_called() def test_interrupting_speech_invalidates_pending_say_all_continuation(self): server = self._make_server() first = self._make_say_all_context("first") second = self._make_say_all_context("second") callback = mock.Mock() iterator = iter([[first, None], [second, None]]) server.sayAll(iterator, callback) server._say_all(iterator, callback, 1) speechd_callback = server._speak.call_args.kwargs["callback"] server.speak("new speech", interrupt=True) speechd_callback(speechd.CallbackType.END) self.assertEqual(2, server._speak.call_count) self.assertEqual("new speech", server._speak.call_args.args[0]) callback.assert_not_called() def test_send_command_logs_and_recovers_from_command_errors(self): server = self._make_server() server.reset = mock.Mock() command = mock.Mock( side_effect=[ speechd.SSIPCommandError(500, "bad ssml", "bad ssml"), "recovered", ] ) with mock.patch.object(speechdispatcherfactory.debug, "printMessage") as print_message: result = speechdispatcherfactory.SpeechServer._send_command(server, command, "payload") self.assertEqual("recovered", result) server.reset.assert_called_once_with() self.assertEqual(2, command.call_count) logged_messages = [call.args[1] for call in print_message.call_args_list] self.assertTrue(any("SSIPCommandError" in message for message in logged_messages)) def test_debug_sd_values_logs_when_backend_state_queries_return_none(self): server = self._make_server() server._current_voice_properties = {} server._id = "default" server.reset = mock.Mock() server._send_command = speechdispatcherfactory.SpeechServer._send_command.__get__( server, speechdispatcherfactory.SpeechServer ) fake_app = mock.Mock() fake_app.settingsManager.getSetting.return_value = speechdispatcherfactory.settings.PUNCTUATION_STYLE_MOST fake_script = mock.Mock() fake_script.utilities.adjustForDigits.side_effect = lambda text: text fake_state = mock.Mock(activeScript=fake_script) server._client.get_rate.return_value = None server._client.get_pitch.return_value = None server._client.get_volume.return_value = None server._client.get_language.return_value = None with mock.patch.object(speechdispatcherfactory, "cthulhu") as fake_cthulhu, \ mock.patch.object(speechdispatcherfactory, "cthulhu_state", fake_state), \ mock.patch.object(speechdispatcherfactory.debug, "debugLevel", speechdispatcherfactory.debug.LEVEL_INFO), \ mock.patch.object(speechdispatcherfactory.debug, "printMessage") as print_message: fake_cthulhu.cthulhuApp = fake_app speechdispatcherfactory.SpeechServer._debug_sd_values(server, "prefix") logged_messages = [str(call.args[1]) for call in print_message.call_args_list] self.assertTrue(any("returned None" in message for message in logged_messages)) self.assertEqual(4, server.reset.call_count) if __name__ == "__main__": unittest.main()