| import signal |
| from types import SimpleNamespace |
|
|
| import pytest |
|
|
| import TerraFin.interface.server as server_module |
|
|
|
|
| def test_stop_server_waits_for_process_exit_and_port_release(monkeypatch) -> None: |
| kill_calls: list[tuple[int, int]] = [] |
| wait_process_calls: list[tuple[int, float, float]] = [] |
| wait_port_calls: list[tuple[str, int, float, float]] = [] |
| removed: list[str] = [] |
|
|
| monkeypatch.setattr(server_module, "_read_pid", lambda: 1234) |
| monkeypatch.setattr(server_module, "_is_process_alive", lambda pid: pid == 1234) |
| monkeypatch.setattr( |
| server_module, |
| "get_runtime_config", |
| lambda: SimpleNamespace(host="127.0.0.1", port=8001), |
| ) |
| monkeypatch.setattr(server_module, "_find_listener_pid", lambda port: 1234) |
| monkeypatch.setattr(server_module.os, "kill", lambda pid, sig: kill_calls.append((pid, sig))) |
| monkeypatch.setattr( |
| server_module, |
| "_wait_for_process_exit", |
| lambda pid, timeout_s=5.0, poll_interval_s=0.1: ( |
| wait_process_calls.append((pid, timeout_s, poll_interval_s)) or True |
| ), |
| ) |
| monkeypatch.setattr( |
| server_module, |
| "_wait_for_port_release", |
| lambda host, port, timeout_s=5.0, poll_interval_s=0.1: ( |
| wait_port_calls.append((host, port, timeout_s, poll_interval_s)) or True |
| ), |
| ) |
| monkeypatch.setattr(server_module, "_remove_pid_file", lambda: removed.append("removed")) |
|
|
| assert server_module.stop_server() is True |
| assert kill_calls == [(1234, signal.SIGTERM)] |
| assert wait_process_calls == [(1234, 5.0, 0.1)] |
| assert wait_port_calls == [("127.0.0.1", 8001, 5.0, 0.1)] |
| assert removed == ["removed"] |
|
|
|
|
| def test_stop_server_escalates_to_sigkill_when_process_does_not_exit(monkeypatch) -> None: |
| kill_calls: list[tuple[int, int]] = [] |
| wait_process_calls: list[tuple[int, float, float]] = [] |
|
|
| wait_results = iter([False, True]) |
|
|
| monkeypatch.setattr(server_module, "_read_pid", lambda: 5678) |
| monkeypatch.setattr(server_module, "_is_process_alive", lambda pid: pid == 5678) |
| monkeypatch.setattr( |
| server_module, |
| "get_runtime_config", |
| lambda: SimpleNamespace(host="127.0.0.1", port=8001), |
| ) |
| monkeypatch.setattr(server_module, "_find_listener_pid", lambda port: 5678) |
| monkeypatch.setattr(server_module.os, "kill", lambda pid, sig: kill_calls.append((pid, sig))) |
| monkeypatch.setattr( |
| server_module, |
| "_wait_for_process_exit", |
| lambda pid, timeout_s=5.0, poll_interval_s=0.1: ( |
| wait_process_calls.append((pid, timeout_s, poll_interval_s)) or next(wait_results) |
| ), |
| ) |
| monkeypatch.setattr( |
| server_module, "_wait_for_port_release", lambda host, port, timeout_s=5.0, poll_interval_s=0.1: True |
| ) |
| monkeypatch.setattr(server_module, "_remove_pid_file", lambda: None) |
|
|
| assert server_module.stop_server() is True |
| assert kill_calls == [(5678, signal.SIGTERM), (5678, signal.SIGKILL)] |
| assert wait_process_calls == [(5678, 5.0, 0.1), (5678, 2.0, 0.1)] |
|
|
|
|
| def test_server_status_recovers_listener_when_pid_file_is_missing(monkeypatch) -> None: |
| monkeypatch.setattr(server_module, "_read_pid", lambda: None) |
| monkeypatch.setattr( |
| server_module, |
| "get_runtime_config", |
| lambda: SimpleNamespace(host="127.0.0.1", port=8001), |
| ) |
| monkeypatch.setattr(server_module, "_find_listener_pid", lambda port: 2468) |
|
|
| assert server_module.server_status() == (True, 2468) |
|
|
|
|
| def test_stop_server_uses_listener_pid_when_pid_file_is_missing(monkeypatch) -> None: |
| kill_calls: list[tuple[int, int]] = [] |
| removed: list[str] = [] |
|
|
| monkeypatch.setattr(server_module, "_read_pid", lambda: None) |
| monkeypatch.setattr( |
| server_module, |
| "get_runtime_config", |
| lambda: SimpleNamespace(host="127.0.0.1", port=8001), |
| ) |
| monkeypatch.setattr(server_module, "_find_listener_pid", lambda port: 2468) |
| monkeypatch.setattr(server_module, "_is_process_alive", lambda pid: pid == 2468) |
| monkeypatch.setattr(server_module.os, "kill", lambda pid, sig: kill_calls.append((pid, sig))) |
| monkeypatch.setattr( |
| server_module, |
| "_wait_for_process_exit", |
| lambda pid, timeout_s=5.0, poll_interval_s=0.1: True, |
| ) |
| monkeypatch.setattr( |
| server_module, |
| "_wait_for_port_release", |
| lambda host, port, timeout_s=5.0, poll_interval_s=0.1: True, |
| ) |
| monkeypatch.setattr(server_module, "_remove_pid_file", lambda: removed.append("removed")) |
|
|
| assert server_module.stop_server() is True |
| assert kill_calls == [(2468, signal.SIGTERM)] |
| assert removed == ["removed"] |
|
|
|
|
| def test_start_server_raises_when_process_never_binds_port(monkeypatch, tmp_path) -> None: |
| class _FakeProc: |
| pid = 4321 |
|
|
| @staticmethod |
| def poll(): |
| return None |
|
|
| writes: list[int] = [] |
| removed: list[str] = [] |
|
|
| monkeypatch.setattr( |
| server_module, |
| "get_runtime_config", |
| lambda: SimpleNamespace(host="127.0.0.1", port=8001), |
| ) |
| monkeypatch.setattr(server_module, "_resolve_server_pid", lambda runtime_config=None: None) |
| monkeypatch.setattr(server_module, "_port_has_listener", lambda host, port: False) |
| monkeypatch.setattr(server_module.subprocess, "Popen", lambda *args, **kwargs: _FakeProc()) |
| monkeypatch.setattr( |
| server_module, |
| "_wait_for_server_startup", |
| lambda proc, host, port, timeout_s=5.0, poll_interval_s=0.1: False, |
| ) |
| monkeypatch.setattr(server_module, "_write_pid", lambda pid: writes.append(pid)) |
| monkeypatch.setattr(server_module, "_remove_pid_file", lambda: removed.append("removed")) |
| monkeypatch.setattr(server_module, "SERVER_LOG_FILE", tmp_path / "interface_server.log") |
|
|
| with pytest.raises(RuntimeError, match="Check interface_server.log"): |
| server_module.start_server() |
|
|
| assert writes == [4321] |
| assert removed == ["removed"] |
|
|