Files
wecker/tests/test_single_instance.py
gurix 3a7b6ac7a3 refactor: styles own their ringing tone directly (Pattern C)
Drop the injected AlarmSound Protocol and the wecker.Sound class. Each style
now owns its tone by using pygame directly; the runner owns only the audio
engine lifecycle (mixer init/quit), button sampling, and cleanup. This is the
seam for future styles that handle their own tone — a 'talk' style would just
import pygame and play speech, with no shared interface to extend.

- styles/base.py: AlarmStyle.__init__(set_led, music_file) + start() + update()
- styles/blink.py: start() loads+plays music via pygame.mixer.music
- styles/simple.py: owns its beep — synthesises a square-wave buffer in
  module (stdlib array+math) and plays it via pygame.mixer.Sound
- wecker.py: setup() brings up the mixer only; run_alarm constructs the style
  and guards start() with a clean log+exit on failure

Tests: a shared tests/conftest.py stubs RPi.GPIO/pygame in sys.modules before
any SUT import (order-independent, removes duplicated inline mocking); the
wecker mock_pygame fixture patches one fresh pygame mock into wecker + both
style modules so assertions see the same calls.

README: tone-ownership and plugin-contract updated.
2026-08-02 21:50:00 +02:00

67 lines
2.1 KiB
Python

import pytest
from unittest.mock import patch
# Hardware/audio mocks live in tests/conftest.py (shared across all test files).
import common
import wecker
@pytest.fixture
def mock_pid_file(tmp_path):
"""Provides a temporary PID file path and resets the lock fd."""
pid_file = tmp_path / "wecker.pid"
with patch("wecker.PID_FILE", str(pid_file)):
wecker._pid_lock_fd = None
yield pid_file
if wecker._pid_lock_fd is not None:
try:
wecker._pid_lock_fd.close()
except OSError:
pass
wecker._pid_lock_fd = None
def test_ensure_single_instance_success(mock_pid_file):
"""Test that the script can start if no other instance holds the lock."""
with patch("os.getpid", return_value=1234):
wecker.ensure_single_instance()
assert mock_pid_file.read_text() == "1234"
def test_ensure_single_instance_already_running(mock_pid_file):
"""Test that the script exits if another instance holds the lock."""
with patch("fcntl.flock", side_effect=BlockingIOError), pytest.raises(
SystemExit
) as excinfo:
wecker.ensure_single_instance()
assert excinfo.value.code == 1
def test_ensure_single_instance_overwrites_stale_pid(mock_pid_file):
"""Test that a stale PID file is overwritten once the lock is acquired."""
mock_pid_file.write_text("5678")
with patch("os.getpid", return_value=1234):
wecker.ensure_single_instance()
assert mock_pid_file.read_text() == "1234"
def test_remove_pid_file_success(mock_pid_file):
"""Test that remove_pid_file closes the lock and removes the file."""
with patch("os.getpid", return_value=1234):
wecker.ensure_single_instance()
wecker.remove_pid_file()
assert not mock_pid_file.exists()
def test_pid_file_defined_once_across_modules():
"""DRY principle: PID_FILE must be defined in common.py and imported."""
assert wecker.PID_FILE is common.PID_FILE, (
"wecker.PID_FILE must reference common.PID_FILE, not redefine it"
)
assert common.PID_FILE.endswith("wecker.pid")