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
9 changes: 7 additions & 2 deletions .github/workflows/test-example-collection.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
paths:
- "examples/mcp_server/**"
- "examples/openai_api/**"
- "examples/industrial_data_pretraining/fun_asr_nano/client_test.py"
- "tests/test_streaming_client_collection.py"
- "tests/test_example_smoke_collection.py"
- "tests/test_mcp_server_example.py"
- "tests/test_openai_api_docker_validation.py"
Expand All @@ -14,6 +16,8 @@ on:
paths:
- "examples/mcp_server/**"
- "examples/openai_api/**"
- "examples/industrial_data_pretraining/fun_asr_nano/client_test.py"
- "tests/test_streaming_client_collection.py"
- "tests/test_example_smoke_collection.py"
- "tests/test_mcp_server_example.py"
- "tests/test_openai_api_docker_validation.py"
Expand All @@ -37,14 +41,15 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install test runner
run: python -m pip install pytest
- name: Install test runner and client dependencies
run: python -m pip install pytest numpy soundfile 'websockets>=10,<16'
- name: Check collection and standalone CLI compatibility
run: |
git rev-parse HEAD > source-commit.txt
python -m pip freeze > resolved-dependencies.txt
python -m pytest -q \
tests/test_example_smoke_collection.py \
tests/test_streaming_client_collection.py \
tests/test_mcp_server_example.py \
tests/test_openai_api_docker_validation.py \
--junitxml=example-results.xml
Expand Down
12 changes: 6 additions & 6 deletions examples/industrial_data_pretraining/fun_asr_nano/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def load_audio(file_path):
return audio.astype(np.float32)


async def test_basic(server, audio, hotwords=None):
async def check_basic(server, audio, hotwords=None):
"""Test basic streaming ASR flow."""
print("=" * 60)
print("TEST: Basic Streaming ASR")
Expand Down Expand Up @@ -136,7 +136,7 @@ async def test_basic(server, audio, hotwords=None):
return True


async def test_empty_audio(server):
async def check_empty_audio(server):
"""Test with very short/empty audio."""
print("\n" + "=" * 60)
print("TEST: Empty/Short Audio Handling")
Expand Down Expand Up @@ -167,7 +167,7 @@ async def test_empty_audio(server):
return True


async def test_multiple_sessions(server, audio):
async def check_multiple_sessions(server, audio):
"""Test multiple consecutive sessions on same connection."""
print("\n" + "=" * 60)
print("TEST: Multiple Sessions")
Expand Down Expand Up @@ -210,9 +210,9 @@ async def run_tests(args):
failed = 0

tests = [
("Basic Streaming", lambda: test_basic(args.server, audio, args.hotwords)),
("Empty Audio", lambda: test_empty_audio(args.server)),
("Multiple Sessions", lambda: test_multiple_sessions(args.server, audio)),
("Basic Streaming", lambda: check_basic(args.server, audio, args.hotwords)),
("Empty Audio", lambda: check_empty_audio(args.server)),
("Multiple Sessions", lambda: check_multiple_sessions(args.server, audio)),
]

for name, test_fn in tests:
Expand Down
106 changes: 106 additions & 0 deletions tests/test_streaming_client_collection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Exercise the manual client CLI without a speech model or external service."""

import asyncio
import json
import os
from pathlib import Path
import shutil
import struct
import subprocess
import sys
import wave

import pytest
from websockets.legacy.server import serve


ROOT = Path(__file__).resolve().parents[1]
CLIENT = ROOT / "examples/industrial_data_pretraining/fun_asr_nano/client_test.py"


@pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"])
def test_manual_client_does_not_request_pytest_fixtures(tmp_path, import_mode):
client = tmp_path / "client_test.py"
shutil.copyfile(CLIENT, client)
sentinel = tmp_path / "test_sentinel.py"
sentinel.write_text("def test_sentinel():\n pass\n", encoding="utf-8")
env = dict(os.environ, PYTEST_DISABLE_PLUGIN_AUTOLOAD="1")
env.pop("PYTEST_ADDOPTS", None)
env.pop("PYTEST_PLUGINS", None)
result = subprocess.run(
[sys.executable, "-m", "pytest", "--collect-only", "-q",
"--import-mode=" + import_mode, str(client), str(sentinel)],
cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30,
)
assert result.returncode == 0, result.stdout + result.stderr
assert "1 test collected" in result.stdout, result.stdout
assert "test_sentinel.py::test_sentinel" in result.stdout


def test_manual_client_help_preserves_cli_arguments():
result = subprocess.run(
[sys.executable, str(CLIENT), "--help"], cwd=CLIENT.parent,
capture_output=True, text=True, timeout=30,
)
assert result.returncode == 0, result.stdout + result.stderr
for option in ("--server", "--file", "--hotwords"):
assert option in result.stdout


@pytest.mark.parametrize("hotwords", ["", "hello,world"])
@pytest.mark.parametrize("missing_final", [False, True])
def test_manual_client_checks_real_loopback_protocol(tmp_path, hotwords, missing_final):
audio_path = tmp_path / "short.wav"
pcm = struct.pack("<1600h", *(i % 300 for i in range(1600)))
with wave.open(str(audio_path), "wb") as audio:
audio.setnchannels(1)
audio.setsampwidth(2)
audio.setframerate(16000)
audio.writeframes(pcm)

async def run():
sessions = []
received_hotwords = []

async def handler(ws, path=None):
chunks = []
async for message in ws:
if message == "START":
chunks = []
await ws.send(json.dumps({"event": "started"}))
elif isinstance(message, bytes):
chunks.append(message)
elif message.startswith("HOTWORDS:"):
received_hotwords.append(message[9:])
await ws.send(json.dumps({"event": "hotwords_set", "hotwords": hotwords.split(",")}))
elif message == "STOP":
sessions.append(b"".join(chunks))
if not missing_final:
await ws.send(json.dumps({"is_final": True, "sentences": [
{"text": "local protocol", "start": 0, "end": 100},
]}))
await ws.send(json.dumps({"event": "stopped"}))
else:
raise AssertionError("Unexpected client command: " + message)

async with serve(handler, "127.0.0.1", 0) as server:
port = server.sockets[0].getsockname()[1]
command = [sys.executable, str(CLIENT), "--server", f"ws://127.0.0.1:{port}",
"--file", str(audio_path), "--hotwords", hotwords]
process = await asyncio.create_subprocess_exec(
*command, cwd=CLIENT.parent,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=20)
finally:
if process.returncode is None:
process.kill()
await process.wait()
return process.returncode, stdout.decode(), stderr.decode(), sessions, received_hotwords

code, output, errors, sessions, received_hotwords = asyncio.run(run())
assert code == (1 if missing_final else 0), output + errors
assert ("RESULTS: 0 passed, 3 failed" if missing_final else "RESULTS: 3 passed, 0 failed") in output
assert sessions == ([pcm, b"\0" * 1600, pcm] if missing_final else [pcm, b"\0" * 1600, pcm, pcm])
assert received_hotwords == ([hotwords] if hotwords else [])
Loading