Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion include/boost/capy/concept/read_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion include/boost/capy/concept/write_stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 9 additions & 3 deletions include/boost/capy/read.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,19 @@ 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:

@li @c cond::canceled — Operation was cancelled,
@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.
Expand Down Expand Up @@ -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};
}

Expand Down
13 changes: 9 additions & 4 deletions include/boost/capy/write.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
Expand Down Expand Up @@ -100,7 +103,9 @@ auto write(S& stream, CB buffers) -> io_task<std::size_t>
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};
}

Expand Down
80 changes: 80 additions & 0 deletions test/unit/read.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<MutableBufferSequence MB>
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<std::size_t>
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
{
//----------------------------------------------------------
Expand Down Expand Up @@ -351,13 +388,56 @@ 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<void>
{
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<void>
{
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<void>
{
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()
{
testReadSingleBuffer();
testReadBufferArray();
testReadBufferPair();
testReadStoredAwaitableTemporarySequence();
testFullTransferContingency();
}

//----------------------------------------------------------
Expand Down
64 changes: 64 additions & 0 deletions test/unit/write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConstBufferSequence CB>
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<std::size_t>
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
{
//----------------------------------------------------------
Expand Down Expand Up @@ -283,13 +319,41 @@ 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<void>
{
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<void>
{
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()
{
testWriteSingleBuffer();
testWriteBufferArray();
testWriteBufferPair();
testWriteStoredAwaitableTemporarySequence();
testFullTransferContingency();
}

//----------------------------------------------------------
Expand Down
Loading