#!/usr/bin/env python3 """Regression tests for bounded lease-broker read/handle/send deadlines.""" from __future__ import annotations import importlib.util import json import socket import threading import time import unittest from pathlib import Path DAEMON_PATH = Path(__file__).parents[2] / "framework/tools/lease-broker/daemon.py" SPEC = importlib.util.spec_from_file_location("lease_broker_deadline_daemon", DAEMON_PATH) if SPEC is None or SPEC.loader is None: raise RuntimeError("unable to load lease broker daemon") DAEMON = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(DAEMON) def original_connection_budget() -> float: read_budget = getattr(DAEMON, "READ_DEADLINE_SECONDS", None) if isinstance(read_budget, (int, float)): return float(read_budget) return float(DAEMON.CONNECTION_DEADLINE_SECONDS) class SlowBroker: def __init__(self, delay: float) -> None: self.delay = delay self.calls = 0 def handle(self, _peer: tuple[int, int, int], request: dict[str, object]) -> dict[str, object]: self.calls += 1 time.sleep(self.delay) return {"ok": True, "echo": request.get("action")} class BrokerDeadlineTest(unittest.TestCase): def test_lock_queue_timeout_returns_explicit_fail_closed_reply_without_handling(self) -> None: broker = SlowBroker(0) broker_lock = threading.Lock() broker_lock.acquire() server, client = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM) client.settimeout(DAEMON.HANDLE_QUEUE_TIMEOUT_SECONDS + 2.0) worker = threading.Thread( target=DAEMON.handle_connection, args=(server, broker, broker_lock), daemon=True, ) worker.start() client.sendall(b'{"action":"probe"}\n') client.shutdown(socket.SHUT_WR) reply = bytearray() try: while True: chunk = client.recv(4096) if not chunk: break reply.extend(chunk) finally: broker_lock.release() worker.join(timeout=2.0) client.close() self.assertFalse(worker.is_alive()) self.assertEqual(broker.calls, 0) self.assertEqual(json.loads(reply), {"ok": False, "code": "BROKER_BUSY"}) def test_completed_slow_handle_gets_a_complete_framed_reply(self) -> None: broker = SlowBroker(original_connection_budget() + 0.1) broker_lock = threading.Lock() server, client = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM) client.settimeout(original_connection_budget() + 2.0) worker = threading.Thread( target=DAEMON.handle_connection, args=(server, broker, broker_lock), daemon=True, ) worker.start() client.sendall(b'{"action":"probe"}\n') client.shutdown(socket.SHUT_WR) reply = bytearray() while True: chunk = client.recv(4096) if not chunk: break reply.extend(chunk) worker.join(timeout=2.0) client.close() self.assertFalse(worker.is_alive()) self.assertEqual(broker.calls, 1) self.assertTrue(reply.endswith(b"\n"), f"unframed reply: {bytes(reply)!r}") self.assertEqual(json.loads(reply), {"ok": True, "echo": "probe"}) if __name__ == "__main__": unittest.main()