Is it possible to make mocked AsyncWrite and AsyncRead behavior use toio::time::sleep() and other async functions?

⚓ Rust    📅 2025-07-17    👤 surdeus    👁️ 2      

surdeus

Thanks to lots of help from the community over in this question, I've managed to get a mocked test rig up and running that allows for arbitrary behavior when AsyncWrite and AsyncRead poll_* functions are called by the AsyncReadExt and AsyncWriteExt convenience wrappers. However, I never actually noticed until now that the functions of the aforementioned traits are not themselves marked async; a little digging suggests Rust doesn't fully support async trait functions yet? Regardless, this presents a problem as I was hoping to simulate I/O timeout with a call to tokio::time::sleep() inside my mocked poll_write() implementation, as follows:

trait AsyncReadWrite: AsyncRead + AsyncWrite + Unpin {}
mock! {
    AsyncReadWrite {}
    impl AsyncRead for AsyncReadWrite {
        fn poll_read<'ctx, 'buf>(
            self: Pin<&mut Self>,
            cx: &mut std::task::Context<'ctx>,
            buf: &mut ReadBuf<'buf>,
        ) -> Poll<Result<(), std::io::Error>>;
    }
    impl AsyncWrite for AsyncReadWrite {
        fn poll_write<'a>(
            self: Pin<&mut Self>,
            cx: &mut std::task::Context<'a>,
            buf: &[u8],
        ) -> Poll<Result<usize, std::io::Error>>;
        fn poll_flush<'a>(
            self: Pin<&mut Self>,
            cx: &mut std::task::Context<'a>,
        ) -> Poll<Result<(), std::io::Error>>;
        fn poll_shutdown<'a>(
            self: Pin<&mut Self>,
            cx: &mut std::task::Context<'a>,
        ) -> Poll<Result<(), std::io::Error>>;
    }
}

#[tokio::test]
async fn test_timeout() {
    struct MockNetworkerTimeout {}
    impl Networker for MockNetworkerTimeout {
        async fn connect<A: ToSocketAddrs>(
            &self,
            _addr: A,
        ) -> std::io::Result<impl AsyncRead + AsyncWrite + Unpin> {
            let mut mock_stream = MockAsyncReadWrite::new();
            mock_stream
                .expect_poll_read()
                .returning(|_, _| Poll::Ready(std::io::Result::Ok(())));
            mock_stream.expect_poll_write().returning(|ctx, buf: &[u8]| {
                dbg!("mock async writing timeout");
                sleep(Duration::from_secs(10)).await;
                Poll::Ready(Ok(1))
            });
            std::io::Result::Ok(mock_stream)
        }
    }
    let mock_networker = MockNetworkerTimeout {};

    match timeout(
            Duration::from_secs(5),
            // send_comms calls mock_networker.connect() and then calls AsyncWriteExt's write() on the resultant opaque AsyncRead + AsyncWrite + Unpin impl
            send_comms(&vec![0u8; 1024], &mock_networker)
        ).await {
            Ok(_) => panic!("send_comms succeeded despite delay of 10 seconds that should have caused timeout"),
            Err(e) => {
                dbg!(format!("Timed out as expected with {}", e));
            },
        }
}

But that fails to compile with the error:

error[E0728]: `await` is only allowed inside `async` functions and blocks
    --> ...
     |
1790 |                 mock_stream.expect_poll_write().returning(|ctx, buf: &[u8]| {
     |                                                           ----------------- this is not `async`
...
1796 |                     sleep(Duration::from_secs(10)).await;
     |                                                    ^^^^^ only allowed inside `async` functions and blocks

I can still simulate timeout by simply having the expect_poll_write() return Poll::Pending exclusively, but it would be nice to have a timing mechanism on the inside of the mocked behavior. Is there a way to get a handle to the expected async calling context inside these non-async trait functions?

2 posts - 2 participants

Read full topic

🏷️ rust_feed