77 lines
3.4 KiB
Python
77 lines
3.4 KiB
Python
"""Exercise the real start preflight with disposable TCP sockets, without spawning services."""
|
|
import errno
|
|
from pathlib import Path
|
|
import socket
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from framebridge_expanded import runtime
|
|
|
|
|
|
class SpawnBoundaryReached(Exception):
|
|
pass
|
|
|
|
|
|
@unittest.skipUnless(sys.platform.startswith("linux"), "Linux TCP restart regression")
|
|
class RuntimePortPreflightTests(unittest.TestCase):
|
|
def start_until_spawn(self, state, ports):
|
|
settings = {"role": "central", "ports": ports}
|
|
with patch.object(runtime, "state_path", return_value=state), \
|
|
patch.object(runtime, "load_config", return_value=settings), \
|
|
patch.object(runtime, "status", return_value={"running": False}), \
|
|
patch.object(runtime, "doctor", return_value={"ok": True}), \
|
|
patch.object(runtime, "environment", return_value={}), \
|
|
patch.object(runtime, "_spawn", side_effect=SpawnBoundaryReached) as spawn:
|
|
with self.assertRaises(SpawnBoundaryReached):
|
|
runtime.start(state)
|
|
spawn.assert_called_once()
|
|
|
|
def close_server_first(self):
|
|
with socket.socket() as listener, socket.socket() as client:
|
|
listener.settimeout(2)
|
|
client.settimeout(2)
|
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
listener.bind(("127.0.0.1", 0))
|
|
port = listener.getsockname()[1]
|
|
listener.listen(1)
|
|
client.connect(("127.0.0.1", port))
|
|
with listener.accept()[0] as accepted:
|
|
accepted.settimeout(2)
|
|
accepted.shutdown(socket.SHUT_WR)
|
|
self.assertEqual(client.recv(1), b"")
|
|
client.close()
|
|
self.assertEqual(accepted.recv(1), b"")
|
|
return port
|
|
|
|
def test_closed_connections_do_not_block_central_restart(self):
|
|
ports = {name: self.close_server_first() for name in ("mcp", "hub", "broker")}
|
|
self.assertEqual(len(set(ports.values())), 3)
|
|
# Establish that each port has the regression condition, not an active listener.
|
|
for port in ports.values():
|
|
with socket.socket() as original_probe:
|
|
with self.assertRaises(OSError) as blocked:
|
|
original_probe.bind(("127.0.0.1", port))
|
|
self.assertEqual(blocked.exception.errno, errno.EADDRINUSE)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
self.start_until_spawn(Path(directory), ports)
|
|
|
|
def test_active_loopback_and_wildcard_listeners_still_block_start(self):
|
|
for address in ("127.0.0.1", "0.0.0.0"):
|
|
with self.subTest(address=address), socket.socket() as listener, \
|
|
tempfile.TemporaryDirectory() as directory:
|
|
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
listener.bind((address, 0))
|
|
listener.listen(1)
|
|
port = listener.getsockname()[1]
|
|
# The first preflight port is occupied, so no process may be spawned.
|
|
ports = {name: port for name in ("mcp", "hub", "broker")}
|
|
with self.assertRaises(OSError) as blocked:
|
|
self.start_until_spawn(Path(directory), ports)
|
|
self.assertEqual(blocked.exception.errno, errno.EADDRINUSE)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|