From a80c1b134c79fded5accecae12ebca60c8ad632f Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 14 Jun 2016 10:47:36 +0100 Subject: [PATCH 1/3] Add possible related events. --- h2/events.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/h2/events.py b/h2/events.py index e749101b2..21e57c5be 100644 --- a/h2/events.py +++ b/h2/events.py @@ -23,6 +23,9 @@ class RequestReceived(object): .. versionchanged:: 2.3.0 Changed the type of ``headers`` to :class:`HeaderTuple `. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``stream_ended`` and ``priority_updated`` properties. """ def __init__(self): #: The Stream ID for the stream this request was made on. @@ -31,6 +34,20 @@ def __init__(self): #: The request headers. self.headers = None + #: If this request also ended the stream, the associated + #: :class:`StreamEnded ` event will be available + #: here. + #: + #: .. versionadded:: 2.4.0 + self.stream_ended = None + + #: If this request also had associated priority information, the + #: associated :class:`PriorityUpdated ` + #: event will be available here. + #: + #: .. versionadded:: 2.4.0 + self.priority_updated = None + def __repr__(self): return "" % ( self.stream_id, self.headers @@ -46,6 +63,9 @@ class ResponseReceived(object): .. versionchanged:: 2.3.0 Changed the type of ``headers`` to :class:`HeaderTuple `. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``stream_ended`` and ``priority_updated`` properties. """ def __init__(self): #: The Stream ID for the stream this response was made on. @@ -54,6 +74,20 @@ def __init__(self): #: The response headers. self.headers = None + #: If this response also ended the stream, the associated + #: :class:`StreamEnded ` event will be available + #: here. + #: + #: .. versionadded:: 2.4.0 + self.stream_ended = None + + #: If this response also had associated priority information, the + #: associated :class:`PriorityUpdated ` + #: event will be available here. + #: + #: .. versionadded:: 2.4.0 + self.priority_updated = None + def __repr__(self): return "" % ( self.stream_id, self.headers @@ -72,6 +106,9 @@ class TrailersReceived(object): .. versionchanged:: 2.3.0 Changed the type of ``headers`` to :class:`HeaderTuple `. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``stream_ended`` and ``priority_updated`` properties. """ def __init__(self): #: The Stream ID for the stream on which these trailers were received. @@ -80,6 +117,19 @@ def __init__(self): #: The trailers themselves. self.headers = None + #: Trailers always end streams. This property has the associated + #: :class:`StreamEnded ` in it. + #: + #: .. versionadded:: 2.4.0 + self.stream_ended = None + + #: If the trailers also set associated priority information, the + #: associated :class:`PriorityUpdated ` + #: event will be available here. + #: + #: .. versionadded:: 2.4.0 + self.priority_updated = None + def __repr__(self): return "" % ( self.stream_id, self.headers @@ -104,6 +154,9 @@ class InformationalResponseReceived(object): .. versionchanged:: 2.3.0 Changed the type of ``headers`` to :class:`HeaderTuple `. This has no effect on current users. + + .. versionchanged:: 2.4.0 + Added ``priority_updated`` property. """ def __init__(self): #: The Stream ID for the stream this informational response was made @@ -113,6 +166,13 @@ def __init__(self): #: The headers for this informational response. self.headers = None + #: If this response also had associated priority information, the + #: associated :class:`PriorityUpdated ` + #: event will be available here. + #: + #: .. versionadded:: 2.4.0 + self.priority_updated = None + def __repr__(self): return "" % ( self.stream_id, self.headers @@ -138,6 +198,13 @@ def __init__(self): #: than ``len(data)``. self.flow_controlled_length = None + #: If this data chunk also completed the stream, the associated + #: :class:`StreamEnded ` event will be available + #: here. + #: + #: .. versionadded:: 2.4.0 + self.stream_ended = None + def __repr__(self): return ( " Date: Tue, 14 Jun 2016 11:20:48 +0100 Subject: [PATCH 2/3] Store related events as they occur. --- h2/connection.py | 1 + h2/stream.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/h2/connection.py b/h2/connection.py index 9974942b9..b79a30421 100644 --- a/h2/connection.py +++ b/h2/connection.py @@ -1402,6 +1402,7 @@ def _receive_headers_frame(self, frame): if 'PRIORITY' in frame.flags: p_frames, p_events = self._receive_priority_frame(frame) + stream_events[0].priority_updated = p_events[0] stream_events.extend(p_events) assert not p_frames diff --git a/h2/stream.py b/h2/stream.py index 60fd118cc..e2c83e331 100644 --- a/h2/stream.py +++ b/h2/stream.py @@ -901,9 +901,11 @@ def receive_headers(self, headers, end_stream, header_encoding): events = self.state_machine.process_input(input_) if end_stream: - events += self.state_machine.process_input( + es_events = self.state_machine.process_input( StreamInputs.RECV_END_STREAM ) + events[0].stream_ended = es_events[0] + events += es_events self._initialize_content_length(headers) @@ -926,9 +928,11 @@ def receive_data(self, data, end_stream, flow_control_len): self._track_content_length(len(data), end_stream) if end_stream: - events += self.state_machine.process_input( + es_events = self.state_machine.process_input( StreamInputs.RECV_END_STREAM ) + events[0].stream_ended = es_events[0] + events.extend(es_events) events[0].data = data events[0].flow_controlled_length = flow_control_len From 7fe127f23169803a955dd1195cc130588e5731a9 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Tue, 14 Jun 2016 11:20:55 +0100 Subject: [PATCH 3/3] Test related events. --- test/test_related_events.py | 367 ++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 test/test_related_events.py diff --git a/test/test_related_events.py b/test/test_related_events.py new file mode 100644 index 000000000..7552955cf --- /dev/null +++ b/test/test_related_events.py @@ -0,0 +1,367 @@ +# -*- coding: utf-8 -*- +""" +test_related_events.py +~~~~~~~~~~~~~~~~~~~~~~ + +Specific tests to validate the "related events" logic used by certain events +inside hyper-h2. +""" +import h2.connection +import h2.events + + +class TestRelatedEvents(object): + """ + Related events correlate all those events that happen on a single frame. + """ + example_request_headers = [ + (':authority', 'example.com'), + (':path', '/'), + (':scheme', 'https'), + (':method', 'GET'), + ] + + example_response_headers = [ + (':status', '200'), + ('server', 'fake-serv/0.1.0') + ] + + informational_response_headers = [ + (':status', '100'), + ('server', 'fake-serv/0.1.0') + ] + + example_trailers = [ + ('another', 'field'), + ] + + def test_request_received_related_all(self, frame_factory): + """ + RequestReceived has two possible related events: PriorityUpdated and + StreamEnded, all fired when a single HEADERS frame is received. + """ + c = h2.connection.H2Connection(client_side=False) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_request_headers, + flags=['END_STREAM', 'PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 3 + base_event = events[0] + other_events = events[1:] + + assert base_event.stream_ended in other_events + assert isinstance(base_event.stream_ended, h2.events.StreamEnded) + assert base_event.priority_updated in other_events + assert isinstance( + base_event.priority_updated, h2.events.PriorityUpdated + ) + + def test_request_received_related_priority(self, frame_factory): + """ + RequestReceived can be related to PriorityUpdated. + """ + c = h2.connection.H2Connection(client_side=False) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_request_headers, + flags=['PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + base_event = events[0] + priority_updated_event = events[1] + + assert base_event.priority_updated is priority_updated_event + assert base_event.stream_ended is None + assert isinstance( + base_event.priority_updated, h2.events.PriorityUpdated + ) + + def test_request_received_related_stream_ended(self, frame_factory): + """ + RequestReceived can be related to StreamEnded. + """ + c = h2.connection.H2Connection(client_side=False) + c.initiate_connection() + c.receive_data(frame_factory.preamble()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_request_headers, + flags=['END_STREAM'], + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + base_event = events[0] + stream_ended_event = events[1] + + assert base_event.stream_ended is stream_ended_event + assert base_event.priority_updated is None + assert isinstance(base_event.stream_ended, h2.events.StreamEnded) + + def test_response_received_related_nothing(self, frame_factory): + """ + ResponseReceived is ordinarily related to no events. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_response_headers, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 1 + base_event = events[0] + + assert base_event.stream_ended is None + assert base_event.priority_updated is None + + def test_response_received_related_all(self, frame_factory): + """ + ResponseReceived has two possible related events: PriorityUpdated and + StreamEnded, all fired when a single HEADERS frame is received. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_response_headers, + flags=['END_STREAM', 'PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 3 + base_event = events[0] + other_events = events[1:] + + assert base_event.stream_ended in other_events + assert isinstance(base_event.stream_ended, h2.events.StreamEnded) + assert base_event.priority_updated in other_events + assert isinstance( + base_event.priority_updated, h2.events.PriorityUpdated + ) + + def test_response_received_related_priority(self, frame_factory): + """ + ResponseReceived can be related to PriorityUpdated. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_response_headers, + flags=['PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + base_event = events[0] + priority_updated_event = events[1] + + assert base_event.priority_updated is priority_updated_event + assert base_event.stream_ended is None + assert isinstance( + base_event.priority_updated, h2.events.PriorityUpdated + ) + + def test_response_received_related_stream_ended(self, frame_factory): + """ + ResponseReceived can be related to StreamEnded. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_response_headers, + flags=['END_STREAM'], + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + base_event = events[0] + stream_ended_event = events[1] + + assert base_event.stream_ended is stream_ended_event + assert base_event.priority_updated is None + assert isinstance(base_event.stream_ended, h2.events.StreamEnded) + + def test_trailers_received_related_all(self, frame_factory): + """ + TrailersReceived has two possible related events: PriorityUpdated and + StreamEnded, all fired when a single HEADERS frame is received. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + f = frame_factory.build_headers_frame( + headers=self.example_response_headers, + ) + c.receive_data(f.serialize()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_trailers, + flags=['END_STREAM', 'PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 3 + base_event = events[0] + other_events = events[1:] + + assert base_event.stream_ended in other_events + assert isinstance(base_event.stream_ended, h2.events.StreamEnded) + assert base_event.priority_updated in other_events + assert isinstance( + base_event.priority_updated, h2.events.PriorityUpdated + ) + + def test_trailers_received_related_stream_ended(self, frame_factory): + """ + TrailersReceived can be related to StreamEnded by itself. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + f = frame_factory.build_headers_frame( + headers=self.example_response_headers, + ) + c.receive_data(f.serialize()) + + input_frame = frame_factory.build_headers_frame( + headers=self.example_trailers, + flags=['END_STREAM'], + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + base_event = events[0] + stream_ended_event = events[1] + + assert base_event.stream_ended is stream_ended_event + assert base_event.priority_updated is None + assert isinstance(base_event.stream_ended, h2.events.StreamEnded) + + def test_informational_response_related_nothing(self, frame_factory): + """ + InformationalResponseReceived in the standard case is related to + nothing. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.informational_response_headers, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 1 + base_event = events[0] + + assert base_event.priority_updated is None + + def test_informational_response_received_related_all(self, frame_factory): + """ + InformationalResponseReceived has one possible related event: + PriorityUpdated, fired when a single HEADERS frame is received. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + input_frame = frame_factory.build_headers_frame( + headers=self.informational_response_headers, + flags=['PRIORITY'], + stream_weight=15, + depends_on=0, + exclusive=False, + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + base_event = events[0] + priority_updated_event = events[1] + + assert base_event.priority_updated is priority_updated_event + assert isinstance( + base_event.priority_updated, h2.events.PriorityUpdated + ) + + def test_data_received_normally_relates_to_nothing(self, frame_factory): + """ + A plain DATA frame leads to DataReceieved with no related events. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + f = frame_factory.build_headers_frame( + headers=self.example_response_headers, + ) + c.receive_data(f.serialize()) + + input_frame = frame_factory.build_data_frame( + data=b'some data', + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 1 + base_event = events[0] + + assert base_event.stream_ended is None + + def test_data_received_related_stream_ended(self, frame_factory): + """ + DataReceived can be related to StreamEnded by itself. + """ + c = h2.connection.H2Connection() + c.initiate_connection() + c.send_headers(stream_id=1, headers=self.example_request_headers) + + f = frame_factory.build_headers_frame( + headers=self.example_response_headers, + ) + c.receive_data(f.serialize()) + + input_frame = frame_factory.build_data_frame( + data=b'some data', + flags=['END_STREAM'], + ) + events = c.receive_data(input_frame.serialize()) + + assert len(events) == 2 + base_event = events[0] + stream_ended_event = events[1] + + assert base_event.stream_ended is stream_ended_event + assert isinstance(base_event.stream_ended, h2.events.StreamEnded)