Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions tests/bridge/test_repl_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -1555,17 +1555,28 @@ async def test_pointer_mtime_task_fires_and_advances_updated_at_ms(

# Observe a real refresh. Under CI load a fixed 100 ms sleep can
# expire before the background loop has even started its first timer.
async with asyncio.timeout(3):
# ``asyncio.timeout`` is 3.11+; CI pins 3.11 but the package declares
# ``requires-python = ">=3.10"``, so use ``wait_for`` which is the
# portable equivalent.
seen: dict[str, Any] = {}

async def _await_refresh() -> None:
while True:
refreshed = read_pointer(params.dir, machine_name=params.machine_name)
refreshed = read_pointer(
params.dir, machine_name=params.machine_name
)
if (
refreshed is not None
and refreshed.updated_at_ms > initial.updated_at_ms
):
break
seen["refreshed"] = refreshed
return
await asyncio.sleep(0.01)

await asyncio.wait_for(_await_refresh(), timeout=3)

# Refresh preserves the install time and bridge/environment identity.
refreshed = seen["refreshed"]
assert refreshed.created_at_ms == initial.created_at_ms
assert refreshed.bridge_id == initial.bridge_id
assert refreshed.environment_id == initial.environment_id
Expand Down
47 changes: 43 additions & 4 deletions tests/test_init_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,42 @@ def test_drain_runs_cleanup_and_does_not_block(self) -> None:
# ---------------------------------------------------------------------------


def _drain_until_ready(proc: subprocess.Popen[str]) -> str:
"""Read whatever the child has emitted so far, looking for ``__READY__``.

Non-blocking: returns immediately. The caller loops with a deadline, so a
child that dies during startup cannot hang the test.
"""
import select

while True:
ready, _, _ = select.select([proc.stdout], [], [], 0.05)
if not ready:
return ""
chunk = os.read(proc.stdout.fileno(), 4096)
if not chunk:
return ""
if b"__READY__" in chunk:
return "__READY__"


def _run_in_subprocess(
code: str,
signal_after_ms: float | None = None,
signal_name: str = "SIGTERM",
timeout: float = 5.0,
wait_for_ready: bool = False,
) -> tuple[int, str, str]:
"""Run ``code`` in a fresh Python subprocess, optionally sending it
``signal_name`` after ``signal_after_ms`` ms. Returns
(returncode, stdout, stderr).

``wait_for_ready`` blocks until the child prints the ``__READY__``
marker before signalling. Signal tests MUST use it: a fixed delay races
interpreter startup, and a signal delivered before
``setup_graceful_shutdown()`` installs its handler is taken by the
default disposition instead (exit ``-SIGINT`` rather than the handled
``130``), which reads as a product bug but is purely a startup race.
"""
import signal as _signal
env = dict(os.environ)
Expand All @@ -295,7 +322,13 @@ def _run_in_subprocess(
env=env,
)
if signal_after_ms is not None:
time.sleep(signal_after_ms / 1000.0)
if wait_for_ready:
deadline = time.time() + timeout
while "__READY__" not in _drain_until_ready(proc):
if time.time() > deadline:
break
else:
time.sleep(signal_after_ms / 1000.0)
try:
proc.send_signal(getattr(_signal, signal_name))
except ProcessLookupError:
Expand Down Expand Up @@ -330,13 +363,15 @@ def test_sigterm_triggers_drain(self) -> None:
)
register_cleanup(lambda: print("DRAINED", flush=True))
setup_graceful_shutdown()
# Handlers are installed; only now is a SIGTERM meaningful.
print("__READY__", flush=True)
# Sleep long enough for the parent to send SIGTERM.
time.sleep(3.0)
print("LIVE", flush=True)
"""
)
rc, out, err = _run_in_subprocess(
code, signal_after_ms=200, signal_name="SIGTERM"
code, signal_after_ms=200, signal_name="SIGTERM", wait_for_ready=True
)
self.assertIn("DRAINED", out, msg=f"cleanup did not fire. err={err}")
# 128+15 == 143 (SIGTERM exit code).
Expand All @@ -363,13 +398,15 @@ def test_sigint_during_prefetch_clean_exit(self) -> None:
from src.prefetch import get_or_start_keychain_prefetch
setup_graceful_shutdown()
handle = get_or_start_keychain_prefetch()
# Handlers are installed; only now is a SIGINT meaningful.
print("__READY__", flush=True)
# Sleep long enough for the parent to send SIGINT.
time.sleep(3.0)
print("LIVE", flush=True)
"""
)
rc, out, err = _run_in_subprocess(
code, signal_after_ms=100, signal_name="SIGINT"
code, signal_after_ms=100, signal_name="SIGINT", wait_for_ready=True
)
# SIGINT exit code is 128+2 == 130.
self.assertEqual(rc, 130, msg=f"unexpected rc={rc}. out={out} err={err}")
Expand All @@ -390,12 +427,14 @@ def test_sigint_before_any_prefetch(self) -> None:
import time
from src.utils.graceful_shutdown import setup_graceful_shutdown
setup_graceful_shutdown()
# Handlers are installed; only now is a SIGINT meaningful.
print("__READY__", flush=True)
time.sleep(3.0)
print("LIVE", flush=True)
"""
)
rc, out, err = _run_in_subprocess(
code, signal_after_ms=300, signal_name="SIGINT"
code, signal_after_ms=300, signal_name="SIGINT", wait_for_ready=True
)
self.assertEqual(rc, 130, msg=f"unexpected rc={rc}. err={err}")
# No "cleanup error" lines should appear in stderr.
Expand Down
Loading