[Misc] Cleanup: use consistent pytest-mock in unit tests (#2698)
Signed-off-by: yuanheng <jonathan.zhaoyh@gmail.com>
This commit is contained in:
1 parent
2c67c30550
commit
e0cdbe9a5d
26 files changed
+1598
-1369
No files matched your search
@@ -4,9 +4,9 @@
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from vllm_omni.entrypoints.openai.utils import (
|
||||
get_supported_speakers_from_hf_config,
|
||||
@@ -25,9 +25,9 @@ def serving_chat():
|
||||
return instance
|
||||
|
||||
|
||||
def _make_hf_config(*, speaker_id: dict | None = None, spk_id: dict | None = None):
|
||||
hf_config = MagicMock()
|
||||
talker_config = MagicMock()
|
||||
def _make_hf_config(mocker: MockerFixture, *, speaker_id: dict | None = None, spk_id: dict | None = None):
|
||||
hf_config = mocker.MagicMock()
|
||||
talker_config = mocker.MagicMock()
|
||||
talker_config.speaker_id = speaker_id
|
||||
talker_config.spk_id = spk_id
|
||||
hf_config.talker_config = talker_config
|
||||
@@ -51,14 +51,14 @@ def test_validate_requested_speaker_skips_validation_when_supported_empty():
|
||||
assert validate_requested_speaker(" ", {"vivian"}) is None
|
||||
|
||||
|
||||
def test_get_supported_speakers_from_hf_config_uses_spk_id_fallback():
|
||||
hf_config = _make_hf_config(speaker_id=None, spk_id={"Serena": 0})
|
||||
def test_get_supported_speakers_from_hf_config_uses_spk_id_fallback(mocker: MockerFixture):
|
||||
hf_config = _make_hf_config(mocker, speaker_id=None, spk_id={"Serena": 0})
|
||||
assert get_supported_speakers_from_hf_config(hf_config) == {"serena"}
|
||||
|
||||
|
||||
def test_get_supported_speakers_caches_normalized_keys(serving_chat):
|
||||
serving_chat.model_config = MagicMock()
|
||||
serving_chat.model_config.hf_config = _make_hf_config(speaker_id={"Vivian": 0, "Ethan": 1})
|
||||
def test_get_supported_speakers_caches_normalized_keys(mocker: MockerFixture, serving_chat):
|
||||
serving_chat.model_config = mocker.MagicMock()
|
||||
serving_chat.model_config.hf_config = _make_hf_config(mocker, speaker_id={"Vivian": 0, "Ethan": 1})
|
||||
|
||||
assert serving_chat._get_supported_speakers() == {"vivian", "ethan"}
|
||||
|
||||
@@ -67,15 +67,15 @@ def test_get_supported_speakers_caches_normalized_keys(serving_chat):
|
||||
assert serving_chat._get_supported_speakers() == {"vivian", "ethan"}
|
||||
|
||||
|
||||
def test_create_chat_completion_converts_value_error_to_error_response(serving_chat):
|
||||
def test_create_chat_completion_converts_value_error_to_error_response(mocker: MockerFixture, serving_chat):
|
||||
serving_chat._diffusion_mode = False
|
||||
serving_chat._check_model = AsyncMock(return_value=None)
|
||||
serving_chat.engine_client = MagicMock(errored=False)
|
||||
serving_chat._maybe_get_adapters = MagicMock(return_value=None)
|
||||
serving_chat.models = MagicMock()
|
||||
serving_chat._check_model = mocker.AsyncMock(return_value=None)
|
||||
serving_chat.engine_client = mocker.MagicMock(errored=False)
|
||||
serving_chat._maybe_get_adapters = mocker.MagicMock(return_value=None)
|
||||
serving_chat.models = mocker.MagicMock()
|
||||
serving_chat.models.model_name.return_value = "test-model"
|
||||
serving_chat.renderer = MagicMock()
|
||||
serving_chat.renderer.get_tokenizer.return_value = MagicMock()
|
||||
serving_chat.renderer = mocker.MagicMock()
|
||||
serving_chat.renderer.get_tokenizer.return_value = mocker.MagicMock()
|
||||
serving_chat.reasoning_parser_cls = None
|
||||
serving_chat.tool_parser = None
|
||||
serving_chat.use_harmony = False
|
||||
@@ -85,12 +85,12 @@ def test_create_chat_completion_converts_value_error_to_error_response(serving_c
|
||||
serving_chat.chat_template = None
|
||||
serving_chat.chat_template_content_format = "string"
|
||||
serving_chat.default_chat_template_kwargs = {}
|
||||
serving_chat._validate_chat_template = MagicMock(return_value=None)
|
||||
serving_chat._prepare_extra_chat_template_kwargs = MagicMock(return_value={})
|
||||
serving_chat._preprocess_chat = AsyncMock(
|
||||
serving_chat._validate_chat_template = mocker.MagicMock(return_value=None)
|
||||
serving_chat._prepare_extra_chat_template_kwargs = mocker.MagicMock(return_value={})
|
||||
serving_chat._preprocess_chat = mocker.AsyncMock(
|
||||
side_effect=ValueError("Invalid speaker 'uncle_fu'. Supported: ethan, vivian")
|
||||
)
|
||||
serving_chat.create_error_response = MagicMock(return_value="error-response")
|
||||
serving_chat.create_error_response = mocker.MagicMock(return_value="error-response")
|
||||
|
||||
request = SimpleNamespace(
|
||||
tool_choice=None,
|
||||
|
||||
@@ -6,7 +6,6 @@ import struct
|
||||
from inspect import Signature, signature
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -901,7 +900,7 @@ class TestTTSMethods:
|
||||
# Verify speakers are normalized to lowercase
|
||||
assert server.supported_speakers == {"ryan", "vivian", "aiden"}
|
||||
|
||||
def test_build_tts_params_with_uploaded_voice(self, speech_server):
|
||||
def test_build_tts_params_with_uploaded_voice(self, speech_server, mocker: MockerFixture):
|
||||
"""Test _build_tts_params auto-sets ref_audio for uploaded voices (x_vector only)."""
|
||||
speech_server.uploaded_speakers = {
|
||||
"custom_voice": {
|
||||
@@ -914,18 +913,18 @@ class TestTTSMethods:
|
||||
}
|
||||
speech_server.supported_speakers = {"ryan", "vivian", "custom_voice"}
|
||||
|
||||
with patch.object(speech_server, "_get_uploaded_audio_data") as mock_get_audio:
|
||||
mock_get_audio.return_value = "data:audio/wav;base64,ZmFrZWF1ZGlv"
|
||||
req = OpenAICreateSpeechRequest(input="Hello", voice="custom_voice")
|
||||
params = speech_server._build_tts_params(req)
|
||||
mock_get_audio = mocker.patch.object(speech_server, "_get_uploaded_audio_data")
|
||||
mock_get_audio.return_value = "data:audio/wav;base64,ZmFrZWF1ZGlv"
|
||||
req = OpenAICreateSpeechRequest(input="Hello", voice="custom_voice")
|
||||
params = speech_server._build_tts_params(req)
|
||||
|
||||
assert params["ref_audio"] == ["data:audio/wav;base64,ZmFrZWF1ZGlv"]
|
||||
assert params["x_vector_only_mode"] == [True]
|
||||
assert params["task_type"] == ["Base"]
|
||||
assert params["voice_created_at"] == [1711234567.89]
|
||||
assert "ref_text" not in params
|
||||
assert params["ref_audio"] == ["data:audio/wav;base64,ZmFrZWF1ZGlv"]
|
||||
assert params["x_vector_only_mode"] == [True]
|
||||
assert params["task_type"] == ["Base"]
|
||||
assert params["voice_created_at"] == [1711234567.89]
|
||||
assert "ref_text" not in params
|
||||
|
||||
def test_build_tts_params_with_uploaded_voice_ref_text(self, speech_server):
|
||||
def test_build_tts_params_with_uploaded_voice_ref_text(self, speech_server, mocker: MockerFixture):
|
||||
"""Test _build_tts_params enables in-context cloning when ref_text is stored."""
|
||||
speech_server.uploaded_speakers = {
|
||||
"custom_voice": {
|
||||
@@ -938,16 +937,16 @@ class TestTTSMethods:
|
||||
}
|
||||
speech_server.supported_speakers = {"ryan", "vivian", "custom_voice"}
|
||||
|
||||
with patch.object(speech_server, "_get_uploaded_audio_data") as mock_get_audio:
|
||||
mock_get_audio.return_value = "data:audio/wav;base64,ZmFrZWF1ZGlv"
|
||||
req = OpenAICreateSpeechRequest(input="Hello", voice="custom_voice")
|
||||
params = speech_server._build_tts_params(req)
|
||||
mock_get_audio = mocker.patch.object(speech_server, "_get_uploaded_audio_data")
|
||||
mock_get_audio.return_value = "data:audio/wav;base64,ZmFrZWF1ZGlv"
|
||||
req = OpenAICreateSpeechRequest(input="Hello", voice="custom_voice")
|
||||
params = speech_server._build_tts_params(req)
|
||||
|
||||
assert params["ref_audio"] == ["data:audio/wav;base64,ZmFrZWF1ZGlv"]
|
||||
assert params["x_vector_only_mode"] == [False]
|
||||
assert params["task_type"] == ["Base"]
|
||||
assert params["ref_text"] == ["Hello world transcript"]
|
||||
assert params["voice_created_at"] == [1711234567.89]
|
||||
assert params["ref_audio"] == ["data:audio/wav;base64,ZmFrZWF1ZGlv"]
|
||||
assert params["x_vector_only_mode"] == [False]
|
||||
assert params["task_type"] == ["Base"]
|
||||
assert params["ref_text"] == ["Hello world transcript"]
|
||||
assert params["voice_created_at"] == [1711234567.89]
|
||||
|
||||
def test_build_tts_params_without_uploaded_voice(self, speech_server):
|
||||
"""Test _build_tts_params does not auto-set ref_audio for non-uploaded voices."""
|
||||
@@ -989,45 +988,43 @@ class TestTTSMethods:
|
||||
# x_vector_only_mode should not be set when explicit ref_audio is provided
|
||||
assert "x_vector_only_mode" not in params
|
||||
|
||||
def test_get_uploaded_audio_data(self, speech_server):
|
||||
def test_get_uploaded_audio_data(self, speech_server, mocker: MockerFixture):
|
||||
"""Test _get_uploaded_audio_data function."""
|
||||
# Mock file operations
|
||||
with (
|
||||
patch("builtins.open", create=True) as mock_open,
|
||||
patch("base64.b64encode") as mock_b64encode,
|
||||
patch("pathlib.Path.exists") as mock_exists,
|
||||
):
|
||||
mock_exists.return_value = True
|
||||
mock_b64encode.return_value = b"ZmFrZWF1ZGlv"
|
||||
mock_open = mocker.patch("builtins.open", create=True)
|
||||
mock_b64encode = mocker.patch("base64.b64encode")
|
||||
mock_exists = mocker.patch("pathlib.Path.exists")
|
||||
mock_exists.return_value = True
|
||||
mock_b64encode.return_value = b"ZmFrZWF1ZGlv"
|
||||
|
||||
# Setup mock file
|
||||
mock_file = MagicMock()
|
||||
mock_file.read.return_value = b"fakeaudio"
|
||||
mock_open.return_value.__enter__.return_value = mock_file
|
||||
# Setup mock file
|
||||
mock_file = mocker.MagicMock()
|
||||
mock_file.read.return_value = b"fakeaudio"
|
||||
mock_open.return_value.__enter__.return_value = mock_file
|
||||
|
||||
# Setup uploaded speaker
|
||||
speech_server.uploaded_speakers = {
|
||||
"test_voice": {"name": "test_voice", "file_path": "/tmp/test.wav", "mime_type": "audio/wav"}
|
||||
}
|
||||
result = speech_server._get_uploaded_audio_data("test_voice")
|
||||
# Setup uploaded speaker
|
||||
speech_server.uploaded_speakers = {
|
||||
"test_voice": {"name": "test_voice", "file_path": "/tmp/test.wav", "mime_type": "audio/wav"}
|
||||
}
|
||||
result = speech_server._get_uploaded_audio_data("test_voice")
|
||||
|
||||
assert result == "data:audio/wav;base64,ZmFrZWF1ZGlv"
|
||||
mock_open.assert_called_once_with(Path("/tmp/test.wav"), "rb")
|
||||
mock_b64encode.assert_called_once_with(b"fakeaudio")
|
||||
assert result == "data:audio/wav;base64,ZmFrZWF1ZGlv"
|
||||
mock_open.assert_called_once_with(Path("/tmp/test.wav"), "rb")
|
||||
mock_b64encode.assert_called_once_with(b"fakeaudio")
|
||||
|
||||
def test_get_uploaded_audio_data_missing_file(self, speech_server):
|
||||
def test_get_uploaded_audio_data_missing_file(self, speech_server, mocker: MockerFixture):
|
||||
"""Test _get_uploaded_audio_data when file is missing."""
|
||||
with patch("pathlib.Path.exists") as mock_exists:
|
||||
mock_exists.return_value = False
|
||||
mock_exists = mocker.patch("pathlib.Path.exists")
|
||||
mock_exists.return_value = False
|
||||
|
||||
# Setup uploaded speaker
|
||||
speech_server.uploaded_speakers = {
|
||||
"test_voice": {"name": "test_voice", "file_path": "/tmp/test.wav", "mime_type": "audio/wav"}
|
||||
}
|
||||
# Setup uploaded speaker
|
||||
speech_server.uploaded_speakers = {
|
||||
"test_voice": {"name": "test_voice", "file_path": "/tmp/test.wav", "mime_type": "audio/wav"}
|
||||
}
|
||||
|
||||
result = speech_server._get_uploaded_audio_data("test_voice")
|
||||
result = speech_server._get_uploaded_audio_data("test_voice")
|
||||
|
||||
assert result is None
|
||||
assert result is None
|
||||
|
||||
def test_get_uploaded_audio_data_voice_not_found(self, speech_server):
|
||||
"""Test _get_uploaded_audio_data when voice is not in uploaded_speakers."""
|
||||
@@ -1049,7 +1046,7 @@ class TestTTSMethods:
|
||||
req = OpenAICreateSpeechRequest.model_validate({"input": "Hello", "voice": "custom_voice"})
|
||||
assert req.voice == "custom_voice"
|
||||
|
||||
def test_speaker_alias_in_base_task_with_uploaded_voice(self, speech_server):
|
||||
def test_speaker_alias_in_base_task_with_uploaded_voice(self, speech_server, mocker: MockerFixture):
|
||||
"""Using 'speaker' key with an uploaded voice should work for Base task."""
|
||||
speech_server.uploaded_speakers = {
|
||||
"utesf": {
|
||||
@@ -1061,13 +1058,13 @@ class TestTTSMethods:
|
||||
}
|
||||
req = OpenAICreateSpeechRequest.model_validate({"input": "Hello", "speaker": "UTESF", "task_type": "Base"})
|
||||
assert req.voice == "UTESF"
|
||||
with patch("pathlib.Path.exists", return_value=True):
|
||||
result = speech_server._validate_qwen_tts_request(req)
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
result = speech_server._validate_qwen_tts_request(req)
|
||||
assert result is None
|
||||
|
||||
# ── uploaded voice with embedding ──
|
||||
|
||||
def test_build_tts_params_with_uploaded_voice_embedding(self, speech_server):
|
||||
def test_build_tts_params_with_uploaded_voice_embedding(self, speech_server, mocker: MockerFixture):
|
||||
"""Test _build_tts_params loads embedding for embedding-uploaded voices."""
|
||||
speech_server.uploaded_speakers = {
|
||||
"emb_voice": {
|
||||
@@ -1083,20 +1080,20 @@ class TestTTSMethods:
|
||||
speech_server.supported_speakers = {"ryan", "vivian", "emb_voice"}
|
||||
|
||||
fake_embedding = [0.1] * 1024
|
||||
with patch.object(speech_server, "_get_uploaded_speaker_embedding") as mock_get_emb:
|
||||
mock_get_emb.return_value = fake_embedding
|
||||
req = OpenAICreateSpeechRequest(input="Hello", voice="emb_voice")
|
||||
params = speech_server._build_tts_params(req)
|
||||
mock_get_emb = mocker.patch.object(speech_server, "_get_uploaded_speaker_embedding")
|
||||
mock_get_emb.return_value = fake_embedding
|
||||
req = OpenAICreateSpeechRequest(input="Hello", voice="emb_voice")
|
||||
params = speech_server._build_tts_params(req)
|
||||
|
||||
assert "voice_clone_prompt" in params
|
||||
assert params["voice_clone_prompt"][0]["ref_spk_embedding"] == fake_embedding
|
||||
assert params["task_type"] == ["Base"]
|
||||
assert params["x_vector_only_mode"] == [True]
|
||||
assert "ref_audio" not in params
|
||||
assert "voice_clone_prompt" in params
|
||||
assert params["voice_clone_prompt"][0]["ref_spk_embedding"] == fake_embedding
|
||||
assert params["task_type"] == ["Base"]
|
||||
assert params["x_vector_only_mode"] == [True]
|
||||
assert "ref_audio" not in params
|
||||
|
||||
# ── regression: full flow from issue #1603 ──
|
||||
|
||||
def test_regression_1603_speaker_key_with_uploaded_audio_voice(self, speech_server):
|
||||
def test_regression_1603_speaker_key_with_uploaded_audio_voice(self, speech_server, mocker: MockerFixture):
|
||||
"""Regression test for #1603: upload audio voice, then invoke TTS with 'speaker' key.
|
||||
|
||||
Verifies the full validate → build_params pipeline works end-to-end.
|
||||
@@ -1116,14 +1113,14 @@ class TestTTSMethods:
|
||||
assert req.voice == "UTESF"
|
||||
|
||||
# Validation should pass (file exists)
|
||||
with patch("pathlib.Path.exists", return_value=True):
|
||||
err = speech_server._validate_qwen_tts_request(req)
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
err = speech_server._validate_qwen_tts_request(req)
|
||||
assert err is None, f"Validation failed: {err}"
|
||||
|
||||
# Build params should auto-set ref_audio from stored file
|
||||
with patch.object(speech_server, "_get_uploaded_audio_data") as mock_audio:
|
||||
mock_audio.return_value = "data:audio/wav;base64,ZmFrZQ=="
|
||||
params = speech_server._build_tts_params(req)
|
||||
mock_audio = mocker.patch.object(speech_server, "_get_uploaded_audio_data")
|
||||
mock_audio.return_value = "data:audio/wav;base64,ZmFrZQ=="
|
||||
params = speech_server._build_tts_params(req)
|
||||
|
||||
assert params["task_type"] == ["Base"]
|
||||
assert params["ref_audio"] == ["data:audio/wav;base64,ZmFrZQ=="]
|
||||
@@ -1131,7 +1128,7 @@ class TestTTSMethods:
|
||||
assert params["x_vector_only_mode"] == [False]
|
||||
assert params["speaker"] == ["utesf"]
|
||||
|
||||
def test_regression_1603_speaker_key_with_uploaded_embedding_voice(self, speech_server):
|
||||
def test_regression_1603_speaker_key_with_uploaded_embedding_voice(self, speech_server, mocker: MockerFixture):
|
||||
"""Regression test for #1603: upload embedding voice, then invoke TTS with 'speaker' key.
|
||||
|
||||
Verifies embedding-uploaded voices are loaded as voice_clone_prompt, not as audio.
|
||||
@@ -1154,15 +1151,15 @@ class TestTTSMethods:
|
||||
assert req.voice == "myvoice"
|
||||
|
||||
# Validation should pass
|
||||
with patch("pathlib.Path.exists", return_value=True):
|
||||
err = speech_server._validate_qwen_tts_request(req)
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
err = speech_server._validate_qwen_tts_request(req)
|
||||
assert err is None, f"Validation failed: {err}"
|
||||
|
||||
# Build params should use embedding, NOT audio
|
||||
fake_emb = [0.1] * 1024
|
||||
with patch.object(speech_server, "_get_uploaded_speaker_embedding") as mock_emb:
|
||||
mock_emb.return_value = fake_emb
|
||||
params = speech_server._build_tts_params(req)
|
||||
mock_emb = mocker.patch.object(speech_server, "_get_uploaded_speaker_embedding")
|
||||
mock_emb.return_value = fake_emb
|
||||
params = speech_server._build_tts_params(req)
|
||||
|
||||
assert params["task_type"] == ["Base"]
|
||||
assert params["x_vector_only_mode"] == [True]
|
||||
@@ -1171,7 +1168,7 @@ class TestTTSMethods:
|
||||
# Must NOT have ref_audio — that would fail for safetensors files
|
||||
assert "ref_audio" not in params
|
||||
|
||||
def test_validate_rejects_embedding_voice_with_pending_cache(self, speech_server):
|
||||
def test_validate_rejects_embedding_voice_with_pending_cache(self, speech_server, mocker: MockerFixture):
|
||||
"""Validation should reject embedding voices whose cache is not yet ready."""
|
||||
speech_server.uploaded_speakers = {
|
||||
"myvoice": {
|
||||
@@ -1184,12 +1181,12 @@ class TestTTSMethods:
|
||||
}
|
||||
}
|
||||
req = OpenAICreateSpeechRequest.model_validate({"input": "Hello", "speaker": "myvoice", "task_type": "Base"})
|
||||
with patch("pathlib.Path.exists", return_value=True):
|
||||
err = speech_server._validate_qwen_tts_request(req)
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
err = speech_server._validate_qwen_tts_request(req)
|
||||
assert err is not None
|
||||
assert "not yet ready" in err
|
||||
|
||||
def test_x_vector_only_mode_not_overwritten_for_uploaded_embedding(self, speech_server):
|
||||
def test_x_vector_only_mode_not_overwritten_for_uploaded_embedding(self, speech_server, mocker: MockerFixture):
|
||||
"""x_vector_only_mode set by uploaded embedding must not be overwritten by request field."""
|
||||
speech_server.uploaded_speakers = {
|
||||
"emb_voice": {
|
||||
@@ -1203,11 +1200,11 @@ class TestTTSMethods:
|
||||
}
|
||||
}
|
||||
fake_emb = [0.1] * 1024
|
||||
with patch.object(speech_server, "_get_uploaded_speaker_embedding") as mock_emb:
|
||||
mock_emb.return_value = fake_emb
|
||||
# Client explicitly sends x_vector_only_mode=False, but embedding requires True
|
||||
req = OpenAICreateSpeechRequest(input="Hello", voice="emb_voice", x_vector_only_mode=False)
|
||||
params = speech_server._build_tts_params(req)
|
||||
mock_emb = mocker.patch.object(speech_server, "_get_uploaded_speaker_embedding")
|
||||
mock_emb.return_value = fake_emb
|
||||
# Client explicitly sends x_vector_only_mode=False, but embedding requires True
|
||||
req = OpenAICreateSpeechRequest(input="Hello", voice="emb_voice", x_vector_only_mode=False)
|
||||
params = speech_server._build_tts_params(req)
|
||||
|
||||
assert params["x_vector_only_mode"] == [True]
|
||||
assert "voice_clone_prompt" in params
|
||||
@@ -1654,9 +1651,9 @@ class TestAsyncOmniSupportedTasks:
|
||||
assert "generate" in tasks
|
||||
|
||||
|
||||
def test_api_server_create_speech_wraps_error_response_status():
|
||||
handler = MagicMock()
|
||||
handler.create_speech = AsyncMock(
|
||||
def test_api_server_create_speech_wraps_error_response_status(mocker: MockerFixture):
|
||||
handler = mocker.MagicMock()
|
||||
handler.create_speech = mocker.AsyncMock(
|
||||
return_value=ErrorResponse(
|
||||
error=ErrorInfo(message="bad request", type="BadRequestError", param=None, code=400),
|
||||
)
|
||||
@@ -1851,9 +1848,9 @@ class TestFishSpeechServing:
|
||||
assert "<|speaker:0|>你好,[laughing]欢迎回来。<|speaker:1|>我也来了。" in encoded_texts
|
||||
assert all(allowed_special is None for _, _, allowed_special in tokenizer.calls)
|
||||
|
||||
def test_build_fish_clone_prompt_normalizes_text_fields(self, fish_speech_server):
|
||||
def test_build_fish_clone_prompt_normalizes_text_fields(self, fish_speech_server, mocker: MockerFixture):
|
||||
fish_speech_server._fish_speech_tokenizer = _FakeFishTokenizer()
|
||||
fish_speech_server._estimate_fish_prompt_len = MagicMock(return_value=123)
|
||||
fish_speech_server._estimate_fish_prompt_len = mocker.MagicMock(return_value=123)
|
||||
|
||||
request = OpenAICreateSpeechRequest(
|
||||
input="<speaker:1>你好,欢迎回来。",
|
||||
@@ -1904,8 +1901,10 @@ class TestFishSpeechServing:
|
||||
with pytest.raises(ValueError, match="unsupported control token"):
|
||||
fish_speech_server._build_fish_speech_prompt(request)
|
||||
|
||||
def test_prepare_speech_generation_overrides_fish_default_max_tokens(self, fish_speech_server):
|
||||
fish_speech_server._build_fish_speech_prompt_async = AsyncMock(
|
||||
def test_prepare_speech_generation_overrides_fish_default_max_tokens(
|
||||
self, fish_speech_server, mocker: MockerFixture
|
||||
):
|
||||
fish_speech_server._build_fish_speech_prompt_async = mocker.AsyncMock(
|
||||
return_value={
|
||||
"prompt_token_ids": [1, 2, 3],
|
||||
"additional_information": {},
|
||||
@@ -1924,8 +1923,8 @@ class TestFishSpeechServing:
|
||||
assert sampling_params_list[0].max_tokens == 4096
|
||||
assert fish_speech_server.engine_client.default_sampling_params_list[0].max_tokens == 2048
|
||||
|
||||
def test_prepare_speech_generation_uses_stage_default_max_tokens(self, fish_speech_server):
|
||||
fish_speech_server._build_fish_speech_prompt_async = AsyncMock(
|
||||
def test_prepare_speech_generation_uses_stage_default_max_tokens(self, fish_speech_server, mocker: MockerFixture):
|
||||
fish_speech_server._build_fish_speech_prompt_async = mocker.AsyncMock(
|
||||
return_value={
|
||||
"prompt_token_ids": [1, 2, 3],
|
||||
"additional_information": {},
|
||||
@@ -1956,9 +1955,9 @@ class TestFishSpeechServing:
|
||||
|
||||
fish_speech_server.engine_client.generate.assert_not_called()
|
||||
|
||||
def test_create_speech_batch_allows_fish_text_only_items(self, fish_speech_server):
|
||||
fish_speech_server._check_model = AsyncMock(return_value=None)
|
||||
fish_speech_server._generate_audio_bytes = AsyncMock(return_value=("YWJj", "audio/wav"))
|
||||
def test_create_speech_batch_allows_fish_text_only_items(self, fish_speech_server, mocker: MockerFixture):
|
||||
fish_speech_server._check_model = mocker.AsyncMock(return_value=None)
|
||||
fish_speech_server._generate_audio_bytes = mocker.AsyncMock(return_value=("YWJj", "audio/wav"))
|
||||
|
||||
batch = BatchSpeechRequest(items=[SpeechBatchItem(input="hello fish")])
|
||||
response = asyncio.run(fish_speech_server.create_speech_batch(batch))
|
||||
@@ -2154,8 +2153,8 @@ class TestCosyVoice3Serving:
|
||||
assert error is not None
|
||||
assert "max_new_tokens" in error
|
||||
|
||||
def test_prepare_speech_generation_cosyvoice3(self, cosyvoice3_server):
|
||||
cosyvoice3_server._build_cosyvoice3_prompt = AsyncMock(
|
||||
def test_prepare_speech_generation_cosyvoice3(self, cosyvoice3_server, mocker: MockerFixture):
|
||||
cosyvoice3_server._build_cosyvoice3_prompt = mocker.AsyncMock(
|
||||
return_value={
|
||||
"prompt": "Hello",
|
||||
"multi_modal_data": {"audio": (np.zeros(24000), 24000)},
|
||||
@@ -2236,9 +2235,9 @@ class TestTTSAsyncOffloading:
|
||||
yield server
|
||||
server.shutdown()
|
||||
|
||||
def test_prepare_speech_generation_awaits_voxtral_async(self, voxtral_server):
|
||||
def test_prepare_speech_generation_awaits_voxtral_async(self, voxtral_server, mocker: MockerFixture):
|
||||
"""Voxtral path in _prepare_speech_generation should call the async wrapper."""
|
||||
voxtral_server._build_voxtral_prompt_async = AsyncMock(
|
||||
voxtral_server._build_voxtral_prompt_async = mocker.AsyncMock(
|
||||
return_value={
|
||||
"prompt_token_ids": [1, 2, 3],
|
||||
"additional_information": {"voice": ["test"]},
|
||||
@@ -2248,13 +2247,13 @@ class TestTTSAsyncOffloading:
|
||||
asyncio.run(voxtral_server._prepare_speech_generation(request))
|
||||
voxtral_server._build_voxtral_prompt_async.assert_awaited_once()
|
||||
|
||||
def test_prepare_speech_generation_awaits_qwen3_tts_async(self, qwen3_tts_server):
|
||||
def test_prepare_speech_generation_awaits_qwen3_tts_async(self, qwen3_tts_server, mocker: MockerFixture):
|
||||
"""Qwen3 TTS path should call _estimate_prompt_len_async."""
|
||||
qwen3_tts_server._validate_tts_request = MagicMock(return_value=None)
|
||||
qwen3_tts_server._build_tts_params = MagicMock(
|
||||
qwen3_tts_server._validate_tts_request = mocker.MagicMock(return_value=None)
|
||||
qwen3_tts_server._build_tts_params = mocker.MagicMock(
|
||||
return_value={"text": ["hello"], "task_type": ["CustomVoice"], "speaker": ["Vivian"]}
|
||||
)
|
||||
qwen3_tts_server._estimate_prompt_len_async = AsyncMock(return_value=512)
|
||||
qwen3_tts_server._estimate_prompt_len_async = mocker.AsyncMock(return_value=512)
|
||||
request = OpenAICreateSpeechRequest(input="hello")
|
||||
asyncio.run(qwen3_tts_server._prepare_speech_generation(request))
|
||||
qwen3_tts_server._build_tts_params.assert_called_once()
|
||||
@@ -2281,8 +2280,8 @@ class TestTTSAsyncOffloading:
|
||||
server.shutdown() # Should not raise
|
||||
assert server._tts_executor is None
|
||||
|
||||
def test_diffusion_instance_shutdown_safe(self):
|
||||
def test_diffusion_instance_shutdown_safe(self, mocker: MockerFixture):
|
||||
"""Diffusion instances (created via for_diffusion) should have safe shutdown."""
|
||||
server = OmniOpenAIServingSpeech.for_diffusion(diffusion_engine=MagicMock(), model_name="test-model")
|
||||
server = OmniOpenAIServingSpeech.for_diffusion(diffusion_engine=mocker.MagicMock(), model_name="test-model")
|
||||
assert server._tts_executor is None
|
||||
server.shutdown() # Should not raise
|
||||
@@ -1,8 +1,8 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, WebSocket
|
||||
from pytest_mock import MockerFixture
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
@@ -13,19 +13,26 @@ from vllm_omni.entrypoints.openai.serving_speech_stream import OmniStreamingSpee
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _build_test_app(speech_service=None, *, idle_timeout=30.0, config_timeout=10.0):
|
||||
def _build_test_app(
|
||||
speech_service=None,
|
||||
*,
|
||||
idle_timeout=30.0,
|
||||
config_timeout=10.0,
|
||||
mocker: MockerFixture | None = None,
|
||||
):
|
||||
if speech_service is None:
|
||||
speech_service = MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = AsyncMock(return_value=(b"RIFF" + b"\x00" * 32, "audio/wav"))
|
||||
speech_service._prepare_speech_generation = AsyncMock(return_value=("req-1", object(), {}))
|
||||
assert mocker is not None
|
||||
speech_service = mocker.MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = mocker.AsyncMock(return_value=(b"RIFF" + b"\x00" * 32, "audio/wav"))
|
||||
speech_service._prepare_speech_generation = mocker.AsyncMock(return_value=("req-1", object(), {}))
|
||||
|
||||
async def mock_generate_pcm_chunks(_generator, _request_id):
|
||||
for chunk in (b"\x01\x02", b"\x03\x04\x05"):
|
||||
yield chunk
|
||||
|
||||
speech_service._generate_pcm_chunks = mock_generate_pcm_chunks
|
||||
speech_service.engine_client = MagicMock()
|
||||
speech_service.engine_client.abort = AsyncMock()
|
||||
speech_service.engine_client = mocker.MagicMock()
|
||||
speech_service.engine_client.abort = mocker.AsyncMock()
|
||||
|
||||
handler = OmniStreamingSpeechHandler(
|
||||
speech_service=speech_service,
|
||||
@@ -42,8 +49,8 @@ def _build_test_app(speech_service=None, *, idle_timeout=30.0, config_timeout=10
|
||||
|
||||
|
||||
class TestStreamingSpeechWebSocket:
|
||||
def test_non_streaming_single_frame(self):
|
||||
app, speech_service = _build_test_app()
|
||||
def test_non_streaming_single_frame(self, mocker: MockerFixture):
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -68,13 +75,13 @@ class TestStreamingSpeechWebSocket:
|
||||
|
||||
assert speech_service._generate_audio_bytes.await_count == 1
|
||||
|
||||
def test_streaming_multiple_binary_frames(self):
|
||||
def test_streaming_multiple_binary_frames(self, mocker: MockerFixture):
|
||||
captured_requests = []
|
||||
|
||||
speech_service = MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = AsyncMock(return_value=(b"", "audio/wav"))
|
||||
speech_service.engine_client = MagicMock()
|
||||
speech_service.engine_client.abort = AsyncMock()
|
||||
speech_service = mocker.MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = mocker.AsyncMock(return_value=(b"", "audio/wav"))
|
||||
speech_service.engine_client = mocker.MagicMock()
|
||||
speech_service.engine_client.abort = mocker.AsyncMock()
|
||||
|
||||
async def mock_prepare_speech_generation(request):
|
||||
captured_requests.append(request)
|
||||
@@ -123,8 +130,8 @@ class TestStreamingSpeechWebSocket:
|
||||
assert captured_requests[0].initial_codec_chunk_frames == 12
|
||||
assert speech_service._generate_audio_bytes.await_count == 0
|
||||
|
||||
def test_flush_on_input_done(self):
|
||||
app, _ = _build_test_app()
|
||||
def test_flush_on_input_done(self, mocker: MockerFixture):
|
||||
app, _ = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -142,8 +149,8 @@ class TestStreamingSpeechWebSocket:
|
||||
}
|
||||
assert ws.receive_json() == {"type": "session.done", "total_sentences": 1}
|
||||
|
||||
def test_invalid_streaming_config(self):
|
||||
app, _ = _build_test_app()
|
||||
def test_invalid_streaming_config(self, mocker: MockerFixture):
|
||||
app, _ = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -159,8 +166,8 @@ class TestStreamingSpeechWebSocket:
|
||||
assert error["type"] == "error"
|
||||
assert "response_format='pcm'" in error["message"]
|
||||
|
||||
def test_empty_input_text_emits_no_audio(self):
|
||||
app, speech_service = _build_test_app()
|
||||
def test_empty_input_text_emits_no_audio(self, mocker: MockerFixture):
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -172,8 +179,8 @@ class TestStreamingSpeechWebSocket:
|
||||
|
||||
assert speech_service._generate_audio_bytes.await_count == 0
|
||||
|
||||
def test_multiple_sentences_increment_indices(self):
|
||||
app, _ = _build_test_app()
|
||||
def test_multiple_sentences_increment_indices(self, mocker: MockerFixture):
|
||||
app, _ = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -203,8 +210,8 @@ class TestStreamingSpeechWebSocket:
|
||||
ws.send_json({"type": "input.done"})
|
||||
assert ws.receive_json() == {"type": "session.done", "total_sentences": 2}
|
||||
|
||||
def test_unknown_message_type_keeps_session_open(self):
|
||||
app, _ = _build_test_app()
|
||||
def test_unknown_message_type_keeps_session_open(self, mocker: MockerFixture):
|
||||
app, _ = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -227,21 +234,21 @@ class TestStreamingSpeechWebSocket:
|
||||
ws.send_json({"type": "input.done"})
|
||||
assert ws.receive_json() == {"type": "session.done", "total_sentences": 1}
|
||||
|
||||
def test_config_timeout_closes_session(self):
|
||||
app, _ = _build_test_app(config_timeout=0.01)
|
||||
def test_config_timeout_closes_session(self, mocker: MockerFixture):
|
||||
app, _ = _build_test_app(config_timeout=0.01, mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
error = ws.receive_json()
|
||||
assert error == {"type": "error", "message": "Timeout waiting for session.config"}
|
||||
|
||||
def test_generation_error_marks_audio_done(self):
|
||||
speech_service = MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = AsyncMock(side_effect=RuntimeError("boom"))
|
||||
speech_service._prepare_speech_generation = AsyncMock(return_value=("req-err", object(), {}))
|
||||
speech_service._generate_pcm_chunks = AsyncMock()
|
||||
speech_service.engine_client = MagicMock()
|
||||
speech_service.engine_client.abort = AsyncMock()
|
||||
def test_generation_error_marks_audio_done(self, mocker: MockerFixture):
|
||||
speech_service = mocker.MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = mocker.AsyncMock(side_effect=RuntimeError("boom"))
|
||||
speech_service._prepare_speech_generation = mocker.AsyncMock(return_value=("req-err", object(), {}))
|
||||
speech_service._generate_pcm_chunks = mocker.AsyncMock()
|
||||
speech_service.engine_client = mocker.MagicMock()
|
||||
speech_service.engine_client.abort = mocker.AsyncMock()
|
||||
app, _ = _build_test_app(speech_service)
|
||||
|
||||
with TestClient(app) as client:
|
||||
@@ -256,12 +263,12 @@ class TestStreamingSpeechWebSocket:
|
||||
ws.send_json({"type": "input.done"})
|
||||
assert ws.receive_json() == {"type": "session.done", "total_sentences": 1}
|
||||
|
||||
def test_streaming_generation_error_marks_audio_done(self):
|
||||
speech_service = MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = AsyncMock(return_value=(b"", "audio/wav"))
|
||||
speech_service._prepare_speech_generation = AsyncMock(return_value=("req-stream-err", object(), {}))
|
||||
speech_service.engine_client = MagicMock()
|
||||
speech_service.engine_client.abort = AsyncMock()
|
||||
def test_streaming_generation_error_marks_audio_done(self, mocker: MockerFixture):
|
||||
speech_service = mocker.MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = mocker.AsyncMock(return_value=(b"", "audio/wav"))
|
||||
speech_service._prepare_speech_generation = mocker.AsyncMock(return_value=("req-stream-err", object(), {}))
|
||||
speech_service.engine_client = mocker.MagicMock()
|
||||
speech_service.engine_client.abort = mocker.AsyncMock()
|
||||
|
||||
async def mock_generate_pcm_chunks(_generator, _request_id):
|
||||
yield b"\x01\x02"
|
||||
@@ -298,8 +305,8 @@ class TestStreamingSpeechWebSocket:
|
||||
ws.send_json({"type": "input.done"})
|
||||
assert ws.receive_json() == {"type": "session.done", "total_sentences": 1}
|
||||
|
||||
def test_invalid_input_text_type_returns_validation_error(self):
|
||||
app, speech_service = _build_test_app()
|
||||
def test_invalid_input_text_type_returns_validation_error(self, mocker: MockerFixture):
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -316,9 +323,9 @@ class TestStreamingSpeechWebSocket:
|
||||
|
||||
assert speech_service._generate_audio_bytes.await_count == 0
|
||||
|
||||
def test_input_text_message_too_large(self, monkeypatch):
|
||||
def test_input_text_message_too_large(self, monkeypatch, mocker: MockerFixture):
|
||||
monkeypatch.setattr(streaming_speech_module, "_MAX_INPUT_TEXT_MESSAGE_SIZE", 32)
|
||||
app, speech_service = _build_test_app()
|
||||
app, speech_service = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -335,9 +342,9 @@ class TestStreamingSpeechWebSocket:
|
||||
|
||||
assert speech_service._generate_audio_bytes.await_count == 0
|
||||
|
||||
def test_session_config_message_too_large(self, monkeypatch):
|
||||
def test_session_config_message_too_large(self, monkeypatch, mocker: MockerFixture):
|
||||
monkeypatch.setattr(streaming_speech_module, "_MAX_CONFIG_MESSAGE_SIZE", 64)
|
||||
app, _ = _build_test_app()
|
||||
app, _ = _build_test_app(mocker=mocker)
|
||||
|
||||
with TestClient(app) as client:
|
||||
with client.websocket_connect("/v1/audio/speech/stream") as ws:
|
||||
@@ -348,12 +355,12 @@ class TestStreamingSpeechWebSocket:
|
||||
"message": "session.config message too large",
|
||||
}
|
||||
|
||||
def test_disconnect_aborts_streaming_request(self):
|
||||
speech_service = MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = AsyncMock(return_value=(b"", "audio/wav"))
|
||||
speech_service._prepare_speech_generation = AsyncMock(return_value=("req-abort", object(), {}))
|
||||
speech_service.engine_client = MagicMock()
|
||||
speech_service.engine_client.abort = AsyncMock()
|
||||
def test_disconnect_aborts_streaming_request(self, mocker: MockerFixture):
|
||||
speech_service = mocker.MagicMock(spec=OmniOpenAIServingSpeech)
|
||||
speech_service._generate_audio_bytes = mocker.AsyncMock(return_value=(b"", "audio/wav"))
|
||||
speech_service._prepare_speech_generation = mocker.AsyncMock(return_value=("req-abort", object(), {}))
|
||||
speech_service.engine_client = mocker.MagicMock()
|
||||
speech_service.engine_client.abort = mocker.AsyncMock()
|
||||
|
||||
async def mock_generate_pcm_chunks(_generator, _request_id):
|
||||
yield b"\x01\x02"
|
||||
@@ -361,11 +368,11 @@ class TestStreamingSpeechWebSocket:
|
||||
speech_service._generate_pcm_chunks = mock_generate_pcm_chunks
|
||||
handler = OmniStreamingSpeechHandler(speech_service=speech_service)
|
||||
|
||||
websocket = MagicMock()
|
||||
websocket.send_json = AsyncMock(side_effect=[None, WebSocketDisconnect()])
|
||||
websocket.send_bytes = AsyncMock(side_effect=WebSocketDisconnect())
|
||||
websocket = mocker.MagicMock()
|
||||
websocket.send_json = mocker.AsyncMock(side_effect=[None, WebSocketDisconnect()])
|
||||
websocket.send_bytes = mocker.AsyncMock(side_effect=WebSocketDisconnect())
|
||||
|
||||
config = MagicMock()
|
||||
config = mocker.MagicMock()
|
||||
config.model = None
|
||||
config.voice = "Vivian"
|
||||
config.task_type = None
|
||||
|
||||
Reference in new issue
Block a user