test: replace /bin/sleep with portable Python sleep in test_gateway_init
test / integration-firecracker (pull_request) Successful in 17s
test / integration-docker (pull_request) Successful in 18s
test / coverage (pull_request) Failing after 33s
lint / lint (push) Successful in 54s
test / unit (pull_request) Successful in 1m34s
tracker-policy-pr / check-pr (pull_request) Failing after 11m24s

/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:
2026-07-19 02:07:08 +00:00
committed by didericis
parent 2738b0fe6c
commit 9e75fce7cf
+28 -31
View File
@@ -2,9 +2,8 @@
Tests both the helper functions in `bot_bottle.gateway_init`
and the supervisor's end-to-end signal / exit-code behavior. The
end-to-end tests use real subprocesses (`/bin/sleep`,
`/bin/sh -c '...'`) — short-lived, no docker required — so they
run under `tests/unit/` rather than `tests/integration/`."""
end-to-end tests use real subprocesses — short-lived, no docker required
— so they run under `tests/unit/` rather than `tests/integration/`."""
from __future__ import annotations
@@ -15,7 +14,6 @@ import sys
import time
import unittest
import warnings
from pathlib import Path
from unittest.mock import patch
from bot_bottle.gateway_init import (
@@ -26,6 +24,11 @@ from bot_bottle.gateway_init import (
_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):
"""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.
specs = [
_DaemonSpec("crasher", ("/bin/sh", "-c", "exit 1")),
_DaemonSpec("longrun", ("/bin/sleep", "30")),
_DaemonSpec("longrun", _SLEEP_30),
]
sup = _Supervisor(specs)
sup.start_all()
@@ -214,7 +217,7 @@ class TestSupervisor(unittest.TestCase):
# signal-killed longrun's negative returncode.
specs = [
_DaemonSpec("crasher", ("/bin/sh", "-c", "exit 1")),
_DaemonSpec("longrun", ("/bin/sleep", "30")),
_DaemonSpec("longrun", _SLEEP_30),
]
sup = _Supervisor(specs)
sup.start_all()
@@ -259,7 +262,7 @@ class TestSupervisor(unittest.TestCase):
)
specs = [
_DaemonSpec("egress", sighup_marker),
_DaemonSpec("other", ("/bin/sleep", "30")),
_DaemonSpec("other", _SLEEP_30),
]
sup = _Supervisor(specs)
sup.start_all()
@@ -282,7 +285,7 @@ class TestSupervisor(unittest.TestCase):
self._drive(sup)
def test_forward_signal_unknown_daemon_no_op(self):
specs = [_DaemonSpec("a", ("/bin/sleep", "30"))]
specs = [_DaemonSpec("a", _SLEEP_30)]
sup = _Supervisor(specs)
sup.start_all()
delivered = sup.forward_signal(signal.SIGHUP, "ghost")
@@ -294,8 +297,8 @@ class TestSupervisor(unittest.TestCase):
# Restart one daemon; the other (supervise, the MCP server
# in production) must remain untouched.
specs = [
_DaemonSpec("git-gate", ("/bin/sleep", "30")),
_DaemonSpec("supervise", ("/bin/sleep", "30")),
_DaemonSpec("git-gate", _SLEEP_30),
_DaemonSpec("supervise", _SLEEP_30),
]
sup = _Supervisor(specs)
sup.start_all()
@@ -319,8 +322,8 @@ class TestSupervisor(unittest.TestCase):
def test_request_restart_is_drained_by_tick(self):
specs = [
_DaemonSpec("git-gate", ("/bin/sleep", "30")),
_DaemonSpec("supervise", ("/bin/sleep", "30")),
_DaemonSpec("git-gate", _SLEEP_30),
_DaemonSpec("supervise", _SLEEP_30),
]
sup = _Supervisor(specs)
sup.start_all()
@@ -343,7 +346,7 @@ class TestSupervisor(unittest.TestCase):
self._drive(sup)
def test_repeated_restart_requests_coalesce(self):
specs = [_DaemonSpec("git-gate", ("/bin/sleep", "30"))]
specs = [_DaemonSpec("git-gate", _SLEEP_30)]
sup = _Supervisor(specs)
sup.start_all()
time.sleep(0.1)
@@ -366,7 +369,7 @@ class TestSupervisor(unittest.TestCase):
self._drive(sup)
def test_request_restart_unknown_daemon_no_op(self):
specs = [_DaemonSpec("a", ("/bin/sleep", "30"))]
specs = [_DaemonSpec("a", _SLEEP_30)]
sup = _Supervisor(specs)
sup.start_all()
ok = sup.request_restart("ghost")
@@ -376,7 +379,7 @@ class TestSupervisor(unittest.TestCase):
self._drive(sup)
def test_restart_unknown_daemon_no_op(self):
specs = [_DaemonSpec("a", ("/bin/sleep", "30"))]
specs = [_DaemonSpec("a", _SLEEP_30)]
sup = _Supervisor(specs)
sup.start_all()
ok = sup.restart_daemon("ghost")
@@ -385,7 +388,7 @@ class TestSupervisor(unittest.TestCase):
self._drive(sup)
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.start_all()
sup.request_shutdown(reason="test")
@@ -395,7 +398,7 @@ class TestSupervisor(unittest.TestCase):
self._drive(sup)
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.start_all()
time.sleep(0.1)
@@ -413,8 +416,8 @@ class TestSupervisor(unittest.TestCase):
# both should receive SIGTERM and exit. Signal-only
# shutdown clamps to a zero supervisor exit code.
specs = [
_DaemonSpec("a", ("/bin/sleep", "60")),
_DaemonSpec("b", ("/bin/sleep", "60")),
_DaemonSpec("a", _SLEEP_60),
_DaemonSpec("b", _SLEEP_60),
]
sup = _Supervisor(specs)
sup.start_all()
@@ -449,7 +452,7 @@ class TestSupervisor(unittest.TestCase):
self.assertEqual(0, rc)
def test_idempotent_shutdown_requests(self):
specs = [_DaemonSpec("a", ("/bin/sleep", "60"))]
specs = [_DaemonSpec("a", _SLEEP_60)]
sup = _Supervisor(specs)
sup.start_all()
time.sleep(0.1)
@@ -465,28 +468,22 @@ class TestSupervisor(unittest.TestCase):
class TestMainEndToEnd(unittest.TestCase):
"""Run gateway_init.py as a real subprocess to cover the
signal-handler installation path. Skipped on platforms
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}")
signal-handler installation path."""
def _run(self, daemons_csv: str, send_signal: int | None,
wait_before_signal: float = 0.4,
overall_timeout: float = 6.0) -> tuple[int, str]:
"""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)."""
helper = (
"import os, runpy, sys\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._DaemonSpec('alpha', ('/bin/sleep','30')),\n"
" si._DaemonSpec('beta', ('/bin/sleep','30')),\n"
" si._DaemonSpec('alpha', sleep_cmd),\n"
" si._DaemonSpec('beta', sleep_cmd),\n"
")\n"
"sys.exit(si.main([]))\n"
)