Skip to content

Commit 8b5add8

Browse files
committed
Add first test of ASGI API
1 parent f0ee517 commit 8b5add8

File tree

3 files changed

+46
-7
lines changed

3 files changed

+46
-7
lines changed

tornado/asgi.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from asyncio import create_task, Future, Task
1+
from asyncio import create_task, Future, Task, wait
22
from collections.abc import Awaitable, Callable
33
from dataclasses import dataclass
44
from typing import Optional, Union
@@ -30,13 +30,12 @@ class ASGIHTTPConnection(HTTPConnection):
3030
This provides the API for sending the response.
3131
"""
3232

33-
def __init__(
34-
self, send_cb: SendCallable, context: ASGIHTTPRequestContext, task_holder: set
35-
):
33+
def __init__(self, send_cb: SendCallable, context: ASGIHTTPRequestContext):
3634
self.send_cb = send_cb
3735
self.context = context
38-
self.task_holder = task_holder
36+
self.task_holder: set[Task] = set()
3937
self._close_callback: Callable[[], None] | None = None
38+
self._request_finished: Future[None] = Future()
4039

4140
# Various tornado APIs (e.g. RequestHandler.flush()) return a Future which
4241
# application code does not need to await. The operations these represent
@@ -90,10 +89,12 @@ def finish(self) -> None:
9089
self.send_cb(
9190
{
9291
"type": "http.response.body",
92+
"body": b"",
9393
"more_body": False,
9494
}
9595
)
9696
)
97+
self._request_finished.set_result(None)
9798

9899
def set_close_callback(self, callback: Optional[Callable[[], None]]) -> None:
99100
self._close_callback = callback
@@ -103,14 +104,19 @@ def _on_connection_close(self) -> None:
103104
callback = self._close_callback
104105
self._close_callback = None
105106
callback()
107+
self._request_finished.set_result(None)
108+
109+
async def wait_finish(self) -> None:
110+
"""For the ASGI interface: wait for all input & output to finish"""
111+
await self._request_finished
112+
await wait(self.task_holder)
106113

107114

108115
class ASGIAdapter:
109116
"""Wrap a tornado application object to use with an ASGI server"""
110117

111118
def __init__(self, application: Application):
112119
self.application = application
113-
self.task_holder: set[Task] = set()
114120

115121
async def __call__(
116122
self, scope: dict, receive: ReceiveCallable, send: SendCallable
@@ -128,7 +134,7 @@ async def http_scope(
128134
ctx.address = tuple(client_addr)
129135
ctx.remote_ip = client_addr[0]
130136

131-
conn = ASGIHTTPConnection(send, ctx, self.task_holder)
137+
conn = ASGIHTTPConnection(send, ctx)
132138
req_target = scope["path"]
133139
if qs := scope["query_string"]:
134140
req_target += "?" + qs.decode("latin1")
@@ -156,3 +162,5 @@ async def http_scope(
156162
msg_delegate.on_connection_close()
157163
conn._on_connection_close()
158164
break
165+
166+
await conn.wait_finish()

tornado/test/asgi_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from async_asgi_testclient import TestClient
2+
3+
from tornado.asgi import ASGIAdapter
4+
from tornado.web import Application, RequestHandler
5+
from tornado.testing import AsyncTestCase, gen_test
6+
7+
8+
class BasicHandler(RequestHandler):
9+
def get(self):
10+
name = self.get_argument("name", "world")
11+
self.write(f"Hello, {name}")
12+
13+
14+
class AsyncASGITestCase(AsyncTestCase):
15+
def setUp(self) -> None:
16+
super().setUp()
17+
self.asgi_app = ASGIAdapter(
18+
Application(
19+
[
20+
(r"/", BasicHandler),
21+
]
22+
)
23+
)
24+
25+
@gen_test(timeout=None)
26+
async def test_basic_request(self):
27+
client = TestClient(self.asgi_app)
28+
resp = await client.get("/?name=foo")
29+
assert resp.status_code == 200
30+
assert resp.text == "Hello, foo"

tornado/test/runtests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"tornado.httputil.doctests",
2121
"tornado.iostream.doctests",
2222
"tornado.util.doctests",
23+
"tornado.test.asgi_test",
2324
"tornado.test.asyncio_test",
2425
"tornado.test.auth_test",
2526
"tornado.test.autoreload_test",

0 commit comments

Comments
 (0)