use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::time::{Duration, Instant};
use brazen::{Transport, WireRequest};
use super::{error_chain, HttpTransport};
const HEAD: &[u8] =
b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n";
const SLOW_CHUNKS: usize = 6;
const SLOW_GAP: Duration = Duration::from_millis(300);
fn chunk_text(i: usize) -> String {
format!("chunk{i} ")
}
fn serve<F>(handler: F) -> String
where
F: FnOnce(TcpStream) + Send + 'static,
{
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
thread::spawn(move || {
if let Ok((stream, _)) = listener.accept() {
handler(stream);
}
});
format!("http://127.0.0.1:{port}/")
}
fn drain_request(stream: &mut TcpStream) {
let mut buf = [0u8; 4096];
let _ = stream.read(&mut buf);
}
fn write_chunk(stream: &mut TcpStream, data: &str) -> io::Result<()> {
write!(stream, "{:x}\r\n", data.len())?;
stream.write_all(data.as_bytes())?;
stream.write_all(b"\r\n")?;
stream.flush()
}
fn stall_handler(mut stream: TcpStream) {
drain_request(&mut stream);
let _ = stream.write_all(HEAD);
let _ = write_chunk(&mut stream, "hello ");
let mut buf = [0u8; 64];
while let Ok(n) = stream.read(&mut buf) {
if n == 0 {
break;
}
}
}
fn slow_handler(mut stream: TcpStream) {
drain_request(&mut stream);
let _ = stream.write_all(HEAD);
for i in 0..SLOW_CHUNKS {
if write_chunk(&mut stream, &chunk_text(i)).is_err() {
return;
}
thread::sleep(SLOW_GAP);
}
let _ = stream.write_all(b"0\r\n\r\n");
let _ = stream.flush();
}
fn drain_body(url: &str, idle: u64) -> (Vec<u8>, usize, Option<io::Error>, Duration) {
let mut wire = WireRequest::new(url, b"{}".to_vec());
wire.timeouts.idle = Some(idle);
let resp = HttpTransport::new()
.send(wire)
.expect("connect + response headers succeed");
let start = Instant::now();
let mut body = Vec::new();
let mut chunks = 0;
let mut err = None;
for item in resp.body {
match item {
Ok(b) => {
chunks += 1;
body.extend_from_slice(&b);
}
Err(e) => {
err = Some(e);
break;
}
}
}
(body, chunks, err, start.elapsed())
}
#[test]
fn stall_after_first_chunk_times_out() {
let url = serve(stall_handler);
let (_body, chunks, err, elapsed) = drain_body(&url, 1);
assert!(
chunks >= 1,
"the one pre-stall chunk should arrive: got {chunks}"
);
let err = err.expect("a stalled stream must yield an error, not hang");
assert_eq!(
err.kind(),
io::ErrorKind::TimedOut,
"an idle stall surfaces as TimedOut (the exit-69 class): {err}"
);
assert!(
elapsed >= Duration::from_millis(900),
"must wait ~idle (1s) before firing, waited {elapsed:?}"
);
assert!(
elapsed < Duration::from_secs(30),
"must fire (not hang forever) once the stall trips: waited {elapsed:?}"
);
}
#[test]
fn slow_but_live_stream_completes_without_timeout() {
let url = serve(slow_handler);
let (body, chunks, err, elapsed) = drain_body(&url, 1);
assert!(
err.is_none(),
"a live stream slower-than-idle-per-chunk must NOT time out: {err:?}"
);
let expected: String = (0..SLOW_CHUNKS).map(chunk_text).collect();
assert_eq!(
body,
expected.as_bytes(),
"every live chunk should arrive intact (chunks read: {chunks})"
);
assert!(
elapsed >= Duration::from_secs(1),
"stream should outlast a single idle window, proving the per-chunk reset, took {elapsed:?}"
);
}
#[derive(Debug)]
struct Link {
msg: &'static str,
src: Option<Box<Link>>,
}
impl std::fmt::Display for Link {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.msg)
}
}
impl std::error::Error for Link {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.src
.as_deref()
.map(|s| s as &(dyn std::error::Error + 'static))
}
}
#[test]
fn error_chain_joins_every_source_link_with_colon() {
let err = Link {
msg: "connection failed",
src: Some(Box::new(Link {
msg: "TLS handshake",
src: Some(Box::new(Link {
msg: "certificate not trusted",
src: None,
})),
})),
};
assert_eq!(
error_chain(&err),
"connection failed: TLS handshake: certificate not trusted"
);
}
#[test]
fn error_chain_of_a_sourceless_error_is_its_display() {
let err = Link {
msg: "host down",
src: None,
};
assert_eq!(error_chain(&err), "host down");
}