import pytest import sys from unittest.mock import MagicMock, patch # Mock RPi.GPIO and pygame before importing wecker sys.modules["RPi"] = MagicMock() sys.modules["RPi.GPIO"] = MagicMock() sys.modules["pygame"] = MagicMock() import wecker # noqa: E402 @pytest.fixture def mock_gpio(): with patch.object(wecker, "GPIO") as mock: mock.LOW = 0 mock.HIGH = 1 yield mock @pytest.fixture def mock_pygame(): with patch.object(wecker, "pygame") as mock: yield mock def test_set_led(mock_gpio): wecker.set_led(True) mock_gpio.output.assert_called_with(wecker.LED_PIN, mock_gpio.LOW) wecker.set_led(False) mock_gpio.output.assert_called_with(wecker.LED_PIN, mock_gpio.HIGH) @patch("wecker.time.time") def test_run_alarm_start_to_wait(mock_time, mock_gpio, mock_pygame): # Mock pygame.mixer.get_init() to return True so music plays mock_pygame.mixer.get_init.return_value = True # Just run it in test mode, it should execute the loop once and exit wecker.run_alarm(test_mode=True) assert mock_pygame.mixer.music.play.called def test_state_machine_evaluation(mock_gpio, mock_pygame): clock = wecker.AlarmClock() # Transition to ringing -> wait before blink clock.update(100.0, True) # button press assert clock.state == wecker.STATE_WAIT_BEFORE_BLINK # Wait 3 seconds -> blinking with patch("wecker.random.randint", return_value=3): clock.update(103.1, False) assert clock.state == wecker.STATE_BLINKING assert clock.target_blinks == 3 # Advance time past the blink sequence (3 blinks * 2 phases * 0.3s = 1.8s) clock.update(105.0, False) assert clock.state == wecker.STATE_WAIT_FOR_INPUT # Press 1 clock.update(105.5, True) clock.update(105.6, False) # Press 2 clock.update(106.0, True) clock.update(106.1, False) # Press 3 clock.update(106.5, True) clock.update(106.6, False) assert clock.user_presses == 3 # Wait 3 seconds to evaluate clock.update(109.7, False) # Triggers state change keep_running = clock.update(109.8, False) # Triggers evaluation # It should evaluate, see it's correct, and return False (stop running) assert clock.state == wecker.STATE_EVALUATING assert not keep_running def test_state_machine_incorrect(mock_gpio, mock_pygame): clock = wecker.AlarmClock() clock.state = wecker.STATE_WAIT_FOR_INPUT clock.target_blinks = 3 # Only press once clock.update(100.0, True) clock.update(100.1, False) # Wait to evaluate clock.last_interaction_time = 100.1 clock.update(103.2, False) # triggers eval state keep_running = clock.update(103.3, False) # evals to incorrect # Evaluated incorrectly, should wait before retry assert keep_running assert clock.state == wecker.STATE_WAIT_BEFORE_RETRY def test_blinking_is_non_blocking(mock_gpio, mock_pygame): clock = wecker.AlarmClock() clock.state = wecker.STATE_BLINKING clock.target_blinks = 2 # Start of blinking: LED on clock.update(100.0, False) assert clock.state == wecker.STATE_BLINKING mock_gpio.output.assert_called_with(wecker.LED_PIN, mock_gpio.LOW) # Mid-blink: LED toggles based on elapsed time clock.update(100.3, False) mock_gpio.output.assert_called_with(wecker.LED_PIN, mock_gpio.HIGH) # After sequence completes (2 blinks * 2 phases * 0.3s = 1.2s) clock.update(101.2, False) assert clock.state == wecker.STATE_WAIT_FOR_INPUT mock_gpio.output.assert_called_with(wecker.LED_PIN, mock_gpio.HIGH) def test_blinking_updates_time_correctly(mock_gpio, mock_pygame): clock = wecker.AlarmClock() clock.state = wecker.STATE_BLINKING clock.target_blinks = 4 # Start blinking, then advance to the end of the sequence. # 4 blinks * 2 phases * 0.3s = 2.4s clock.update(100.0, False) clock.update(102.4, False) assert clock.state == wecker.STATE_WAIT_FOR_INPUT assert clock.last_interaction_time == 102.4 def test_setup_uses_env_music_file(mock_gpio, mock_pygame, monkeypatch): monkeypatch.setenv("MUSIC_FILE", "/custom/track.mp3") wecker.setup() mock_pygame.mixer.music.load.assert_called_with("/custom/track.mp3") def test_setup_music_file_argument_overrides_env(mock_gpio, mock_pygame, monkeypatch): monkeypatch.setenv("MUSIC_FILE", "/env/track.mp3") wecker.setup(music_file="/arg/track.mp3") mock_pygame.mixer.music.load.assert_called_with("/arg/track.mp3")