test: replace /bin/sleep with portable Python sleep in test_gateway_init
test / integration-firecracker (pull_request) Successful in 7s
test / integration-docker (pull_request) Successful in 9s
tracker-policy-pr / check-pr (pull_request) Successful in 7s
test / unit (pull_request) Successful in 33s
test / coverage (pull_request) Failing after 32s
lint / lint (push) Successful in 46s
test / integration-firecracker (pull_request) Successful in 7s
test / integration-docker (pull_request) Successful in 9s
tracker-policy-pr / check-pr (pull_request) Successful in 7s
test / unit (pull_request) Successful in 33s
test / coverage (pull_request) Failing after 32s
lint / lint (push) Successful in 46s
/bin/sleep does not exist at FHS paths on NixOS (the KVM self-hosted runner's OS). All 13 FileNotFoundError failures in TestSupervisor were caused by _DaemonSpec tuples spawning /bin/sleep directly. Replace with a module-level _SLEEP_30/_SLEEP_60 tuple using sys.executable so the supervisor tests run on any platform. Also fix TestMainEndToEnd._run() and remove the now-unnecessary setUpClass guard.
This commit is contained in:
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
Tests both the helper functions in `bot_bottle.gateway_init`
|
Tests both the helper functions in `bot_bottle.gateway_init`
|
||||||
and the supervisor's end-to-end signal / exit-code behavior. The
|
and the supervisor's end-to-end signal / exit-code behavior. The
|
||||||
end-to-end tests use real subprocesses (`/bin/sleep`,
|
end-to-end tests use real subprocesses — short-lived, no docker required
|
||||||
`/bin/sh -c '...'`) — short-lived, no docker required — so they
|
— so they run under `tests/unit/` rather than `tests/integration/`."""
|
||||||
run under `tests/unit/` rather than `tests/integration/`."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from bot_bottle.gateway_init import (
|
from bot_bottle.gateway_init import (
|
||||||
@@ -26,6 +24,11 @@ from bot_bottle.gateway_init import (
|
|||||||
_selected_daemons,
|
_selected_daemons,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# /bin/sleep does not exist on FHS-free systems (e.g. NixOS). Use a
|
||||||
|
# portable Python one-liner so supervisor tests run on any platform.
|
||||||
|
_SLEEP_30 = (sys.executable, "-c", "import time; time.sleep(30)")
|
||||||
|
_SLEEP_60 = (sys.executable, "-c", "import time; time.sleep(60)")
|
||||||
|
|
||||||
|
|
||||||
class TestEnvForDaemon(unittest.TestCase):
|
class TestEnvForDaemon(unittest.TestCase):
|
||||||
"""Scope egress-only credential env vars to the egress daemon.
|
"""Scope egress-only credential env vars to the egress daemon.
|
||||||
@@ -182,7 +185,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
# up and the supervisor never set shutdown_at.
|
# up and the supervisor never set shutdown_at.
|
||||||
specs = [
|
specs = [
|
||||||
_DaemonSpec("crasher", ("/bin/sh", "-c", "exit 1")),
|
_DaemonSpec("crasher", ("/bin/sh", "-c", "exit 1")),
|
||||||
_DaemonSpec("longrun", ("/bin/sleep", "30")),
|
_DaemonSpec("longrun", _SLEEP_30),
|
||||||
]
|
]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
@@ -214,7 +217,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
# signal-killed longrun's negative returncode.
|
# signal-killed longrun's negative returncode.
|
||||||
specs = [
|
specs = [
|
||||||
_DaemonSpec("crasher", ("/bin/sh", "-c", "exit 1")),
|
_DaemonSpec("crasher", ("/bin/sh", "-c", "exit 1")),
|
||||||
_DaemonSpec("longrun", ("/bin/sleep", "30")),
|
_DaemonSpec("longrun", _SLEEP_30),
|
||||||
]
|
]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
@@ -259,7 +262,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
specs = [
|
specs = [
|
||||||
_DaemonSpec("egress", sighup_marker),
|
_DaemonSpec("egress", sighup_marker),
|
||||||
_DaemonSpec("other", ("/bin/sleep", "30")),
|
_DaemonSpec("other", _SLEEP_30),
|
||||||
]
|
]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
@@ -282,7 +285,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
self._drive(sup)
|
self._drive(sup)
|
||||||
|
|
||||||
def test_forward_signal_unknown_daemon_no_op(self):
|
def test_forward_signal_unknown_daemon_no_op(self):
|
||||||
specs = [_DaemonSpec("a", ("/bin/sleep", "30"))]
|
specs = [_DaemonSpec("a", _SLEEP_30)]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
delivered = sup.forward_signal(signal.SIGHUP, "ghost")
|
delivered = sup.forward_signal(signal.SIGHUP, "ghost")
|
||||||
@@ -294,8 +297,8 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
# Restart one daemon; the other (supervise, the MCP server
|
# Restart one daemon; the other (supervise, the MCP server
|
||||||
# in production) must remain untouched.
|
# in production) must remain untouched.
|
||||||
specs = [
|
specs = [
|
||||||
_DaemonSpec("git-gate", ("/bin/sleep", "30")),
|
_DaemonSpec("git-gate", _SLEEP_30),
|
||||||
_DaemonSpec("supervise", ("/bin/sleep", "30")),
|
_DaemonSpec("supervise", _SLEEP_30),
|
||||||
]
|
]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
@@ -319,8 +322,8 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
|
|
||||||
def test_request_restart_is_drained_by_tick(self):
|
def test_request_restart_is_drained_by_tick(self):
|
||||||
specs = [
|
specs = [
|
||||||
_DaemonSpec("git-gate", ("/bin/sleep", "30")),
|
_DaemonSpec("git-gate", _SLEEP_30),
|
||||||
_DaemonSpec("supervise", ("/bin/sleep", "30")),
|
_DaemonSpec("supervise", _SLEEP_30),
|
||||||
]
|
]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
@@ -343,7 +346,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
self._drive(sup)
|
self._drive(sup)
|
||||||
|
|
||||||
def test_repeated_restart_requests_coalesce(self):
|
def test_repeated_restart_requests_coalesce(self):
|
||||||
specs = [_DaemonSpec("git-gate", ("/bin/sleep", "30"))]
|
specs = [_DaemonSpec("git-gate", _SLEEP_30)]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@@ -366,7 +369,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
self._drive(sup)
|
self._drive(sup)
|
||||||
|
|
||||||
def test_request_restart_unknown_daemon_no_op(self):
|
def test_request_restart_unknown_daemon_no_op(self):
|
||||||
specs = [_DaemonSpec("a", ("/bin/sleep", "30"))]
|
specs = [_DaemonSpec("a", _SLEEP_30)]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
ok = sup.request_restart("ghost")
|
ok = sup.request_restart("ghost")
|
||||||
@@ -376,7 +379,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
self._drive(sup)
|
self._drive(sup)
|
||||||
|
|
||||||
def test_restart_unknown_daemon_no_op(self):
|
def test_restart_unknown_daemon_no_op(self):
|
||||||
specs = [_DaemonSpec("a", ("/bin/sleep", "30"))]
|
specs = [_DaemonSpec("a", _SLEEP_30)]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
ok = sup.restart_daemon("ghost")
|
ok = sup.restart_daemon("ghost")
|
||||||
@@ -385,7 +388,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
self._drive(sup)
|
self._drive(sup)
|
||||||
|
|
||||||
def test_restart_during_shutdown_is_no_op(self):
|
def test_restart_during_shutdown_is_no_op(self):
|
||||||
specs = [_DaemonSpec("git-gate", ("/bin/sleep", "30"))]
|
specs = [_DaemonSpec("git-gate", _SLEEP_30)]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
sup.request_shutdown(reason="test")
|
sup.request_shutdown(reason="test")
|
||||||
@@ -395,7 +398,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
self._drive(sup)
|
self._drive(sup)
|
||||||
|
|
||||||
def test_pending_restart_dropped_during_shutdown(self):
|
def test_pending_restart_dropped_during_shutdown(self):
|
||||||
specs = [_DaemonSpec("git-gate", ("/bin/sleep", "30"))]
|
specs = [_DaemonSpec("git-gate", _SLEEP_30)]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@@ -413,8 +416,8 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
# both should receive SIGTERM and exit. Signal-only
|
# both should receive SIGTERM and exit. Signal-only
|
||||||
# shutdown clamps to a zero supervisor exit code.
|
# shutdown clamps to a zero supervisor exit code.
|
||||||
specs = [
|
specs = [
|
||||||
_DaemonSpec("a", ("/bin/sleep", "60")),
|
_DaemonSpec("a", _SLEEP_60),
|
||||||
_DaemonSpec("b", ("/bin/sleep", "60")),
|
_DaemonSpec("b", _SLEEP_60),
|
||||||
]
|
]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
@@ -449,7 +452,7 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
self.assertEqual(0, rc)
|
self.assertEqual(0, rc)
|
||||||
|
|
||||||
def test_idempotent_shutdown_requests(self):
|
def test_idempotent_shutdown_requests(self):
|
||||||
specs = [_DaemonSpec("a", ("/bin/sleep", "60"))]
|
specs = [_DaemonSpec("a", _SLEEP_60)]
|
||||||
sup = _Supervisor(specs)
|
sup = _Supervisor(specs)
|
||||||
sup.start_all()
|
sup.start_all()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@@ -465,28 +468,22 @@ class TestSupervisor(unittest.TestCase):
|
|||||||
|
|
||||||
class TestMainEndToEnd(unittest.TestCase):
|
class TestMainEndToEnd(unittest.TestCase):
|
||||||
"""Run gateway_init.py as a real subprocess to cover the
|
"""Run gateway_init.py as a real subprocess to cover the
|
||||||
signal-handler installation path. Skipped on platforms
|
signal-handler installation path."""
|
||||||
without /bin/sleep + /bin/sh."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
for p in ("/bin/sh", "/bin/sleep"):
|
|
||||||
if not Path(p).exists():
|
|
||||||
raise unittest.SkipTest(f"missing {p}")
|
|
||||||
|
|
||||||
def _run(self, daemons_csv: str, send_signal: int | None,
|
def _run(self, daemons_csv: str, send_signal: int | None,
|
||||||
wait_before_signal: float = 0.4,
|
wait_before_signal: float = 0.4,
|
||||||
overall_timeout: float = 6.0) -> tuple[int, str]:
|
overall_timeout: float = 6.0) -> tuple[int, str]:
|
||||||
"""Spawn gateway_init.main() in a child process with the
|
"""Spawn gateway_init.main() in a child process with the
|
||||||
DAEMONS list patched to harmless `sleep 30` commands.
|
DAEMONS list patched to harmless long-sleep commands.
|
||||||
Returns (returncode, captured stdout)."""
|
Returns (returncode, captured stdout)."""
|
||||||
|
|
||||||
helper = (
|
helper = (
|
||||||
"import os, runpy, sys\n"
|
"import os, runpy, sys\n"
|
||||||
"from bot_bottle import gateway_init as si\n"
|
"from bot_bottle import gateway_init as si\n"
|
||||||
|
"sleep_cmd = (sys.executable, '-c', 'import time; time.sleep(30)')\n"
|
||||||
"si._DAEMONS = (\n"
|
"si._DAEMONS = (\n"
|
||||||
" si._DaemonSpec('alpha', ('/bin/sleep','30')),\n"
|
" si._DaemonSpec('alpha', sleep_cmd),\n"
|
||||||
" si._DaemonSpec('beta', ('/bin/sleep','30')),\n"
|
" si._DaemonSpec('beta', sleep_cmd),\n"
|
||||||
")\n"
|
")\n"
|
||||||
"sys.exit(si.main([]))\n"
|
"sys.exit(si.main([]))\n"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user