Skip to content

Commit

Permalink
Intercept the call to self.c.receive_data in MockConnection.read to m…
Browse files Browse the repository at this point in the history
…ake recently received client events assertable.

Document the complete connect/request/await/close cycle tested in test_mock.

See #1.
  • Loading branch information
nmlorg committed Oct 29, 2024
1 parent 7525d95 commit d48f9df
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 3 deletions.
5 changes: 4 additions & 1 deletion nh2/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async def read(self):
return ()

async with self.h2_lock:
for event in self.c.receive_data(data):
for event in self._receive_data(data):
if isinstance(event, h2.events.DataReceived):
# Update flow control so the server doesn't starve us.
self.c.acknowledge_received_data(event.flow_controlled_length, event.stream_id)
Expand All @@ -85,6 +85,9 @@ async def read(self):
live_request.ended()
await self.flush()

def _receive_data(self, data):
return self.c.receive_data(data)

async def flush(self):
"""Send any pending data to the server."""

Expand Down
22 changes: 20 additions & 2 deletions nh2/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,21 @@ async def expect_connect(host, port, *, live=False):
class MockConnection(nh2.connection.Connection):
"""An HTTP/2 client that connects to a previously prepared MockServer."""

mock_server = None

async def _connect(self, host, port):
mock_server = _servers.pop((host, port))
if mock_server is True:
return await super()._connect(host, port)
self.mock_server = mock_server
return self.mock_server.client_pipe_end

def _receive_data(self, data):
events = super()._receive_data(data)
if self.mock_server:
self.mock_server.client_events.append(events)
return events


class MockServer:
"""An HTTP/2 server connection."""
Expand All @@ -48,6 +56,7 @@ async def __init__(self, host, port):
self.host = host
self.port = port
self.client_pipe_end, self.s = nh2.anyio_util.create_pipe()
self.client_events = []

self.c = h2.connection.H2Connection(
config=h2.config.H2Configuration(client_side=False, header_encoding='utf8'))
Expand All @@ -66,7 +75,16 @@ async def read(self):
await self.flush()
if not events:
return ''
return _DedentingString(_format(events).strip())
return _DedentingString(_format(events))

def get_client_events(self):
"""Return a string representing any h2 events recently received by the connected client."""

events = self.client_events
if not events:
return ''
self.client_events = []
return _DedentingString(_format(events))

async def flush(self):
"""Send any pending data to the client."""
Expand All @@ -76,7 +94,7 @@ async def flush(self):


def _format(obj):
return ''.join(_do_format(obj, 0))
return ''.join(_do_format(obj, 0)).strip()


def _simple(obj):
Expand Down
36 changes: 36 additions & 0 deletions nh2/test_mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ async def test_simple():

mock_server = await nh2.mock.expect_connect('example.com', 443)
conn = await nh2.connection.Connection('example.com', 443)
# On connect, client and server each send settings, but client does not block to receive them.
assert mock_server.get_client_events() == ''
assert await mock_server.read() == """
- [RemoteSettingsChanged header_table_size=4096 enable_push=1 initial_window_size=65535 max_frame_size=16384 enable_connect_protocol=0 max_concurrent_streams=100 max_header_list_size=65536]
"""
# Server sends an acknowledgment of client's settings, but client still does not receive it.
assert mock_server.get_client_events() == ''

# On request, client sends request, data, and stream-end frames:
live_request = await conn.request('POST', '/dummy', json={'a': 1})
assert await mock_server.read() == """
- [RequestReceived]
Expand All @@ -37,25 +42,56 @@ async def test_simple():
stream_ended: [StreamEnded stream_id=1]
- [StreamEnded stream_id=1]
"""
# but still does not attempt to read anything from server.
assert mock_server.get_client_events() == ''

mock_server.c.send_headers(1, [(':status', '200')])
mock_server.c.send_data(1, b'dummy response', end_stream=True)
await mock_server.flush()
assert mock_server.get_client_events() == ''

response = await live_request.wait()
# When a request is awaited, client finally begins reading and dispatching data from server in a
# loop (until it gets a stream-end for the request). Client finally sees server's on-connect
# settings (which client acks) as well as server's ack of client's on-connect settings.
assert mock_server.get_client_events() == """
-
- [RemoteSettingsChanged header_table_size=4096 enable_push=0 initial_window_size=65535 max_frame_size=16384 enable_connect_protocol=0 max_concurrent_streams=100 max_header_list_size=65536]
-
- [SettingsAcknowledged]
changed_settings: []
-
- [ResponseReceived]
stream_id: 1
headers:
- (':status', '200')
stream_ended: None
priority_updated: None
- [DataReceived]
stream_id: 1
data: b'dummy response'
flow_controlled_length: 14
stream_ended: [StreamEnded stream_id=1]
- [StreamEnded stream_id=1]
"""
assert response.body == 'dummy response'
# After the first request is awaited, server finally sees client's ack of its on-connect
# settings.
assert await mock_server.read() == """
- [SettingsAcknowledged]
changed_settings: []
"""

# Closing client sends a terminate event and then immediately closes the socket.
await conn.close()
assert await mock_server.read() == """
- [ConnectionTerminated error_code=<ErrorCodes.NO_ERROR: 0> last_stream_id=0 additional_data=None]
"""
assert mock_server.get_client_events() == ''
assert await mock_server.read() == """
SOCKET CLOSED
"""
assert mock_server.get_client_events() == ''


async def test_opaque_workflow():
Expand Down

0 comments on commit d48f9df

Please sign in to comment.