This commit is contained in:
163
tests/test_launcher.py
Normal file
163
tests/test_launcher.py
Normal file
@@ -0,0 +1,163 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from couchd.launcher import (
|
||||
LaunchRequest,
|
||||
ProcessRegistry,
|
||||
RunningProcess,
|
||||
default_kill,
|
||||
default_terminate,
|
||||
)
|
||||
|
||||
|
||||
class DuplicateLaunchTests(unittest.TestCase):
|
||||
def test_refuses_second_launch_when_pid_is_still_alive(self) -> None:
|
||||
launches: list[LaunchRequest] = []
|
||||
|
||||
registry = ProcessRegistry(
|
||||
is_pid_running=lambda pid: pid == 4321,
|
||||
spawn=lambda request: launches.append(request) or 9876,
|
||||
process_probe=lambda: None,
|
||||
process_identity=lambda pid: "proc-4321" if pid == 4321 else None,
|
||||
)
|
||||
registry.record_running(pid=4321, state="melee")
|
||||
|
||||
result = registry.launch(LaunchRequest(kind="melee", argv=["/bin/true"]))
|
||||
|
||||
self.assertEqual(result["state"], "already_running")
|
||||
self.assertEqual(result["pid"], 4321)
|
||||
self.assertEqual(launches, [])
|
||||
|
||||
def test_stop_escalates_from_term_to_kill_after_timeout(self) -> None:
|
||||
signals: list[tuple[str, int]] = []
|
||||
checks = iter([True, True, True, False])
|
||||
|
||||
registry = ProcessRegistry(
|
||||
is_pid_running=lambda pid: next(checks),
|
||||
spawn=lambda request: 9876,
|
||||
terminate=lambda pid: signals.append(("TERM", pid)),
|
||||
kill=lambda pid: signals.append(("KILL", pid)),
|
||||
sleep=lambda seconds: None,
|
||||
process_probe=lambda: None,
|
||||
process_identity=lambda pid: "proc-4321" if pid == 4321 else None,
|
||||
)
|
||||
registry.record_running(pid=4321, state="melee")
|
||||
|
||||
result = registry.stop(timeout_seconds=0.0)
|
||||
|
||||
self.assertEqual(result, {"state": "stopped", "pid": None})
|
||||
self.assertEqual(signals, [("TERM", 4321), ("KILL", 4321)])
|
||||
|
||||
def test_adopts_running_process_from_probe_when_state_file_is_stale(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
state_file = Path(temp_dir) / "state.json"
|
||||
state_file.write_text(json.dumps({"pid": 9999, "state": "melee"}), encoding="utf-8")
|
||||
launches: list[LaunchRequest] = []
|
||||
|
||||
registry = ProcessRegistry(
|
||||
is_pid_running=lambda pid: False,
|
||||
spawn=lambda request: launches.append(request) or 7777,
|
||||
state_file=state_file,
|
||||
process_probe=lambda: RunningProcess(pid=4321, state="melee", identity="proc-a"),
|
||||
process_identity=lambda pid: "proc-a" if pid == 4321 else None,
|
||||
)
|
||||
|
||||
result = registry.launch(LaunchRequest(kind="melee", argv=["/bin/true"]))
|
||||
|
||||
self.assertEqual(result, {"state": "already_running", "pid": 4321})
|
||||
self.assertEqual(launches, [])
|
||||
|
||||
def test_ignores_corrupt_state_file_and_recovers(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
state_file = Path(temp_dir) / "state.json"
|
||||
state_file.write_text("{not-json", encoding="utf-8")
|
||||
|
||||
registry = ProcessRegistry(
|
||||
is_pid_running=lambda pid: False,
|
||||
spawn=lambda request: 9876,
|
||||
state_file=state_file,
|
||||
process_probe=lambda: None,
|
||||
process_identity=lambda pid: None,
|
||||
)
|
||||
|
||||
self.assertEqual(registry.current_state(), {"state": "idle", "pid": None})
|
||||
|
||||
def test_refuses_to_signal_reused_pid_with_different_identity(self) -> None:
|
||||
signals: list[tuple[str, int]] = []
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
state_file = Path(temp_dir) / "state.json"
|
||||
state_file.write_text(
|
||||
json.dumps({"pid": 4321, "state": "melee", "identity": "old-proc"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
registry = ProcessRegistry(
|
||||
is_pid_running=lambda pid: True,
|
||||
spawn=lambda request: 9876,
|
||||
terminate=lambda pid: signals.append(("TERM", pid)),
|
||||
kill=lambda pid: signals.append(("KILL", pid)),
|
||||
state_file=state_file,
|
||||
process_probe=lambda: None,
|
||||
process_identity=lambda pid: "new-proc",
|
||||
)
|
||||
|
||||
result = registry.stop(timeout_seconds=0.0)
|
||||
|
||||
self.assertEqual(result, {"state": "stopped", "pid": None})
|
||||
self.assertEqual(signals, [])
|
||||
|
||||
def test_refuses_to_signal_same_executable_pid_reuse_when_start_time_changes(self) -> None:
|
||||
signals: list[tuple[str, int]] = []
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
state_file = Path(temp_dir) / "state.json"
|
||||
state_file.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"pid": 4321,
|
||||
"state": "melee",
|
||||
"identity": "1000:/home/couch/Applications/Dolphin-x86_64.AppImage:111",
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
registry = ProcessRegistry(
|
||||
is_pid_running=lambda pid: True,
|
||||
spawn=lambda request: 9876,
|
||||
terminate=lambda pid: signals.append(("TERM", pid)),
|
||||
kill=lambda pid: signals.append(("KILL", pid)),
|
||||
state_file=state_file,
|
||||
process_probe=lambda: None,
|
||||
process_identity=lambda pid: "1000:/home/couch/Applications/Dolphin-x86_64.AppImage:222",
|
||||
)
|
||||
|
||||
result = registry.stop(timeout_seconds=0.0)
|
||||
|
||||
self.assertEqual(result, {"state": "stopped", "pid": None})
|
||||
self.assertEqual(signals, [])
|
||||
|
||||
|
||||
class DefaultSignalTests(unittest.TestCase):
|
||||
def test_terminate_signals_non_group_leader_process_directly(self) -> None:
|
||||
with mock.patch("couchd.launcher.os.getpgid", return_value=9999), mock.patch(
|
||||
"couchd.launcher.os.kill"
|
||||
) as kill, mock.patch("couchd.launcher.os.killpg") as killpg:
|
||||
default_terminate(4321)
|
||||
|
||||
kill.assert_called_once()
|
||||
self.assertEqual(kill.call_args.args[0], 4321)
|
||||
killpg.assert_not_called()
|
||||
|
||||
def test_kill_signals_group_leader_process_group(self) -> None:
|
||||
with mock.patch("couchd.launcher.os.getpgid", return_value=4321), mock.patch(
|
||||
"couchd.launcher.os.kill"
|
||||
) as kill, mock.patch("couchd.launcher.os.killpg") as killpg:
|
||||
default_kill(4321)
|
||||
|
||||
kill.assert_not_called()
|
||||
killpg.assert_called_once()
|
||||
Reference in New Issue
Block a user