7 Commits

Author SHA1 Message Date
Pim Hutting
eab2481b85 chore: fixed flakiness of tests 2026-01-14 16:34:34 +01:00
Pim Hutting
a55acd57b6 chore: finalized tests, added queue
ref: N25B-387
2025-12-18 12:55:09 +01:00
Storm
5d5c8553c2 test: added test configuration to always mock zmq context to fix zmq port congestion issues
ref: N25B-386
2025-12-16 16:37:36 +01:00
Storm
79db2c77c8 feat: added queue-size log message
ref: N25B-386
2025-12-16 16:17:09 +01:00
Storm
b6f2893c25 style: moved one line
ref: N25B-386
2025-12-16 14:54:02 +01:00
Storm
b3e3a1eb80 feat: implemented force speech functionality in RI and refactored actuation_receiver tests
Before actuation_receiver tests started a receiver with real zmq context. This led to flaky tests because of port congestion issues.

ref: N25B-386
2025-12-16 14:49:44 +01:00
Storm
912af8d821 feat: implemented force speech functionality in RI and refactored actuation_receiver tests
Before actuation_receiver tests started a receiver with real zmq context. This led to flaky tests because of port congestion issues.

ref: N25B-386
2025-12-12 14:38:06 +01:00
37 changed files with 213 additions and 438 deletions

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals from __future__ import unicode_literals
from robot_interface.utils.get_config import get_config from robot_interface.utils.get_config import get_config
@@ -68,7 +61,7 @@ class VideoConfig(object):
): ):
self.camera_index = get_config(camera_index, "VIDEO__CAMERA_INDEX", 0, int) self.camera_index = get_config(camera_index, "VIDEO__CAMERA_INDEX", 0, int)
self.resolution = get_config(resolution, "VIDEO__RESOLUTION", 2, int) self.resolution = get_config(resolution, "VIDEO__RESOLUTION", 2, int)
self.color_space = get_config(color_space, "VIDEO__COLOR_SPACE", 13, int) self.color_space = get_config(color_space, "VIDEO__COLOR_SPACE", 11, int)
self.fps = get_config(fps, "VIDEO__FPS", 15, int) self.fps = get_config(fps, "VIDEO__FPS", 15, int)
self.stream_name = get_config(stream_name, "VIDEO__STREAM_NAME", "Pepper Video") self.stream_name = get_config(stream_name, "VIDEO__STREAM_NAME", "Pepper Video")
self.image_buffer = get_config(image_buffer, "VIDEO__IMAGE_BUFFER", 6, int) self.image_buffer = get_config(image_buffer, "VIDEO__IMAGE_BUFFER", 6, int)

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,12 +1,6 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals # So that we can log texts with Unicode characters from __future__ import unicode_literals # So that we can log texts with Unicode characters
import logging import logging
import time
from threading import Thread from threading import Thread
import Queue import Queue
@@ -14,6 +8,7 @@ import zmq
from robot_interface.endpoints.receiver_base import ReceiverBase from robot_interface.endpoints.receiver_base import ReceiverBase
from robot_interface.state import state from robot_interface.state import state
from robot_interface.core.config import settings from robot_interface.core.config import settings
from robot_interface.endpoints.gesture_settings import GestureTags from robot_interface.endpoints.gesture_settings import GestureTags
@@ -41,8 +36,11 @@ class ActuationReceiver(ReceiverBase):
self._tts_service = None self._tts_service = None
self._animation_service = None self._animation_service = None
self._message_queue = Queue.Queue() self._message_queue = Queue.Queue()
self._gesture_queue = Queue.Queue()
self.message_thread = Thread(target=self._handle_messages) self.message_thread = Thread(target=self._handle_messages)
self.message_thread.start() self.message_thread.start()
self.gesture_thread = Thread(target=self._handle_gestures)
self.gesture_thread.start()
def _handle_speech(self, message): def _handle_speech(self, message):
""" """
@@ -69,7 +67,7 @@ class ActuationReceiver(ReceiverBase):
if not self._tts_service: if not self._tts_service:
self._tts_service = state.qi_session.service("ALTextToSpeech") self._tts_service = state.qi_session.service("ALTextToSpeech")
if message.get("is_priority"): if (message.get("is_priority")):
# Bypass queue and speak immediately # Bypass queue and speak immediately
self.clear_queue() self.clear_queue()
self._message_queue.put(text) self._message_queue.put(text)
@@ -77,6 +75,7 @@ class ActuationReceiver(ReceiverBase):
else: else:
self._message_queue.put(text) self._message_queue.put(text)
def clear_queue(self): def clear_queue(self):
""" """
Safely drains all pending messages from the queue. Safely drains all pending messages from the queue.
@@ -90,6 +89,21 @@ class ActuationReceiver(ReceiverBase):
pass pass
logging.info("Message queue cleared.") logging.info("Message queue cleared.")
def clear_gesture_queue(self):
"""
Safely drains all pending gestures from the gesture queue.
"""
logging.info("Gesture queue size: {}".format(self._gesture_queue.qsize()))
try:
while True:
# Remove items one by one without waiting
self._gesture_queue.get_nowait()
except Queue.Empty:
pass
logging.info("Gesture queue cleared.")
logging.info("Gesture queue size: {}".format(self._gesture_queue.qsize()))
def _handle_gesture(self, message, is_single): def _handle_gesture(self, message, is_single):
""" """
Handle a gesture actuation request. Handle a gesture actuation request.
@@ -130,12 +144,16 @@ class ActuationReceiver(ReceiverBase):
# Play the gesture. Pepper comes with predefined animations like "Wave", "Greet", "Clap" # Play the gesture. Pepper comes with predefined animations like "Wave", "Greet", "Clap"
# You can also create custom animations using Choregraphe and upload them to the robot. # You can also create custom animations using Choregraphe and upload them to the robot.
if (message.get("is_priority")):
# Clear queue and play
self.clear_gesture_queue()
logging.debug("Force playing gesture immediately: {}".format(gesture))
if is_single: if is_single:
logging.debug("Playing single gesture: {}".format(gesture)) logging.debug("Adding single gesture to queue: {}".format(gesture))
getattr(qi, "async")(self._animation_service.run, gesture)
else: else:
logging.debug("Playing tag gesture: {}".format(gesture)) logging.debug("Adding tag gesture to queue: {}".format(gesture))
getattr(qi, "async")(self._animation_service.runTag, gesture) self._gesture_queue.put(gesture)
def handle_message(self, message): def handle_message(self, message):
""" """
@@ -155,14 +173,26 @@ class ActuationReceiver(ReceiverBase):
while not state.exit_event.is_set(): while not state.exit_event.is_set():
try: try:
text = self._message_queue.get(timeout=0.1) text = self._message_queue.get(timeout=0.1)
if not state.is_speaking: print("Started speaking.")
state.is_speaking = True state.is_speaking = True
self._tts_service.say(text) self._tts_service.say(text)
except Queue.Empty: except Queue.Empty:
if state.is_speaking: print("Finished speaking.")
state.is_speaking = False state.is_speaking = False
except RuntimeError: except RuntimeError:
logging.error("Lost connection to Pepper. Please check if you're connected to the " logging.warn("Lost connection to Pepper. Please check if you're connected to the local WiFi and restart this application.")
"local WiFi and restart this application.") state.exit_event.set()
state.exit_event.set()
def _handle_gestures(self):
while not state.exit_event.is_set():
try:
gesture = self._gesture_queue.get(timeout=0.1)
self._animation_service.run(gesture)
except Queue.Empty:
pass
except RuntimeError:
logging.warn("Lost connection to Pepper. Please check if you're connected to the local WiFi and restart this application.")
state.exit_event.set()
def endpoint_description(self): def endpoint_description(self):
""" """

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals # So that `logging` can use Unicode characters in names from __future__ import unicode_literals # So that `logging` can use Unicode characters in names
import threading import threading
import logging import logging
@@ -96,7 +89,6 @@ class AudioSender(SocketBase):
try: try:
while not state.exit_event.is_set(): while not state.exit_event.is_set():
data = stream.read(chunk) data = stream.read(chunk)
if (state.is_speaking): continue # Do not send audio while the robot is speaking
self.socket.send(data) self.socket.send(data)
except IOError as e: except IOError as e:
logger.error("Stopped listening: failed to get audio from microphone.", exc_info=e) logger.error("Stopped listening: failed to get audio from microphone.", exc_info=e)

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
class GestureTags: class GestureTags:
tags = ["above", "affirmative", "afford", "agitated", "all", "allright", "alright", "any", tags = ["above", "affirmative", "afford", "agitated", "all", "allright", "alright", "any",
"assuage", "assuage", "attemper", "back", "bashful", "beg", "beseech", "blank", "assuage", "assuage", "attemper", "back", "bashful", "beg", "beseech", "blank",

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import zmq import zmq
from robot_interface.endpoints.receiver_base import ReceiverBase from robot_interface.endpoints.receiver_base import ReceiverBase

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from robot_interface.endpoints.socket_base import SocketBase from robot_interface.endpoints.socket_base import SocketBase

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from abc import ABCMeta from abc import ABCMeta
import zmq import zmq

View File

@@ -1,21 +1,11 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import zmq import zmq
import threading import threading
import logging import logging
import struct
from robot_interface.endpoints.socket_base import SocketBase from robot_interface.endpoints.socket_base import SocketBase
from robot_interface.state import state from robot_interface.state import state
from robot_interface.core.config import settings from robot_interface.core.config import settings
class VideoSender(SocketBase): class VideoSender(SocketBase):
""" """
Video sender endpoint, responsible for sending video frames. Video sender endpoint, responsible for sending video frames.
@@ -28,7 +18,7 @@ class VideoSender(SocketBase):
""" """
def __init__(self, zmq_context, port=settings.agent_settings.video_sender_port): def __init__(self, zmq_context, port=settings.agent_settings.video_sender_port):
super(VideoSender, self).__init__("video") super(VideoSender, self).__init__("video")
self.create_socket(zmq_context, zmq.PUB, port, [(zmq.SNDHWM,3)]) self.create_socket(zmq_context, zmq.PUB, port, [(zmq.CONFLATE,1)])
def start_video_rcv(self): def start_video_rcv(self):
""" """
@@ -61,23 +51,10 @@ class VideoSender(SocketBase):
:param vid_stream_name: The name of a camera subscription on the video service object vid_service :param vid_stream_name: The name of a camera subscription on the video service object vid_service
:type vid_stream_name: str :type vid_stream_name: str
""" """
try: while not state.exit_event.is_set():
while not state.exit_event.is_set(): try:
try: img = vid_service.getImageRemote(vid_stream_name)
img = vid_service.getImageRemote(vid_stream_name) #Possibly limit images sent if queuing issues arise
if img is not None: self.socket.send(img[settings.video_config.image_buffer])
raw_data = img[6] except:
width = img[0] logging.warn("Failed to retrieve video image from robot.")
height = img[1]
width_bytes = struct.pack('<I', width)
height_bytes = struct.pack('<I', height)
self.socket.send_multipart([width_bytes, height_bytes, raw_data])
except KeyboardInterrupt:
logging.info("Video receiving loop interrupted by user.")
except:
logging.warn("Failed to retrieve video image from robot.")
finally:
vid_service.unsubscribe(vid_stream_name)
logging.info("Unsubscribed from video stream.")

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import logging import logging
from robot_interface.endpoints.audio_sender import AudioSender from robot_interface.endpoints.audio_sender import AudioSender

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import logging import logging
import signal import signal
import threading import threading

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals # So that `print` can print Unicode characters in names from __future__ import unicode_literals # So that `print` can print Unicode characters in names
import logging import logging
import sys import sys

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import logging import logging
import sys import sys

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import time import time

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from __future__ import unicode_literals # So that we can format strings with Unicode characters from __future__ import unicode_literals # So that we can format strings with Unicode characters
import random import random
import sys import sys

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from mock import patch, MagicMock from mock import patch, MagicMock
import pytest import pytest

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from mock import patch, mock from mock import patch, mock
from robot_interface.core.config import Settings from robot_interface.core.config import Settings

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import pyaudio import pyaudio
import pytest import pytest

View File

@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""

View File

@@ -1,32 +1,13 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import sys import sys
import time
import mock import mock
import pytest import pytest
import zmq import zmq
import Queue
from robot_interface.endpoints.actuation_receiver import ActuationReceiver from robot_interface.endpoints.actuation_receiver import ActuationReceiver
from robot_interface.endpoints.gesture_settings import GestureTags from robot_interface.endpoints.gesture_settings import GestureTags
@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_force_speech_clears_queue(mocker): def test_force_speech_clears_queue(mocker):
""" """
Tests that a force speech message clears the existing queue Tests that a force speech message clears the existing queue
@@ -61,7 +42,6 @@ def test_force_speech_clears_queue(mocker):
queued_item = receiver._message_queue.get() queued_item = receiver._message_queue.get()
assert queued_item == "Emergency Notification" assert queued_item == "Emergency Notification"
def test_handle_unimplemented_endpoint(mocker): def test_handle_unimplemented_endpoint(mocker):
""" """
Tests handling of unknown endpoints. Tests handling of unknown endpoints.
@@ -77,7 +57,6 @@ def test_handle_unimplemented_endpoint(mocker):
"data": None, "data": None,
}) })
def test_speech_message_no_data(mocker): def test_speech_message_no_data(mocker):
""" """
Tests that if the message data is empty, the receiver returns immediately Tests that if the message data is empty, the receiver returns immediately
@@ -235,7 +214,7 @@ def test_handle_messages_loop(mocker):
receiver._tts_service = mock_tts_service receiver._tts_service = mock_tts_service
# This ensures the while loop iterates exactly once # This ensures the while loop iterates exactly once
mock_state.exit_event.is_set.side_effect = [False, True] mock_state.exit_event.is_set.side_effect = [False, True, True , True, True]
# Put an item in the queue # Put an item in the queue
receiver._message_queue.put("Hello World") receiver._message_queue.put("Hello World")
@@ -250,40 +229,24 @@ def test_handle_messages_loop(mocker):
assert mock_state.is_speaking is True assert mock_state.is_speaking is True
def test_handle_messages_queue_empty(mocker): def test_handle_gestures_runtime_error(mocker):
"""
Tests the Queue.Empty exception handler in the consumer loop.
This covers the logic that resets 'state.is_speaking' to False.
"""
# Prevent the real background thread from starting
mocker.patch("threading.Thread") mocker.patch("threading.Thread")
# Mock the state object
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
# Setup 'is_speaking' property mock # Use a side_effect that returns False then True thereafter
# We set return_value=True so the code enters the 'if state.is_speaking:' block. mock_state.exit_event.is_set.side_effect = [False, True, True, True, True]
# We use PropertyMock to track when this attribute is set.
type(mock_state).is_speaking = True
mock_zmq_ctx = mock.Mock() mock_zmq_ctx = mock.Mock()
receiver = ActuationReceiver(mock_zmq_ctx) receiver = ActuationReceiver(mock_zmq_ctx)
# This ensures the while loop body runs exactly once for our test # ... rest of your setup ...
mock_state.exit_event.is_set.side_effect = [False, True] mock_anim = mock.Mock()
mock_anim.run.side_effect = RuntimeError("Wifi Lost")
# Force get() to raise Queue.Empty immediately (simulate timeout) receiver._animation_service = mock_anim
# We patch the 'get' method on the specific queue instance of our receiver
#mocker.patch.object(receiver._message_queue, 'get', side_effect=Queue.Empty)
# Run the loop logic manually (synchronously) receiver._gesture_queue.put("wave")
receiver._handle_messages()
receiver._handle_gestures()
# Final Assertion: Verify is_speaking was set to False
# The code execution order is: read (returns True) -> print -> set (to False)
# assert_called_with checks the arguments of the LAST call, which is the setter.
assert mock_state.is_speaking is False
def test_handle_messages_runtime_error(mocker): def test_handle_messages_runtime_error(mocker):
""" """
@@ -303,7 +266,7 @@ def test_handle_messages_runtime_error(mocker):
# Initialize receiver with the mock context # Initialize receiver with the mock context
receiver = ActuationReceiver(mock_zmq_ctx) receiver = ActuationReceiver(mock_zmq_ctx)
mock_state.exit_event.is_set.side_effect = [False, True] mock_state.exit_event.is_set.side_effect = [False, True, True, True ]
receiver._message_queue.put("Test Message") receiver._message_queue.put("Test Message")
@@ -341,54 +304,61 @@ def test_clear_queue(mocker):
# Assert the queue is empty # Assert the queue is empty
assert receiver._message_queue.qsize() == 0 assert receiver._message_queue.qsize() == 0
def test_gesture_no_data(zmq_context, mocker): def test_gesture_no_data(mocker):
receiver = ActuationReceiver(zmq_context) mock_zmq = mock.Mock()
receiver = ActuationReceiver(mock_zmq)
receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": ""}, True) receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": ""}, True)
# Just ensuring no crash # Just ensuring no crash
def test_gesture_invalid_data(zmq_context, mocker): def test_gesture_invalid_data(mocker):
receiver = ActuationReceiver(zmq_context) mock_zmq = mock.Mock()
receiver = ActuationReceiver(mock_zmq)
receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": 123}, True) receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": 123}, True)
# No crash expected # No crash expected
def test_gesture_single_not_found(zmq_context, mocker): def test_gesture_single_not_found(mocker):
mock_zmq = mock.Mock()
mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags")
mock_tags.single_gestures = ["wave", "bow"] # allowed single gestures mock_tags.single_gestures = ["wave", "bow"] # allowed single gestures
receiver = ActuationReceiver(mock_zmq)
receiver = ActuationReceiver(zmq_context)
receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "unknown_gesture"}, True) receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "unknown_gesture"}, True)
# No crash expected # No crash expected
def test_gesture_tag_not_found(zmq_context, mocker): def test_gesture_tag_not_found(mocker):
mock_zmq = mock.Mock()
mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags")
mock_tags.tags = ["happy", "sad"] mock_tags.tags = ["happy", "sad"]
receiver = ActuationReceiver(zmq_context) receiver = ActuationReceiver(mock_zmq)
receiver._handle_gesture({"endpoint": "actuate/gesture/tag", "data": "not_a_tag"}, False) receiver._handle_gesture({"endpoint": "actuate/gesture/tag", "data": "not_a_tag"}, False)
# No crash expected # No crash expected
def test_gesture_no_qi_session(zmq_context, mocker): def test_gesture_no_qi_session( mocker):
mock_zmq = mock.Mock()
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
mock_state.qi_session = None mock_state.qi_session = None
mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags")
mock_tags.single_gestures = ["hello"] mock_tags.single_gestures = ["hello"]
receiver = ActuationReceiver(zmq_context) receiver = ActuationReceiver(mock_zmq)
receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "hello"}, True) receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "hello"}, True)
# No crash, path returns early # No crash, path returns early
def test_gesture_single_success(zmq_context, mocker): def test_gesture_single_success(mocker):
mock_zmq = mock.Mock()
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
# Allow loops to run
mock_state.exit_event.is_set.return_value = False
mock_qi = mock.Mock() mock_qi = mock.Mock()
sys.modules["qi"] = mock_qi sys.modules["qi"] = mock_qi
# Setup gesture settings
mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags")
mock_tags.single_gestures = ["wave"] mock_tags.single_gestures = ["wave"]
@@ -396,17 +366,25 @@ def test_gesture_single_success(zmq_context, mocker):
mock_state.qi_session = mock.Mock() mock_state.qi_session = mock.Mock()
mock_state.qi_session.service.return_value = mock_animation_service mock_state.qi_session.service.return_value = mock_animation_service
receiver = ActuationReceiver(zmq_context) receiver = ActuationReceiver(mock_zmq)
receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "wave"}, True) receiver._handle_gesture({"endpoint": "actuate/gesture/single", "data": "wave"}, True)
mock_state.qi_session.service.assert_called_once_with("ALAnimationPlayer") time.sleep(0.2)
getattr(mock_qi, "async").assert_called_once()
assert getattr(mock_qi, "async").call_args[0][0] == mock_animation_service.run mock_state.qi_session.service.assert_called_with("ALAnimationPlayer")
assert getattr(mock_qi, "async").call_args[0][1] == "wave" mock_animation_service.run.assert_called_with("wave")
# CLEANUP: Signal exit AND join threads
mock_state.exit_event.is_set.return_value = True
receiver.message_thread.join(timeout=1.0)
receiver.gesture_thread.join(timeout=1.0)
def test_gesture_tag_success(zmq_context, mocker): def test_gesture_tag_success(mocker):
mock_zmq = mock.Mock()
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
mock_state.exit_event.is_set.return_value = False
mock_qi = mock.Mock() mock_qi = mock.Mock()
sys.modules["qi"] = mock_qi sys.modules["qi"] = mock_qi
@@ -417,21 +395,26 @@ def test_gesture_tag_success(zmq_context, mocker):
mock_state.qi_session = mock.Mock() mock_state.qi_session = mock.Mock()
mock_state.qi_session.service.return_value = mock_animation_service mock_state.qi_session.service.return_value = mock_animation_service
receiver = ActuationReceiver(zmq_context) receiver = ActuationReceiver(mock_zmq)
receiver._handle_gesture({"endpoint": "actuate/gesture/tag", "data": "greeting"}, False) receiver._handle_gesture({"endpoint": "actuate/gesture/tag", "data": "greeting"}, False)
mock_state.qi_session.service.assert_called_once_with("ALAnimationPlayer") time.sleep(0.2)
getattr(mock_qi, "async").assert_called_once()
assert getattr(mock_qi, "async").call_args[0][0] == mock_animation_service.runTag mock_state.qi_session.service.assert_called_with("ALAnimationPlayer")
assert getattr(mock_qi, "async").call_args[0][1] == "greeting" mock_animation_service.run.assert_called_with("greeting")
# CLEANUP: Signal exit AND join threads
mock_state.exit_event.is_set.return_value = True
receiver.message_thread.join(timeout=1.0)
receiver.gesture_thread.join(timeout=1.0)
def test_handle_message_all_routes(zmq_context, mocker): def test_handle_message_all_routes(mocker):
""" """
Ensures all handle_message endpoint branches route correctly. Ensures all handle_message endpoint branches route correctly.
""" """
receiver = ActuationReceiver(zmq_context) mock_zmq = mock.Mock()
receiver = ActuationReceiver(mock_zmq)
mock_speech = mocker.patch.object(receiver, "_handle_speech") mock_speech = mocker.patch.object(receiver, "_handle_speech")
mock_gesture = mocker.patch.object(receiver, "_handle_gesture") mock_gesture = mocker.patch.object(receiver, "_handle_gesture")
@@ -443,12 +426,13 @@ def test_handle_message_all_routes(zmq_context, mocker):
assert mock_gesture.call_count == 2 assert mock_gesture.call_count == 2
def test_endpoint_description(zmq_context, mocker): def test_endpoint_description(mocker):
mock_zmq = mock.Mock()
mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags") mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags")
mock_tags.tags = ["happy"] mock_tags.tags = ["happy"]
mock_tags.single_gestures = ["wave"] mock_tags.single_gestures = ["wave"]
receiver = ActuationReceiver(zmq_context) receiver = ActuationReceiver(mock_zmq)
desc = receiver.endpoint_description() desc = receiver.endpoint_description()
assert "gestures" in desc assert "gestures" in desc
@@ -458,26 +442,20 @@ def test_endpoint_description(zmq_context, mocker):
assert desc["single_gestures"] == ["wave"] assert desc["single_gestures"] == ["wave"]
def test_gesture_single_real_gesturetags(zmq_context, mocker): def test_gesture_single_real_gesturetags(mocker):
""" mock_zmq = mock.Mock()
Uses the real GestureTags (no mocking) to ensure the receiver
references GestureTags.single_gestures correctly.
"""
# Ensure qi session exists so we pass the early return
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state") mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
mock_state.qi_session = mock.Mock() mock_state.qi_session = mock.Mock()
mock_state.exit_event.is_set.return_value = False
# Mock qi.async to avoid real async calls
mock_qi = mock.Mock() mock_qi = mock.Mock()
sys.modules["qi"] = mock_qi sys.modules["qi"] = mock_qi
# Mock animation service
mock_animation_service = mock.Mock() mock_animation_service = mock.Mock()
mock_state.qi_session.service.return_value = mock_animation_service mock_state.qi_session.service.return_value = mock_animation_service
receiver = ActuationReceiver(zmq_context) receiver = ActuationReceiver(mock_zmq)
# Pick a real gesture from GestureTags.single_gestures
assert len(GestureTags.single_gestures) > 0, "GestureTags.single_gestures must not be empty" assert len(GestureTags.single_gestures) > 0, "GestureTags.single_gestures must not be empty"
gesture = GestureTags.single_gestures[0] gesture = GestureTags.single_gestures[0]
@@ -486,8 +464,85 @@ def test_gesture_single_real_gesturetags(zmq_context, mocker):
is_single=True, is_single=True,
) )
mock_state.qi_session.service.assert_called_once_with("ALAnimationPlayer") time.sleep(0.2)
getattr(mock_qi, "async").assert_called_once() mock_state.qi_session.service.assert_called_with("ALAnimationPlayer")
assert getattr(mock_qi, "async").call_args[0][0] == mock_animation_service.run mock_animation_service.run.assert_called_with(gesture)
assert getattr(mock_qi, "async").call_args[0][1] == gesture
# CLEANUP: Signal exit AND join threads
mock_state.exit_event.is_set.return_value = True
receiver.message_thread.join(timeout=1.0)
receiver.gesture_thread.join(timeout=1.0)
def test_clear_gesture_queue(mocker):
# Prevent background threads from eating the items
mocker.patch("threading.Thread")
mock_zmq_ctx = mock.Mock()
receiver = ActuationReceiver(mock_zmq_ctx)
# Populate the queue
receiver._gesture_queue.put("gesture1")
receiver._gesture_queue.put("gesture2")
assert receiver._gesture_queue.qsize() == 2
# Clear the queue
receiver.clear_gesture_queue()
# Assert the queue is empty
assert receiver._gesture_queue.qsize() == 0
def test_gesture_priority_clears_queue(mocker):
mocker.patch("threading.Thread")
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
# Mock QI and Tags so valid checks pass
mock_qi = mock.Mock()
sys.modules["qi"] = mock_qi
mock_tags = mocker.patch("robot_interface.endpoints.actuation_receiver.GestureTags")
mock_tags.single_gestures = ["urgent_wave"]
# Setup Animation Service
mock_anim = mock.Mock()
mock_state.qi_session.service.return_value = mock_anim
mock_zmq_ctx = mock.Mock()
receiver = ActuationReceiver(mock_zmq_ctx)
# Pre-fill queue with "slow" gestures
receiver._gesture_queue.put("slow_gesture_1")
receiver._gesture_queue.put("slow_gesture_2")
assert receiver._gesture_queue.qsize() == 2
# Send priority gesture
priority_msg = {
"endpoint": "actuate/gesture/single",
"data": "urgent_wave",
"is_priority": True,
}
receiver._handle_gesture(priority_msg, is_single=True)
# Assert old items are gone and only new one remains
assert receiver._gesture_queue.qsize() == 1
assert receiver._gesture_queue.get() == "urgent_wave"
def test_handle_gestures_loop_empty(mocker):
mocker.patch("threading.Thread")
mock_state = mocker.patch("robot_interface.endpoints.actuation_receiver.state")
mock_zmq_ctx = mock.Mock()
receiver = ActuationReceiver(mock_zmq_ctx)
# Run loop exactly once
mock_state.exit_event.is_set.side_effect = [False, True, True, True]
# We don't put anything in the queue, so .get(timeout=0.1) will raise Queue.Empty.
# The code should catch it and pass.
receiver._handle_gestures()
# If we reached here without raising an exception, the test passes.
# We can assert that the queue is still valid/empty.
assert receiver._gesture_queue.empty()

View File

@@ -1,10 +1,4 @@
# -*- coding: utf-8 -*- # coding=utf-8
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import os import os
import mock import mock
@@ -83,8 +77,7 @@ def test_sending_audio(mocker):
mock_zmq_context = mock.Mock() mock_zmq_context = mock.Mock()
send_socket = mock.Mock() send_socket = mock.Mock()
mock_state.is_speaking = False
# If there's something wrong with the microphone, it will raise an IOError when `read`ing. # If there's something wrong with the microphone, it will raise an IOError when `read`ing.
stream = mock.Mock() stream = mock.Mock()
stream.read = _fake_read stream.read = _fake_read
@@ -100,36 +93,6 @@ def test_sending_audio(mocker):
send_socket.assert_called() send_socket.assert_called()
def test_no_sending_if_speaking(mocker):
"""
Tests the successful sending of audio data over a ZeroMQ socket.
"""
mock_choose_mic = mocker.patch("robot_interface.endpoints.audio_sender.choose_mic")
mock_choose_mic.return_value = {"name": u"Some mic", "index": 0L}
mock_state = mocker.patch("robot_interface.endpoints.audio_sender.state")
mock_state.exit_event.is_set.side_effect = [False, True]
mock_zmq_context = mock.Mock()
send_socket = mock.Mock()
mock_state.is_speaking = True
# If there's something wrong with the microphone, it will raise an IOError when `read`ing.
stream = mock.Mock()
stream.read = _fake_read
sender = AudioSender(mock_zmq_context)
sender.socket.send = send_socket
sender.audio.open = mock.Mock()
sender.audio.open.return_value = stream
sender.start()
sender.wait_until_done()
send_socket.assert_not_called()
def _fake_read_error(num_frames): def _fake_read_error(num_frames):
""" """
Helper function to simulate an I/O error during microphone stream reading. Helper function to simulate an I/O error during microphone stream reading.

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
from robot_interface.utils.get_config import get_config from robot_interface.utils.get_config import get_config

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import pytest import pytest
import threading import threading
import zmq import zmq

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import mock import mock
import pytest import pytest
import zmq import zmq

View File

@@ -1,10 +1,4 @@
# -*- coding: utf-8 -*- # coding=utf-8
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import mock import mock
import pytest import pytest

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import sys import sys
# Import module under test # Import module under test

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import pytest import pytest
from robot_interface.endpoints.receiver_base import ReceiverBase from robot_interface.endpoints.receiver_base import ReceiverBase

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import mock import mock
import zmq import zmq
from robot_interface.endpoints.socket_base import SocketBase from robot_interface.endpoints.socket_base import SocketBase

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import threading import threading
import signal import signal
import pytest import pytest

View File

@@ -1,10 +1,3 @@
# -*- coding: utf-8 -*-
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import time import time
import mock import mock

View File

@@ -1,11 +1,5 @@
# -*- coding: utf-8 -*- # coding=utf-8
"""
This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
"""
import struct
import mock import mock
import pytest import pytest
import zmq import zmq
@@ -54,10 +48,8 @@ def test_video_streaming(zmq_context, mocker):
# Pepper's image buffer lives at index 6 # Pepper's image buffer lives at index 6
mocker.patch.object(settings.video_config, "image_buffer", 6) mocker.patch.object(settings.video_config, "image_buffer", 6)
test_width = 320
test_height = 240
mock_video_service = mock.Mock() mock_video_service = mock.Mock()
mock_video_service.getImageRemote.return_value = [test_width, test_height, None, None, None, None, b"fake_img"] mock_video_service.getImageRemote.return_value = [None]*6 + ["fake_img"]
fake_session = mock.Mock() fake_session = mock.Mock()
fake_session.service.return_value = mock_video_service fake_session.service.return_value = mock_video_service
@@ -71,16 +63,12 @@ def test_video_streaming(zmq_context, mocker):
sender = VideoSender(zmq_context) sender = VideoSender(zmq_context)
send_socket = mock.Mock() send_socket = mock.Mock()
sender.socket.send_multipart = send_socket sender.socket.send = send_socket
sender.start_video_rcv() sender.start_video_rcv()
sender.video_rcv_loop(mock_video_service, "stream_name") sender.video_rcv_loop(mock_video_service, "stream_name")
send_socket.assert_called_with([ send_socket.assert_called_with("fake_img")
struct.pack('<I', 320),
struct.pack('<I', 240),
b"fake_img"
])
def test_video_receive_error(zmq_context, mocker): def test_video_receive_error(zmq_context, mocker):
@@ -103,30 +91,9 @@ def test_video_receive_error(zmq_context, mocker):
sender = VideoSender(zmq_context) sender = VideoSender(zmq_context)
send_socket = mock.Mock() send_socket = mock.Mock()
sender.socket.send_multipart = send_socket sender.socket.send = send_socket
sender.start_video_rcv() sender.start_video_rcv()
sender.video_rcv_loop(mock_video_service, "stream_name") sender.video_rcv_loop(mock_video_service, "stream_name")
send_socket.assert_not_called() send_socket.assert_not_called()
def test_video_loop_keyboard_interrupt(zmq_context, mocker):
"""Video loop should handle KeyboardInterrupt gracefully and unsubscribe."""
_patch_basics(mocker)
_patch_exit_event(mocker)
# We mock the video service to raise KeyboardInterrupt when accessed
mock_video_service = mock.Mock()
mock_video_service.getImageRemote.side_effect = KeyboardInterrupt
# Mock logging to verify the specific interrupt message is logged
mock_logger = mocker.patch("robot_interface.endpoints.video_sender.logging")
sender = VideoSender(zmq_context)
# Execute the loop
sender.video_rcv_loop(mock_video_service, "stream_name")
# Verify the 'finally' block executed (unsubscribe)
mock_video_service.unsubscribe.assert_called_with("stream_name")
mock_logger.info.assert_any_call("Unsubscribed from video stream.")