Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
75 changes: 60 additions & 15 deletions conformance/test/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,22 +415,64 @@ def _server_env(request: ServerCompatRequest) -> dict[str, str]:


async def _tee_to_stderr(stream: asyncio.StreamReader) -> AsyncIterator[bytes]:
try:
while True:
line = await stream.readline()
if not line:
break
print(line.decode("utf-8"), end="", file=sys.stderr) # noqa: T201
yield line
except asyncio.CancelledError:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had accidentally added this before I think. It ends up swallowing the error preventing terminate from being called on the subprocesses

pass
while True:
line = await stream.readline()
if not line:
break
print(line.decode("utf-8"), end="", file=sys.stderr) # noqa: T201
yield line


async def _consume_log(stream: AsyncIterator[bytes]) -> None:
async for _ in stream:
pass


async def serve_daphne(
request: ServerCompatRequest,
certfile: str | None,
keyfile: str | None,
cafile: str | None,
port_future: asyncio.Future[int],
):
args = []
ssl_endpoint_parts = []
if certfile:
ssl_endpoint_parts.append(f"certKey={certfile}")
if keyfile:
ssl_endpoint_parts.append(f"privateKey={keyfile}")
if cafile:
ssl_endpoint_parts.append(f"extraCertChain={cafile}")
if ssl_endpoint_parts:
args.append("-e")
args.append(f"ssl:port=0:{':'.join(ssl_endpoint_parts)}")
else:
args.append("-p=0")

args.append("server:asgi_app")

proc = await asyncio.create_subprocess_exec(
"daphne",
*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)
try:
async for line in stdout:
if b"Listening on TCP address" in line:
port = line.decode("utf-8").strip().rsplit(":", 1)[1]
port_future.set_result(int(port))
break
await _consume_log(stdout)
except asyncio.CancelledError:
proc.terminate()
await proc.wait()


async def serve_granian(
request: ServerCompatRequest,
mode: Literal["sync", "async"],
Expand Down Expand Up @@ -465,7 +507,6 @@ async def serve_granian(
*args,
stderr=asyncio.subprocess.STDOUT,
stdout=asyncio.subprocess.PIPE,
limit=1024,
env=_server_env(request),
)
stdout = proc.stdout
Expand Down Expand Up @@ -506,7 +547,6 @@ async def serve_gunicorn(
*args,
stderr=asyncio.subprocess.STDOUT,
stdout=asyncio.subprocess.PIPE,
limit=1024,
env=_server_env(request),
)
stdout = proc.stdout
Expand Down Expand Up @@ -551,7 +591,6 @@ async def serve_hypercorn(
*args,
stderr=asyncio.subprocess.STDOUT,
stdout=asyncio.subprocess.PIPE,
limit=1024,
env=_server_env(request),
)
stdout = proc.stdout
Expand Down Expand Up @@ -592,7 +631,6 @@ async def serve_uvicorn(
*args,
stderr=asyncio.subprocess.STDOUT,
stdout=asyncio.subprocess.PIPE,
limit=1024,
env=_server_env(request),
)
stdout = proc.stdout
Expand All @@ -619,14 +657,14 @@ def _find_free_port():

class Args(argparse.Namespace):
mode: Literal["sync", "async"]
server: Literal["granian", "hypercorn", "uvicorn"]
server: Literal["daphne", "granian", "hypercorn", "uvicorn"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should this also have "gunicorn" in the list?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess there's something causing pyright to not flag this



async def main() -> None:
parser = argparse.ArgumentParser(description="Conformance server")
parser.add_argument("--mode", choices=["sync", "async"])
parser.add_argument(
"--server", choices=["granian", "gunicorn", "hypercorn", "uvicorn"]
"--server", choices=["daphne", "granian", "gunicorn", "hypercorn", "uvicorn"]
)
args = parser.parse_args(namespace=Args())

Expand Down Expand Up @@ -663,6 +701,13 @@ async def main() -> None:
with cleanup:
port_future: asyncio.Future[int] = asyncio.get_event_loop().create_future()
match args.server:
case "daphne":
if args.mode == "sync":
msg = "daphne does not support sync mode"
raise ValueError(msg)
serve_task = asyncio.create_task(
serve_daphne(request, certfile, keyfile, cafile, port_future)
)
case "granian":
serve_task = asyncio.create_task(
serve_granian(
Expand Down
12 changes: 11 additions & 1 deletion conformance/test/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,23 @@ def test_server_sync(server: str) -> None:
pytest.fail(f"\n{result.stdout}\n{result.stderr}")


@pytest.mark.parametrize("server", ["granian", "hypercorn", "uvicorn"])
@pytest.mark.parametrize("server", ["daphne", "granian", "hypercorn", "uvicorn"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should "daphne" be added to test_server_sync, above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Daphne doesn't support wsgi so only async.

def test_server_async(server: str) -> None:
args = maybe_patch_args_with_debug(
[sys.executable, _server_py_path, "--mode", "async", "--server", server]
)
opts = []
match server:
case "daphne":
opts = [
# daphne doesn't support h2c
"--skip",
"**/HTTPVersion:2/**/TLS:false/**",
# daphne seems to block on the request body so can't do full duplex even with h2,
# it only works with websockets
"--skip",
"**/full-duplex/**",
]
case "granian" | "hypercorn":
# granian and hypercorn seem to have issues with concurrency
opts = ["--parallel", "1"]
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dev = [
"asgiref==3.9.1",
"brotli==1.1.0",
"connect-python-example",
"daphne==4.2.1",
"httpx[http2]==0.28.1",
"hypercorn==0.17.3",
"granian==2.5.5",
Expand All @@ -53,6 +54,7 @@ dev = [
"pytest-cov==7.0.0",
"ruff~=0.13.2",
"uvicorn==0.37.0",
"Twisted[tls,http2]==25.5.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does daphne dep on a lower version of twisted? (oh, ah, I guess it's for http2 support: https://github.com/django/daphne?tab=readme-ov-file#http2-support)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add a comment

"typing_extensions==4.15.0",
"zstandard==0.25.0",
]
Expand Down
Loading