diff --git a/include/boost/capy/io/any_write_stream.hpp b/include/boost/capy/io/any_write_stream.hpp index de9d37f10..efe681d23 100644 --- a/include/boost/capy/io/any_write_stream.hpp +++ b/include/boost/capy/io/any_write_stream.hpp @@ -400,23 +400,25 @@ any_write_stream::write_some(CB buffers) } bool - await_ready() const noexcept + await_ready() { - return ba_.to_span().empty(); - } + // An empty write never touches the underlying stream + if(ba_.to_span().empty()) + return true; - std::coroutine_handle<> - await_suspend(std::coroutine_handle<> h, io_env const* env) - { self_->vt_->construct_awaitable( self_->stream_, self_->cached_awaitable_, ba_.to_span()); self_->awaitable_active_ = true; - if(self_->vt_->await_ready(self_->cached_awaitable_)) - return h; + return self_->vt_->await_ready( + self_->cached_awaitable_); + } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const* env) + { return self_->vt_->await_suspend( self_->cached_awaitable_, h, env); } diff --git a/test/unit/io/any_read_stream.cpp b/test/unit/io/any_read_stream.cpp index 3e1b5675f..6671f7025 100644 --- a/test/unit/io/any_read_stream.cpp +++ b/test/unit/io/any_read_stream.cpp @@ -57,6 +57,29 @@ struct pending_read_stream { return pending_read_awaitable{counter_}; } }; +// Reports readiness without consulting the io_env, like a stream +// whose data is already in user-space memory. +struct ready_read_awaitable +{ + int* suspended_; + bool await_ready() const noexcept { return true; } + std::coroutine_handle<> await_suspend(std::coroutine_handle<>, io_env const*) + { + ++(*suspended_); + return std::noop_coroutine(); + } + io_result await_resume() + { return {std::error_code(), 7}; } +}; + +struct ready_read_stream +{ + int* suspended_; + ready_read_awaitable read_some( + MutableBufferSequence auto) + { return ready_read_awaitable{suspended_}; } +}; + // Reports not-ready, then resumes the awaiting coroutine from // await_suspend. This exercises the type-erased await_suspend // thunk, which the always-ready test mocks never reach. @@ -497,6 +520,25 @@ class any_read_stream_test BOOST_TEST(r.success); } + void + testReadyStreamSkipsSuspension() + { + // A concrete awaitable that reports readiness completes the + // erased read without suspending the coroutine: the wrapper + // forwards await_ready and never calls await_suspend. + int suspended = 0; + ready_read_stream rs{&suspended}; + any_read_stream ars(&rs); + + char buf[8]; + auto aw = ars.read_some(mutable_buffer(buf, sizeof(buf))); + BOOST_TEST(aw.await_ready()); + auto [ec, n] = aw.await_resume(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 7u); + BOOST_TEST_EQ(suspended, 0); + } + void testDestroyWithActiveAwaitable() { @@ -598,6 +640,7 @@ class any_read_stream_test testTrichotomyEofAfterDrain(); testTrichotomyEmptyBufferExhausted(); testReadSomeManyBuffers(); + testReadyStreamSkipsSuspension(); testDestroyWithActiveAwaitable(); testMoveAssignWithActiveAwaitable(); testMoveAssignOwning(); diff --git a/test/unit/io/any_write_stream.cpp b/test/unit/io/any_write_stream.cpp index 282b19796..1d0884681 100644 --- a/test/unit/io/any_write_stream.cpp +++ b/test/unit/io/any_write_stream.cpp @@ -57,6 +57,29 @@ struct pending_write_stream { return pending_write_awaitable{counter_}; } }; +// Reports readiness without consulting the io_env, like a stream +// whose data is already in user-space memory. +struct ready_write_awaitable +{ + int* suspended_; + bool await_ready() const noexcept { return true; } + std::coroutine_handle<> await_suspend(std::coroutine_handle<>, io_env const*) + { + ++(*suspended_); + return std::noop_coroutine(); + } + io_result await_resume() + { return {std::error_code(), 7}; } +}; + +struct ready_write_stream +{ + int* suspended_; + ready_write_awaitable write_some( + ConstBufferSequence auto) + { return ready_write_awaitable{suspended_}; } +}; + // Move constructor throws so owning construction fails after storage // is allocated but before the stream is constructed. struct throwing_move_write_stream @@ -437,11 +460,30 @@ class any_write_stream_test BOOST_TEST(r.success); } + void + testReadyStreamSkipsSuspension() + { + // A concrete awaitable that reports readiness completes the + // erased write without suspending the coroutine: the wrapper + // forwards await_ready and never calls await_suspend. + int suspended = 0; + ready_write_stream rs{&suspended}; + any_write_stream aws(&rs); + + char const data[] = "x"; + auto aw = aws.write_some(const_buffer(data, 1)); + BOOST_TEST(aw.await_ready()); + auto [ec, n] = aw.await_resume(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 7u); + BOOST_TEST_EQ(suspended, 0); + } + void testDestroyWithActiveAwaitable() { - // Flat vtable, construct-in-await_suspend variant: - // await_suspend constructs the inner awaitable. + // await_ready constructs the inner awaitable; a pending + // stream then suspends through await_suspend. int destroyed = 0; pending_write_stream ps{&destroyed}; { @@ -496,6 +538,7 @@ class any_write_stream_test testWriteSomeManyBuffers(); testTrichotomySuccess(); testTrichotomyError(); + testReadyStreamSkipsSuspension(); testDestroyWithActiveAwaitable(); testMoveAssignWithActiveAwaitable(); testMoveAssignOwning();