From 342d283cc5458c11ac83c66eb519eb5cffca56c5 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Thu, 7 Sep 2017 13:09:57 -0700 Subject: [PATCH] add ergonomics to testing with frames --- tests/flow_control.rs | 182 ++++++++++++++++++++--------------- tests/support/src/frames.rs | 159 ++++++++++++++++++++++++++++++ tests/support/src/lib.rs | 1 + tests/support/src/prelude.rs | 9 +- 4 files changed, 269 insertions(+), 82 deletions(-) create mode 100644 tests/support/src/frames.rs diff --git a/tests/flow_control.rs b/tests/flow_control.rs index 9ff672a..a554f15 100644 --- a/tests/flow_control.rs +++ b/tests/flow_control.rs @@ -53,90 +53,120 @@ fn send_data_without_requesting_capacity() { fn release_capacity_sends_window_update() { let _ = ::env_logger::init(); - let payload = vec![0; 65_535]; + let payload = vec![0u8; 65_535]; - let mock = mock_io::Builder::new() - .handshake() - .write(&[ - // GET / - 0, 0, 16, 1, 5, 0, 0, 0, 1, 130, 135, 65, 139, 157, 41, - 172, 75, 143, 168, 233, 25, 151, 33, 233, 132, + let (io, srv) = mock::new(); + + fn make_request() -> Request<()> { + Request::builder() + .method(Method::GET) + .uri("https://http2.akamai.com/") + .body(()).unwrap() + } + + + /* // End Goal: + let mock = srv.assert_client_handshake().unwrap() + // .assert_settings ? + .and_then(|(_settings, srv)| { + srv.into_future().unwrap() + }) + .recv( + frames::headers(1) + .request(head.method, head.uri) + .eos() + ) + .send(&[ + frames::headers(1) + .response(200), + frames::data(1, &payload[0..16_384]), + frames::data(1, &payload[0..16_384]), + frames::data(1, &payload[0..16_384]), ]) - .write(frames::SETTINGS_ACK) - // Read response - .read(&[0, 0, 1, 1, 4, 0, 0, 0, 1, 0x88]) - .read(&[ - // DATA - 0, 64, 0, 0, 0, 0, 0, 0, 1, - ]) - .read(&payload[0..16_384]) - .read(&[ - // DATA - 0, 64, 0, 0, 0, 0, 0, 0, 1, - ]) - .read(&payload[16_384..16_384*2]) - .read(&[ - // DATA - 0, 64, 0, 0, 0, 0, 0, 0, 1, - ]) - .read(&payload[16_384*2..16_384*3]) - .write(&[0, 0, 4, 8, 0, 0, 0, 0, 0, 0, 0, 128, 0]) - .write(&[0, 0, 4, 8, 0, 0, 0, 0, 1, 0, 0, 128, 0]) - .read(&[ - // DATA - 0, 63, 255, 0, 1, 0, 0, 0, 1, - ]) - .read(&payload[16_384*3..16_384*4 - 1]) + .recv( + frames::window_update(0, 32_768) + ) + .recv( + frames::window_update(1, 32_768) + ) + */ - // we send a 2nd stream in order to test the window update is - // is actually written to the socket - .write(&[ - // GET / - 0, 0, 4, 1, 5, 0, 0, 0, 3, 130, 135, 190, 132, - ]) - .read(&[0, 0, 1, 1, 5, 0, 0, 0, 3, 0x88]) - .build(); + let mock = srv.assert_client_handshake().unwrap() + .and_then(|(_settings, srv)| { + srv.into_future().unwrap() + }) + .and_then(|(frame, mut srv)| { + let head = make_request().into_parts().0; + let expected = frames::headers(1) + .request(head.method, head.uri) + .fields(head.headers) + .eos() + .into(); + assert_eq!(frame.unwrap(), expected); - let mut h2 = Client::handshake(mock) - .wait().unwrap(); + let res = frames::headers(1) + .response(200) + .into(); + srv.send(res).unwrap(); + let data = frames::data(1, &payload[0..16_384]); + srv.send(data.into()).unwrap(); + let data = frames::data(1, &payload[16_384..16_384 * 2]); + srv.send(data.into()).unwrap(); + let data = frames::data(1, &payload[16_384 * 2..16_384 * 3]); + srv.send(data.into()).unwrap(); + srv.into_future().unwrap() + }) + .and_then(|(frame, srv)| { + let expected = frames::window_update(0, 32_768); + assert_eq!(frame.unwrap(), expected.into()); + srv.into_future().unwrap() + }) + .and_then(|(frame, mut srv)| { + let expected = frames::window_update(1, 32_768); + assert_eq!(frame.unwrap(), expected.into()); - let request = Request::builder() - .method(Method::GET) - .uri("https://http2.akamai.com/") - .body(()).unwrap(); + let data = frames::data(1, &payload[16_384 * 3..]) + .eos(); + srv.send(data.into()).unwrap(); + Ok(()) + }); - let mut stream = h2.request(request, true).unwrap(); + let h2 = Client::handshake(io).unwrap() + .and_then(|mut h2| { + eprintln!("h2.request"); + let request = make_request(); - // Get the response - let resp = h2.run(poll_fn(|| stream.poll_response())).unwrap(); - assert_eq!(resp.status(), StatusCode::OK); + let req = h2.request(request, true).unwrap() + .unwrap() + // Get the response + .and_then(|resp| { + assert_eq!(resp.status(), StatusCode::OK); + let body = resp.into_parts().1; + body.into_future().unwrap() + }) - // read some body to use up window size to below half - let mut body = resp.into_parts().1; - let buf = h2.run(poll_fn(|| body.poll())).unwrap().unwrap(); - assert_eq!(buf.len(), 16_384); - let buf = h2.run(poll_fn(|| body.poll())).unwrap().unwrap(); - assert_eq!(buf.len(), 16_384); - let buf = h2.run(poll_fn(|| body.poll())).unwrap().unwrap(); - assert_eq!(buf.len(), 16_384); - - // release some capacity to send a window_update - body.release_capacity(buf.len() * 2).unwrap(); - - let buf = h2.run(poll_fn(|| body.poll())).unwrap().unwrap(); - assert_eq!(buf.len(), 16_383); - - - // send a 2nd stream to force flushing of window updates - let request = Request::builder() - .method(Method::GET) - .uri("https://http2.akamai.com/") - .body(()).unwrap(); - let mut stream = h2.request(request, true).unwrap(); - let resp = h2.run(poll_fn(|| stream.poll_response())).unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - - h2.wait().unwrap(); + // read some body to use up window size to below half + .and_then(|(buf, body)| { + assert_eq!(buf.unwrap().len(), 16_384); + body.into_future().unwrap() + }) + .and_then(|(buf, body)| { + assert_eq!(buf.unwrap().len(), 16_384); + body.into_future().unwrap() + }) + .and_then(|(buf, mut body)| { + let buf = buf.unwrap(); + assert_eq!(buf.len(), 16_384); + body.release_capacity(buf.len() * 2).unwrap(); + body.into_future().unwrap() + }) + .and_then(|(buf, _)| { + assert_eq!(buf.unwrap().len(), 16_383); + Ok(()) + }); + h2.unwrap().join(req) + }); + h2.join(mock).wait().unwrap(); } #[test] diff --git a/tests/support/src/frames.rs b/tests/support/src/frames.rs new file mode 100644 index 0000000..1be4bb0 --- /dev/null +++ b/tests/support/src/frames.rs @@ -0,0 +1,159 @@ +use std::fmt; + +use bytes::{Bytes, IntoBuf}; +use http::{self, HeaderMap, HttpTryFrom}; + +use h2::frame::{self, Frame, StreamId}; +use super::SendFrame; + +pub const SETTINGS: &'static [u8] = &[0, 0, 0, 4, 0, 0, 0, 0, 0]; +pub const SETTINGS_ACK: &'static [u8] = &[0, 0, 0, 4, 1, 0, 0, 0, 0]; + +// ==== helper functions to easily construct h2 Frames ==== + +pub fn headers(id: T) -> MockHeaders +where T: Into, +{ + MockHeaders(frame::Headers::new( + id.into(), + frame::Pseudo::default(), + HeaderMap::default(), + )) +} + +pub fn data(id: T, buf: B) -> MockData +where T: Into, + B: Into, +{ + MockData(frame::Data::new(id.into(), buf.into())) +} + +pub fn window_update(id: T, sz: u32) -> frame::WindowUpdate +where T: Into, +{ + frame::WindowUpdate::new(id.into(), sz) +} + +// Headers helpers + +pub struct MockHeaders(frame::Headers); + +impl MockHeaders { + pub fn request(self, method: M, uri: U) -> Self + where M: HttpTryInto, + U: HttpTryInto, + { + let method = method.try_into().unwrap(); + let uri = uri.try_into().unwrap(); + let (id, _, fields) = self.into_parts(); + let frame = frame::Headers::new( + id, + frame::Pseudo::request(method, uri), + fields + ); + MockHeaders(frame) + } + + pub fn response(self, status: S) -> Self + where S: HttpTryInto, + { + let status = status.try_into().unwrap(); + let (id, _, fields) = self.into_parts(); + let frame = frame::Headers::new( + id, + frame::Pseudo::response(status), + fields + ); + MockHeaders(frame) + } + + pub fn fields(self, fields: HeaderMap) -> Self { + let (id, pseudo, _) = self.into_parts(); + let frame = frame::Headers::new(id, pseudo, fields); + MockHeaders(frame) + } + + pub fn eos(mut self) -> Self { + self.0.set_end_stream(); + self + } + + fn into_parts(self) -> (StreamId, frame::Pseudo, HeaderMap) { + assert!(!self.0.is_end_stream(), "eos flag will be lost"); + assert!(self.0.is_end_headers(), "unset eoh will be lost"); + let id = self.0.stream_id(); + let parts = self.0.into_parts(); + (id, parts.0, parts.1) + } +} + +impl fmt::Debug for MockHeaders { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl From for Frame { + fn from(src: MockHeaders) -> Self { + Frame::Headers(src.0) + } +} + +impl From for SendFrame { + fn from(src: MockHeaders) -> Self { + Frame::Headers(src.0) + } +} + +// Data helpers + +pub struct MockData(frame::Data); + +impl MockData { + pub fn eos(mut self) -> Self { + self.0.set_end_stream(true); + self + } +} + +impl fmt::Debug for MockData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.0, f) + } +} + +impl From for Frame { + fn from(src: MockData) -> Self { + Frame::Data(src.0) + } +} + +impl From for SendFrame { + fn from(src: MockData) -> Self { + let id = src.0.stream_id(); + let eos = src.0.is_end_stream(); + let payload = src.0.into_payload(); + let mut frame = frame::Data::new(id, payload.into_buf()); + frame.set_end_stream(eos); + Frame::Data(frame) + } +} + +// ==== "trait alias" for types that are HttpTryFrom and have Debug Errors ==== + +pub trait HttpTryInto { + type Error: fmt::Debug; + + fn try_into(self) -> Result; +} + +impl HttpTryInto for U +where T: HttpTryFrom, + T::Error: fmt::Debug, +{ + type Error = T::Error; + + fn try_into(self) -> Result { + T::try_from(self) + } +} diff --git a/tests/support/src/lib.rs b/tests/support/src/lib.rs index 9fe0814..d060fbd 100644 --- a/tests/support/src/lib.rs +++ b/tests/support/src/lib.rs @@ -16,6 +16,7 @@ mod assert; #[macro_use] pub mod raw; +pub mod frames; pub mod prelude; pub mod mock; diff --git a/tests/support/src/prelude.rs b/tests/support/src/prelude.rs index 2c2f849..e3cad03 100644 --- a/tests/support/src/prelude.rs +++ b/tests/support/src/prelude.rs @@ -9,6 +9,9 @@ pub use self::h2::server::{self, Server}; // Re-export mock pub use super::mock; +// Re-export frames helpers +pub use super::frames; + // Re-export some type defines pub use super::{Codec, SendFrame}; @@ -99,9 +102,3 @@ impl ClientExt for Client } } -pub mod frames { - //! Some useful frames - - pub const SETTINGS: &'static [u8] = &[0, 0, 0, 4, 0, 0, 0, 0, 0]; - pub const SETTINGS_ACK: &'static [u8] = &[0, 0, 0, 4, 1, 0, 0, 0, 0]; -}