import sys import mock import pytest import zmq from robot_interface.endpoints.actuation_receiver import ActuationReceiver @pytest.fixture def zmq_context(): """ A pytest fixture that creates and yields a ZMQ context. :return: An initialized ZeroMQ context. :rtype: zmq.Context """ context = zmq.Context() yield context def test_handle_unimplemented_endpoint(zmq_context): """ Tests that the ``ActuationReceiver.handle_message`` method can handle an unknown or unimplemented endpoint without raising an error. :param zmq_context: The ZeroMQ context fixture. :type zmq_context: zmq.Context """ receiver = ActuationReceiver(zmq_context) # Should not error receiver.handle_message({ "endpoint": "some_endpoint_that_definitely_does_not_exist", "data": None, }) def test_speech_message_no_data(zmq_context, mocker): """ Tests that the message handler logs a warning when a speech actuation request (`actuate/speech`) is received but contains empty string data. :param zmq_context: The ZeroMQ context fixture. :type zmq_context: zmq.Context :param mocker: The pytest-mock fixture used to patch `logging.warn`. :type mocker: pytest_mock.plugin.MockerFixture """ mock_warn = mocker.patch("logging.warn") receiver = ActuationReceiver(zmq_context) receiver.handle_message({"endpoint": "actuate/speech", "data": ""}) mock_warn.assert_called_with(mock.ANY) def test_speech_message_invalid_data(zmq_context, mocker): """ Tests that the message handler logs a warning when a speech actuation request (`actuate/speech`) is received with data that is not a string (e.g., a boolean). :param zmq_context: The ZeroMQ context fixture. :type zmq_context: zmq.Context :param mocker: The pytest-mock fixture used to patch `logging.warn`. :type mocker: pytest_mock.plugin.MockerFixture """ mock_warn = mocker.patch("logging.warn") receiver = ActuationReceiver(zmq_context) receiver.handle_message({"endpoint": "actuate/speech", "data": True}) mock_warn.assert_called_with(mock.ANY) def test_speech_no_qi(zmq_context, mocker): """ Tests the actuation receiver's behavior when processing a speech request but the global state does not have an active QI session. :param zmq_context: The ZeroMQ context fixture. :type zmq_context: zmq.Context :param mocker: The pytest-mock fixture used to patch the global state. :type mocker: pytest_mock.plugin.MockerFixture """ mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") mock_qi_session = mock.PropertyMock(return_value=None) type(mock_state).qi_session = mock_qi_session receiver = ActuationReceiver(zmq_context) receiver._handle_speech({"endpoint": "actuate/speech", "data": "Some message to speak."}) mock_qi_session.assert_called() def test_speech(zmq_context, mocker): """ Tests the core speech actuation functionality by mocking the QI TextToSpeech service and verifying that it is called correctly. :param zmq_context: The ZeroMQ context fixture. :type zmq_context: zmq.Context :param mocker: The pytest-mock fixture used to patch state and modules. :type mocker: pytest_mock.plugin.MockerFixture """ mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") mock_qi = mock.Mock() sys.modules["qi"] = mock_qi mock_tts_service = mock.Mock() mock_state.qi_session = mock.Mock() mock_state.qi_session.service.return_value = mock_tts_service receiver = ActuationReceiver(zmq_context) receiver._tts_service = None receiver._handle_speech({"endpoint": "actuate/speech", "data": "Some message to speak."}) mock_state.qi_session.service.assert_called_once_with("ALTextToSpeech") mock_qi.async.assert_called_once() call_args = mock_qi.async.call_args[0] assert call_args[0] == mock_tts_service.say assert call_args[1] == "Some message to speak."