[Refactor][CI] Reorganize test tree, and move slow e2e to weekly (#4951)
Signed-off-by: wangyu <410167048@qq.com>
This commit is contained in:
1 parent
925347332f
commit
7c17111ff5
158 files changed
+3479
-4130
No files matched your search
@@ -0,0 +1,172 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm_omni.experimental.fullduplex.core import protocol as ev
|
||||
from vllm_omni.experimental.fullduplex.core.adapter import DuplexCapability, OutputChunk
|
||||
from vllm_omni.experimental.fullduplex.core.runtime import DuplexRuntime
|
||||
from vllm_omni.experimental.fullduplex.core.session import DuplexSession, DuplexSessionConfig
|
||||
from vllm_omni.experimental.fullduplex.joyvl.adapter import JoyVLDuplexAdapter
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
async def _feed(events):
|
||||
for e in events:
|
||||
yield e
|
||||
|
||||
|
||||
def _collector():
|
||||
out: list[dict] = []
|
||||
|
||||
async def emit(event: dict) -> None:
|
||||
out.append(event)
|
||||
|
||||
return out, emit
|
||||
|
||||
|
||||
class _FakeAdapter:
|
||||
def __init__(self, chunks, barge_after=None):
|
||||
self._chunks = chunks
|
||||
self._barge_after = barge_after
|
||||
|
||||
def capabilities(self):
|
||||
return DuplexCapability(frozenset({"text"}), frozenset({"text"}), proactive=False)
|
||||
|
||||
async def on_input(self, session, modality, data):
|
||||
pass
|
||||
|
||||
def should_respond(self, session):
|
||||
return True
|
||||
|
||||
async def respond(self, session) -> AsyncIterator[OutputChunk]:
|
||||
for i, c in enumerate(self._chunks):
|
||||
if self._barge_after is not None and i == self._barge_after:
|
||||
session.barge_in()
|
||||
yield OutputChunk("text", c)
|
||||
|
||||
async def on_barge_in(self, session):
|
||||
pass
|
||||
|
||||
async def on_playback_ack(self, session, cursor):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_basic_response():
|
||||
session = DuplexSession("s", DuplexSessionConfig(output_modalities=("text",)))
|
||||
rt = DuplexRuntime(session, _FakeAdapter(["a", "b"]))
|
||||
out, emit = _collector()
|
||||
await rt.run(_feed([{"type": ev.INPUT_COMMIT}, {"type": ev.CLOSE}]), emit)
|
||||
types = [e["type"] for e in out]
|
||||
assert types == [ev.RESPONSE_CREATED, ev.RESPONSE_DELTA, ev.RESPONSE_DELTA, ev.RESPONSE_DONE]
|
||||
assert [e["data"] for e in out if e["type"] == ev.RESPONSE_DELTA] == ["a", "b"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_barge_in_drops_stale_output():
|
||||
session = DuplexSession("s")
|
||||
rt = DuplexRuntime(session, _FakeAdapter(["a", "b", "c"], barge_after=1))
|
||||
out, emit = _collector()
|
||||
await rt.run(_feed([{"type": ev.INPUT_COMMIT}, {"type": ev.CLOSE}]), emit)
|
||||
data = [e["data"] for e in out if e["type"] == ev.RESPONSE_DELTA]
|
||||
assert data == ["a"]
|
||||
assert ev.RESPONSE_DONE not in [e["type"] for e in out]
|
||||
|
||||
|
||||
class _SlowAdapter:
|
||||
def capabilities(self):
|
||||
return DuplexCapability(frozenset({"text"}), frozenset({"text"}), proactive=False)
|
||||
|
||||
async def on_input(self, session, modality, data):
|
||||
pass
|
||||
|
||||
def should_respond(self, session):
|
||||
return True
|
||||
|
||||
async def respond(self, session) -> AsyncIterator[OutputChunk]:
|
||||
for c in ["a", "b", "c", "d", "e"]:
|
||||
await asyncio.sleep(0.02)
|
||||
yield OutputChunk("text", c)
|
||||
|
||||
async def on_barge_in(self, session):
|
||||
pass
|
||||
|
||||
async def on_playback_ack(self, session, cursor):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_cancel_event_interrupts_active_response():
|
||||
session = DuplexSession("s")
|
||||
rt = DuplexRuntime(session, _SlowAdapter())
|
||||
out, emit = _collector()
|
||||
|
||||
async def feed():
|
||||
yield {"type": ev.INPUT_COMMIT}
|
||||
await asyncio.sleep(0.03)
|
||||
yield {"type": ev.RESPONSE_CANCEL}
|
||||
yield {"type": ev.CLOSE}
|
||||
|
||||
await rt.run(feed(), emit)
|
||||
deltas = [e["data"] for e in out if e["type"] == ev.RESPONSE_DELTA]
|
||||
types = [e["type"] for e in out]
|
||||
assert len(deltas) < 5
|
||||
assert ev.RESPONSE_CANCELLED in types
|
||||
assert ev.RESPONSE_DONE not in types
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_new_response_supersedes_inflight_without_blocking():
|
||||
# A second response trigger arriving while the first is still streaming must
|
||||
# supersede it (barge-in) rather than block the input loop until it finishes.
|
||||
session = DuplexSession("s")
|
||||
rt = DuplexRuntime(session, _SlowAdapter())
|
||||
out, emit = _collector()
|
||||
|
||||
async def feed():
|
||||
yield {"type": ev.INPUT_COMMIT}
|
||||
await asyncio.sleep(0.03) # let the first response emit ~one chunk
|
||||
yield {"type": ev.INPUT_COMMIT} # supersedes the in-flight first response
|
||||
await asyncio.sleep(0.2) # let the second response finish
|
||||
yield {"type": ev.CLOSE}
|
||||
|
||||
await rt.run(feed(), emit)
|
||||
created = [e for e in out if e["type"] == ev.RESPONSE_CREATED]
|
||||
done = [e for e in out if e["type"] == ev.RESPONSE_DONE]
|
||||
# two responses created; only the latest one completes (the first was cancelled)
|
||||
assert len(created) == 2
|
||||
assert len(done) == 1
|
||||
assert done[0]["response_index"] == created[-1]["response_index"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_joyvl_adapter_speaks_then_silences():
|
||||
replies = iter(["</response> a fire is breaking out", "</silence>"])
|
||||
|
||||
async def fake_generate(messages):
|
||||
return next(replies)
|
||||
|
||||
adapter = JoyVLDuplexAdapter(fake_generate, num_frames=4)
|
||||
cfg = DuplexSessionConfig(input_modalities=("video", "text"), output_modalities=("text",), proactive=True)
|
||||
rt = DuplexRuntime(DuplexSession("s", cfg), adapter)
|
||||
out, emit = _collector()
|
||||
await rt.run(
|
||||
_feed(
|
||||
[
|
||||
{"type": ev.INPUT_APPEND, "modality": "text", "data": "alert me if a fire breaks out"},
|
||||
{"type": ev.INPUT_APPEND, "modality": "video", "data": "data:image/jpeg;base64,AAA"},
|
||||
{"type": ev.INPUT_APPEND, "modality": "video", "data": "data:image/jpeg;base64,BBB"},
|
||||
{"type": ev.CLOSE},
|
||||
]
|
||||
),
|
||||
emit,
|
||||
)
|
||||
deltas = [e["data"] for e in out if e["type"] == ev.RESPONSE_DELTA]
|
||||
assert "a fire is breaking out" in deltas
|
||||
|
||||
assert deltas.count("a fire is breaking out") == 1
|
||||
Reference in new issue
Block a user