From 69cd99bcb83a7b540168da0b1e56da660fcf9b8b Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Tue, 30 Jun 2026 16:13:24 -0600 Subject: [PATCH] feat: add read_at_least and write_at_least algorithms (#260) Add read_at_least and write_at_least as straightforward extensions of read/write. Where read/write transfer exactly buffer_size(buffers) bytes, the _at_least variants stop as soon as a minimum of n bytes have been transferred, keeping any bytes a single read_some/write_some delivers beyond n without looping again. This serves the buffered-source case where n bytes are required but the remaining buffer capacity is optional. If n exceeds buffer_size(buffers) the request is impossible to satisfy and the operation fails immediately with {std::errc::invalid_argument, 0}. A contingency that coincides with reaching n is suppressed: a satisfied request is a success, mirroring read/write. --- .../ROOT/pages/6.streams/6e.algorithms.adoc | 63 ++++ include/boost/capy.hpp | 2 + include/boost/capy/read_at_least.hpp | 141 +++++++++ include/boost/capy/write_at_least.hpp | 135 ++++++++ test/unit/read_at_least.cpp | 289 ++++++++++++++++++ test/unit/write_at_least.cpp | 252 +++++++++++++++ 6 files changed, 882 insertions(+) create mode 100644 include/boost/capy/read_at_least.hpp create mode 100644 include/boost/capy/write_at_least.hpp create mode 100644 test/unit/read_at_least.cpp create mode 100644 test/unit/write_at_least.cpp diff --git a/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc b/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc index f6895c58..96d3dbb2 100644 --- a/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc +++ b/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc @@ -38,6 +38,41 @@ auto [ec, n] = co_await read(stream, make_buffer(buf)); // n == 1024, or ec indicates why not ---- +=== read_at_least + +A straightforward extension of `read`: instead of filling `buffers` +completely, it stops as soon as at least `n` bytes have been read: + +[source,cpp] +---- +#include + +template +io_task +read_at_least(Stream& stream, Buffers buffers, std::size_t n); +---- + +The required amount `n` must be met or exceeded; the remaining capacity of +`buffers` is optional, so a single `read_some` that delivers more than `n` +bytes keeps the extra without looping again. Keeps reading until: + +* At least `n` bytes have been read (`n \<= return value \<= buffer_size(buffers)`) +* The underlying `read_some` reports a condition before `n` bytes are read (the condition is propagated with the partial count) + +If `n` exceeds `buffer_size(buffers)` the request is impossible to satisfy and +the operation fails immediately with `std::errc::invalid_argument` and a count +of `0`, without reading. + +Example: + +[source,cpp] +---- +char buf[4096]; +// Require 16 bytes; opportunistically take whatever else is ready. +auto [ec, n] = co_await read_at_least(stream, make_buffer(buf), 16); +// n >= 16 on success, possibly up to 4096 +---- + === write Writes all data by looping `write_some`: @@ -63,6 +98,28 @@ Example: co_await write(stream, make_buffer("Hello, World!")); ---- +=== write_at_least + +The mirror of `read_at_least`, provided for symmetry: it stops as soon as at +least `n` bytes have been written, rather than draining `buffers` entirely: + +[source,cpp] +---- +#include + +template +io_task +write_at_least(Stream& stream, Buffers buffers, std::size_t n); +---- + +Keeps writing until: + +* At least `n` bytes have been written (`n \<= return value \<= buffer_size(buffers)`) +* The underlying `write_some` reports a condition before `n` bytes are written (the condition is propagated with the partial count) + +If `n` exceeds `buffer_size(buffers)` the operation fails immediately with +`std::errc::invalid_argument` and a count of `0`, without writing. + === write_now `write_now` eagerly writes a complete buffer sequence, attempting to finish @@ -250,9 +307,15 @@ else if (ec) | `` | Composed read operations +| `` +| Read at least a minimum number of bytes + | `` | Composed write operations +| `` +| Write at least a minimum number of bytes + | `` | Eager write with frame caching diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index 6644b44d..dd84839e 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -28,10 +28,12 @@ // Algorithms #include #include +#include #include #include #include #include +#include // Buffers #include diff --git a/include/boost/capy/read_at_least.hpp b/include/boost/capy/read_at_least.hpp new file mode 100644 index 00000000..5362df68 --- /dev/null +++ b/include/boost/capy/read_at_least.hpp @@ -0,0 +1,141 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_READ_AT_LEAST_HPP +#define BOOST_CAPY_READ_AT_LEAST_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace boost { +namespace capy { + +/** Read at least a minimum number of bytes from a stream. + + This is a straightforward extension of @ref read. While @ref read + transfers exactly `buffer_size(buffers)` bytes, `read_at_least` + transfers at least `n` bytes: the loop stops as soon as `n` bytes + have been read, even if `buffers` is not yet full. Any bytes beyond + `n` that a single `stream.read_some` happens to deliver (up to the + capacity of `buffers`) are kept, but no further awaiting is performed + to fill the remainder. + + This is useful when a caller has a required amount of data `n` that + must be met or exceeded, while the subsequent capacity of `buffers` + is optional and should not block. + + @par Await-effects + + If `n > buffer_size(buffers)` the request is impossible to satisfy + and the operation fails immediately with + `{std::errc::invalid_argument, 0}` without awaiting `stream.read_some`. + + Otherwise reads data from `stream` via awaiting `stream.read_some` + repeatedly until: + + @li either at least `n` bytes have been read, + @li or a contingency occurs on `stream.read_some`. + + If `n == 0` then no awaiting `stream.read_some` is performed. This is + not a contingency. + + @par Await-returns + An object of type `io_result` destructuring as `[ec, n]`. + + Upon a contingency, the count represents the number of bytes read so + far, inclusive of the last partial read. + + Contingencies: + + @li The first contingency reported from awaiting @c stream.read_some + while fewer than `n` bytes have been read. A contingency that + accompanies the read which reaches `n` is not reported: a + satisfied request is a success. + + Notable conditions: + + @li @c std::errc::invalid_argument — `n` exceeds `buffer_size(buffers)`, + @li @c cond::canceled — Operation was cancelled, + @li @c cond::eof — Stream reached end before `n` bytes were read. + + @par Await-postcondition + On success the returned count is greater than or equal to `n` and + less than or equal to `buffer_size(buffers)`, 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. + + @param buffers The buffer sequence to read into. If the lifetime of the + buffer sequence represented by `buffers` ends before the coroutine + finishes, the behavior is undefined. + + @param n The minimum number of bytes to read. Must not exceed + `buffer_size(buffers)`. + + @par Remarks + Supports _IoAwaitable cancellation_. + + @par Example + + @code + capy::task<> fill_buffer(capy::ReadStream auto& stream) + { + std::vector storage(4096); // generous capacity + // Require 16 header bytes; opportunistically take more. + auto [ec, n] = co_await capy::read_at_least( + stream, capy::make_buffer(storage), 16); + if(ec) + throw std::system_error(ec); + + // at least 16 bytes are available; n may be larger + } + @endcode + + @see read, ReadStream, MutableBufferSequence +*/ +template + requires ReadStream && MutableBufferSequence +auto +read_at_least(S& stream, MB buffers, std::size_t n) -> + io_task +{ + consuming_buffers consuming(buffers); + std::size_t const total_size = buffer_size(buffers); + + if(n > total_size) + co_return {make_error_code(std::errc::invalid_argument), 0}; + + std::size_t total_read = 0; + + while(total_read < n) + { + auto [ec, m] = co_await stream.read_some(consuming.data()); + consuming.consume(m); + total_read += m; + // A contingency that still satisfied the request is a success: + // report it only when fewer than n bytes were read. + if(ec && total_read < n) + co_return {ec, total_read}; + } + + co_return {{}, total_read}; +} + +} // namespace capy +} // namespace boost + +#endif diff --git a/include/boost/capy/write_at_least.hpp b/include/boost/capy/write_at_least.hpp new file mode 100644 index 00000000..1a910a3b --- /dev/null +++ b/include/boost/capy/write_at_least.hpp @@ -0,0 +1,135 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_WRITE_AT_LEAST_HPP +#define BOOST_CAPY_WRITE_AT_LEAST_HPP + +#include +#include +#include +#include +#include + +#include +#include + +namespace boost { +namespace capy { + +/** Write at least a minimum number of bytes to a stream. + + This is a straightforward extension of @ref write. While @ref write + transfers exactly `buffer_size(buffers)` bytes, `write_at_least` + transfers at least `n` bytes: the loop stops as soon as `n` bytes + have been written, even if `buffers` has not been fully consumed. + Any bytes beyond `n` that a single `stream.write_some` happens to + transfer are counted, but no further awaiting is performed to write + the remainder. + + Provided for symmetry with @ref read_at_least. + + @par Await-effects + + If `n > buffer_size(buffers)` the request is impossible to satisfy + and the operation fails immediately with + `{std::errc::invalid_argument, 0}` without awaiting `stream.write_some`. + + Otherwise writes the contents of `buffers` to `stream` via awaiting + `stream.write_some` with consecutive portions of data from `buffers` + until: + + @li either at least `n` bytes have been written, + @li or a contingency in `stream.write_some` occurs. + + If `n == 0` then no awaiting `stream.write_some` is performed. This is + not a contingency. + + @par Await-returns + An object of type `io_result` destructuring as `[ec, n]`. + + Upon a contingency, the count represents the number of bytes written + so far. + + Contingencies: + + @li The first contingency reported from awaiting @c stream.write_some + while fewer than `n` bytes have been written. A contingency that + accompanies the write which reaches `n` is not reported: a + satisfied request is a success. + + Notable conditions: + + @li @c std::errc::invalid_argument — `n` exceeds `buffer_size(buffers)`, + @li @c cond::canceled — Operation was cancelled, + @li @c std::errc::broken_pipe — Peer closed connection. + + @par Await-postcondition + On success the returned count is greater than or equal to `n` and + less than or equal to `buffer_size(buffers)`, and `ec` is success; + otherwise `ec` is set. + + @param stream The stream to write to. If the lifetime of `stream` ends + before the coroutine finishes, the behavior is undefined. + + @param buffers The buffer sequence to write. If the lifetime of the + buffer sequence represented by `buffers` ends before the coroutine + finishes, the behavior is undefined. + + @param n The minimum number of bytes to write. Must not exceed + `buffer_size(buffers)`. + + @par Remarks + Supports _IoAwaitable cancellation_. + + @par Example + + @code + capy::task<> flush_at_least(capy::WriteStream auto& stream, std::string_view data) + { + auto [ec, n] = co_await capy::write_at_least( + stream, capy::make_buffer(data), 8); + if(ec) + throw std::system_error(ec); + + // at least 8 bytes written; n may be larger + } + @endcode + + @see write, WriteStream, ConstBufferSequence +*/ +template +auto +write_at_least(S& stream, CB buffers, std::size_t n) -> io_task +{ + consuming_buffers consuming(buffers); + std::size_t const total_size = buffer_size(buffers); + + if(n > total_size) + co_return {make_error_code(std::errc::invalid_argument), 0}; + + std::size_t total_written = 0; + + while(total_written < n) + { + auto [ec, m] = co_await stream.write_some(consuming.data()); + consuming.consume(m); + total_written += m; + // A contingency that still satisfied the request is a success: + // report it only when fewer than n bytes were written. + if(ec && total_written < n) + co_return {ec, total_written}; + } + + co_return {{}, total_written}; +} + +} // namespace capy +} // namespace boost + +#endif diff --git a/test/unit/read_at_least.cpp b/test/unit/read_at_least.cpp new file mode 100644 index 00000000..926dc4bb --- /dev/null +++ b/test/unit/read_at_least.cpp @@ -0,0 +1,289 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include + +namespace boost { +namespace capy { + +namespace { + +struct single_buffer_factory +{ + char storage[1024]; + std::size_t size; + + explicit single_buffer_factory(std::size_t n) + : size(n) + { + std::memset(storage, 0, sizeof(storage)); + } + + mutable_buffer + buffer() + { + return mutable_buffer(storage, size); + } + + std::string_view + view(std::size_t n) const + { + return std::string_view(storage, n); + } +}; + +} // 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 +// "request satisfied 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_at_least_test +{ + void + testSatisfiedExact() + { + // A single read delivers exactly n; request satisfied. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::read_stream rs(f); + rs.provide("hello"); + + single_buffer_factory bf(5); + auto [ec, n] = co_await read_at_least(rs, bf.buffer(), 5); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(bf.view(n), "hello"); + })); + } + + void + testSatisfiedWithBonus() + { + // A single read delivers more than n (up to buffer capacity); + // the extra bytes are kept and no further read is performed. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::read_stream rs(f); + rs.provide("hello world"); // 11 bytes available + + single_buffer_factory bf(32); // generous capacity + auto [ec, n] = co_await read_at_least(rs, bf.buffer(), 5); + if(ec) + co_return; + + // One read_some returns all 11 available bytes; 11 >= 5, + // so we stop with the bonus bytes included. + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bf.view(n), "hello world"); + })); + } + + void + testLoopUntilN() + { + // Chunked delivery forces multiple reads to reach n. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::read_stream rs(f, /*max_read_size*/ 4); + rs.provide("abcdefghij"); // 10 bytes, delivered 4 at a time + + single_buffer_factory bf(32); + auto [ec, n] = co_await read_at_least(rs, bf.buffer(), 10); + if(ec) + co_return; + + // reads of 4 + 4 + 2 (data exhausted) -> 10 >= 10 + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(bf.view(n), "abcdefghij"); + })); + } + + void + testZeroMinimum() + { + // n == 0 returns immediately without awaiting read_some. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::read_stream rs(f); + rs.provide("data"); + + single_buffer_factory bf(32); + auto [ec, n] = co_await read_at_least(rs, bf.buffer(), 0); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + // Stream was not consumed. + BOOST_TEST_EQ(rs.available(), 4u); + })); + } + + void + testImpossibleRequest() + { + // n > buffer_size(buffers) fails immediately with EINVAL and + // does not touch the stream. + BOOST_TEST(test::fuse().inert([](test::fuse&) -> task + { + test::read_stream rs; + rs.provide("plenty of data here"); + + single_buffer_factory bf(8); + auto [ec, n] = co_await read_at_least(rs, bf.buffer(), 16); + BOOST_TEST(ec == std::errc::invalid_argument); + BOOST_TEST_EQ(n, 0u); + // Stream was not consumed. + BOOST_TEST_EQ(rs.available(), 19u); + })); + } + + void + testEofBeforeN() + { + // EOF before n bytes are read reports the condition with the + // partial count. Use an inert fuse so only the real-EOF path is + // exercised (an armed fuse would inject a different error). + BOOST_TEST(test::fuse().inert([](test::fuse& f) -> task + { + test::read_stream rs(f); + rs.provide("abc"); // only 3 bytes + + single_buffer_factory bf(32); + auto [ec, n] = co_await read_at_least(rs, bf.buffer(), 10); + + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, 3u); + BOOST_TEST_EQ(bf.view(n), "abc"); + })); + } + + void + testContingencyCoincidentWithN() + { + // A contingency on the read that reaches n is a success + // (count >= n); a contingency on a short transfer is reported. + + // eof coincident with reaching n -> 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_at_least(rs, bf.buffer(), 8); + 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_at_least(rs, bf.buffer(), 8); + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, 5u); + })); + } + + // Regression: capy#263. read_at_least() must take its buffer + // sequence by value so that storing the returned awaitable past + // the full-expression that created the sequence does not dangle. + void + testStoredAwaitableTemporarySequence() + { + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::read_stream rs(f); + rs.provide("helloworld"); + + char storage[10] = {}; + + auto aw = read_at_least(rs, std::array{{ + mutable_buffer(storage, 5), + mutable_buffer(storage + 5, 5) + }}, 10); + + auto [ec, n] = co_await std::move(aw); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(std::string_view(storage, 10), "helloworld"); + })); + } + + void + run() + { + testSatisfiedExact(); + testSatisfiedWithBonus(); + testLoopUntilN(); + testZeroMinimum(); + testImpossibleRequest(); + testEofBeforeN(); + testContingencyCoincidentWithN(); + testStoredAwaitableTemporarySequence(); + } +}; + +TEST_SUITE( + read_at_least_test, + "boost.capy.read_at_least"); + +} // namespace capy +} // namespace boost diff --git a/test/unit/write_at_least.cpp b/test/unit/write_at_least.cpp new file mode 100644 index 00000000..04dcf0e0 --- /dev/null +++ b/test/unit/write_at_least.cpp @@ -0,0 +1,252 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include + +namespace boost { +namespace capy { + +namespace { + +struct single_buffer_factory +{ + std::string data; + + explicit single_buffer_factory(std::string_view sv) + : data(sv) + { + } + + const_buffer + buffer() const + { + return make_buffer(data); + } +}; + +} // namespace + +// Mock whose write_some reports a contingency in the SAME completion that +// transfers bytes, to exercise the "request satisfied 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_at_least_test +{ + void + testSatisfiedExact() + { + // A single write transfers exactly n; request satisfied. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::write_stream ws(f); + + single_buffer_factory bf("hello"); + auto [ec, n] = co_await write_at_least(ws, bf.buffer(), 5); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(ws.data(), "hello"); + })); + } + + void + testSatisfiedWithBonus() + { + // A single write transfers more than n; the extra bytes are + // counted and no further write is performed. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::write_stream ws(f); + + single_buffer_factory bf("hello world"); // 11 bytes + auto [ec, n] = co_await write_at_least(ws, bf.buffer(), 5); + if(ec) + co_return; + + // One write_some transfers all 11 bytes; 11 >= 5. + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + })); + } + + void + testLoopUntilN() + { + // Chunked delivery forces multiple writes to reach n, and the + // loop stops before the whole buffer is consumed. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::write_stream ws(f, /*max_write_size*/ 4); + + single_buffer_factory bf("abcdefghijklmnopqrst"); // 20 bytes + auto [ec, n] = co_await write_at_least(ws, bf.buffer(), 10); + if(ec) + co_return; + + // writes of 4 + 4 + 4 -> 12 >= 10, then stop. + BOOST_TEST_EQ(n, 12u); + BOOST_TEST_EQ(ws.data(), "abcdefghijkl"); + })); + } + + void + testZeroMinimum() + { + // n == 0 returns immediately without awaiting write_some. + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::write_stream ws(f); + + single_buffer_factory bf("data"); + auto [ec, n] = co_await write_at_least(ws, bf.buffer(), 0); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + })); + } + + void + testImpossibleRequest() + { + // n > buffer_size(buffers) fails immediately with EINVAL and + // does not touch the stream. + BOOST_TEST(test::fuse().inert([](test::fuse&) -> task + { + test::write_stream ws; + + single_buffer_factory bf("12345678"); // 8 bytes + auto [ec, n] = co_await write_at_least(ws, bf.buffer(), 16); + BOOST_TEST(ec == std::errc::invalid_argument); + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + })); + } + + void + testContingencyCoincidentWithN() + { + // A contingency on the write that reaches n is a success + // (count >= n); a contingency on a short transfer is reported. + + // contingency coincident with reaching n -> 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_at_least(ws, bf.buffer(), 8); + 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_at_least(ws, bf.buffer(), 8); + BOOST_TEST(!! ec); + BOOST_TEST_EQ(n, 5u); + })); + } + + // Regression: capy#263. write_at_least() must take its buffer + // sequence by value so that storing the returned awaitable past + // the full-expression that created the sequence does not dangle. + void + testStoredAwaitableTemporarySequence() + { + BOOST_TEST(test::fuse().armed([](test::fuse& f) -> task + { + test::write_stream ws(f); + + char const data1[] = "hello"; + char const data2[] = "world"; + + auto aw = write_at_least(ws, std::array{{ + const_buffer(data1, 5), + const_buffer(data2, 5) + }}, 10); + + auto [ec, n] = co_await std::move(aw); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(ws.data(), "helloworld"); + })); + } + + void + run() + { + testSatisfiedExact(); + testSatisfiedWithBonus(); + testLoopUntilN(); + testZeroMinimum(); + testImpossibleRequest(); + testContingencyCoincidentWithN(); + testStoredAwaitableTemporarySequence(); + } +}; + +TEST_SUITE( + write_at_least_test, + "boost.capy.write_at_least"); + +} // namespace capy +} // namespace boost