2026-05-19 14:45:26 +02:00
|
|
|
import pytest
|
2026-06-18 15:27:21 +02:00
|
|
|
import sys
|
|
|
|
|
from unittest.mock import MagicMock, patch
|
2026-05-19 15:15:05 +02:00
|
|
|
|
2026-06-18 15:27:21 +02:00
|
|
|
# Mock hardware modules before importing wecker (tests run off the Pi).
|
|
|
|
|
sys.modules["RPi"] = MagicMock()
|
|
|
|
|
sys.modules["RPi.GPIO"] = MagicMock()
|
|
|
|
|
sys.modules["pygame"] = MagicMock()
|
|
|
|
|
|
|
|
|
|
import common # noqa: E402
|
|
|
|
|
import wecker # noqa: E402
|
2026-05-19 14:45:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def mock_pid_file(tmp_path):
|
2026-06-18 15:27:21 +02:00
|
|
|
"""Provides a temporary PID file path and resets the lock fd."""
|
2026-05-19 14:45:26 +02:00
|
|
|
pid_file = tmp_path / "wecker.pid"
|
|
|
|
|
with patch("wecker.PID_FILE", str(pid_file)):
|
2026-06-18 15:27:21 +02:00
|
|
|
wecker._pid_lock_fd = None
|
2026-05-19 14:45:26 +02:00
|
|
|
yield pid_file
|
2026-06-18 15:27:21 +02:00
|
|
|
if wecker._pid_lock_fd is not None:
|
|
|
|
|
try:
|
|
|
|
|
wecker._pid_lock_fd.close()
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
wecker._pid_lock_fd = None
|
|
|
|
|
|
2026-05-19 14:45:26 +02:00
|
|
|
|
|
|
|
|
def test_ensure_single_instance_success(mock_pid_file):
|
2026-06-18 15:27:21 +02:00
|
|
|
"""Test that the script can start if no other instance holds the lock."""
|
|
|
|
|
with patch("os.getpid", return_value=1234):
|
2026-05-19 14:45:26 +02:00
|
|
|
wecker.ensure_single_instance()
|
2026-06-18 15:27:21 +02:00
|
|
|
|
|
|
|
|
assert mock_pid_file.read_text() == "1234"
|
|
|
|
|
|
2026-05-19 14:45:26 +02:00
|
|
|
|
|
|
|
|
def test_ensure_single_instance_already_running(mock_pid_file):
|
2026-06-18 15:27:21 +02:00
|
|
|
"""Test that the script exits if another instance holds the lock."""
|
|
|
|
|
with patch("fcntl.flock", side_effect=BlockingIOError), pytest.raises(
|
|
|
|
|
SystemExit
|
|
|
|
|
) as excinfo:
|
2026-05-19 14:45:26 +02:00
|
|
|
wecker.ensure_single_instance()
|
2026-06-18 15:27:21 +02:00
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
2026-05-19 14:45:26 +02:00
|
|
|
|
|
|
|
|
def test_remove_pid_file_success(mock_pid_file):
|
2026-06-18 15:27:21 +02:00
|
|
|
"""Test that remove_pid_file closes the lock and removes the file."""
|
|
|
|
|
with patch("os.getpid", return_value=1234):
|
|
|
|
|
wecker.ensure_single_instance()
|
2026-05-19 14:45:26 +02:00
|
|
|
|
2026-06-18 15:27:21 +02:00
|
|
|
wecker.remove_pid_file()
|
|
|
|
|
assert not mock_pid_file.exists()
|
2026-05-19 15:15:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_pid_file_defined_once_across_modules():
|
2026-06-18 15:27:21 +02:00
|
|
|
"""DRY principle: PID_FILE must be defined in common.py and imported."""
|
2026-05-19 15:15:05 +02:00
|
|
|
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")
|