Files
2026-07-13 13:12:33 +08:00

143 lines
4.0 KiB
Python

"""Tests for GatewayPidLock PID file placement (AC-C1)."""
from __future__ import annotations
import json
import os
import sys
import types
from pathlib import Path
import pytest
from opensquilla.gateway import pidlock
from opensquilla.gateway.pidlock import GatewayPidLock
def test_pid_file_in_state_dir_not_parent(tmp_path: Path) -> None:
"""AC-C1-1/AC-C1-2: PID file must land in state_dir, not state_dir.parent."""
state_dir = tmp_path / "state"
lock = GatewayPidLock(state_dir)
lock.acquire()
try:
# PID file must be inside state_dir
assert (state_dir / "gateway.pid").exists(), (
f"gateway.pid not found in {state_dir}"
)
# PID file must NOT be in the parent directory
assert not (tmp_path / "gateway.pid").exists(), (
f"gateway.pid incorrectly written to parent {tmp_path}"
)
finally:
lock.release()
def test_pid_lock_rejects_second_acquisition_while_first_is_held(tmp_path: Path) -> None:
state_dir = tmp_path / "state"
first = GatewayPidLock(state_dir)
first.acquire()
try:
second = GatewayPidLock(state_dir)
with pytest.raises(SystemExit) as exc_info:
second.acquire()
assert exc_info.value.code == 1
finally:
first.release()
def test_pid_lock_overwrites_live_stale_pid_when_lock_is_free(tmp_path: Path) -> None:
state_dir = tmp_path / "state"
state_dir.mkdir()
pid_path = state_dir / "gateway.pid"
pid_path.write_text(
json.dumps({"pid": os.getpid(), "start_ts": "2000-01-01T00:00:00+00:00"}),
encoding="utf-8",
)
lock = GatewayPidLock(state_dir)
lock.acquire()
try:
payload = json.loads(pid_path.read_text(encoding="utf-8"))
assert payload["pid"] == os.getpid()
assert payload["start_ts"] != "2000-01-01T00:00:00+00:00"
finally:
lock.release()
def test_pid_lock_release_is_idempotent(tmp_path: Path) -> None:
state_dir = tmp_path / "state"
lock = GatewayPidLock(state_dir)
lock.acquire()
lock.release()
lock.release()
assert not (state_dir / "gateway.pid").exists()
assert (state_dir / "gateway.pid.lock").exists()
def test_pid_lock_release_keeps_lock_file_identity_for_successors(
tmp_path: Path,
) -> None:
state_dir = tmp_path / "state"
first = GatewayPidLock(state_dir)
first.acquire()
first.release()
second = GatewayPidLock(state_dir)
second.acquire()
try:
assert (state_dir / "gateway.pid.lock").exists()
third = GatewayPidLock(state_dir)
with pytest.raises(SystemExit) as exc_info:
third.acquire()
assert exc_info.value.code == 1
finally:
second.release()
def test_windows_is_alive_rejects_opened_process_with_non_active_exit_code(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class Dword:
def __init__(self) -> None:
self.value = 0
class Kernel32:
def __init__(self) -> None:
self.closed_handles: list[int] = []
def open_process(self, access: int, inherit_handle: bool, process_id: int) -> int:
return 123
def get_exit_code_process(self, handle: int, exit_code: Dword) -> int:
exit_code.value = 0
return 1
def close_handle(self, handle: int) -> int:
self.closed_handles.append(handle)
return 1
def byref(value: Dword) -> Dword:
return value
kernel32 = Kernel32()
fake_ctypes = types.ModuleType("ctypes")
fake_ctypes.windll = types.SimpleNamespace(
kernel32=types.SimpleNamespace(
OpenProcess=kernel32.open_process,
GetExitCodeProcess=kernel32.get_exit_code_process,
CloseHandle=kernel32.close_handle,
)
)
fake_ctypes.wintypes = types.SimpleNamespace(DWORD=Dword)
fake_ctypes.byref = byref
monkeypatch.setattr(pidlock.os, "name", "nt")
monkeypatch.setitem(sys.modules, "ctypes", fake_ctypes)
assert pidlock._is_alive(456) is False
assert kernel32.closed_handles == [123]