aioduct 0.2.5

Async-native HTTP client built directly on hyper 1.x — no hyper-util, no legacy
Documentation
use super::super::*;
use super::support::{ScriptedWrite, connected_budget_client};

#[tokio::test]
async fn poll_read_reports_zero_while_draining_pending_tls_write() {
    let (mut client_tls, mut server_stream, mut srv_conn, control) =
        connected_budget_client().await;
    client_tls
        .tls
        .writer()
        .write_all(b"pending client plaintext")
        .unwrap();
    server_write(&mut srv_conn, &mut server_stream, b"incoming")
        .await
        .unwrap();
    control.script_writes([
        ScriptedWrite::Error(io::ErrorKind::Interrupted),
        ScriptedWrite::Error(io::ErrorKind::Interrupted),
        ScriptedWrite::Zero,
    ]);

    let waker = futures_util::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    let mut storage = [0u8; 16];
    let mut read_buf = rt::ReadBuf::new(&mut storage);
    match Pin::new(&mut client_tls).poll_read(&mut cx, read_buf.unfilled()) {
        Poll::Ready(Err(e)) if e.kind() == io::ErrorKind::WriteZero => {}
        result => panic!("read-side TLS write zero must fail, got {result:?}"),
    }
    assert!(read_buf.filled().is_empty());
    assert_eq!(control.write_calls(), 3);
}

#[tokio::test]
async fn poll_read_returns_plaintext_while_outbound_ciphertext_is_backpressured() {
    let (mut client_tls, mut server_stream, mut srv_conn, control) =
        connected_budget_client().await;
    client_tls
        .tls
        .writer()
        .write_all(b"pending client plaintext")
        .unwrap();
    server_write(&mut srv_conn, &mut server_stream, b"incoming")
        .await
        .unwrap();
    control.set_write_budget(Some(0));

    let waker = futures_util::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    let mut storage = [0u8; 16];
    let mut read_buf = rt::ReadBuf::new(&mut storage);
    match Pin::new(&mut client_tls).poll_read(&mut cx, read_buf.unfilled()) {
        Poll::Ready(Ok(())) => {}
        result => panic!("available plaintext must not wait for outbound capacity, got {result:?}"),
    }
    assert_eq!(read_buf.filled(), b"incoming");
    assert!(client_tls.tls.wants_write());
    assert!(control.has_pending_write_waker());
    assert_eq!(control.write_calls(), 1);
}

#[tokio::test]
async fn poll_read_propagates_transport_would_block_without_converting_it_to_pending() {
    let (mut client_tls, _server_stream, _srv_conn, control) = connected_budget_client().await;
    control.script_reads([io::ErrorKind::WouldBlock]);

    let waker = futures_util::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    let mut storage = [0u8; 16];
    let mut read_buf = rt::ReadBuf::new(&mut storage);
    match Pin::new(&mut client_tls).poll_read(&mut cx, read_buf.unfilled()) {
        Poll::Ready(Err(e)) if e.kind() == io::ErrorKind::WouldBlock => {}
        result => panic!("a ready WouldBlock error must remain ready, got {result:?}"),
    }
    assert_eq!(control.read_calls(), 1);
}

#[tokio::test]
async fn poll_read_retries_transient_interrupted_operations() {
    let (mut client_tls, mut server_stream, mut srv_conn, control) =
        connected_budget_client().await;
    server_write(&mut srv_conn, &mut server_stream, b"incoming")
        .await
        .unwrap();
    control.script_reads([io::ErrorKind::Interrupted, io::ErrorKind::Interrupted]);

    let waker = futures_util::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    let mut storage = [0u8; 16];
    let mut read_buf = rt::ReadBuf::new(&mut storage);
    match Pin::new(&mut client_tls).poll_read(&mut cx, read_buf.unfilled()) {
        Poll::Ready(Ok(())) => {}
        result => panic!("transient read interruptions should be retried, got {result:?}"),
    }
    assert_eq!(read_buf.filled(), b"incoming");
    assert!(control.read_calls() >= 3);
}

#[tokio::test]
async fn poll_read_bounds_persistent_interrupted_operations() {
    let (mut client_tls, _server_stream, _srv_conn, control) = connected_budget_client().await;
    control.script_reads((0..33).map(|_| io::ErrorKind::Interrupted));

    let waker = futures_util::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    let mut storage = [0u8; 16];
    let mut read_buf = rt::ReadBuf::new(&mut storage);
    for remaining in [17, 1] {
        match Pin::new(&mut client_tls).poll_read(&mut cx, read_buf.unfilled()) {
            Poll::Ready(Err(e)) if e.kind() == io::ErrorKind::Interrupted => {}
            result => panic!("persistent Interrupted should be surfaced, got {result:?}"),
        }
        assert_eq!(control.remaining_scripted_reads(), remaining);
    }
    match Pin::new(&mut client_tls).poll_read(&mut cx, read_buf.unfilled()) {
        Poll::Pending => {}
        result => panic!("the real transport Pending should remain Pending, got {result:?}"),
    }
    assert_eq!(control.remaining_scripted_reads(), 0);
    assert!(
        control.read_calls() <= 35,
        "one poll must not loop indefinitely on persistent Interrupted"
    );
}