Files
2026-07-13 13:17:40 +08:00

124 lines
3.2 KiB
Python

"""Shared fixtures for benchmark tests."""
import asyncio
import json
import socket
import threading
from collections.abc import Generator
from typing import Any
import aiohttp.web
import pytest
@pytest.fixture()
def free_port() -> int:
"""Find and return a free TCP port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
def make_sse_chunk(data: dict[str, Any]) -> bytes:
"""Encode a dict as an SSE data line."""
return f"data: {json.dumps(data)}\n\n".encode()
async def mock_chat_handler(
request: aiohttp.web.Request,
) -> aiohttp.web.StreamResponse:
"""Mock OpenAI streaming chat completions endpoint."""
body = await request.json()
assert body["model"] is not None
assert body["stream"] is True
resp = aiohttp.web.StreamResponse(
status=200,
headers={"Content-Type": "text/event-stream"},
)
await resp.prepare(request)
words = ["this", " is", " a", " test"]
for word in words:
chunk = {
"id": "chatcmpl-test",
"object": "chat.completion.chunk",
"choices": [
{
"index": 0,
"delta": {"content": word},
"finish_reason": None,
}
],
}
await resp.write(make_sse_chunk(chunk))
finish_chunk = {
"id": "chatcmpl-test",
"object": "chat.completion.chunk",
"choices": [
{
"index": 0,
"delta": {},
"finish_reason": "stop",
}
],
}
await resp.write(make_sse_chunk(finish_chunk))
usage_chunk = {
"id": "chatcmpl-test",
"object": "chat.completion.chunk",
"choices": [],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 4,
"total_tokens": 14,
},
}
await resp.write(make_sse_chunk(usage_chunk))
await resp.write(b"data: [DONE]\n\n")
return resp
def _run_server(
loop: asyncio.AbstractEventLoop,
runner: aiohttp.web.AppRunner,
port: int,
started: threading.Event,
) -> None:
asyncio.set_event_loop(loop)
site = aiohttp.web.TCPSite(runner, "127.0.0.1", port)
loop.run_until_complete(site.start())
started.set()
loop.run_forever()
@pytest.fixture()
def mock_server(free_port: int) -> Generator[str, None, None]:
"""Start a mock SSE server in a background thread and yield its base URL."""
port = free_port
app = aiohttp.web.Application()
app.router.add_post("/v1/chat/completions", mock_chat_handler)
loop = asyncio.new_event_loop()
runner = aiohttp.web.AppRunner(app)
loop.run_until_complete(runner.setup())
started = threading.Event()
thread = threading.Thread(
target=_run_server, args=(loop, runner, port, started), daemon=True
)
thread.start()
assert started.wait(timeout=5), "Mock server failed to start"
base_url = f"http://127.0.0.1:{port}"
yield base_url
loop.call_soon_threadsafe(loop.stop)
thread.join(timeout=5)
loop.run_until_complete(runner.cleanup())
loop.close()