From d48f9df9fe7436b52e3254f9200fa91d04144758 Mon Sep 17 00:00:00 2001 From: Daniel Reed Date: Tue, 29 Oct 2024 14:09:22 -0700 Subject: [PATCH] Intercept the call to self.c.receive_data in MockConnection.read to make recently received client events assertable. Document the complete connect/request/await/close cycle tested in test_mock. See #1. --- nh2/connection.py | 5 ++++- nh2/mock.py | 22 ++++++++++++++++++++-- nh2/test_mock.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/nh2/connection.py b/nh2/connection.py index 3319049..dc27757 100644 --- a/nh2/connection.py +++ b/nh2/connection.py @@ -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) @@ -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.""" diff --git a/nh2/mock.py b/nh2/mock.py index 53ba30e..425af86 100644 --- a/nh2/mock.py +++ b/nh2/mock.py @@ -28,6 +28,8 @@ 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: @@ -35,6 +37,12 @@ async def _connect(self, 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.""" @@ -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')) @@ -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.""" @@ -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): diff --git a/nh2/test_mock.py b/nh2/test_mock.py index e9c08ee..38d34fc 100644 --- a/nh2/test_mock.py +++ b/nh2/test_mock.py @@ -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] @@ -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= 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():