import pytest import sys from unittest.mock import MagicMock, patch # 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 @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")