litellm/tests/integration/_support/asgi.py
Yuneng Jiang c00f1b4a5c
Some checks failed
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
test: add extension and browser integration contracts
Adds integration contracts for MCP lifecycle, protocol errors and OAuth
configuration, A2A wire versions, the OpenAI consumer path, persisted
toolsets, callback delivery, guardrail effects, configured prices, the
filtered spend ledger, and a CircleCI-owned browser flow for project
detachment, with the ASGI, browser-state, client and MCP helpers they use.
Consolidates the eleven commits previously stacked on
litellm_integration_providers onto its rebased tip
2026-09-16 13:15:53 -07:00

81 lines
3 KiB
Python

import asyncio
import logging
import queue
import socket
import threading
import time
from concurrent.futures import Future
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Final
import uvicorn
from starlette.types import ASGIApp
@contextmanager
def asgi_server(app: ASGIApp) -> Iterator[str]:
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
port: Final = listener.getsockname()[1]
server: Final = uvicorn.Server(
uvicorn.Config(
app,
host="127.0.0.1",
port=port,
lifespan="on",
log_level="warning",
timeout_keep_alive=1,
timeout_graceful_shutdown=5,
)
)
errors: Final[queue.SimpleQueue[str]] = queue.SimpleQueue()
loop_ready: Final[Future[asyncio.AbstractEventLoop]] = Future()
def serve() -> None:
with asyncio.Runner() as runner:
loop_ready.set_result(runner.get_loop())
try:
runner.run(server.serve(sockets=[listener]))
except BaseException as error:
errors.put(type(error).__name__ + ": " + str(error))
if asyncio.all_tasks(runner.get_loop()):
errors.put("Owned ASGI loop retained unfinished tasks")
worker: Final = threading.Thread(target=serve)
class Capture(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
if record.thread == worker.ident and record.levelno >= logging.ERROR:
errors.put(record.getMessage())
handler: Final = Capture()
logger: Final = logging.getLogger("uvicorn.error")
logger.addHandler(handler)
worker.start()
try:
deadline: Final = time.monotonic() + 8
while not server.started:
assert worker.is_alive() and time.monotonic() < deadline, "Owned ASGI peer failed readiness"
time.sleep(0.01)
yield f"http://127.0.0.1:{port}"
finally:
server.should_exit = True
worker.join(timeout=8)
forced: Final = worker.is_alive()
if forced:
server.force_exit = True
loop: Final = loop_ready.result(timeout=1)
def cancel_owned() -> None:
for task in asyncio.all_tasks(loop):
task.cancel()
loop.call_soon_threadsafe(cancel_owned)
worker.join(timeout=3)
logger.removeHandler(handler)
assert not worker.is_alive(), "Owned ASGI peer survived forced cleanup"
assert not forced, "Owned ASGI peer required forced cleanup"
assert not server.server_state.tasks, "Owned ASGI peer retained request tasks"
assert not server.lifespan.error_occurred and not server.lifespan.shutdown_failed
assert errors.empty(), tuple(errors.get_nowait() for _ in range(errors.qsize()))