Simplify ReadWrite interface (#530)

Previously, ReadWrite had methods `is_poolable` and `written_bytes`, which
were solely for the use of unittests.

This replaces `written_bytes` and `TestStream` with a `struct Recorder`
that implements `ReadWrite` and allows unittests to access its recorded
bytes via an `Arc<Mutex<Vec<u8>>>`. It eliminates `is_poolable`; it's fine
to pool a Stream of any kind.

The new `Recorder` also has some convenience methods that abstract away
boilerplate code from many of our unittests.

I got rid of `Stream::from_vec` and `Stream::from_vec_poolable` because
they depended on `TestStream`. They've been replaced by `NoopStream` for
the pool.rs tests, and `ReadOnlyStream` for constructing `Response`s from
`&str` and some test cases.
This commit is contained in:
Jacob Hoffman-Andrews
2022-07-09 10:13:44 -07:00
committed by GitHub
parent 0cf1f8dbb9
commit 9908c446d6
11 changed files with 211 additions and 226 deletions

View File

@@ -1,79 +1,61 @@
use crate::test;
use crate::test::Recorder;
use super::super::*;
#[test]
fn content_length_on_str() {
test::set_handler("/content_length_on_str", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = post("test://host/content_length_on_str")
let recorder = Recorder::register("/content_length_on_str");
post("test://host/content_length_on_str")
.send_string("Hello World!!!")
.unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nContent-Length: 14\r\n"));
assert!(recorder.contains("\r\nContent-Length: 14\r\n"));
}
#[test]
fn user_set_content_length_on_str() {
test::set_handler("/user_set_content_length_on_str", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = post("test://host/user_set_content_length_on_str")
let recorder = Recorder::register("/user_set_content_length_on_str");
post("test://host/user_set_content_length_on_str")
.set("Content-Length", "12345")
.send_string("Hello World!!!")
.unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nContent-Length: 12345\r\n"));
assert!(recorder.contains("\r\nContent-Length: 12345\r\n"));
}
#[test]
#[cfg(feature = "json")]
fn content_length_on_json() {
test::set_handler("/content_length_on_json", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let recorder = Recorder::register("/content_length_on_json");
let mut json = serde_json::Map::new();
json.insert(
"Hello".to_string(),
serde_json::Value::String("World!!!".to_string()),
);
let resp = post("test://host/content_length_on_json")
post("test://host/content_length_on_json")
.send_json(serde_json::Value::Object(json))
.unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nContent-Length: 20\r\n"));
assert!(recorder.contains("\r\nContent-Length: 20\r\n"));
}
#[test]
fn content_length_and_chunked() {
test::set_handler("/content_length_and_chunked", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = post("test://host/content_length_and_chunked")
let recorder = Recorder::register("/content_length_and_chunked");
post("test://host/content_length_and_chunked")
.set("Transfer-Encoding", "chunked")
.send_string("Hello World!!!")
.unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("Transfer-Encoding: chunked\r\n"));
assert!(!s.contains("\r\nContent-Length:\r\n"));
assert!(recorder.contains("Transfer-Encoding: chunked\r\n"));
assert!(!recorder.contains("\r\nContent-Length:\r\n"));
}
#[test]
#[cfg(feature = "charset")]
fn str_with_encoding() {
test::set_handler("/str_with_encoding", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = post("test://host/str_with_encoding")
let recorder = Recorder::register("/str_with_encoding");
post("test://host/str_with_encoding")
.set("Content-Type", "text/plain; charset=iso-8859-1")
.send_string("Hällo Wörld!!!")
.unwrap();
let vec = resp.into_written_bytes();
let vec = recorder.to_vec();
assert_eq!(
&vec[vec.len() - 14..],
//H ä l l o _ W ö r l d ! ! !
@@ -84,38 +66,30 @@ fn str_with_encoding() {
#[test]
#[cfg(feature = "json")]
fn content_type_on_json() {
test::set_handler("/content_type_on_json", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let recorder = Recorder::register("/content_type_on_json");
let mut json = serde_json::Map::new();
json.insert(
"Hello".to_string(),
serde_json::Value::String("World!!!".to_string()),
);
let resp = post("test://host/content_type_on_json")
post("test://host/content_type_on_json")
.send_json(serde_json::Value::Object(json))
.unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nContent-Type: application/json\r\n"));
assert!(recorder.contains("\r\nContent-Type: application/json\r\n"));
}
#[test]
#[cfg(feature = "json")]
fn content_type_not_overriden_on_json() {
test::set_handler("/content_type_not_overriden_on_json", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let recorder = Recorder::register("/content_type_not_overriden_on_json");
let mut json = serde_json::Map::new();
json.insert(
"Hello".to_string(),
serde_json::Value::String("World!!!".to_string()),
);
let resp = post("test://host/content_type_not_overriden_on_json")
post("test://host/content_type_not_overriden_on_json")
.set("content-type", "text/plain")
.send_json(serde_json::Value::Object(json))
.unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\ncontent-type: text/plain\r\n"));
assert!(recorder.contains("\r\ncontent-type: text/plain\r\n"));
}

View File

@@ -1,9 +1,12 @@
use crate::error::Error;
use crate::stream::Stream;
use crate::stream::{ReadOnlyStream, Stream};
use crate::unit::Unit;
use crate::ReadWrite;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::io::Write;
use std::fmt;
use std::io::{self, Cursor, Read, Write};
use std::net::TcpStream;
use std::sync::{Arc, Mutex};
mod agent_test;
@@ -48,7 +51,7 @@ pub(crate) fn make_response(
}
write!(&mut buf, "\r\n").ok();
buf.append(&mut body);
Ok(Stream::from_vec(buf))
Ok(Stream::new(ReadOnlyStream::new(buf)))
}
pub(crate) fn resolve_handler(unit: &Unit) -> Result<Stream, Error> {
@@ -66,3 +69,87 @@ pub(crate) fn resolve_handler(unit: &Unit) -> Result<Stream, Error> {
drop(handlers);
handler(unit)
}
#[derive(Default, Clone)]
pub(crate) struct Recorder {
contents: Arc<Mutex<Vec<u8>>>,
}
impl Recorder {
fn register(path: &str) -> Self {
let recorder = Recorder::default();
let recorder2 = recorder.clone();
set_handler(path, move |_unit| Ok(recorder.stream()));
recorder2
}
#[cfg(feature = "charset")]
fn to_vec(self) -> Vec<u8> {
self.contents.lock().unwrap().clone()
}
fn contains(&self, s: &str) -> bool {
std::str::from_utf8(&self.contents.lock().unwrap())
.unwrap()
.contains(s)
}
fn stream(&self) -> Stream {
let cursor = Cursor::new(b"HTTP/1.1 200 OK\r\n\r\n");
Stream::new(TestStream::new(cursor, self.clone()))
}
}
impl Write for Recorder {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.contents.lock().unwrap().write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub(crate) struct TestStream(
Box<dyn Read + Send + Sync>,
Box<dyn Write + Send + Sync>,
bool,
);
impl TestStream {
#[cfg(test)]
pub(crate) fn new(
response: impl Read + Send + Sync + 'static,
recorder: impl Write + Send + Sync + 'static,
) -> Self {
Self(Box::new(response), Box::new(recorder), false)
}
}
impl ReadWrite for TestStream {
fn socket(&self) -> Option<&TcpStream> {
None
}
}
impl Read for TestStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}
impl Write for TestStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.1.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl fmt::Debug for TestStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("TestStream").finish()
}
}

View File

@@ -1,58 +1,37 @@
use crate::test;
use super::super::*;
use super::Recorder;
use crate::get;
#[test]
fn no_query_string() {
test::set_handler("/no_query_string", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://host/no_query_string").call().unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("GET /no_query_string HTTP/1.1"))
let recorder = Recorder::register("/no_query_string");
get("test://host/no_query_string").call().unwrap();
assert!(recorder.contains("GET /no_query_string HTTP/1.1"))
}
#[test]
fn escaped_query_string() {
test::set_handler("/escaped_query_string", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://host/escaped_query_string")
let recorder = Recorder::register("/escaped_query_string");
get("test://host/escaped_query_string")
.query("foo", "bar")
.query("baz", "yo lo")
.call()
.unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(
s.contains("GET /escaped_query_string?foo=bar&baz=yo+lo HTTP/1.1"),
"req: {}",
s
);
assert!(recorder.contains("GET /escaped_query_string?foo=bar&baz=yo+lo HTTP/1.1"));
}
#[test]
fn query_in_path() {
test::set_handler("/query_in_path", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://host/query_in_path?foo=bar").call().unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("GET /query_in_path?foo=bar HTTP/1.1"))
let recorder = Recorder::register("/query_in_path");
get("test://host/query_in_path?foo=bar").call().unwrap();
assert!(recorder.contains("GET /query_in_path?foo=bar HTTP/1.1"))
}
#[test]
fn query_in_path_and_req() {
test::set_handler("/query_in_path_and_req", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://host/query_in_path_and_req?foo=bar")
let recorder = Recorder::register("/query_in_path_and_req");
get("test://host/query_in_path_and_req?foo=bar")
.query("baz", "1 2 3")
.call()
.unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("GET /query_in_path_and_req?foo=bar&baz=1+2+3 HTTP/1.1"))
assert!(recorder.contains("GET /query_in_path_and_req?foo=bar&baz=1+2+3 HTTP/1.1"));
}

View File

@@ -1,7 +1,7 @@
use crate::test;
use std::io::Read;
use super::super::*;
use super::{super::*, Recorder};
#[test]
fn header_passing() {
@@ -116,13 +116,9 @@ fn body_as_reader() {
#[test]
fn escape_path() {
test::set_handler("/escape_path%20here", |_unit| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://host/escape_path here").call().unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("GET /escape_path%20here HTTP/1.1"))
let recorder = Recorder::register("/escape_path%20here");
get("test://host/escape_path here").call().unwrap();
assert!(recorder.contains("GET /escape_path%20here HTTP/1.1"))
}
#[test]
@@ -194,22 +190,14 @@ pub fn header_with_spaces_before_value() {
#[test]
pub fn host_no_port() {
test::set_handler("/host_no_port", |_| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://myhost/host_no_port").call().unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nHost: myhost\r\n"));
let recorder = Recorder::register("/host_no_port");
get("test://myhost/host_no_port").call().unwrap();
assert!(recorder.contains("\r\nHost: myhost\r\n"));
}
#[test]
pub fn host_with_port() {
test::set_handler("/host_with_port", |_| {
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://myhost:234/host_with_port").call().unwrap();
let vec = resp.into_written_bytes();
let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nHost: myhost:234\r\n"));
let recorder = Recorder::register("/host_with_port");
get("test://myhost:234/host_with_port").call().unwrap();
assert!(recorder.contains("\r\nHost: myhost:234\r\n"));
}