use super::*;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
fn duplex_pair() -> (tokio::io::DuplexStream, tokio::io::DuplexStream) {
tokio::io::duplex(64 * 1024)
}
async fn socket_pair() -> (tokio::net::TcpStream, tokio::net::TcpStream) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind loopback");
let addr = listener.local_addr().expect("local addr");
let connect = tokio::spawn(async move { tokio::net::TcpStream::connect(addr).await });
let (server, _) = listener.accept().await.expect("accept");
let client = connect.await.expect("join").expect("connect");
(server, client)
}
#[tokio::test(start_paused = true)]
async fn a_silent_peer_is_disconnected() {
let (server, _client) = duplex_pair();
let mut io = IdleTimeout::new(server, Some(Duration::from_secs(75)));
let mut buf = [0_u8; 64];
let err = tokio::time::timeout(Duration::from_secs(600), io.read(&mut buf))
.await
.expect("the idle deadline was never enforced; the read hung")
.expect_err("a connection that never speaks must not be held open");
assert_eq!(err.kind(), std::io::ErrorKind::TimedOut);
assert!(
err.to_string().contains("idle"),
"the error should say why the connection was closed, got: {err}"
);
}
#[tokio::test(start_paused = true)]
async fn a_slow_but_progressing_peer_is_not_disconnected() {
let (server, mut client) = duplex_pair();
let mut io = IdleTimeout::new(server, Some(Duration::from_secs(75)));
tokio::spawn(async move {
for i in 0..5_u8 {
tokio::time::sleep(Duration::from_secs(60)).await;
client.write_all(&[i]).await.expect("peer write");
}
tokio::time::sleep(Duration::from_secs(3600)).await;
});
for expected in 0..5_u8 {
let mut buf = [0_u8; 1];
io.read_exact(&mut buf)
.await
.unwrap_or_else(|e| panic!("byte {expected} should arrive, got: {e}"));
assert_eq!(buf[0], expected);
}
}
#[tokio::test(start_paused = true)]
async fn a_write_only_stream_stays_alive() {
let (server, mut client) = duplex_pair();
let mut io = IdleTimeout::new(server, Some(Duration::from_secs(75)));
tokio::spawn(async move {
let mut sink = vec![0_u8; 1024];
loop {
if client.read(&mut sink).await.unwrap_or(0) == 0 {
break;
}
}
});
let mut buf = [0_u8; 64];
for i in 0..10_u8 {
tokio::select! {
read = io.read(&mut buf) => {
let n = read.unwrap_or_else(|e| panic!(
"the pending read failed at frame {i}, so an outbound-only \
stream was treated as idle: {e}"
));
assert_eq!(n, 0, "the peer never writes, so any read is EOF");
break;
}
() = tokio::time::sleep(Duration::from_secs(30)) => {
io.write_all(&[i])
.await
.unwrap_or_else(|e| panic!("frame {i} should be writable: {e}"));
}
}
}
}
#[tokio::test(start_paused = true)]
async fn activity_does_not_buy_permanent_immunity() {
let (server, mut client) = duplex_pair();
let mut io = IdleTimeout::new(server, Some(Duration::from_secs(75)));
client.write_all(b"hello").await.expect("peer write");
let mut buf = [0_u8; 5];
io.read_exact(&mut buf).await.expect("first read arrives");
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(3600)).await;
drop(client);
});
let err = io
.read(&mut buf)
.await
.expect_err("going quiet after being busy still ends the connection");
assert_eq!(err.kind(), std::io::ErrorKind::TimedOut);
}
#[tokio::test(start_paused = true)]
async fn end_of_stream_reads_as_eof_not_activity() {
let (server, client) = duplex_pair();
let mut io = IdleTimeout::new(server, Some(Duration::from_secs(75)));
let armed = io.deadline.deadline();
tokio::time::sleep(Duration::from_secs(10)).await;
drop(client);
let mut buf = [0_u8; 64];
let n = io.read(&mut buf).await.expect("a closed peer reads as EOF");
assert_eq!(n, 0, "EOF, reported as EOF rather than as a timeout");
assert_eq!(
io.deadline.deadline(),
armed,
"EOF re-armed the idle timer, so a half-closed peer would hold the \
connection open forever by saying nothing"
);
}
#[tokio::test]
async fn vectored_write_support_is_reported_from_the_socket() {
let (server, _client) = socket_pair().await;
let expected = server.is_write_vectored();
let io = IdleTimeout::new(server, Some(Duration::from_secs(75)));
assert_eq!(io.is_write_vectored(), expected);
}