-
-
Notifications
You must be signed in to change notification settings - Fork 960
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
collect errors more reliably from websocket test client #2814
Changes from 8 commits
5442570
3978a17
8eec435
44c1bc2
c210c27
3fde98b
4d2ddbc
57701e7
3dc727d
afc65e7
6e60cda
3c8071f
2ef7a53
509b9c0
c013d14
9b016c5
82dad82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
from __future__ import annotations | ||
|
||
import contextlib | ||
import enum | ||
import inspect | ||
import io | ||
import json | ||
|
@@ -9,7 +10,6 @@ | |
import sys | ||
import typing | ||
from concurrent.futures import Future | ||
from functools import cached_property | ||
from types import GeneratorType | ||
from urllib.parse import unquote, urljoin | ||
|
||
|
@@ -85,6 +85,14 @@ class WebSocketDenialResponse( # type: ignore[misc] | |
""" | ||
|
||
|
||
class _Eof(enum.Enum): | ||
EOF = enum.auto() | ||
|
||
|
||
EOF: typing.Final = _Eof.EOF | ||
Eof = typing.Literal[_Eof.EOF] | ||
|
||
|
||
class WebSocketTestSession: | ||
def __init__( | ||
self, | ||
|
@@ -97,63 +105,53 @@ def __init__( | |
self.accepted_subprotocol = None | ||
self.portal_factory = portal_factory | ||
self._receive_queue: queue.Queue[Message] = queue.Queue() | ||
self._send_queue: queue.Queue[Message | BaseException] = queue.Queue() | ||
self._send_queue: queue.Queue[Message | Eof | BaseException] = queue.Queue() | ||
self.extra_headers = None | ||
self.should_close: anyio.Event | ||
|
||
def __enter__(self) -> WebSocketTestSession: | ||
self.exit_stack = contextlib.ExitStack() | ||
self.portal = self.exit_stack.enter_context(self.portal_factory()) | ||
with contextlib.ExitStack() as stack: | ||
self.portal = portal = stack.enter_context(self.portal_factory()) | ||
|
||
try: | ||
_: Future[None] = self.portal.start_task_soon(self._run) | ||
fut, cs = self.portal.start_task(self._run) | ||
self.send({"type": "websocket.connect"}) | ||
message = self.receive() | ||
self._raise_on_close(message) | ||
except Exception: | ||
self.exit_stack.close() | ||
raise | ||
self.accepted_subprotocol = message.get("subprotocol", None) | ||
self.extra_headers = message.get("headers", None) | ||
return self | ||
|
||
@cached_property | ||
def should_close(self) -> anyio.Event: | ||
return anyio.Event() | ||
|
||
async def _notify_close(self) -> None: | ||
self.should_close.set() | ||
self.accepted_subprotocol = message.get("subprotocol", None) | ||
self.extra_headers = message.get("headers", None) | ||
stack.callback(fut.result) | ||
stack.callback(portal.call, cs.cancel) | ||
stack.callback(self.close, 1000) | ||
self.exit_stack = stack.pop_all() | ||
return self | ||
|
||
def __exit__(self, *args: typing.Any) -> None: | ||
try: | ||
self.close(1000) | ||
finally: | ||
self.portal.start_task_soon(self._notify_close) | ||
self.exit_stack.close() | ||
while not self._send_queue.empty(): | ||
self.exit_stack.close() | ||
|
||
while True: | ||
message = self._send_queue.get() | ||
if message is EOF: | ||
break | ||
if isinstance(message, BaseException): | ||
raise message | ||
raise message # pragma: no cover (defensive, should be impossible) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why it should be impossible? The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is impossible because the exit stack will raise the exception out of fut.result() and so the queue won't be consumed. This is only possible to be hit if I'm currently sketching out another slight refactor that uses MemoryObjectStreams here instead that should clean this up a bit |
||
|
||
async def _run(self) -> None: | ||
async def _run(self, *, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None: | ||
""" | ||
The sub-thread in which the websocket session runs. | ||
""" | ||
|
||
async def run_app(tg: anyio.abc.TaskGroup) -> None: | ||
try: | ||
try: | ||
await self.app(self.scope, self._asgi_receive, self._asgi_send) | ||
except anyio.get_cancelled_exc_class(): | ||
... | ||
self.should_close = anyio.Event() | ||
graingert marked this conversation as resolved.
Show resolved
Hide resolved
|
||
with anyio.CancelScope() as cs: | ||
task_status.started(cs) | ||
await self.app(self.scope, self._asgi_receive, self._asgi_send) | ||
except BaseException as exc: | ||
self._send_queue.put(exc) | ||
raise | ||
finally: | ||
tg.cancel_scope.cancel() | ||
|
||
async with anyio.create_task_group() as tg: | ||
tg.start_soon(run_app, tg) | ||
await self.should_close.wait() | ||
tg.cancel_scope.cancel() | ||
self.should_close.set() | ||
finally: | ||
self._send_queue.put(EOF) # TODO: use self._send_queue.shutdown() on 3.13+ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's better to stick to the EOF approach until someone puts up a Queue.shutdown backport, or we can use a MemoryObjectStream with portal |
||
|
||
async def _asgi_receive(self) -> Message: | ||
while self._receive_queue.empty(): | ||
|
@@ -202,6 +200,7 @@ def close(self, code: int = 1000, reason: str | None = None) -> None: | |
|
||
def receive(self) -> Message: | ||
message = self._send_queue.get() | ||
assert message is not EOF | ||
if isinstance(message, BaseException): | ||
raise message | ||
return message | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an analogous to EOF from the standard library on 3.13?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It raises an exception