diff --git a/conformance/test/client.py b/conformance/test/client.py index b27a601..8e89524 100644 --- a/conformance/test/client.py +++ b/conformance/test/client.py @@ -125,6 +125,8 @@ def pyqwest_client_kwargs(test_request: ClientCompatRequest) -> dict: kwargs["http_version"] = PyQwestHTTPVersion.HTTP1 case HTTPVersion.HTTP_VERSION_2: kwargs["http_version"] = PyQwestHTTPVersion.HTTP2 + case HTTPVersion.HTTP_VERSION_3: + kwargs["http_version"] = PyQwestHTTPVersion.HTTP3 if test_request.server_tls_cert: kwargs["tls_ca_cert"] = test_request.server_tls_cert if test_request.HasField("client_tls_creds"): diff --git a/conformance/test/config.yaml b/conformance/test/config.yaml index 91d2cc2..6c6225f 100644 --- a/conformance/test/config.yaml +++ b/conformance/test/config.yaml @@ -2,6 +2,7 @@ features: versions: - HTTP_VERSION_1 - HTTP_VERSION_2 + - HTTP_VERSION_3 protocols: - PROTOCOL_CONNECT - PROTOCOL_GRPC diff --git a/conformance/test/server.py b/conformance/test/server.py index c7ecbd9..ef39ecc 100644 --- a/conformance/test/server.py +++ b/conformance/test/server.py @@ -2,6 +2,7 @@ import argparse import asyncio +import contextlib import os import re import signal @@ -10,6 +11,7 @@ import sys import time from contextlib import ExitStack, closing +from pathlib import Path from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, Literal, TypeVar, get_args @@ -17,6 +19,7 @@ import _cov_embed # noqa: F401 from _util import create_standard_streams from gen.connectrpc.conformance.v1.config_pb2 import Code as ConformanceCode +from gen.connectrpc.conformance.v1.config_pb2 import HTTPVersion from gen.connectrpc.conformance.v1.server_compat_pb2 import ( ServerCompatRequest, ServerCompatResponse, @@ -43,6 +46,7 @@ UnaryResponseDefinition, ) from google.protobuf.any_pb2 import Any +from pyvoy import PyvoyServer from connectrpc.code import Code from connectrpc.compression.brotli import BrotliCompression @@ -635,40 +639,47 @@ async def serve_pyvoy( cafile: str | None, port_future: asyncio.Future[int], ): - args = ["--port=0"] - if certfile: - args.append(f"--tls-cert={certfile}") - if keyfile: - args.append(f"--tls-key={keyfile}") - if cafile: - args.append(f"--tls-ca-cert={cafile}") + tls_cert = Path(certfile) if certfile else None + tls_key = Path(keyfile) if keyfile else None + tls_ca_cert = Path(cafile) if cafile else None + tls_port = 0 if tls_key else None if mode == "sync": - args.append("--interface=wsgi") - args.append("server:wsgi_app") + interface = "wsgi" + app = "server:wsgi_app" else: - args.append("server:asgi_app") - - proc = await asyncio.create_subprocess_exec( - "pyvoy", - *args, - stderr=asyncio.subprocess.STDOUT, - stdout=asyncio.subprocess.PIPE, - env=_server_env(request), - ) - stdout = proc.stdout - assert stdout is not None - stdout = _tee_to_stderr(stdout) + interface = "asgi" + app = "server:asgi_app" + + async def start(): + # Currently explicit env not accepted but it's safe to set it here. + os.environ["READ_MAX_BYTES"] = str(request.message_receive_limit) + async with PyvoyServer( + app, + port=0, + tls_port=tls_port, + interface=interface, + tls_key=tls_key, + tls_cert=tls_cert, + tls_ca_cert=tls_ca_cert, + ) as server: + if ( + request.http_version == HTTPVersion.HTTP_VERSION_3 + and server.listener_port_quic + ): + port = server.listener_port_quic + else: + port = server.listener_port_tls or server.listener_port + port_future.set_result(port) + await asyncio.Future() # run until cancelled + + task = asyncio.create_task(start()) try: - async for line in stdout: - if b"listening on" in line: - port = int(line.strip().split(b"127.0.0.1:")[1]) - port_future.set_result(port) - break - await _consume_log(stdout) + await task except asyncio.CancelledError: - proc.terminate() - await proc.wait() + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task async def serve_uvicorn( diff --git a/conformance/test/test_server.py b/conformance/test/test_server.py index 4670593..a38ac35 100644 --- a/conformance/test/test_server.py +++ b/conformance/test/test_server.py @@ -51,8 +51,8 @@ def test_server_sync(server: str, cov: Coverage) -> None: opts = [] match server: case "gunicorn": - # gunicorn doesn't support HTTP/2 - opts = ["--skip", "**/HTTPVersion:2/**"] + # gunicorn doesn't support HTTP/2 or 3 + opts = ["--skip", "**/HTTPVersion:2/**", "--skip", "**/HTTPVersion:3/**"] result = subprocess.run( [ @@ -89,6 +89,9 @@ def test_server_async(server: str, cov: Coverage) -> None: # daphne doesn't support h2c "--skip", "**/HTTPVersion:2/**/TLS:false/**", + # daphne doesn't support HTTP/3 + "--skip", + "**/HTTPVersion:3/**", # daphne seems to block on the request body so can't do full duplex even with h2, # it only works with websockets "--skip", @@ -102,8 +105,8 @@ def test_server_async(server: str, cov: Coverage) -> None: "gRPC Unexpected Requests/**", ] case "uvicorn": - # uvicorn doesn't support HTTP/2 - opts = ["--skip", "**/HTTPVersion:2/**"] + # uvicorn doesn't support HTTP/2 or 3 + opts = ["--skip", "**/HTTPVersion:2/**", "--skip", "**/HTTPVersion:3/**"] result = subprocess.run( [ "go",