Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions tests/actions/emotion/connector/test_unitree_sdk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Tests for the Emotion Unitree SDK connector."""

import sys
from unittest.mock import MagicMock, Mock, patch

import pytest

# Mock the unitree module at module load time BEFORE any imports
mock_audio_client = MagicMock()
mock_unitree = MagicMock()
mock_unitree.unitree_sdk2py.g1.audio.g1_audio_client.AudioClient = mock_audio_client
sys.modules["unitree"] = mock_unitree
sys.modules["unitree.unitree_sdk2py"] = mock_unitree.unitree_sdk2py
sys.modules["unitree.unitree_sdk2py.g1"] = mock_unitree.unitree_sdk2py.g1
sys.modules["unitree.unitree_sdk2py.g1.audio"] = mock_unitree.unitree_sdk2py.g1.audio
sys.modules["unitree.unitree_sdk2py.g1.audio.g1_audio_client"] = (
mock_unitree.unitree_sdk2py.g1.audio.g1_audio_client
)

from actions.emotion.connector.unitree_sdk import ( # noqa: E402
EmotionUnitreeConfig,
EmotionUnitreeConnector,
)
from actions.emotion.interface import EmotionAction, EmotionInput # noqa: E402


@pytest.fixture
def default_config():
"""Create a default config for testing."""
return EmotionUnitreeConfig()


@pytest.fixture
def ethernet_config():
"""Create a config with ethernet for testing."""
return EmotionUnitreeConfig(unitree_ethernet="eth0")


@pytest.fixture
def emotion_input_happy():
"""Create an EmotionInput instance with happy action."""
return EmotionInput(action=EmotionAction.HAPPY)


@pytest.fixture
def emotion_input_sad():
"""Create an EmotionInput instance with sad action."""
return EmotionInput(action=EmotionAction.SAD)


@pytest.fixture
def emotion_input_mad():
"""Create an EmotionInput instance with mad action."""
return EmotionInput(action=EmotionAction.MAD)


@pytest.fixture
def emotion_input_curious():
"""Create an EmotionInput instance with curious action."""
return EmotionInput(action=EmotionAction.CURIOUS)


@pytest.fixture(autouse=True)
def reset_mocks():
"""Reset all mock objects between tests."""
mock_audio_client.reset_mock()
mock_audio_client.return_value = MagicMock()
yield


class TestEmotionUnitreeConfig:
"""Tests for the EmotionUnitreeConfig."""

def test_default_config(self):
"""Test default configuration values."""
config = EmotionUnitreeConfig()
assert config.unitree_ethernet == ""

def test_custom_config(self):
"""Test custom configuration values."""
config = EmotionUnitreeConfig(unitree_ethernet="eth0")
assert config.unitree_ethernet == "eth0"


class TestEmotionUnitreeConnector:
"""Tests for the EmotionUnitreeConnector."""

def test_init_without_ethernet(self, default_config):
"""Test initialization without ethernet configured."""
connector = EmotionUnitreeConnector(default_config)

assert connector.ao_client is None
assert connector.unitree_ethernet == ""

def test_init_with_ethernet(self, ethernet_config):
"""Test initialization with ethernet configured."""
mock_client_instance = Mock()
mock_audio_client.return_value = mock_client_instance

connector = EmotionUnitreeConnector(ethernet_config)

assert connector.ao_client is not None
assert connector.unitree_ethernet == "eth0"
# Verify SetTimeout, Init, and LedControl calls
mock_client_instance.SetTimeout.assert_called_once_with(10.0)
mock_client_instance.Init.assert_called_once()
mock_client_instance.LedControl.assert_called_once_with(0, 255, 0)

@pytest.mark.asyncio
async def test_connect_without_client(self, default_config, emotion_input_happy):
"""Test connect when no audio client is available."""
connector = EmotionUnitreeConnector(default_config)
await connector.connect(emotion_input_happy)
# Should not raise, just log error

@pytest.mark.asyncio
async def test_connect_happy(self, ethernet_config, emotion_input_happy):
"""Test connect with happy emotion."""
mock_client_instance = Mock()
mock_audio_client.return_value = mock_client_instance

connector = EmotionUnitreeConnector(ethernet_config)
await connector.connect(emotion_input_happy)

mock_client_instance.LedControlNoReply.assert_called_with(0, 255, 0)

@pytest.mark.asyncio
async def test_connect_sad(self, ethernet_config, emotion_input_sad):
"""Test connect with sad emotion."""
mock_client_instance = Mock()
mock_audio_client.return_value = mock_client_instance

connector = EmotionUnitreeConnector(ethernet_config)
await connector.connect(emotion_input_sad)

mock_client_instance.LedControlNoReply.assert_called_with(255, 255, 0)

@pytest.mark.asyncio
async def test_connect_mad(self, ethernet_config, emotion_input_mad):
"""Test connect with mad emotion."""
mock_client_instance = Mock()
mock_audio_client.return_value = mock_client_instance

connector = EmotionUnitreeConnector(ethernet_config)
await connector.connect(emotion_input_mad)

mock_client_instance.LedControlNoReply.assert_called_with(255, 0, 0)

@pytest.mark.asyncio
async def test_connect_curious(self, ethernet_config, emotion_input_curious):
"""Test connect with curious emotion."""
mock_client_instance = Mock()
mock_audio_client.return_value = mock_client_instance

connector = EmotionUnitreeConnector(ethernet_config)
await connector.connect(emotion_input_curious)

mock_client_instance.LedControlNoReply.assert_called_with(0, 0, 255)

def test_tick(self, default_config):
"""Test tick method."""
connector = EmotionUnitreeConnector(default_config)

with patch.object(connector, "sleep") as mock_sleep:
connector.tick()
mock_sleep.assert_called_once_with(5)
57 changes: 57 additions & 0 deletions tests/actions/emotion/test_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Tests for the Emotion action interface."""

from actions.emotion.interface import Emotion, EmotionAction, EmotionInput


class TestEmotionAction:
"""Tests for the EmotionAction enum."""

def test_emotion_action_values(self):
"""Test that EmotionAction enum has correct values."""
assert EmotionAction.HAPPY.value == "happy"
assert EmotionAction.SAD.value == "sad"
assert EmotionAction.MAD.value == "mad"
assert EmotionAction.CURIOUS.value == "curious"

def test_emotion_action_is_string_enum(self):
"""Test that EmotionAction values are strings."""
for action in EmotionAction:
assert isinstance(action.value, str)

def test_emotion_action_count(self):
"""Test that EmotionAction has expected number of emotions."""
assert len(EmotionAction) == 4


class TestEmotionInput:
"""Tests for the EmotionInput dataclass."""

def test_emotion_input_creation(self):
"""Test creating EmotionInput with valid action."""
emotion_input = EmotionInput(action=EmotionAction.HAPPY)
assert emotion_input.action == EmotionAction.HAPPY

def test_emotion_input_all_actions(self):
"""Test creating EmotionInput with all possible actions."""
for action in EmotionAction:
emotion_input = EmotionInput(action=action)
assert emotion_input.action == action


class TestEmotion:
"""Tests for the Emotion interface."""

def test_emotion_creation(self):
"""Test creating Emotion with input and output."""
emotion_input = EmotionInput(action=EmotionAction.HAPPY)
emotion = Emotion(input=emotion_input, output=emotion_input)
assert emotion.input == emotion_input
assert emotion.output == emotion_input

def test_emotion_different_input_output(self):
"""Test creating Emotion with different input and output."""
input_emotion = EmotionInput(action=EmotionAction.HAPPY)
output_emotion = EmotionInput(action=EmotionAction.SAD)
emotion = Emotion(input=input_emotion, output=output_emotion)
assert emotion.input.action == EmotionAction.HAPPY
assert emotion.output.action == EmotionAction.SAD