f1commentator / reachy_f1_commentator /tests /test_commentary_system.py
d10g's picture
Initial Import
c95ad37
Raw
History Blame Contribute Delete
17 kB
"""Tests for CommentarySystem orchestrator.
This module tests the main system orchestrator including initialization,
startup, shutdown, and signal handling.
"""
import pytest
import time
import signal
import os
from unittest.mock import Mock, patch, MagicMock
from reachy_f1_commentator.src.commentary_system import CommentarySystem
from reachy_f1_commentator.src.config import Config
class TestCommentarySystemInitialization:
"""Test system initialization."""
def test_init_loads_config(self, tmp_path):
"""Test that __init__ loads configuration."""
# Create a temporary config file
config_file = tmp_path / "test_config.json"
config_file.write_text('{"log_level": "DEBUG"}')
system = CommentarySystem(config_path=str(config_file))
assert system.config is not None
assert system.config.log_level == "DEBUG"
assert not system._initialized
assert not system._running
def test_init_registers_signal_handlers(self):
"""Test that signal handlers are registered."""
with patch('signal.signal') as mock_signal:
system = CommentarySystem()
# Verify SIGTERM and SIGINT handlers were registered
assert mock_signal.call_count >= 2
calls = [call[0] for call in mock_signal.call_args_list]
assert (signal.SIGTERM,) in [call[:1] for call in calls]
assert (signal.SIGINT,) in [call[:1] for call in calls]
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_initialize_creates_all_components(
self, mock_resource_monitor, mock_qa, mock_data_ingestion,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that initialize() creates all system components."""
# Setup mocks
mock_motion_ctrl.return_value.reachy.is_connected.return_value = False
mock_resource_monitor.return_value.start.return_value = None
mock_commentary_gen.return_value.is_enhanced_mode.return_value = True
mock_commentary_gen.return_value.load_static_data.return_value = True
system = CommentarySystem()
system.config.replay_mode = True # Skip API verification
result = system.initialize()
assert result is True
assert system._initialized is True
assert system.race_state_tracker is not None
assert system.event_queue is not None
assert system.motion_controller is not None
assert system.speech_synthesizer is not None
assert system.commentary_generator is not None
assert system.data_ingestion is not None
assert system.qa_manager is not None
assert system.resource_monitor is not None
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_initialize_moves_head_to_neutral(
self, mock_resource_monitor, mock_qa, mock_data_ingestion,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that initialize() moves robot head to neutral position."""
# Setup mocks
mock_motion = Mock()
mock_motion_ctrl.return_value = mock_motion
mock_motion.reachy.is_connected.return_value = False
mock_resource_monitor.return_value.start.return_value = None
mock_commentary_gen.return_value.is_enhanced_mode.return_value = True
mock_commentary_gen.return_value.load_static_data.return_value = True
system = CommentarySystem()
system.config.replay_mode = True
system.config.enable_movements = True
with patch('time.sleep'): # Skip sleep
system.initialize()
# Verify return_to_neutral was called
mock_motion.return_to_neutral.assert_called_once()
def test_initialize_returns_false_on_error(self):
"""Test that initialize() returns False on error."""
with patch('src.commentary_system.RaceStateTracker', side_effect=Exception("Test error")):
system = CommentarySystem()
result = system.initialize()
assert result is False
assert system._initialized is False
class TestCommentarySystemStartStop:
"""Test system start and stop."""
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_start_requires_initialization(
self, mock_resource_monitor, mock_qa, mock_data_ingestion,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that start() requires system to be initialized."""
system = CommentarySystem()
result = system.start()
assert result is False
assert not system._running
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_start_starts_data_ingestion(
self, mock_resource_monitor, mock_qa, mock_data_ingestion_cls,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that start() starts data ingestion."""
# Setup mocks
mock_data_ingestion = Mock()
mock_data_ingestion.start.return_value = True
mock_data_ingestion_cls.return_value = mock_data_ingestion
mock_motion_ctrl.return_value.reachy.is_connected.return_value = False
mock_resource_monitor.return_value.start.return_value = None
mock_commentary_gen.return_value.is_enhanced_mode.return_value = True
mock_commentary_gen.return_value.load_static_data.return_value = True
system = CommentarySystem()
system.config.replay_mode = True
with patch('time.sleep'):
system.initialize()
result = system.start()
assert result is True
assert system._running is True
mock_data_ingestion.start.assert_called_once()
class TestCommentarySystemShutdown:
"""Test system shutdown."""
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_shutdown_waits_for_current_commentary(
self, mock_resource_monitor, mock_qa, mock_data_ingestion,
mock_commentary_gen, mock_speech_synth_cls, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that shutdown() waits for current commentary to complete."""
# Setup mocks
mock_speech_synth = Mock()
mock_speech_synth.is_speaking.side_effect = [True, True, False] # Speaking, then done
mock_speech_synth_cls.return_value = mock_speech_synth
mock_motion_ctrl.return_value.reachy.is_connected.return_value = False
mock_resource_monitor.return_value.start.return_value = None
mock_commentary_gen.return_value.is_enhanced_mode.return_value = True
mock_commentary_gen.return_value.load_static_data.return_value = True
system = CommentarySystem()
system.config.replay_mode = True
with patch('time.sleep'):
system.initialize()
system.shutdown()
# Verify is_speaking was called to check status
assert mock_speech_synth.is_speaking.call_count >= 1
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_shutdown_returns_head_to_neutral(
self, mock_resource_monitor, mock_qa, mock_data_ingestion,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl_cls,
mock_event_queue, mock_race_state
):
"""Test that shutdown() returns robot head to neutral position."""
# Setup mocks
mock_motion_ctrl = Mock()
mock_motion_ctrl_cls.return_value = mock_motion_ctrl
mock_motion_ctrl.reachy.is_connected.return_value = False
mock_resource_monitor.return_value.start.return_value = None
mock_commentary_gen.return_value.is_enhanced_mode.return_value = True
mock_commentary_gen.return_value.load_static_data.return_value = True
system = CommentarySystem()
system.config.replay_mode = True
system.config.enable_movements = True
with patch('time.sleep'):
system.initialize()
system.shutdown()
# Verify return_to_neutral was called during shutdown
assert mock_motion_ctrl.return_to_neutral.call_count >= 1
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_shutdown_closes_api_connections(
self, mock_resource_monitor, mock_qa, mock_data_ingestion_cls,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that shutdown() closes API connections."""
# Setup mocks
mock_data_ingestion = Mock()
mock_client = Mock()
mock_data_ingestion.client = mock_client
mock_data_ingestion_cls.return_value = mock_data_ingestion
mock_motion_ctrl.return_value.reachy.is_connected.return_value = False
mock_resource_monitor.return_value.start.return_value = None
mock_commentary_gen.return_value.is_enhanced_mode.return_value = True
mock_commentary_gen.return_value.load_static_data.return_value = True
system = CommentarySystem()
system.config.replay_mode = True
with patch('time.sleep'):
system.initialize()
system.shutdown()
# Verify client.close() was called
mock_client.close.assert_called_once()
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_signal_handler_triggers_shutdown(
self, mock_resource_monitor, mock_qa, mock_data_ingestion,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that signal handler triggers graceful shutdown."""
# Setup mocks
mock_motion_ctrl.return_value.reachy.is_connected.return_value = False
mock_resource_monitor.return_value.start.return_value = None
mock_commentary_gen.return_value.is_enhanced_mode.return_value = True
mock_commentary_gen.return_value.load_static_data.return_value = True
system = CommentarySystem()
system.config.replay_mode = True
with patch('time.sleep'):
system.initialize()
# Mock sys.exit to prevent actual exit
with patch('sys.exit'):
system._signal_handler(signal.SIGTERM, None)
# Verify shutdown was triggered
assert system._shutdown_requested is True
class TestCommentarySystemQuestionProcessing:
"""Test Q&A question processing."""
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_process_question_requires_running_system(
self, mock_resource_monitor, mock_qa_cls, mock_data_ingestion,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that process_question() requires system to be running."""
system = CommentarySystem()
# Should not process if not initialized
system.process_question("Who is leading?")
# No assertions needed - just verify it doesn't crash
@patch('src.commentary_system.RaceStateTracker')
@patch('src.commentary_system.PriorityEventQueue')
@patch('src.commentary_system.MotionController')
@patch('src.commentary_system.SpeechSynthesizer')
@patch('src.commentary_system.EnhancedCommentaryGenerator')
@patch('src.commentary_system.DataIngestionModule')
@patch('src.commentary_system.QAManager')
@patch('src.commentary_system.ResourceMonitor')
def test_process_question_resumes_queue_on_error(
self, mock_resource_monitor, mock_qa_cls, mock_data_ingestion,
mock_commentary_gen, mock_speech_synth, mock_motion_ctrl,
mock_event_queue, mock_race_state
):
"""Test that process_question() resumes queue even on error."""
# Setup mocks
mock_qa = Mock()
mock_qa.process_question.side_effect = Exception("Test error")
mock_qa_cls.return_value = mock_qa
mock_motion_ctrl.return_value.reachy.is_connected.return_value = False
mock_resource_monitor.return_value.start.return_value = None
mock_data_ingestion.return_value.start.return_value = True
mock_commentary_gen.return_value.is_enhanced_mode.return_value = True
mock_commentary_gen.return_value.load_static_data.return_value = True
system = CommentarySystem()
system.config.replay_mode = True
with patch('time.sleep'):
system.initialize()
system.start()
# Process question (should handle error gracefully)
system.process_question("Who is leading?")
# Verify resume was called even though error occurred
mock_qa.resume_event_queue.assert_called()
class TestCommentarySystemStatus:
"""Test system status methods."""
def test_is_running_returns_false_initially(self):
"""Test that is_running() returns False initially."""
system = CommentarySystem()
assert system.is_running() is False
def test_is_initialized_returns_false_initially(self):
"""Test that is_initialized() returns False initially."""
system = CommentarySystem()
assert system.is_initialized() is False