Skip to content

Commit d588e7c

Browse files
Lukasapgjones
authored andcommitted
Initial test harness for h2spec
1 parent ab5b891 commit d588e7c

File tree

3 files changed

+99
-23
lines changed

3 files changed

+99
-23
lines changed

examples/asyncio/asyncio-server.py

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
This example demonstrates handling requests with bodies, as well as handling
99
those without. In particular, it demonstrates the fact that DataReceived may
1010
be called multiple times, and that applications must handle that possibility.
11-
12-
Please note that this example does not handle flow control, and so only works
13-
properly for relatively small requests. Please see other examples to understand
14-
how flow control should work.
1511
"""
1612
import asyncio
1713
import io
@@ -23,10 +19,11 @@
2319
from h2.config import H2Configuration
2420
from h2.connection import H2Connection
2521
from h2.events import (
26-
ConnectionTerminated, DataReceived, RequestReceived, StreamEnded
22+
ConnectionTerminated, DataReceived, RequestReceived, StreamEnded,
23+
StreamReset
2724
)
2825
from h2.errors import ErrorCodes
29-
from h2.exceptions import ProtocolError
26+
from h2.exceptions import ProtocolError, StreamClosedError
3027

3128

3229
RequestData = collections.namedtuple('RequestData', ['headers', 'data'])
@@ -38,12 +35,18 @@ def __init__(self):
3835
self.conn = H2Connection(config=config)
3936
self.transport = None
4037
self.stream_data = {}
38+
self.flow_control_futures = {}
4139

4240
def connection_made(self, transport: asyncio.Transport):
4341
self.transport = transport
4442
self.conn.initiate_connection()
4543
self.transport.write(self.conn.data_to_send())
4644

45+
def connection_lost(self, exc):
46+
for future in self.flow_control_futures.values():
47+
future.cancel()
48+
self.flow_control_futures = {}
49+
4750
def data_received(self, data: bytes):
4851
try:
4952
events = self.conn.receive_data(data)
@@ -61,18 +64,15 @@ def data_received(self, data: bytes):
6164
self.stream_complete(event.stream_id)
6265
elif isinstance(event, ConnectionTerminated):
6366
self.transport.close()
67+
elif isinstance(event, StreamReset):
68+
self.stream_reset(event.stream_id)
6469

6570
self.transport.write(self.conn.data_to_send())
6671

6772
def request_received(self, headers: List[Tuple[str, str]], stream_id: int):
6873
headers = collections.OrderedDict(headers)
6974
method = headers[':method']
7075

71-
# We only support GET and POST.
72-
if method not in ('GET', 'POST'):
73-
self.return_405(headers, stream_id)
74-
return
75-
7676
# Store off the request data.
7777
request_data = RequestData(headers, io.BytesIO())
7878
self.stream_data[stream_id] = request_data
@@ -101,18 +101,7 @@ def stream_complete(self, stream_id: int):
101101
('server', 'asyncio-h2'),
102102
)
103103
self.conn.send_headers(stream_id, response_headers)
104-
self.conn.send_data(stream_id, data, end_stream=True)
105-
106-
def return_405(self, headers: List[Tuple[str, str]], stream_id: int):
107-
"""
108-
We don't support the given method, so we want to return a 405 response.
109-
"""
110-
response_headers = (
111-
(':status', '405'),
112-
('content-length', '0'),
113-
('server', 'asyncio-h2'),
114-
)
115-
self.conn.send_headers(stream_id, response_headers, end_stream=True)
104+
asyncio.ensure_future(self.send_data(data, stream_id))
116105

117106
def receive_data(self, data: bytes, stream_id: int):
118107
"""
@@ -128,6 +117,67 @@ def receive_data(self, data: bytes, stream_id: int):
128117
else:
129118
stream_data.data.write(data)
130119

120+
def stream_reset(self, stream_id):
121+
"""
122+
A stream reset was sent. Stop sending data.
123+
"""
124+
if stream_id in self.flow_control_futures:
125+
future = self.flow_control_futures.pop(stream_id)
126+
future.cancel()
127+
128+
async def send_data(self, data, stream_id):
129+
"""
130+
Send data according to the flow control rules.
131+
"""
132+
while data:
133+
while not self.conn.local_flow_control_window(stream_id):
134+
try:
135+
await self.wait_for_flow_control(stream_id)
136+
except asyncio.CancelledError:
137+
return
138+
139+
chunk_size = min(
140+
self.conn.local_flow_control_window(stream_id),
141+
len(data),
142+
self.conn.max_outbound_frame_size,
143+
)
144+
145+
try:
146+
self.conn.send_data(
147+
stream_id,
148+
data[:chunk_size],
149+
end_stream=(chunk_size == len(data))
150+
)
151+
except (StreamClosedError, ProtocolError):
152+
# The stream got closed and we didn't get told. We're done
153+
# here.
154+
break
155+
156+
self.transport.write(self.conn.data_to_send())
157+
data = data[chunk_size:]
158+
159+
async def wait_for_flow_control(self, stream_id):
160+
"""
161+
Waits for a Future that fires when the flow control window is opened.
162+
"""
163+
f = asyncio.Future()
164+
self.flow_control_futures[stream_id] = f
165+
await f
166+
167+
def window_updated(self, stream_id, delta):
168+
"""
169+
A window update frame was received. Unblock some number of flow control
170+
Futures.
171+
"""
172+
if stream_id and stream_id in self.flow_control_futures:
173+
f = self.flow_control_futures.pop(stream_id)
174+
f.set_result(delta)
175+
elif not stream_id:
176+
for f in self.flow_control_futures.values():
177+
f.set_result(delta)
178+
179+
self.flow_control_futures = {}
180+
131181

132182
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
133183
ssl_context.options |= (

test/h2spectest.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
# A test script that runs the example Python Twisted server and then runs
3+
# h2spec against it. Prints the output of h2spec. This script does not expect
4+
# to be run directly, but instead via `tox -e h2spec`.
5+
6+
set -x
7+
8+
# Kill all background jobs on exit.
9+
trap 'kill $(jobs -p)' EXIT
10+
11+
pushd examples/asyncio
12+
python asyncio-server.py &
13+
popd
14+
15+
# Wait briefly to let the server start up
16+
sleep 2
17+
18+
# Go go go!
19+
h2spec -k -t -v -p 8443

tox.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,10 @@ deps =
3939
commands =
4040
check-manifest
4141
python setup.py check --metadata --restructuredtext --strict
42+
43+
[testenv:h2spec]
44+
basepython=python3.6
45+
deps = twisted[tls]==17.1.0
46+
whitelist_externals = {toxinidir}/test/h2spectest.sh
47+
commands =
48+
{toxinidir}/test/h2spectest.sh

0 commit comments

Comments
 (0)