| """Tests for jarvis.robot.controller.""" |
|
|
| import numpy as np |
| import time |
| from unittest.mock import MagicMock, patch |
|
|
| from jarvis.robot.controller import RobotController, HeadPose, MotionStep |
|
|
|
|
| class TestHeadPose: |
| def test_defaults(self): |
| p = HeadPose() |
| assert p.x == 0.0 |
| assert p.yaw == 0.0 |
|
|
| def test_keyword_args(self): |
| p = HeadPose(yaw=30.0, pitch=-10.0, z=5.0) |
| assert p.yaw == 30.0 |
| assert p.pitch == -10.0 |
| assert p.z == 5.0 |
|
|
|
|
| class TestRobotControllerSim: |
| """Tests with sim=True — no real robot needed.""" |
|
|
| def test_sim_connect(self, mock_robot): |
| |
| assert mock_robot._sim is True |
|
|
| def test_move_head_in_sim(self, mock_robot): |
| |
| mock_robot.move_head(HeadPose(yaw=30.0)) |
|
|
| def test_set_head_realtime_in_sim(self, mock_robot): |
| mock_robot.set_head_realtime(HeadPose(pitch=10.0)) |
|
|
| def test_turn_body_in_sim(self, mock_robot): |
| mock_robot.turn_body(45.0) |
|
|
| def test_set_antennas_in_sim(self, mock_robot): |
| mock_robot.set_antennas(left=30.0, right=30.0) |
|
|
| def test_play_emotion_in_sim(self, mock_robot): |
| mock_robot.play_emotion("happy") |
|
|
| def test_play_dance_in_sim(self, mock_robot): |
| mock_robot.play_dance("groove") |
|
|
| def test_list_emotions_empty_in_sim(self, mock_robot): |
| assert mock_robot.list_emotions() == [] |
|
|
| def test_list_dances_empty_in_sim(self, mock_robot): |
| assert mock_robot.list_dances() == [] |
|
|
| def test_get_frame_none_in_sim(self, mock_robot): |
| assert mock_robot.get_frame() is None |
|
|
| def test_disconnect_in_sim(self, mock_robot): |
| mock_robot.disconnect() |
|
|
|
|
| class TestRobotControllerReal: |
| """Tests with mocked ReachyMini SDK.""" |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_connect_success(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
|
|
| mock_mini.__enter__.assert_called_once() |
| assert rc._connected is True |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| def test_connect_failure_falls_back_to_sim(self, mock_mini_cls): |
| mock_mini_cls.side_effect = ConnectionError("Robot not found") |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
|
|
| assert rc._sim is True |
| assert rc._mini is None |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_disconnect(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
| rc.disconnect() |
|
|
| mock_mini.__exit__.assert_called_once_with(None, None, None) |
| assert rc._connected is False |
|
|
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_disconnect_does_not_close_externally_owned_connection(self, mock_moves_cls): |
| mock_mini = MagicMock() |
|
|
| rc = RobotController(sim=False) |
| rc.attach(mock_mini, owns_connection=False) |
| rc.disconnect() |
|
|
| mock_mini.__exit__.assert_not_called() |
| assert rc._connected is False |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_run_sequence_calls_goto_target(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
| rc.run_sequence([ |
| MotionStep(kind="head", pose=HeadPose(yaw=10.0), duration=0.2), |
| MotionStep(kind="body", body_yaw=15.0, duration=0.2), |
| ], blocking=True) |
|
|
| assert mock_mini.goto_target.call_count >= 2 |
|
|
| rc.run_sequence([MotionStep(kind="pause", duration=0.2)]) |
| time.sleep(0.05) |
| assert rc.is_sequence_active() |
| rc.stop_sequence() |
| assert not rc.is_sequence_active() |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_run_macro_acknowledge(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
| rc.run_macro("acknowledge", intensity=1.0, blocking=True) |
|
|
| assert mock_mini.goto_target.call_count >= 1 |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_move_head_calls_goto_target(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
| rc.move_head(HeadPose(yaw=30.0), duration=1.5) |
|
|
| mock_mini.goto_target.assert_called_once() |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_move_head_clamps_duration(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
| rc.move_head(HeadPose(), duration=-1.0) |
|
|
| |
| call_kwargs = mock_mini.goto_target.call_args |
| assert call_kwargs.kwargs.get("duration", call_kwargs[1].get("duration")) >= 0.1 |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_turn_body_clamps_yaw(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
| rc.turn_body(999.0) |
|
|
| |
| call_args = mock_mini.goto_target.call_args |
| body_yaw = call_args.kwargs.get("body_yaw", call_args[1].get("body_yaw")) |
| assert body_yaw <= np.deg2rad(160.0) |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_get_doa_preserves_none_speech_flag(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini.media.get_DoA.return_value = (0.42, None) |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
|
|
| doa, is_speech = rc.get_doa() |
| assert doa == 0.42 |
| assert is_speech is None |
|
|
| @patch("jarvis.robot.controller.ReachyMini") |
| @patch("jarvis.robot.controller.RecordedMoves") |
| def test_get_doa_handles_invalid_angle_with_speech_flag(self, mock_moves_cls, mock_mini_cls): |
| mock_mini = MagicMock() |
| mock_mini.media.get_DoA.return_value = ("not-a-number", True) |
| mock_mini_cls.return_value = mock_mini |
|
|
| rc = RobotController(sim=False) |
| rc.connect() |
|
|
| doa, is_speech = rc.get_doa() |
| assert doa is None |
| assert is_speech is True |
|
|