diff --git a/include/boost/capy/concept/read_stream.hpp b/include/boost/capy/concept/read_stream.hpp index 15180b872..2e138da2b 100644 --- a/include/boost/capy/concept/read_stream.hpp +++ b/include/boost/capy/concept/read_stream.hpp @@ -43,10 +43,17 @@ namespace capy { @li If `!ec`, then `n >= 1 && n <= buffer_size( buffers )`. `n` bytes were read into the buffer sequence. - @li If `ec`, then `n >= 0 && n <= buffer_size( buffers )`. + @li If `ec`, then `n >= 0 && n < buffer_size( buffers )`. `n` is the number of bytes read before the I/O condition arose. + Equivalently, `n == buffer_size( buffers )` implies `!ec`: a + completion that fills the buffer sequence is a success, even when + the underlying operation also signals a condition such as + end-of-stream. That condition is reported on a subsequent read. + This lets generic composition algorithms such as `when_all` and + `when_any` distinguish a completed transfer from a failure. + If `buffer_empty( buffers )` is `true`, `n` is 0. The empty buffer is not itself a cause for error, but `ec` may reflect the state of the stream. diff --git a/include/boost/capy/concept/write_stream.hpp b/include/boost/capy/concept/write_stream.hpp index c4bd725d2..d55a4218c 100644 --- a/include/boost/capy/concept/write_stream.hpp +++ b/include/boost/capy/concept/write_stream.hpp @@ -47,10 +47,17 @@ namespace capy { @li If `!ec`, then `n >= 1 && n <= buffer_size( buffers )`. `n` bytes were written from the buffer sequence. - @li If `ec`, then `n >= 0 && n <= buffer_size( buffers )`. + @li If `ec`, then `n >= 0 && n < buffer_size( buffers )`. `n` is the number of bytes written before the I/O condition arose. + Equivalently, `n == buffer_size( buffers )` implies `!ec`: a + completion that writes the entire buffer sequence is a success, even + when the underlying operation also signals a condition. That + condition is reported on a subsequent write. This lets generic + composition algorithms such as `when_all` and `when_any` distinguish + a completed transfer from a failure. + If `buffer_empty( buffers )` is `true`, `n` is 0. The empty buffer is not itself a cause for error, but `ec` may reflect the state of the stream. diff --git a/include/boost/capy/read.hpp b/include/boost/capy/read.hpp index 4a85aa9cd..1185ce8ba 100644 --- a/include/boost/capy/read.hpp +++ b/include/boost/capy/read.hpp @@ -46,7 +46,10 @@ namespace capy { Contingencies: - @li The first contingency reported from awaiting @c stream.read_some . + @li The first contingency reported from awaiting @c stream.read_some + while `buffers` is not yet filled. A contingency that accompanies + the read which fills `buffers` is not reported: a completed + transfer is a success. Notable conditions: @@ -54,7 +57,8 @@ namespace capy { @li @c cond::eof — Stream reached end before `buffers` was filled. @par Await-postcondition - `ec || n == buffer_size(buffers)`. + If `n == buffer_size(buffers)` the transfer completed and `ec` is + success; otherwise `ec` is set. @param stream The stream to read from. If the lifetime of `stream` ends before the coroutine finishes, the behavior is undefined. @@ -100,7 +104,9 @@ read(S& stream, MB buffers) -> auto [ec, n] = co_await stream.read_some(consuming.data()); consuming.remove_prefix(n); total_read += n; - if(ec) + // A contingency that still completed the transfer is a success: + // report it only when the buffer was not filled. + if(ec && total_read < total_size) co_return {ec, total_read}; } diff --git a/include/boost/capy/write.hpp b/include/boost/capy/write.hpp index 70256af32..f8f2a72a5 100644 --- a/include/boost/capy/write.hpp +++ b/include/boost/capy/write.hpp @@ -48,8 +48,10 @@ namespace capy { Contingencies: - @li The first contingency reported from - awaiting @c stream.write_some . + @li The first contingency reported from awaiting @c stream.write_some + while not all bytes have been written. A contingency that accompanies + the write which transfers the last bytes is not reported: a completed + transfer is a success. Notable conditions: @@ -59,7 +61,8 @@ namespace capy { @par Await-postcondition - `ec || n == buffer_size(buffers)`. + If `n == buffer_size(buffers)` the transfer completed and `ec` is + success; otherwise `ec` is set. @param stream The stream to write to. If the lifetime of `stream` ends @@ -100,7 +103,9 @@ auto write(S& stream, CB buffers) -> io_task auto [ec, n] = co_await stream.write_some(consuming.data()); consuming.remove_prefix(n); total_written += n; - if(ec) + // A contingency that still completed the transfer is a success: + // report it only when not all bytes were written. + if(ec && total_written < total_size) co_return {ec, total_written}; } diff --git a/test/unit/read.cpp b/test/unit/read.cpp index c73ba4f1f..53f118992 100644 --- a/test/unit/read.cpp +++ b/test/unit/read.cpp @@ -180,6 +180,43 @@ struct circular_dynamic_buffer_factory } // namespace +// Mock whose read_some reports a contingency in the SAME completion that +// transfers bytes. The test read_stream cannot do this (it reports errors +// and eof with zero bytes), so it is needed to exercise the +// "buffer filled but ec set" boundary. +struct contingent_read_stream +{ + std::error_code ec; + std::size_t deliver; + + template + auto + read_some(MB buffers) + { + struct awaitable + { + contingent_read_stream* self_; + MB buffers_; + + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, io_env const*) const noexcept {} + + io_result + await_resume() + { + std::size_t const cap = buffer_size(buffers_); + std::size_t const n = + self_->deliver < cap ? self_->deliver : cap; + self_->deliver -= n; + return {self_->ec, n}; + } + }; + return awaitable{this, buffers}; + } +}; + struct read_test { //---------------------------------------------------------- @@ -351,6 +388,48 @@ struct read_test })); } + void + testFullTransferContingency() + { + // A contingency on the read that fills the buffer is a success + // (n == buffer_size); a contingency on a short transfer is + // reported. + + // eof coincident with a full fill -> success + BOOST_TEST(test::fuse().inert([](test::fuse&) -> task + { + contingent_read_stream rs{error::eof, 8}; + single_buffer_factory bf(8); + auto [ec, n] = co_await read(rs, bf.buffer()); + BOOST_TEST(! ec); + BOOST_TEST_EQ(n, 8u); + })); + + // contingency with a short transfer -> reported + BOOST_TEST(test::fuse().inert([](test::fuse&) -> task + { + contingent_read_stream rs{error::eof, 5}; + single_buffer_factory bf(8); + auto [ec, n] = co_await read(rs, bf.buffer()); + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, 5u); + })); + + // the suppressed condition is deferred, not lost: the next read + // surfaces it (here the stream is at eof with no more data). + BOOST_TEST(test::fuse().inert([](test::fuse&) -> task + { + contingent_read_stream rs{error::eof, 8}; + single_buffer_factory bf(8); + auto [ec1, n1] = co_await read(rs, bf.buffer()); + BOOST_TEST(! ec1); + BOOST_TEST_EQ(n1, 8u); + auto [ec2, n2] = co_await read(rs, bf.buffer()); + BOOST_TEST(ec2 == cond::eof); + BOOST_TEST_EQ(n2, 0u); + })); + } + void testReadStream() { @@ -358,6 +437,7 @@ struct read_test testReadBufferArray(); testReadBufferPair(); testReadStoredAwaitableTemporarySequence(); + testFullTransferContingency(); } //---------------------------------------------------------- diff --git a/test/unit/write.cpp b/test/unit/write.cpp index 3dcbe98a5..5806c366b 100644 --- a/test/unit/write.cpp +++ b/test/unit/write.cpp @@ -120,6 +120,42 @@ struct buffer_pair_factory } // namespace +// Mock whose write_some reports a contingency in the SAME completion that +// transfers bytes, to exercise the "all bytes written but ec set" +// boundary. The test write_stream reports errors with zero bytes. +struct contingent_write_stream +{ + std::error_code ec; + std::size_t accept; + + template + auto + write_some(CB buffers) + { + struct awaitable + { + contingent_write_stream* self_; + CB buffers_; + + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, io_env const*) const noexcept {} + + io_result + await_resume() + { + std::size_t const cap = buffer_size(buffers_); + std::size_t const n = + self_->accept < cap ? self_->accept : cap; + self_->accept -= n; + return {self_->ec, n}; + } + }; + return awaitable{this, buffers}; + } +}; + struct write_test { //---------------------------------------------------------- @@ -283,6 +319,33 @@ struct write_test })); } + void + testFullTransferContingency() + { + // A contingency on the write that transfers all bytes is a + // success (n == buffer_size); a short transfer reports it. + + // contingency coincident with a full write -> success + BOOST_TEST(test::fuse().inert([](test::fuse&) -> task + { + contingent_write_stream ws{error::canceled, 8}; + single_buffer_factory bf("12345678"); + auto [ec, n] = co_await write(ws, bf.buffer()); + BOOST_TEST(! ec); + BOOST_TEST_EQ(n, 8u); + })); + + // contingency with a short write -> reported + BOOST_TEST(test::fuse().inert([](test::fuse&) -> task + { + contingent_write_stream ws{error::canceled, 5}; + single_buffer_factory bf("12345678"); + auto [ec, n] = co_await write(ws, bf.buffer()); + BOOST_TEST(!! ec); + BOOST_TEST_EQ(n, 5u); + })); + } + void testWriteStream() { @@ -290,6 +353,7 @@ struct write_test testWriteBufferArray(); testWriteBufferPair(); testWriteStoredAwaitableTemporarySequence(); + testFullTransferContingency(); } //----------------------------------------------------------