test(sidecar): isolate imports and await process termination

This commit is contained in:
Palash Debnath
2026-09-17 15:03:22 +05:30
parent a0154778a4
commit 7a7a1d379d
2 changed files with 38 additions and 37 deletions
+2 -5
View File
@@ -27,11 +27,7 @@ from pathlib import Path
import pytest
from services.subprocess_backend import (
RECV_TIMEOUT_S,
GENERATE_RECV_TIMEOUT_S,
SubprocessBackend,
)
from services.subprocess_backend import RECV_TIMEOUT_S, SubprocessBackend
from services.tts_backend import OmniVoiceBackend, get_backend_class, list_backends
from engines.omnivoice_subprocess import (
OmniVoiceMPSSubprocessBackend,
@@ -325,6 +321,7 @@ class _PlainBackend(SubprocessBackend):
def test_base_default_recv_timeout_covers_a_generation():
# A sidecar that does not choose gets a deadline that outlasts the
# wall-clock budget its own job was granted (#2103).
from services.subprocess_backend import GENERATE_RECV_TIMEOUT_S
assert SubprocessBackend.recv_timeout_s == GENERATE_RECV_TIMEOUT_S == 600.0
assert _PlainBackend().recv_timeout_s == 600.0
# The ping budget itself is unchanged: health_check() still wants 60s.
+36 -32
View File
@@ -17,14 +17,6 @@ omission, which is the part #1611 could not do by fixing one engine.
"""
import pytest
from engines.omnivoice_subprocess import OmniVoiceSubprocessBackend
from services.model_manager import GPU_JOB_TIMEOUT_S
from services.subprocess_backend import (
GENERATE_RECV_TIMEOUT_S,
RECV_TIMEOUT_S,
SubprocessBackend,
)
from services.tts_backend import get_backend_class, list_backends
# The engines named in #2103 that inherited the ping budget. Listed explicitly
@@ -34,6 +26,8 @@ REGRESSED_ENGINE_IDS = ("confucius4-tts", "dots-tts", "moss-tts-v15", "supertoni
def _subprocess_backend_classes():
"""Every SubprocessBackend the registry can hand a user, by id."""
from services.tts_backend import get_backend_class
from services.tts_backend import list_backends
found = {}
for row in list_backends(include_hidden=True):
try:
@@ -47,6 +41,8 @@ def _subprocess_backend_classes():
def test_ping_budget_and_generate_budget_are_separate_constants():
# A ping must stay fast; a generation must not be cut off at a ping's deadline.
from services.subprocess_backend import GENERATE_RECV_TIMEOUT_S
from services.subprocess_backend import RECV_TIMEOUT_S
assert RECV_TIMEOUT_S == 60.0
assert GENERATE_RECV_TIMEOUT_S > RECV_TIMEOUT_S
@@ -55,12 +51,15 @@ def test_default_generate_deadline_covers_the_cpu_job_budget():
# Lockstep with model_manager: raising either budget there without raising
# this one re-opens #2103 for every engine that does not override.
# Imported rather than duplicated so the two cannot drift silently.
from services.subprocess_backend import GENERATE_RECV_TIMEOUT_S
from services.subprocess_backend import SubprocessBackend
assert GENERATE_RECV_TIMEOUT_S >= 600.0
assert SubprocessBackend.recv_timeout_s == GENERATE_RECV_TIMEOUT_S
@pytest.mark.parametrize("engine_id", REGRESSED_ENGINE_IDS)
def test_regressed_engines_no_longer_inherit_the_ping_budget(engine_id):
from services.subprocess_backend import RECV_TIMEOUT_S
cls = _subprocess_backend_classes().get(engine_id)
if cls is None:
pytest.fail(f"{engine_id} is not registered in this build")
@@ -75,6 +74,7 @@ def test_no_registered_sidecar_undercuts_the_accelerated_job_budget():
now inherits a deadline that already satisfies this; one that overrides it
with something too small fails here rather than in a user's generation.
"""
from services.model_manager import GPU_JOB_TIMEOUT_S
too_short = {}
for engine_id, cls in _subprocess_backend_classes().items():
deadline = cls.__new__(cls).recv_timeout_s
@@ -89,32 +89,35 @@ def test_no_registered_sidecar_undercuts_the_accelerated_job_budget():
# ── a constant is not enough: the budget scales with the text (#2109 review) ─
class _SilentBackend(SubprocessBackend):
"""A sidecar that expresses no opinion — the shape all four regressions had."""
id = "silent"
@classmethod
def is_available(cls):
return True, "ok"
@property
def sample_rate(self):
return 24000
@property
def supported_languages(self):
return ["multi"]
def _SilentBackend():
from services.subprocess_backend import SubprocessBackend
from services.subprocess_backend import SubprocessBackend
class SilentBackend(SubprocessBackend):
"""A sidecar with no custom deadline."""
id = "silent"
@classmethod
def is_available(cls):
return True, "ok"
@property
def sample_rate(self):
return 24000
@property
def supported_languages(self):
return ["multi"]
return SilentBackend()
class _OpinionatedBackend(_SilentBackend):
"""A sidecar that deliberately opts *down*, which #2103 asks to keep working."""
id = "opinionated"
recv_timeout_s = 45.0
def _OpinionatedBackend():
backend = _SilentBackend()
type(backend).id = "opinionated"
type(backend).recv_timeout_s = 45.0
return backend
def test_a_long_passage_raises_the_deadline_past_the_flat_default():
# generate_timeout_s adds 1s per 40 characters past a 1200-char allowance,
# so a long passage is granted more than the flat floor.
from services.subprocess_backend import GENERATE_RECV_TIMEOUT_S
backend = _SilentBackend()
short = backend._effective_recv_timeout_s("hello")
long_text = "x" * 200_000
@@ -136,6 +139,7 @@ def test_an_engine_that_opts_down_keeps_its_own_deadline():
def test_budget_probe_failure_falls_back_instead_of_failing_the_generate(monkeypatch):
from services.subprocess_backend import GENERATE_RECV_TIMEOUT_S
import services.model_manager as mm
def _boom(*a, **kw):
@@ -174,6 +178,7 @@ def test_timeout_error_names_the_deadline_instead_of_blaming_the_pipe(
VoiceStudio stopped the sidecar on its own deadline — appeared only in the
backend log, and reporters reasonably concluded the engine had crashed.
"""
from engines.omnivoice_subprocess import OmniVoiceSubprocessBackend
script = tmp_path / "wedging_sidecar.py"
script.write_text(WEDGING_SIDECAR)
monkeypatch.setattr(
@@ -218,6 +223,7 @@ def test_explicit_generation_budget_is_authoritative(monkeypatch):
@pytest.mark.asyncio
@pytest.mark.parametrize('guard_kind', ['asr', 'tts'])
async def test_outer_abandonment_terminates_owned_sidecar(tmp_path, monkeypatch, guard_kind):
from engines.omnivoice_subprocess import OmniVoiceSubprocessBackend
import asyncio
from concurrent.futures import ThreadPoolExecutor
from services.model_manager import run_on_gpu_pool_guarded
@@ -236,10 +242,8 @@ async def test_outer_abandonment_terminates_owned_sidecar(tmp_path, monkeypatch,
work = run_on_gpu_pool_guarded(lambda: backend.generate('hang'), executor=executor, timeout=0.5)
with pytest.raises(TimeoutError):
await work
for _ in range(100):
if backend._proc is not None and backend._proc.poll() is not None:
break
await asyncio.sleep(0.02)
assert backend._proc is not None and backend._proc.poll() is not None
assert backend._proc is not None
await asyncio.wait_for(asyncio.to_thread(backend._proc.wait, timeout=5), timeout=6)
assert backend._proc.poll() is not None
finally:
backend.shutdown()