Ctrl K
ComunidadGuías…

tests/uvicorn_thread.py

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""One way for a test to run an ASGI app on a loopback port in a daemon thread."""

from __future__ import annotations

import socket
import threading
import time

import httpx
import uvicorn


class UvicornThread(threading.Thread):
    """An ASGI app served from a daemon thread on a socket this object already owns.

    The socket is bound in ``__init__`` and handed to uvicorn, so the port is known (and reserved) before
    the server starts: a caller can put ``url`` into the app's own settings first.

    Args:
        app: The ASGI application.
        port: A specific loopback port to bind; 0 picks a free one.
        sock: An already-bound socket to serve on instead (``port`` is then ignored).
        log_level: uvicorn's log level.
        access_log: Whether uvicorn logs each request.
    """

    def __init__(
        self,
        app,
        port: int = 0,
        *,
        sock: socket.socket | None = None,
        log_level: str = "warning",
        access_log: bool = True,
    ) -> None:
        super().__init__(daemon=True)
        if sock is None:
            sock = socket.socket()
            sock.bind(("127.0.0.1", port))
        self._sock = sock
        self.port = int(sock.getsockname()[1])
        self.url = f"http://127.0.0.1:{self.port}"
        self.server = uvicorn.Server(uvicorn.Config(app, log_level=log_level, access_log=access_log))

    def run(self) -> None:
        self.server.run(sockets=[self._sock])

    def start(self, timeout_s: float = 15) -> UvicornThread:  # type: ignore[override]
        """Start serving and block until uvicorn reports it is accepting connections."""
        super().start()
        deadline = time.monotonic() + timeout_s
        while not self.server.started:
            if not self.is_alive() or time.monotonic() > deadline:
                raise RuntimeError(f"uvicorn on port {self.port} did not start within {timeout_s}s")
            time.sleep(0.01)
        return self

    def wait_until_up(self, path: str, timeout_s: float = 20) -> None:
        """Block until ``GET path`` answers; for a server whose readiness is a real endpoint."""
        deadline = time.monotonic() + timeout_s
        while time.monotonic() < deadline:
            try:
                httpx.get(f"{self.url}{path}", timeout=1)
                return
            except httpx.HTTPError:
                time.sleep(0.05)
        raise AssertionError(f"loopback server on port {self.port} did not become ready")

    def stop(self) -> None:
        self.server.should_exit = True
        self.join(timeout=10)
        self._sock.close()