import pytest from unittest.mock import patch import common import wecker # We need to mock the PID file existence and os.kill to test the logic # because we are running in a test environment. @pytest.fixture def mock_pid_file(tmp_path): """Provides a temporary PID file path and ensures it's cleaned up.""" pid_file = tmp_path / "wecker.pid" with patch("wecker.PID_FILE", str(pid_file)): yield pid_file def test_ensure_single_instance_success(mock_pid_file): """Test that the script can start if no PID file exists.""" # Ensure file doesn't exist if mock_pid_file.exists(): mock_pid_file.unlink() with patch("os.path.exists", return_value=False), \ patch("os.getpid", return_value=1234): wecker.ensure_single_instance() # Verify it wrote the PID assert mock_pid_file.exists() 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 is running.""" # 1. Create the PID file with a dummy PID mock_pid_file.write_text("5678") # 2. Mock os.path.exists to find the file # 3. Mock os.kill to succeed (meaning process 5678 is alive) # Note: We DON'T mock builtins.open here, so it reads the real file we just wrote. with patch("os.path.exists", return_value=True), \ patch("os.kill") as mock_kill: mock_kill.return_value = None # Success means process is alive with pytest.raises(SystemExit) as excinfo: wecker.ensure_single_instance() assert excinfo.value.code == 1 mock_kill.assert_called_with(5678, 0) def test_ensure_single_instance_stale_pid(mock_pid_file): """Test that the script continues if the PID in the file is dead.""" mock_pid_file.write_text("5678") with patch("os.path.exists", return_value=True), \ patch("os.kill", side_effect=ProcessLookupError), \ patch("os.getpid", return_value=1234): wecker.ensure_single_instance() # Should have overwritten with new PID assert mock_pid_file.read_text() == "1234" def test_remove_pid_file_success(mock_pid_file): """Test that remove_pid_file removes the correct PID file.""" mock_pid_file.write_text("1234") with patch("os.path.exists", return_value=True), \ patch("os.getpid", return_value=1234): wecker.remove_pid_file() assert not mock_pid_file.exists() def test_remove_pid_file_wrong_pid(mock_pid_file): """Test that remove_pid_file does NOT remove if PID doesn't match.""" mock_pid_file.write_text("5678") with patch("os.path.exists", return_value=True), \ patch("os.getpid", return_value=1234): wecker.remove_pid_file() assert mock_pid_file.exists() def test_pid_file_defined_once_across_modules(): """ DRY principle: PID_FILE must be defined in common.py and imported by both wecker.py and api/schema.py — not redefined in each. """ # wecker.py imports PID_FILE from common — verify it's the same object assert wecker.PID_FILE is common.PID_FILE, ( "wecker.PID_FILE must reference common.PID_FILE, not redefine it" ) # Verify the path ends with "wecker.pid" assert common.PID_FILE.endswith("wecker.pid")