diff --git a/include/boost/http/concept/buffer_sink.hpp b/include/boost/http/concept/buffer_sink.hpp new file mode 100644 index 00000000..37a20e74 --- /dev/null +++ b/include/boost/http/concept/buffer_sink.hpp @@ -0,0 +1,145 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_CONCEPT_BUFFER_SINK_HPP +#define BOOST_HTTP_CONCEPT_BUFFER_SINK_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost { +namespace http { + +/** Concept for types that consume buffer data using callee-owned buffers. + + A type satisfies `BufferSink` if it provides a synchronous `prepare` + member function that fills a caller-provided span with mutable buffer + descriptors pointing to the sink's internal storage, and asynchronous + `commit` and `commit_eof` member functions to finalize written data. + + This concept models the "callee owns buffers" pattern where the sink + provides writable memory and the caller writes directly into it, + enabling zero-copy data transfer. Compare with @ref WriteSink which + uses the "caller owns buffers" pattern. + + @tparam T The sink type. + + @par Syntactic Requirements + + @li `T` must provide a synchronous `prepare` member function accepting + a `std::span` and returning a span of filled buffers + @li `T` must provide `commit(n)` returning an @ref capy::IoAwaitable that + decomposes to `(error_code)` + @li `T` must provide `commit_eof(n)` returning an @ref capy::IoAwaitable + that decomposes to `(error_code)` + + @par Semantic Requirements + + The `prepare` operation provides writable buffer space: + + @li Returns a span of buffer descriptors that were filled + @li The returned buffers point to the sink's internal storage + @li If the returned span is empty, the sink has no available space; + caller should call `commit` to flush data and try again + + The `commit` operation finalizes written data: + + @li Commits `n` bytes written to the most recent `prepare` buffers + @li May trigger underlying I/O (flush to socket, compression, etc.) + @li On success: `ec` is `false` + @li On error: `ec` is `true` + + The `commit_eof` operation commits final data and signals end-of-stream: + + @li Commits `n` bytes written to the most recent `prepare` buffers + and finalizes the sink + @li After success, no further operations are permitted + @li On success: `ec` is `false`, sink is finalized + @li On error: `ec` is `true` + + @par Buffer Lifetime + + Buffers returned by `prepare` remain valid until the next call to + `prepare`, `commit`, `commit_eof`, or until the sink is destroyed. + + @par Conforming Signatures + + @code + std::span prepare( std::span dest ); + + capy::IoAwaitable auto commit( std::size_t n ); + capy::IoAwaitable auto commit_eof( std::size_t n ); + @endcode + + @par Example + + @code + template + io_task transfer( Source& source, Sink& sink ) + { + capy::const_buffer src_arr[16]; + capy::mutable_buffer dst_arr[16]; + std::size_t total = 0; + + for(;;) + { + auto [ec1, src_bufs] = co_await source.pull( src_arr ); + if( ec1 == cond::eof ) + { + auto [eof_ec] = co_await sink.commit_eof( 0 ); + co_return {eof_ec, total}; + } + if( ec1 ) + co_return {ec1, total}; + + auto dst_bufs = sink.prepare( dst_arr ); + std::size_t n = buffer_copy( dst_bufs, src_bufs ); + + auto [ec2] = co_await sink.commit( n ); + if( ec2 ) + co_return {ec2, total}; + + total += n; + } + } + @endcode + + @see BufferSource, WriteSink, capy::IoAwaitable, capy::awaitable_decomposes_to +*/ +template +concept BufferSink = + requires(T& sink, std::span dest, std::size_t n) + { + // Synchronous: get writable buffers from sink's internal storage + { sink.prepare(dest) } -> std::same_as>; + + // Async: commit n bytes written + { sink.commit(n) } -> capy::IoAwaitable; + requires capy::awaitable_decomposes_to< + decltype(sink.commit(n)), + std::error_code>; + + // Async: commit n final bytes and signal end of data + { sink.commit_eof(n) } -> capy::IoAwaitable; + requires capy::awaitable_decomposes_to< + decltype(sink.commit_eof(n)), + std::error_code>; + }; + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/concept/buffer_source.hpp b/include/boost/http/concept/buffer_source.hpp new file mode 100644 index 00000000..ca5d1278 --- /dev/null +++ b/include/boost/http/concept/buffer_source.hpp @@ -0,0 +1,121 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_CONCEPT_BUFFER_SOURCE_HPP +#define BOOST_HTTP_CONCEPT_BUFFER_SOURCE_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost { +namespace http { + +/** Concept for types that produce buffer data asynchronously. + + A type satisfies `BufferSource` if it provides a `pull` member function + that fills a caller-provided span of buffer descriptors and + await-returns `(error_code, std::span)`, plus a `consume` member function + to indicate how many bytes were used. + + Use this concept when you need to produce data asynchronously for + transfer to a sink, such as streaming HTTP request bodies or reading + file contents for transmission. + + @tparam T The source type. + + @par Syntactic Requirements + + @li `T` must provide a `pull` member function accepting a + `std::span` for output + @li The return type must satisfy @ref capy::IoAwaitable + @li The awaitable must decompose to `(error_code,std::span)` + via structured bindings + @li `T` must provide a `consume` member function accepting a byte count + + @par Semantic Requirements + + The `pull` operation fills the provided buffer span with data starting + from the current unconsumed position. On return, exactly one of the + following is true: + + @li **Data available**: `!ec` and `bufs.size() > 0`. + The returned span contains buffer descriptors. + @li **Source exhausted**: `ec == cond::eof` and `bufs.empty()`. + No more data is available; the transfer is complete. + @li **Error**: `ec` is `true` and `ec != cond::eof`. + An error occurred. + + Calling `pull` multiple times without intervening `consume` returns + the same unconsumed data. The `consume` operation advances the read + position by the specified number of bytes. The next `pull` returns + data starting after the consumed bytes. + + @par Buffer Lifetime + + The memory referenced by the returned buffer descriptors must remain + valid until the next call to `pull`, `consume`, or until the source + is destroyed. + + @par Conforming Signatures + + @code + some_io_awaitable>> + pull( std::span dest ); + + void consume( std::size_t n ) noexcept; + @endcode + + @par Example + + @code + template + io_task transfer( Source& source, Stream& stream ) + { + capy::const_buffer arr[16]; + std::size_t total = 0; + for(;;) + { + auto [ec, bufs] = co_await source.pull( arr ); + if( ec == cond::eof ) + co_return {{}, total}; + if( ec ) + co_return {ec, total}; + auto [write_ec, n] = co_await stream.write_some( bufs ); + if( write_ec ) + co_return {write_ec, total}; + source.consume( n ); + total += n; + } + } + @endcode + + @see capy::IoAwaitable, WriteSink, capy::awaitable_decomposes_to +*/ +template +concept BufferSource = + requires(T& src, std::span dest, std::size_t n) + { + { src.pull(dest) } -> capy::IoAwaitable; + requires capy::awaitable_decomposes_to< + decltype(src.pull(dest)), + std::error_code, std::span>; + src.consume(n); + }; + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/concept/read_source.hpp b/include/boost/http/concept/read_source.hpp new file mode 100644 index 00000000..2e69e9a3 --- /dev/null +++ b/include/boost/http/concept/read_source.hpp @@ -0,0 +1,136 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_CONCEPT_READ_SOURCE_HPP +#define BOOST_HTTP_CONCEPT_READ_SOURCE_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace boost { +namespace http { + +/** Concept for types providing complete reads from a data source. + + A type satisfies `ReadSource` if it satisfies @ref capy::ReadStream + and additionally provides a `read` member function that accepts + any @ref MutableBufferSequence and await-returns + `(error_code, std::size_t)`. + + `ReadSource` refines `capy::ReadStream`. Every `ReadSource` is a + `capy::ReadStream`. Algorithms constrained on `capy::ReadStream` accept both + raw streams and sources. + + @tparam T The source type. + + @par Syntactic Requirements + + @li `T` must satisfy @ref capy::ReadStream (provides `read_some`) + @li `T` must provide a `read` member function template accepting + any @ref MutableBufferSequence + @li The return type of `read` must satisfy @ref capy::IoAwaitable + @li The awaitable must decompose to `(error_code, std::size_t)` + via structured bindings + + @par Semantic Requirements + + The inherited `read_some` operation attempts to read up to + `buffer_size( buffers )` bytes (partial read). See @ref capy::ReadStream. + + The `read` operation fills the entire buffer sequence. On return, + exactly one of the following is true: + + @li **Success**: `!ec` and `n` equals `buffer_size( buffers )`. + The entire buffer sequence was filled. + @li **End-of-stream**: `ec == cond::eof` and `n` indicates the + number of bytes transferred before EOF was reached. + @li **Error**: `ec` and `n` indicates the number of bytes + transferred before the error. + + Successful partial reads are not permitted; either the entire + buffer is filled or the operation returns with an error. + + If `buffer_empty( buffers )` is `true`, the operation completes + immediately with `!ec` and `n` equal to 0. + + When the buffer sequence contains multiple buffers, each buffer is + filled completely before proceeding to the next. + + @par Error Reporting + I/O conditions arising from the underlying I/O system (EOF, + connection reset, broken pipe, etc.) are reported via the + `error_code` component of the return value. Failures in the + library wrapper itself (such as memory allocation failure) + are reported via exceptions. + + @throws std::bad_alloc If coroutine frame allocation fails. + + @par Buffer Lifetime + + The caller must ensure that the memory referenced by the buffer + sequence remains valid until the `co_await` expression returns. + + @par Conforming Signatures + + @code + template< MutableBufferSequence MB > + capy::IoAwaitable auto read_some( MB buffers ); // inherited from capy::ReadStream + + template< MutableBufferSequence MB > + capy::IoAwaitable auto read( MB buffers ); + @endcode + + @warning **Coroutine Buffer Lifetime**: When implementing coroutine + member functions, prefer accepting buffer sequences **by value** + rather than by reference. Buffer sequences passed by reference may + become dangling if the caller's stack frame is destroyed before the + coroutine completes. Passing by value ensures the buffer sequence + is copied into the coroutine frame and remains valid across + suspension points. + + @par Example + + @code + template< ReadSource Source > + task<> read_header( Source& source ) + { + char header[16]; + auto [ec, n] = co_await source.read( + capy::mutable_buffer( header ) ); + if( ec ) + co_return; + // header contains exactly 16 bytes + } + @endcode + + @see capy::ReadStream, capy::IoAwaitable, MutableBufferSequence, + capy::awaitable_decomposes_to +*/ +template +concept ReadSource = + capy::ReadStream && + requires(T& source, capy::mutable_buffer_archetype buffers) + { + { source.read(buffers) } -> capy::IoAwaitable; + requires capy::awaitable_decomposes_to< + decltype(source.read(buffers)), + std::error_code, std::size_t>; + }; + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/concept/write_sink.hpp b/include/boost/http/concept/write_sink.hpp new file mode 100644 index 00000000..293f84ab --- /dev/null +++ b/include/boost/http/concept/write_sink.hpp @@ -0,0 +1,165 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_CONCEPT_WRITE_SINK_HPP +#define BOOST_HTTP_CONCEPT_WRITE_SINK_HPP + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace boost { +namespace http { + +/** Concept for types providing complete writes with EOF signaling. + + A type satisfies `WriteSink` if it satisfies @ref capy::WriteStream + and additionally provides `write`, `write_eof(buffers)`, and + `write_eof()` member functions that await-return + `(error_code, std::size_t)`. + + `WriteSink` refines `capy::WriteStream`. Every `WriteSink` is a + `capy::WriteStream`. Algorithms constrained on `capy::WriteStream` accept + both raw streams and sinks. + + @tparam T The sink type. + + @par Syntactic Requirements + + @li `T` must satisfy @ref capy::WriteStream (provides `write_some`) + @li `T` must provide a `write` member function template accepting + any @ref ConstBufferSequence, returning an awaitable that + decomposes to `(error_code,std::size_t)` + @li `T` must provide a `write_eof` member function template + accepting any @ref ConstBufferSequence, returning an awaitable + that decomposes to `(error_code,std::size_t)` + @li `T` must provide a `write_eof` member function taking no + arguments, returning an awaitable that decomposes to + `(error_code)` + @li All return types must satisfy @ref capy::IoAwaitable + + @par Semantic Requirements + + The inherited `write_some` operation attempts to write up to + `buffer_size( buffers )` bytes (partial write). See @ref capy::WriteStream. + + The `write` operation consumes the entire buffer sequence: + + @li On success: `!ec`, and `n` equals `buffer_size( buffers )`. + @li On error: `ec`, and `n` indicates the number of bytes + written before the error. + + The `write_eof(buffers)` operation writes the entire buffer + sequence and signals end-of-stream atomically: + + @li On success: `!ec`, `n` equals `buffer_size( buffers )`, + and the sink is finalized. + @li On error: `ec`, and `n` indicates the number of bytes + written before the error. + + The `write_eof()` operation signals end-of-stream with no data: + + @li On success: `!ec`, and the sink is finalized. + @li On error: `ec`. + + After `write_eof` (either overload) returns successfully, no + further writes or EOF signals are permitted. + + @par Error Reporting + I/O conditions arising from the underlying I/O system (EOF, + connection reset, broken pipe, etc.) are reported via the + `error_code` component of the return value. Failures in the + library wrapper itself (such as memory allocation failure) + are reported via exceptions. + + @throws std::bad_alloc If coroutine frame allocation fails. + + @par Buffer Lifetime + + The caller must ensure that the memory referenced by the buffer + sequence remains valid until the `co_await` expression returns. + + @par Conforming Signatures + + @code + template< ConstBufferSequence Buffers > + capy::IoAwaitable auto write_some( Buffers buffers ); // inherited + + template< ConstBufferSequence Buffers > + capy::IoAwaitable auto write( Buffers buffers ); + + template< ConstBufferSequence Buffers > + capy::IoAwaitable auto write_eof( Buffers buffers ); + + capy::IoAwaitable auto write_eof(); + @endcode + + @warning **Coroutine Buffer Lifetime**: When implementing coroutine + member functions, prefer accepting buffer sequences **by value** + rather than by reference. Buffer sequences passed by reference may + become dangling if the caller's stack frame is destroyed before the + coroutine completes. Passing by value ensures the buffer sequence + is copied into the coroutine frame and remains valid across + suspension points. + + @par Example + + @code + template< WriteSink Sink > + task<> send_body( Sink& sink, std::string_view data ) + { + // Atomic: write all data and signal EOF + auto [ec, n] = co_await sink.write_eof( + make_buffer( data ) ); + } + + // Or separately: + template< WriteSink Sink > + task<> send_body2( Sink& sink, std::string_view data ) + { + auto [ec, n] = co_await sink.write( + make_buffer( data ) ); + if( ec ) + co_return; + auto [ec2] = co_await sink.write_eof(); + } + @endcode + + @see capy::WriteStream, capy::IoAwaitable, ConstBufferSequence, + capy::awaitable_decomposes_to +*/ +template +concept WriteSink = + capy::WriteStream && + requires(T& sink, capy::const_buffer_archetype buffers) + { + { sink.write(buffers) } -> capy::IoAwaitable; + requires capy::awaitable_decomposes_to< + decltype(sink.write(buffers)), + std::error_code, std::size_t>; + { sink.write_eof(buffers) } -> capy::IoAwaitable; + requires capy::awaitable_decomposes_to< + decltype(sink.write_eof(buffers)), + std::error_code, std::size_t>; + { sink.write_eof() } -> capy::IoAwaitable; + requires capy::awaitable_decomposes_to< + decltype(sink.write_eof()), + std::error_code>; + }; + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/io/any_buffer_sink.hpp b/include/boost/http/io/any_buffer_sink.hpp new file mode 100644 index 00000000..f1fbdfa5 --- /dev/null +++ b/include/boost/http/io/any_buffer_sink.hpp @@ -0,0 +1,1258 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_IO_ANY_BUFFER_SINK_HPP +#define BOOST_HTTP_IO_ANY_BUFFER_SINK_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace http { + +/** Type-erased wrapper for any BufferSink. + + This class provides type erasure for any type satisfying the + @ref BufferSink concept, enabling runtime polymorphism for + buffer sink operations. It uses cached awaitable storage to achieve + zero steady-state allocation after construction. + + The wrapper exposes two interfaces for producing data: + the @ref BufferSink interface (`prepare`, `commit`, `commit_eof`) + and the @ref WriteSink interface (`write_some`, `write`, + `write_eof`). Choose the interface that matches how your data + is produced: + + @par Choosing an Interface + + Use the **BufferSink** interface when you are a generator that + produces data into externally-provided buffers. The sink owns + the memory; you call @ref prepare to obtain writable buffers, + fill them, then call @ref commit or @ref commit_eof. + + Use the **WriteSink** interface when you already have buffers + containing the data to write: + - If the entire body is available up front, call + @ref write_eof(buffers) to send everything atomically. + - If data arrives incrementally, call @ref write or + @ref write_some in a loop, then @ref write_eof() when done. + Prefer `write` (complete) unless your streaming pattern + benefits from partial writes via `write_some`. + + If the wrapped type only satisfies @ref BufferSink, the + @ref WriteSink operations are provided automatically. + + @par Construction Modes + + - **Owning**: Pass by value to transfer ownership. The wrapper + allocates storage and owns the sink. + - **Reference**: Pass a pointer to wrap without ownership. The + pointed-to sink must outlive this wrapper. + + @par Awaitable Preallocation + The constructor preallocates storage for the type-erased awaitable. + This reserves all virtual address space at server startup + so memory usage can be measured up front, rather than + allocating piecemeal as traffic arrives. + + @par Thread Safety + Not thread-safe. Concurrent operations on the same wrapper + are undefined behavior. + + @par Example + @code + // Owning - takes ownership of the sink + any_buffer_sink abs(some_buffer_sink{args...}); + + // Reference - wraps without ownership + some_buffer_sink sink; + any_buffer_sink abs(&sink); + + // BufferSink interface: generate into callee-owned buffers + capy::mutable_buffer arr[16]; + auto bufs = abs.prepare(arr); + // Write data into bufs[0..bufs.size()) + auto [ec] = co_await abs.commit(bytes_written); + auto [ec2] = co_await abs.commit_eof(0); + + // WriteSink interface: send caller-owned buffers + auto [ec3, n] = co_await abs.write(make_buffer("hello", 5)); + auto [ec4] = co_await abs.write_eof(); + + // Or send everything at once + auto [ec5, n2] = co_await abs.write_eof( + make_buffer(body_data)); + @endcode + + @see any_buffer_source, BufferSink, WriteSink +*/ +class any_buffer_sink +{ + struct vtable; + struct awaitable_ops; + struct write_awaitable_ops; + + template + struct vtable_for_impl; + + // hot-path members first for cache locality + void* sink_ = nullptr; + vtable const* vt_ = nullptr; + void* cached_awaitable_ = nullptr; + awaitable_ops const* active_ops_ = nullptr; + write_awaitable_ops const* active_write_ops_ = nullptr; + void* storage_ = nullptr; + +public: + /** Destructor. + + Destroys the owned sink (if any) and releases the cached + awaitable storage. + */ + ~any_buffer_sink(); + + /** Construct a default instance. + + Constructs an empty wrapper. Operations on a default-constructed + wrapper result in undefined behavior. + */ + any_buffer_sink() = default; + + /** Non-copyable. + + The awaitable cache is per-instance and cannot be shared. + */ + any_buffer_sink(any_buffer_sink const&) = delete; + any_buffer_sink& operator=(any_buffer_sink const&) = delete; + + /** Construct by moving. + + Transfers ownership of the wrapped sink (if owned) and + cached awaitable storage from `other`. After the move, `other` is + in a default-constructed state. + + @param other The wrapper to move from. + */ + any_buffer_sink(any_buffer_sink&& other) noexcept + : sink_(std::exchange(other.sink_, nullptr)) + , vt_(std::exchange(other.vt_, nullptr)) + , cached_awaitable_(std::exchange(other.cached_awaitable_, nullptr)) + , active_ops_(std::exchange(other.active_ops_, nullptr)) + , active_write_ops_(std::exchange(other.active_write_ops_, nullptr)) + , storage_(std::exchange(other.storage_, nullptr)) + { + } + + /** Assign by moving. + + Destroys any owned sink and releases existing resources, + then transfers ownership from `other`. + + @param other The wrapper to move from. + @return Reference to this wrapper. + */ + any_buffer_sink& + operator=(any_buffer_sink&& other) noexcept; + + /** Construct by taking ownership of a BufferSink. + + Allocates storage and moves the sink into this wrapper. + The wrapper owns the sink and will destroy it. If `S` also + satisfies @ref WriteSink, native write operations are + forwarded through the virtual boundary. + + @param s The sink to take ownership of. + */ + template + requires (!std::same_as, any_buffer_sink>) + any_buffer_sink(S s); + + /** Construct by wrapping a BufferSink without ownership. + + Wraps the given sink by pointer. The sink must remain + valid for the lifetime of this wrapper. If `S` also + satisfies @ref WriteSink, native write operations are + forwarded through the virtual boundary. + + @param s Pointer to the sink to wrap. + */ + template + any_buffer_sink(S* s); + + /** Check if the wrapper contains a valid sink. + + @return `true` if wrapping a sink, `false` if default-constructed + or moved-from. + */ + bool + has_value() const noexcept + { + return sink_ != nullptr; + } + + /** Check if the wrapper contains a valid sink. + + @return `true` if wrapping a sink, `false` if default-constructed + or moved-from. + */ + explicit + operator bool() const noexcept + { + return has_value(); + } + + /** Prepare writable buffers. + + Fills the provided span with mutable buffer descriptors + pointing to the underlying sink's internal storage. This + operation is synchronous. + + @param dest Span of capy::mutable_buffer to fill. + + @return A span of filled buffers. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + std::span + prepare(std::span dest); + + /** Commit bytes written to the prepared buffers. + + Commits `n` bytes written to the buffers returned by the + most recent call to @ref prepare. The operation may trigger + underlying I/O. + + @param n The number of bytes to commit. + + @return An awaitable that await-returns `(error_code)`. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + auto + commit(std::size_t n); + + /** Commit final bytes and signal end-of-stream. + + Commits `n` bytes written to the buffers returned by the + most recent call to @ref prepare and finalizes the sink. + After success, no further operations are permitted. + + @param n The number of bytes to commit. + + @return An awaitable that await-returns `(error_code)`. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + auto + commit_eof(std::size_t n); + + /** Write some data from a buffer sequence. + + Attempt to write up to `capy::buffer_size( buffers )` bytes from + the buffer sequence to the underlying sink. May consume less + than the full sequence. + + When the wrapped type provides native @ref WriteSink support, + the operation forwards directly. Otherwise it is synthesized + from @ref prepare and @ref commit with a buffer copy. + + @param buffers The buffer sequence to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + template + capy::io_task + write_some(CB buffers); + + /** Write all data from a buffer sequence. + + Writes all data from the buffer sequence to the underlying + sink. This method satisfies the @ref WriteSink concept. + + When the wrapped type provides native @ref WriteSink support, + each window is forwarded directly. Otherwise the data is + copied into the sink via @ref prepare and @ref commit. + + @param buffers The buffer sequence to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + template + capy::io_task + write(CB buffers); + + /** Atomically write data and signal end-of-stream. + + Writes all data from the buffer sequence to the underlying + sink and then signals end-of-stream. + + When the wrapped type provides native @ref WriteSink support, + the final window is sent atomically via the underlying + `write_eof(buffers)`. Otherwise the data is synthesized + through @ref prepare, @ref commit, and @ref commit_eof. + + @param buffers The buffer sequence to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + template + capy::io_task + write_eof(CB buffers); + + /** Signal end-of-stream. + + Indicates that no more data will be written to the sink. + This method satisfies the @ref WriteSink concept. + + When the wrapped type provides native @ref WriteSink support, + the underlying `write_eof()` is called. Otherwise the + operation is implemented as `commit_eof(0)`. + + @return An awaitable that await-returns `(error_code)`. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + auto + write_eof(); + +protected: + /** Rebind to a new sink after move. + + Updates the internal pointer to reference a new sink object. + Used by owning wrappers after move assignment when the owned + object has moved to a new location. + + @param new_sink The new sink to bind to. Must be the same + type as the original sink. + + @note Terminates if called with a sink of different type + than the original. + */ + template + void + rebind(S& new_sink) noexcept + { + if(vt_ != &vtable_for_impl::value) + std::terminate(); + sink_ = &new_sink; + } + +private: + /** Forward a partial write through the vtable. + + Constructs the underlying `write_some` awaitable in + cached storage and returns a type-erased awaitable. + */ + auto + write_some_(std::span buffers); + + /** Forward a complete write through the vtable. + + Constructs the underlying `write` awaitable in + cached storage and returns a type-erased awaitable. + */ + auto + write_(std::span buffers); + + /** Forward an atomic write-with-EOF through the vtable. + + Constructs the underlying `write_eof(buffers)` awaitable + in cached storage and returns a type-erased awaitable. + */ + auto + write_eof_buffers_(std::span buffers); +}; + +/** Type-erased ops for awaitables that await-return `capy::io_result<>`. */ +struct any_buffer_sink::awaitable_ops +{ + bool (*await_ready)(void*); + std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, capy::io_env const*); + capy::io_result<> (*await_resume)(void*); + void (*destroy)(void*) noexcept; +}; + +/** Type-erased ops for awaitables that await-return `capy::io_result`. */ +struct any_buffer_sink::write_awaitable_ops +{ + bool (*await_ready)(void*); + std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, capy::io_env const*); + capy::io_result (*await_resume)(void*); + void (*destroy)(void*) noexcept; +}; + +struct any_buffer_sink::vtable +{ + void (*destroy)(void*) noexcept; + std::span (*do_prepare)( + void* sink, + std::span dest); + std::size_t awaitable_size; + std::size_t awaitable_align; + awaitable_ops const* (*construct_commit_awaitable)( + void* sink, + void* storage, + std::size_t n); + awaitable_ops const* (*construct_commit_eof_awaitable)( + void* sink, + void* storage, + std::size_t n); + + // WriteSink forwarding (null when wrapped type is BufferSink-only) + write_awaitable_ops const* (*construct_write_some_awaitable)( + void* sink, + void* storage, + std::span buffers); + write_awaitable_ops const* (*construct_write_awaitable)( + void* sink, + void* storage, + std::span buffers); + write_awaitable_ops const* (*construct_write_eof_buffers_awaitable)( + void* sink, + void* storage, + std::span buffers); + awaitable_ops const* (*construct_write_eof_awaitable)( + void* sink, + void* storage); +}; + +template +struct any_buffer_sink::vtable_for_impl +{ + using CommitAwaitable = decltype(std::declval().commit( + std::size_t{})); + using CommitEofAwaitable = decltype(std::declval().commit_eof( + std::size_t{})); + + static void + do_destroy_impl(void* sink) noexcept + { + static_cast(sink)->~S(); + } + + static std::span + do_prepare_impl( + void* sink, + std::span dest) + { + auto& s = *static_cast(sink); + return s.prepare(dest); + } + + static awaitable_ops const* + construct_commit_awaitable_impl( + void* sink, + void* storage, + std::size_t n) + { + auto& s = *static_cast(sink); + ::new(storage) CommitAwaitable(s.commit(n)); + + static constexpr awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~CommitAwaitable(); + } + }; + return &ops; + } + + static awaitable_ops const* + construct_commit_eof_awaitable_impl( + void* sink, + void* storage, + std::size_t n) + { + auto& s = *static_cast(sink); + ::new(storage) CommitEofAwaitable(s.commit_eof(n)); + + static constexpr awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~CommitEofAwaitable(); + } + }; + return &ops; + } + + static write_awaitable_ops const* + construct_write_some_awaitable_impl( + void* sink, + void* storage, + std::span buffers) + requires WriteSink + { + using Aw = decltype(std::declval().write_some( + std::span{})); + auto& s = *static_cast(sink); + ::new(storage) Aw(s.write_some(buffers)); + + static constexpr write_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~Aw(); + } + }; + return &ops; + } + + static write_awaitable_ops const* + construct_write_awaitable_impl( + void* sink, + void* storage, + std::span buffers) + requires WriteSink + { + using Aw = decltype(std::declval().write( + std::span{})); + auto& s = *static_cast(sink); + ::new(storage) Aw(s.write(buffers)); + + static constexpr write_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~Aw(); + } + }; + return &ops; + } + + static write_awaitable_ops const* + construct_write_eof_buffers_awaitable_impl( + void* sink, + void* storage, + std::span buffers) + requires WriteSink + { + using Aw = decltype(std::declval().write_eof( + std::span{})); + auto& s = *static_cast(sink); + ::new(storage) Aw(s.write_eof(buffers)); + + static constexpr write_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~Aw(); + } + }; + return &ops; + } + + static awaitable_ops const* + construct_write_eof_awaitable_impl( + void* sink, + void* storage) + requires WriteSink + { + using Aw = decltype(std::declval().write_eof()); + auto& s = *static_cast(sink); + ::new(storage) Aw(s.write_eof()); + + static constexpr awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~Aw(); + } + }; + return &ops; + } + + static consteval std::size_t + compute_max_size() noexcept + { + std::size_t s = sizeof(CommitAwaitable) > sizeof(CommitEofAwaitable) + ? sizeof(CommitAwaitable) + : sizeof(CommitEofAwaitable); + if constexpr (WriteSink) + { + using WS = decltype(std::declval().write_some( + std::span{})); + using W = decltype(std::declval().write( + std::span{})); + using WEB = decltype(std::declval().write_eof( + std::span{})); + using WE = decltype(std::declval().write_eof()); + + if(sizeof(WS) > s) s = sizeof(WS); + if(sizeof(W) > s) s = sizeof(W); + if(sizeof(WEB) > s) s = sizeof(WEB); + if(sizeof(WE) > s) s = sizeof(WE); + } + return s; + } + + static consteval std::size_t + compute_max_align() noexcept + { + std::size_t a = alignof(CommitAwaitable) > alignof(CommitEofAwaitable) + ? alignof(CommitAwaitable) + : alignof(CommitEofAwaitable); + if constexpr (WriteSink) + { + using WS = decltype(std::declval().write_some( + std::span{})); + using W = decltype(std::declval().write( + std::span{})); + using WEB = decltype(std::declval().write_eof( + std::span{})); + using WE = decltype(std::declval().write_eof()); + + if(alignof(WS) > a) a = alignof(WS); + if(alignof(W) > a) a = alignof(W); + if(alignof(WEB) > a) a = alignof(WEB); + if(alignof(WE) > a) a = alignof(WE); + } + return a; + } + + static consteval vtable + make_vtable() noexcept + { + vtable v{}; + v.destroy = &do_destroy_impl; + v.do_prepare = &do_prepare_impl; + v.awaitable_size = compute_max_size(); + v.awaitable_align = compute_max_align(); + v.construct_commit_awaitable = &construct_commit_awaitable_impl; + v.construct_commit_eof_awaitable = &construct_commit_eof_awaitable_impl; + v.construct_write_some_awaitable = nullptr; + v.construct_write_awaitable = nullptr; + v.construct_write_eof_buffers_awaitable = nullptr; + v.construct_write_eof_awaitable = nullptr; + + if constexpr (WriteSink) + { + v.construct_write_some_awaitable = + &construct_write_some_awaitable_impl; + v.construct_write_awaitable = + &construct_write_awaitable_impl; + v.construct_write_eof_buffers_awaitable = + &construct_write_eof_buffers_awaitable_impl; + v.construct_write_eof_awaitable = + &construct_write_eof_awaitable_impl; + } + return v; + } + + static constexpr vtable value = make_vtable(); +}; + +inline +any_buffer_sink::~any_buffer_sink() +{ + if(storage_) + { + vt_->destroy(sink_); + ::operator delete(storage_); + } + if(cached_awaitable_) + ::operator delete(cached_awaitable_); +} + +inline any_buffer_sink& +any_buffer_sink::operator=(any_buffer_sink&& other) noexcept +{ + if(this != &other) + { + if(storage_) + { + vt_->destroy(sink_); + ::operator delete(storage_); + } + if(cached_awaitable_) + ::operator delete(cached_awaitable_); + sink_ = std::exchange(other.sink_, nullptr); + vt_ = std::exchange(other.vt_, nullptr); + cached_awaitable_ = std::exchange(other.cached_awaitable_, nullptr); + storage_ = std::exchange(other.storage_, nullptr); + active_ops_ = std::exchange(other.active_ops_, nullptr); + active_write_ops_ = std::exchange(other.active_write_ops_, nullptr); + } + return *this; +} + +template + requires (!std::same_as, any_buffer_sink>) +any_buffer_sink::any_buffer_sink(S s) + : vt_(&vtable_for_impl::value) +{ + struct guard { + any_buffer_sink* self; + bool committed = false; + ~guard() { + if(!committed && self->storage_) { + if(self->sink_) + self->vt_->destroy(self->sink_); // LCOV_EXCL_LINE OOM rollback: only when the cached-awaitable allocation throws + ::operator delete(self->storage_); + self->storage_ = nullptr; + self->sink_ = nullptr; + } + } + } g{this}; + + storage_ = ::operator new(sizeof(S)); + sink_ = ::new(storage_) S(std::move(s)); + + cached_awaitable_ = ::operator new(vt_->awaitable_size); + + g.committed = true; +} + +template +any_buffer_sink::any_buffer_sink(S* s) + : sink_(s) + , vt_(&vtable_for_impl::value) +{ + cached_awaitable_ = ::operator new(vt_->awaitable_size); +} + +inline std::span +any_buffer_sink::prepare(std::span dest) +{ + return vt_->do_prepare(sink_, dest); +} + +inline auto +any_buffer_sink::commit(std::size_t n) +{ + struct awaitable + { + any_buffer_sink* self_; + std::size_t n_; + + bool + await_ready() + { + self_->active_ops_ = self_->vt_->construct_commit_awaitable( + self_->sink_, + self_->cached_awaitable_, + n_); + return self_->active_ops_->await_ready(self_->cached_awaitable_); + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + return self_->active_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result<> + await_resume() + { + struct guard { + any_buffer_sink* self; + ~guard() { + self->active_ops_->destroy(self->cached_awaitable_); + self->active_ops_ = nullptr; + } + } g{self_}; + return self_->active_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, n}; +} + +inline auto +any_buffer_sink::commit_eof(std::size_t n) +{ + struct awaitable + { + any_buffer_sink* self_; + std::size_t n_; + + bool + await_ready() + { + self_->active_ops_ = self_->vt_->construct_commit_eof_awaitable( + self_->sink_, + self_->cached_awaitable_, + n_); + return self_->active_ops_->await_ready(self_->cached_awaitable_); + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + return self_->active_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result<> + await_resume() + { + struct guard { + any_buffer_sink* self; + ~guard() { + self->active_ops_->destroy(self->cached_awaitable_); + self->active_ops_ = nullptr; + } + } g{self_}; + return self_->active_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, n}; +} + +inline auto +any_buffer_sink::write_some_( + std::span buffers) +{ + struct awaitable + { + any_buffer_sink* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_write_ops_ = + self_->vt_->construct_write_some_awaitable( + self_->sink_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_write_ops_->await_ready( + self_->cached_awaitable_)) + return h; + + return self_->active_write_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_buffer_sink* self; + ~guard() { + self->active_write_ops_->destroy( + self->cached_awaitable_); + self->active_write_ops_ = nullptr; + } + } g{self_}; + return self_->active_write_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +inline auto +any_buffer_sink::write_( + std::span buffers) +{ + struct awaitable + { + any_buffer_sink* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_write_ops_ = + self_->vt_->construct_write_awaitable( + self_->sink_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_write_ops_->await_ready( + self_->cached_awaitable_)) + return h; + + return self_->active_write_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_buffer_sink* self; + ~guard() { + self->active_write_ops_->destroy( + self->cached_awaitable_); + self->active_write_ops_ = nullptr; + } + } g{self_}; + return self_->active_write_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +inline auto +any_buffer_sink::write_eof_buffers_( + std::span buffers) +{ + struct awaitable + { + any_buffer_sink* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_write_ops_ = + self_->vt_->construct_write_eof_buffers_awaitable( + self_->sink_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_write_ops_->await_ready( + self_->cached_awaitable_)) + return h; + + return self_->active_write_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_buffer_sink* self; + ~guard() { + self->active_write_ops_->destroy( + self->cached_awaitable_); + self->active_write_ops_ = nullptr; + } + } g{self_}; + return self_->active_write_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +template +capy::io_task +any_buffer_sink::write_some(CB buffers) +{ + capy::buffer_param bp(buffers); + auto src = bp.data(); + if(src.empty()) + co_return {{}, 0}; + + // Native WriteSink path + if(vt_->construct_write_some_awaitable) + co_return co_await write_some_(src); + + // Synthesized path: prepare + capy::buffer_copy + commit + capy::mutable_buffer arr[capy::detail::max_iovec_]; + auto dst_bufs = prepare(arr); + if(dst_bufs.empty()) + { + auto [ec] = co_await commit(0); + if(ec) + co_return {ec, 0}; + dst_bufs = prepare(arr); + if(dst_bufs.empty()) + co_return {{}, 0}; + } + + auto n = capy::buffer_copy(dst_bufs, src); + auto [ec] = co_await commit(n); + if(ec) + co_return {ec, 0}; + co_return {{}, n}; +} + +template +capy::io_task +any_buffer_sink::write(CB buffers) +{ + capy::buffer_param bp(buffers); + std::size_t total = 0; + + // Native WriteSink path + if(vt_->construct_write_awaitable) + { + for(;;) + { + auto bufs = bp.data(); + if(bufs.empty()) + break; + + auto [ec, n] = co_await write_(bufs); + total += n; + if(ec) + co_return {ec, total}; + bp.consume(n); + } + co_return {{}, total}; + } + + // Synthesized path: prepare + capy::buffer_copy + commit + for(;;) + { + auto src = bp.data(); + if(src.empty()) + break; + + capy::mutable_buffer arr[capy::detail::max_iovec_]; + auto dst_bufs = prepare(arr); + if(dst_bufs.empty()) + { + auto [ec] = co_await commit(0); + if(ec) + co_return {ec, total}; + continue; + } + + auto n = capy::buffer_copy(dst_bufs, src); + auto [ec] = co_await commit(n); + if(ec) + co_return {ec, total}; + bp.consume(n); + total += n; + } + + co_return {{}, total}; +} + +inline auto +any_buffer_sink::write_eof() +{ + struct awaitable + { + any_buffer_sink* self_; + + bool + await_ready() + { + if(self_->vt_->construct_write_eof_awaitable) + { + // Native WriteSink: forward to underlying write_eof() + self_->active_ops_ = + self_->vt_->construct_write_eof_awaitable( + self_->sink_, + self_->cached_awaitable_); + } + else + { + // Synthesized: commit_eof(0) + self_->active_ops_ = + self_->vt_->construct_commit_eof_awaitable( + self_->sink_, + self_->cached_awaitable_, + 0); + } + return self_->active_ops_->await_ready( + self_->cached_awaitable_); + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + return self_->active_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result<> + await_resume() + { + struct guard { + any_buffer_sink* self; + ~guard() { + self->active_ops_->destroy(self->cached_awaitable_); + self->active_ops_ = nullptr; + } + } g{self_}; + return self_->active_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this}; +} + +template +capy::io_task +any_buffer_sink::write_eof(CB buffers) +{ + // Native WriteSink path + if(vt_->construct_write_eof_buffers_awaitable) + { + capy::const_buffer_param bp(buffers); + std::size_t total = 0; + + for(;;) + { + auto bufs = bp.data(); + if(bufs.empty()) + { + auto [ec] = co_await write_eof(); + co_return {ec, total}; + } + + if(!bp.more()) + { + // Last window: send atomically with EOF + auto [ec, n] = co_await write_eof_buffers_(bufs); + total += n; + co_return {ec, total}; + } + + auto [ec, n] = co_await write_(bufs); + total += n; + if(ec) + co_return {ec, total}; + bp.consume(n); + } + } + + // Synthesized path: prepare + capy::buffer_copy + commit + commit_eof + capy::buffer_param bp(buffers); + std::size_t total = 0; + + for(;;) + { + auto src = bp.data(); + if(src.empty()) + break; + + capy::mutable_buffer arr[capy::detail::max_iovec_]; + auto dst_bufs = prepare(arr); + if(dst_bufs.empty()) + { + auto [ec] = co_await commit(0); + if(ec) + co_return {ec, total}; + continue; + } + + auto n = capy::buffer_copy(dst_bufs, src); + auto [ec] = co_await commit(n); + if(ec) + co_return {ec, total}; + bp.consume(n); + total += n; + } + + auto [ec] = co_await commit_eof(0); + if(ec) + co_return {ec, total}; + + co_return {{}, total}; +} + +static_assert(BufferSink); +static_assert(WriteSink); + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/io/any_buffer_source.hpp b/include/boost/http/io/any_buffer_source.hpp new file mode 100644 index 00000000..83d25a5b --- /dev/null +++ b/include/boost/http/io/any_buffer_source.hpp @@ -0,0 +1,834 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_IO_ANY_BUFFER_SOURCE_HPP +#define BOOST_HTTP_IO_ANY_BUFFER_SOURCE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace http { + +/** Type-erased wrapper for any BufferSource. + + This class provides type erasure for any type satisfying the + @ref BufferSource concept, enabling runtime polymorphism for + buffer pull operations. It uses cached awaitable storage to achieve + zero steady-state allocation after construction. + + The wrapper also satisfies @ref ReadSource. When the wrapped type + satisfies only @ref BufferSource, the read operations are + synthesized using @ref pull and @ref consume with an extra + buffer copy. When the wrapped type satisfies both @ref BufferSource + and @ref ReadSource, the native read operations are forwarded + directly across the virtual boundary, avoiding the copy. + + The wrapper supports two construction modes: + - **Owning**: Pass by value to transfer ownership. The wrapper + allocates storage and owns the source. + - **Reference**: Pass a pointer to wrap without ownership. The + pointed-to source must outlive this wrapper. + + Within each mode, the vtable is populated at compile time based + on whether the wrapped type also satisfies @ref ReadSource: + - **BufferSource only**: @ref read_some and @ref read are + synthesized from @ref pull and @ref consume, incurring one + buffer copy per operation. + - **BufferSource + ReadSource**: All read operations are + forwarded natively through the type-erased boundary with + no extra copy. + + @par Awaitable Preallocation + The constructor preallocates storage for the type-erased awaitable. + This reserves all virtual address space at server startup + so memory usage can be measured up front, rather than + allocating piecemeal as traffic arrives. + + @par Thread Safety + Not thread-safe. Concurrent operations on the same wrapper + are undefined behavior. + + @par Example + @code + // Owning - takes ownership of the source + any_buffer_source abs(some_buffer_source{args...}); + + // Reference - wraps without ownership + some_buffer_source src; + any_buffer_source abs(&src); + + capy::const_buffer arr[16]; + auto [ec, bufs] = co_await abs.pull(arr); + + // ReadSource interface also available + char buf[64]; + auto [ec2, n] = co_await abs.read_some(capy::mutable_buffer(buf, 64)); + @endcode + + @see any_buffer_sink, BufferSource, ReadSource +*/ +class any_buffer_source +{ + struct vtable; + struct awaitable_ops; + struct read_awaitable_ops; + + template + struct vtable_for_impl; + + // hot-path members first for cache locality + void* source_ = nullptr; + vtable const* vt_ = nullptr; + void* cached_awaitable_ = nullptr; + awaitable_ops const* active_ops_ = nullptr; + read_awaitable_ops const* active_read_ops_ = nullptr; + void* storage_ = nullptr; + +public: + /** Destructor. + + Destroys the owned source (if any) and releases the cached + awaitable storage. + */ + ~any_buffer_source(); + + /** Construct a default instance. + + Constructs an empty wrapper. Operations on a default-constructed + wrapper result in undefined behavior. + */ + any_buffer_source() = default; + + /** Non-copyable. + + The awaitable cache is per-instance and cannot be shared. + */ + any_buffer_source(any_buffer_source const&) = delete; + any_buffer_source& operator=(any_buffer_source const&) = delete; + + /** Construct by moving. + + Transfers ownership of the wrapped source (if owned) and + cached awaitable storage from `other`. After the move, `other` is + in a default-constructed state. + + @param other The wrapper to move from. + */ + any_buffer_source(any_buffer_source&& other) noexcept + : source_(std::exchange(other.source_, nullptr)) + , vt_(std::exchange(other.vt_, nullptr)) + , cached_awaitable_(std::exchange(other.cached_awaitable_, nullptr)) + , active_ops_(std::exchange(other.active_ops_, nullptr)) + , active_read_ops_(std::exchange(other.active_read_ops_, nullptr)) + , storage_(std::exchange(other.storage_, nullptr)) + { + } + + /** Assign by moving. + + Destroys any owned source and releases existing resources, + then transfers ownership from `other`. + + @param other The wrapper to move from. + @return Reference to this wrapper. + */ + any_buffer_source& + operator=(any_buffer_source&& other) noexcept; + + /** Construct by taking ownership of a BufferSource. + + Allocates storage and moves the source into this wrapper. + The wrapper owns the source and will destroy it. If `S` also + satisfies @ref ReadSource, native read operations are + forwarded through the virtual boundary. + + @param s The source to take ownership of. + */ + template + requires (!std::same_as, any_buffer_source>) + any_buffer_source(S s); + + /** Construct by wrapping a BufferSource without ownership. + + Wraps the given source by pointer. The source must remain + valid for the lifetime of this wrapper. If `S` also + satisfies @ref ReadSource, native read operations are + forwarded through the virtual boundary. + + @param s Pointer to the source to wrap. + */ + template + any_buffer_source(S* s); + + /** Check if the wrapper contains a valid source. + + @return `true` if wrapping a source, `false` if default-constructed + or moved-from. + */ + bool + has_value() const noexcept + { + return source_ != nullptr; + } + + /** Check if the wrapper contains a valid source. + + @return `true` if wrapping a source, `false` if default-constructed + or moved-from. + */ + explicit + operator bool() const noexcept + { + return has_value(); + } + + /** Consume bytes from the source. + + Advances the internal read position of the underlying source + by the specified number of bytes. The next call to @ref pull + returns data starting after the consumed bytes. + + @param n The number of bytes to consume. Must not exceed the + total size of buffers returned by the previous @ref pull. + + @par Preconditions + The wrapper must contain a valid source (`has_value() == true`). + */ + void + consume(std::size_t n) noexcept; + + /** Pull buffer data from the source. + + Fills the provided span with buffer descriptors from the + underlying source. The operation completes when data is + available, the source is exhausted, or an error occurs. + + @param dest Span of capy::const_buffer to fill. + + @return An awaitable that await-returns `(error_code,std::span)`. + On success with data, a non-empty span of filled buffers. + On EOF, `ec == capy::cond::eof` and span is empty. + + @par Preconditions + The wrapper must contain a valid source (`has_value() == true`). + The caller must not call this function again after a prior + call returned an error. + */ + auto + pull(std::span dest); + + /** Read some data into a mutable buffer sequence. + + Attempt to read up to `capy::buffer_size( buffers )` bytes into + the caller's buffers. May fill less than the full sequence. + + When the wrapped type provides native @ref ReadSource support, + the operation forwards directly. Otherwise it is synthesized + from @ref pull, @ref capy::buffer_copy, and @ref consume. + + @param buffers The buffer sequence to fill. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Preconditions + The wrapper must contain a valid source (`has_value() == true`). + The caller must not call this function again after a prior + call returned an error (including EOF). + + @see pull, consume + */ + template + capy::io_task + read_some(MB buffers); + + /** Read data into a mutable buffer sequence. + + Fills the provided buffer sequence completely. When the + wrapped type provides native @ref ReadSource support, each + window is forwarded directly. Otherwise the data is + synthesized from @ref pull, @ref capy::buffer_copy, and @ref consume. + + @param buffers The buffer sequence to fill. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + On success, `n == capy::buffer_size(buffers)`. + On EOF, `ec == error::eof` and `n` is bytes transferred. + + @par Preconditions + The wrapper must contain a valid source (`has_value() == true`). + The caller must not call this function again after a prior + call returned an error (including EOF). + + @see pull, consume + */ + template + capy::io_task + read(MB buffers); + +protected: + /** Rebind to a new source after move. + + Updates the internal pointer to reference a new source object. + Used by owning wrappers after move assignment when the owned + object has moved to a new location. + + @param new_source The new source to bind to. Must be the same + type as the original source. + + @note Terminates if called with a source of different type + than the original. + */ + template + void + rebind(S& new_source) noexcept + { + if(vt_ != &vtable_for_impl::value) + std::terminate(); + source_ = &new_source; + } + +private: + /** Forward a partial read through the vtable. + + Constructs the underlying `read_some` awaitable in + cached storage and returns a type-erased awaitable. + */ + auto + read_some_(std::span buffers); + + /** Forward a complete read through the vtable. + + Constructs the underlying `read` awaitable in + cached storage and returns a type-erased awaitable. + */ + auto + read_(std::span buffers); +}; + +/** Type-erased ops for awaitables that await-return `capy::io_result>`. */ +struct any_buffer_source::awaitable_ops +{ + bool (*await_ready)(void*); + std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, capy::io_env const*); + capy::io_result> (*await_resume)(void*); + void (*destroy)(void*) noexcept; +}; + +/** Type-erased ops for awaitables that await-return `capy::io_result`. */ +struct any_buffer_source::read_awaitable_ops +{ + bool (*await_ready)(void*); + std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, capy::io_env const*); + capy::io_result (*await_resume)(void*); + void (*destroy)(void*) noexcept; +}; + +struct any_buffer_source::vtable +{ + // BufferSource ops (always populated) + void (*destroy)(void*) noexcept; + void (*do_consume)(void* source, std::size_t n) noexcept; + std::size_t awaitable_size; + std::size_t awaitable_align; + awaitable_ops const* (*construct_awaitable)( + void* source, + void* storage, + std::span dest); + + // ReadSource forwarding (null when wrapped type is BufferSource-only) + read_awaitable_ops const* (*construct_read_some_awaitable)( + void* source, + void* storage, + std::span buffers); + read_awaitable_ops const* (*construct_read_awaitable)( + void* source, + void* storage, + std::span buffers); +}; + +template +struct any_buffer_source::vtable_for_impl +{ + using PullAwaitable = decltype(std::declval().pull( + std::declval>())); + + static void + do_destroy_impl(void* source) noexcept + { + static_cast(source)->~S(); + } + + static void + do_consume_impl(void* source, std::size_t n) noexcept + { + static_cast(source)->consume(n); + } + + static awaitable_ops const* + construct_awaitable_impl( + void* source, + void* storage, + std::span dest) + { + auto& s = *static_cast(source); + ::new(storage) PullAwaitable(s.pull(dest)); + + static constexpr awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~PullAwaitable(); + } + }; + return &ops; + } + + static read_awaitable_ops const* + construct_read_some_awaitable_impl( + void* source, + void* storage, + std::span buffers) + requires ReadSource + { + using Aw = decltype(std::declval().read_some( + std::span{})); + auto& s = *static_cast(source); + ::new(storage) Aw(s.read_some(buffers)); + + static constexpr read_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~Aw(); + } + }; + return &ops; + } + + static read_awaitable_ops const* + construct_read_awaitable_impl( + void* source, + void* storage, + std::span buffers) + requires ReadSource + { + using Aw = decltype(std::declval().read( + std::span{})); + auto& s = *static_cast(source); + ::new(storage) Aw(s.read(buffers)); + + static constexpr read_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~Aw(); + } + }; + return &ops; + } + + static consteval std::size_t + compute_max_size() noexcept + { + std::size_t s = sizeof(PullAwaitable); + if constexpr (ReadSource) + { + using RS = decltype(std::declval().read_some( + std::span{})); + using R = decltype(std::declval().read( + std::span{})); + + if(sizeof(RS) > s) s = sizeof(RS); + if(sizeof(R) > s) s = sizeof(R); + } + return s; + } + + static consteval std::size_t + compute_max_align() noexcept + { + std::size_t a = alignof(PullAwaitable); + if constexpr (ReadSource) + { + using RS = decltype(std::declval().read_some( + std::span{})); + using R = decltype(std::declval().read( + std::span{})); + + if(alignof(RS) > a) a = alignof(RS); + if(alignof(R) > a) a = alignof(R); + } + return a; + } + + static consteval vtable + make_vtable() noexcept + { + vtable v{}; + v.destroy = &do_destroy_impl; + v.do_consume = &do_consume_impl; + v.awaitable_size = compute_max_size(); + v.awaitable_align = compute_max_align(); + v.construct_awaitable = &construct_awaitable_impl; + v.construct_read_some_awaitable = nullptr; + v.construct_read_awaitable = nullptr; + + if constexpr (ReadSource) + { + v.construct_read_some_awaitable = + &construct_read_some_awaitable_impl; + v.construct_read_awaitable = + &construct_read_awaitable_impl; + } + return v; + } + + static constexpr vtable value = make_vtable(); +}; + +inline +any_buffer_source::~any_buffer_source() +{ + if(storage_) + { + vt_->destroy(source_); + ::operator delete(storage_); + } + if(cached_awaitable_) + ::operator delete(cached_awaitable_); +} + +inline any_buffer_source& +any_buffer_source::operator=(any_buffer_source&& other) noexcept +{ + if(this != &other) + { + if(storage_) + { + vt_->destroy(source_); + ::operator delete(storage_); + } + if(cached_awaitable_) + ::operator delete(cached_awaitable_); + source_ = std::exchange(other.source_, nullptr); + vt_ = std::exchange(other.vt_, nullptr); + cached_awaitable_ = std::exchange(other.cached_awaitable_, nullptr); + storage_ = std::exchange(other.storage_, nullptr); + active_ops_ = std::exchange(other.active_ops_, nullptr); + active_read_ops_ = std::exchange(other.active_read_ops_, nullptr); + } + return *this; +} + +template + requires (!std::same_as, any_buffer_source>) +any_buffer_source::any_buffer_source(S s) + : vt_(&vtable_for_impl::value) +{ + struct guard { + any_buffer_source* self; + bool committed = false; + ~guard() { + if(!committed && self->storage_) { + if(self->source_) + self->vt_->destroy(self->source_); // LCOV_EXCL_LINE OOM rollback: only when the cached-awaitable allocation throws + ::operator delete(self->storage_); + self->storage_ = nullptr; + self->source_ = nullptr; + } + } + } g{this}; + + storage_ = ::operator new(sizeof(S)); + source_ = ::new(storage_) S(std::move(s)); + + cached_awaitable_ = ::operator new(vt_->awaitable_size); + + g.committed = true; +} + +template +any_buffer_source::any_buffer_source(S* s) + : source_(s) + , vt_(&vtable_for_impl::value) +{ + cached_awaitable_ = ::operator new(vt_->awaitable_size); +} + +inline void +any_buffer_source::consume(std::size_t n) noexcept +{ + vt_->do_consume(source_, n); +} + +inline auto +any_buffer_source::pull(std::span dest) +{ + struct awaitable + { + any_buffer_source* self_; + std::span dest_; + + bool + await_ready() + { + self_->active_ops_ = self_->vt_->construct_awaitable( + self_->source_, + self_->cached_awaitable_, + dest_); + return self_->active_ops_->await_ready(self_->cached_awaitable_); + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + return self_->active_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result> + await_resume() + { + struct guard { + any_buffer_source* self; + ~guard() { + self->active_ops_->destroy(self->cached_awaitable_); + self->active_ops_ = nullptr; + } + } g{self_}; + return self_->active_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, dest}; +} + +inline auto +any_buffer_source::read_some_( + std::span buffers) +{ + struct awaitable + { + any_buffer_source* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_read_ops_ = + self_->vt_->construct_read_some_awaitable( + self_->source_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_read_ops_->await_ready( + self_->cached_awaitable_)) + return h; + + return self_->active_read_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_buffer_source* self; + ~guard() { + self->active_read_ops_->destroy( + self->cached_awaitable_); + self->active_read_ops_ = nullptr; + } + } g{self_}; + return self_->active_read_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +inline auto +any_buffer_source::read_( + std::span buffers) +{ + struct awaitable + { + any_buffer_source* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_read_ops_ = + self_->vt_->construct_read_awaitable( + self_->source_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_read_ops_->await_ready( + self_->cached_awaitable_)) + return h; + + return self_->active_read_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_buffer_source* self; + ~guard() { + self->active_read_ops_->destroy( + self->cached_awaitable_); + self->active_read_ops_ = nullptr; + } + } g{self_}; + return self_->active_read_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +template +capy::io_task +any_buffer_source::read_some(MB buffers) +{ + capy::buffer_param bp(buffers); + auto dest = bp.data(); + if(dest.empty()) + co_return {{}, 0}; + + // Native ReadSource path + if(vt_->construct_read_some_awaitable) + co_return co_await read_some_(dest); + + // Synthesized path: pull + capy::buffer_copy + consume + capy::const_buffer arr[capy::detail::max_iovec_]; + auto [ec, bufs] = co_await pull(arr); + if(ec) + co_return {ec, 0}; + + auto n = capy::buffer_copy(dest, bufs); + consume(n); + co_return {{}, n}; +} + +template +capy::io_task +any_buffer_source::read(MB buffers) +{ + capy::buffer_param bp(buffers); + std::size_t total = 0; + + // Native ReadSource path + if(vt_->construct_read_awaitable) + { + for(;;) + { + auto dest = bp.data(); + if(dest.empty()) + break; + + auto [ec, n] = co_await read_(dest); + total += n; + if(ec) + co_return {ec, total}; + bp.consume(n); + } + co_return {{}, total}; + } + + // Synthesized path: pull + capy::buffer_copy + consume + for(;;) + { + auto dest = bp.data(); + if(dest.empty()) + break; + + capy::const_buffer arr[capy::detail::max_iovec_]; + auto [ec, bufs] = co_await pull(arr); + + if(ec) + co_return {ec, total}; + + auto n = capy::buffer_copy(dest, bufs); + consume(n); + total += n; + bp.consume(n); + } + + co_return {{}, total}; +} + +static_assert(BufferSource); +static_assert(ReadSource); + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/io/any_read_source.hpp b/include/boost/http/io/any_read_source.hpp new file mode 100644 index 00000000..5c229896 --- /dev/null +++ b/include/boost/http/io/any_read_source.hpp @@ -0,0 +1,597 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_IO_ANY_READ_SOURCE_HPP +#define BOOST_HTTP_IO_ANY_READ_SOURCE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace http { + +/** Type-erased wrapper for any ReadSource. + + This class provides type erasure for any type satisfying the + @ref ReadSource concept, enabling runtime polymorphism for + source read operations. It uses cached awaitable storage to achieve + zero steady-state allocation after construction. + + The wrapper supports two construction modes: + - **Owning**: Pass by value to transfer ownership. The wrapper + allocates storage and owns the source. + - **Reference**: Pass a pointer to wrap without ownership. The + pointed-to source must outlive this wrapper. + + @par Awaitable Preallocation + The constructor preallocates storage for the type-erased awaitable. + This reserves all virtual address space at server startup + so memory usage can be measured up front, rather than + allocating piecemeal as traffic arrives. + + @par Immediate Completion + Operations complete immediately without suspending when the + buffer sequence is empty, or when the underlying source's + awaitable reports readiness via `await_ready`. + + @par Thread Safety + Not thread-safe. Concurrent operations on the same wrapper + are undefined behavior. + + @par Example + @code + // Owning - takes ownership of the source + any_read_source rs(some_source{args...}); + + // Reference - wraps without ownership + some_source source; + any_read_source rs(&source); + + capy::mutable_buffer buf(data, size); + auto [ec, n] = co_await rs.read(std::span(&buf, 1)); + @endcode + + @see any_read_stream, ReadSource +*/ +class any_read_source +{ + struct vtable; + struct awaitable_ops; + + template + struct vtable_for_impl; + + void* source_ = nullptr; + vtable const* vt_ = nullptr; + void* cached_awaitable_ = nullptr; + void* storage_ = nullptr; + awaitable_ops const* active_ops_ = nullptr; + +public: + /** Destructor. + + Destroys the owned source (if any) and releases the cached + awaitable storage. + */ + ~any_read_source(); + + /** Construct a default instance. + + Constructs an empty wrapper. Operations on a default-constructed + wrapper result in undefined behavior. + */ + any_read_source() = default; + + /** Non-copyable. + + The awaitable cache is per-instance and cannot be shared. + */ + any_read_source(any_read_source const&) = delete; + any_read_source& operator=(any_read_source const&) = delete; + + /** Construct by moving. + + Transfers ownership of the wrapped source (if owned) and + cached awaitable storage from `other`. After the move, `other` is + in a default-constructed state. + + @param other The wrapper to move from. + */ + any_read_source(any_read_source&& other) noexcept + : source_(std::exchange(other.source_, nullptr)) + , vt_(std::exchange(other.vt_, nullptr)) + , cached_awaitable_(std::exchange(other.cached_awaitable_, nullptr)) + , storage_(std::exchange(other.storage_, nullptr)) + , active_ops_(std::exchange(other.active_ops_, nullptr)) + { + } + + /** Assign by moving. + + Destroys any owned source and releases existing resources, + then transfers ownership from `other`. + + @param other The wrapper to move from. + @return Reference to this wrapper. + */ + any_read_source& + operator=(any_read_source&& other) noexcept; + + /** Construct by taking ownership of a ReadSource. + + Allocates storage and moves the source into this wrapper. + The wrapper owns the source and will destroy it. + + @param s The source to take ownership of. + */ + template + requires (!std::same_as, any_read_source>) + any_read_source(S s); + + /** Construct by wrapping a ReadSource without ownership. + + Wraps the given source by pointer. The source must remain + valid for the lifetime of this wrapper. + + @param s Pointer to the source to wrap. + */ + template + any_read_source(S* s); + + /** Check if the wrapper contains a valid source. + + @return `true` if wrapping a source, `false` if default-constructed + or moved-from. + */ + bool + has_value() const noexcept + { + return source_ != nullptr; + } + + /** Check if the wrapper contains a valid source. + + @return `true` if wrapping a source, `false` if default-constructed + or moved-from. + */ + explicit + operator bool() const noexcept + { + return has_value(); + } + + /** Initiate a partial read operation. + + Attempt to read up to `capy::buffer_size( buffers )` bytes into + the provided buffer sequence. May fill less than the + full sequence. + + @param buffers The buffer sequence to read into. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Immediate Completion + The operation completes immediately without suspending + the calling coroutine when: + @li The buffer sequence is empty, returning `{error_code{}, 0}`. + @li The underlying source's awaitable reports immediate + readiness via `await_ready`. + + @note This is a partial operation and may not process the + entire buffer sequence. Use @ref read for guaranteed + complete transfer. + + @par Preconditions + The wrapper must contain a valid source (`has_value() == true`). + The caller must not call this function again after a prior + call returned an error (including EOF). + */ + template + auto + read_some(MB buffers); + + /** Initiate a complete read operation. + + Reads data into the provided buffer sequence by forwarding + to the underlying source's `read` operation. Large buffer + sequences are processed in windows, with each window + forwarded as a separate `read` call to the underlying source. + The operation completes when the entire buffer sequence is + filled, end-of-file is reached, or an error occurs. + + @param buffers The buffer sequence to read into. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Immediate Completion + The operation completes immediately without suspending + the calling coroutine when: + @li The buffer sequence is empty, returning `{error_code{}, 0}`. + @li The underlying source's `read` awaitable reports + immediate readiness via `await_ready`. + + @par Postconditions + Exactly one of the following is true on return: + @li **Success**: `!ec` and `n == capy::buffer_size(buffers)`. + The entire buffer was filled. + @li **End-of-stream or Error**: `ec` and `n` indicates + the number of bytes transferred before the failure. + + @par Preconditions + The wrapper must contain a valid source (`has_value() == true`). + The caller must not call this function again after a prior + call returned an error (including EOF). + */ + template + capy::io_task + read(MB buffers); + +protected: + /** Rebind to a new source after move. + + Updates the internal pointer to reference a new source object. + Used by owning wrappers after move assignment when the owned + object has moved to a new location. + + @param new_source The new source to bind to. Must be the same + type as the original source. + + @note Terminates if called with a source of different type + than the original. + */ + template + void + rebind(S& new_source) noexcept + { + if(vt_ != &vtable_for_impl::value) + std::terminate(); + source_ = &new_source; + } + +private: + auto + read_(std::span buffers); +}; + +// ordered by call sequence for cache line coherence +struct any_read_source::awaitable_ops +{ + bool (*await_ready)(void*); + std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, capy::io_env const*); + capy::io_result (*await_resume)(void*); + void (*destroy)(void*) noexcept; +}; + +// ordered by call frequency for cache line coherence +struct any_read_source::vtable +{ + awaitable_ops const* (*construct_read_some_awaitable)( + void* source, + void* storage, + std::span buffers); + awaitable_ops const* (*construct_read_awaitable)( + void* source, + void* storage, + std::span buffers); + std::size_t awaitable_size; + std::size_t awaitable_align; + void (*destroy)(void*) noexcept; +}; + +template +struct any_read_source::vtable_for_impl +{ + using ReadSomeAwaitable = decltype(std::declval().read_some( + std::span{})); + using ReadAwaitable = decltype(std::declval().read( + std::span{})); + + static void + do_destroy_impl(void* source) noexcept + { + static_cast(source)->~S(); + } + + static awaitable_ops const* + construct_read_some_awaitable_impl( + void* source, + void* storage, + std::span buffers) + { + auto& s = *static_cast(source); + ::new(storage) ReadSomeAwaitable(s.read_some(buffers)); + + static constexpr awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~ReadSomeAwaitable(); + } + }; + return &ops; + } + + static awaitable_ops const* + construct_read_awaitable_impl( + void* source, + void* storage, + std::span buffers) + { + auto& s = *static_cast(source); + ::new(storage) ReadAwaitable(s.read(buffers)); + + static constexpr awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~ReadAwaitable(); // LCOV_EXCL_LINE runs via destroy tests; gcov miscounts fn-ptr thunk + } + }; + return &ops; + } + + static constexpr std::size_t max_awaitable_size = + sizeof(ReadSomeAwaitable) > sizeof(ReadAwaitable) + ? sizeof(ReadSomeAwaitable) + : sizeof(ReadAwaitable); + static constexpr std::size_t max_awaitable_align = + alignof(ReadSomeAwaitable) > alignof(ReadAwaitable) + ? alignof(ReadSomeAwaitable) + : alignof(ReadAwaitable); + + static constexpr vtable value = { + &construct_read_some_awaitable_impl, + &construct_read_awaitable_impl, + max_awaitable_size, + max_awaitable_align, + &do_destroy_impl + }; +}; + +inline +any_read_source::~any_read_source() +{ + if(storage_) + { + vt_->destroy(source_); + ::operator delete(storage_); + } + if(cached_awaitable_) + { + if(active_ops_) + active_ops_->destroy(cached_awaitable_); + ::operator delete(cached_awaitable_); + } +} + +inline any_read_source& +any_read_source::operator=(any_read_source&& other) noexcept +{ + if(this != &other) + { + if(storage_) + { + vt_->destroy(source_); + ::operator delete(storage_); + } + if(cached_awaitable_) + { + if(active_ops_) + active_ops_->destroy(cached_awaitable_); + ::operator delete(cached_awaitable_); + } + source_ = std::exchange(other.source_, nullptr); + vt_ = std::exchange(other.vt_, nullptr); + cached_awaitable_ = std::exchange(other.cached_awaitable_, nullptr); + storage_ = std::exchange(other.storage_, nullptr); + active_ops_ = std::exchange(other.active_ops_, nullptr); + } + return *this; +} + +template + requires (!std::same_as, any_read_source>) +any_read_source::any_read_source(S s) + : vt_(&vtable_for_impl::value) +{ + struct guard { + any_read_source* self; + bool committed = false; + ~guard() { + if(!committed && self->storage_) { + if(self->source_) + self->vt_->destroy(self->source_); // LCOV_EXCL_LINE OOM rollback: only when the cached-awaitable allocation throws + ::operator delete(self->storage_); + self->storage_ = nullptr; + self->source_ = nullptr; + } + } + } g{this}; + + storage_ = ::operator new(sizeof(S)); + source_ = ::new(storage_) S(std::move(s)); + + // Preallocate the awaitable storage + cached_awaitable_ = ::operator new(vt_->awaitable_size); + + g.committed = true; +} + +template +any_read_source::any_read_source(S* s) + : source_(s) + , vt_(&vtable_for_impl::value) +{ + // Preallocate the awaitable storage + cached_awaitable_ = ::operator new(vt_->awaitable_size); +} + +template +auto +any_read_source::read_some(MB buffers) +{ + struct awaitable + { + any_read_source* self_; + capy::detail::mutable_buffer_array ba_; + + awaitable(any_read_source* self, MB const& buffers) + : self_(self) + , ba_(buffers) + { + } + + bool + await_ready() const noexcept + { + return ba_.to_span().empty(); + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_ops_ = self_->vt_->construct_read_some_awaitable( + self_->source_, + self_->cached_awaitable_, + ba_.to_span()); + + if(self_->active_ops_->await_ready(self_->cached_awaitable_)) + return h; + + return self_->active_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + if(ba_.to_span().empty()) + return {{}, 0}; + + struct guard { + any_read_source* self; + ~guard() { + self->active_ops_->destroy(self->cached_awaitable_); + self->active_ops_ = nullptr; + } + } g{self_}; + return self_->active_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable(this, buffers); +} + +inline auto +any_read_source::read_(std::span buffers) +{ + struct awaitable + { + any_read_source* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_ops_ = self_->vt_->construct_read_awaitable( + self_->source_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_ops_->await_ready(self_->cached_awaitable_)) + return h; + + return self_->active_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_read_source* self; + ~guard() { + self->active_ops_->destroy(self->cached_awaitable_); + self->active_ops_ = nullptr; + } + } g{self_}; + return self_->active_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +template +capy::io_task +any_read_source::read(MB buffers) +{ + capy::buffer_param bp(buffers); + std::size_t total = 0; + + for(;;) + { + auto bufs = bp.data(); + if(bufs.empty()) + break; + + auto [ec, n] = co_await read_(bufs); + total += n; + if(ec) + co_return {ec, total}; + bp.consume(n); + } + + co_return {{}, total}; +} + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/io/any_write_sink.hpp b/include/boost/http/io/any_write_sink.hpp new file mode 100644 index 00000000..4dbabfa0 --- /dev/null +++ b/include/boost/http/io/any_write_sink.hpp @@ -0,0 +1,911 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_IO_ANY_WRITE_SINK_HPP +#define BOOST_HTTP_IO_ANY_WRITE_SINK_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace http { + +/** Type-erased wrapper for any WriteSink. + + This class provides type erasure for any type satisfying the + @ref WriteSink concept, enabling runtime polymorphism for + sink write operations. It uses cached awaitable storage to achieve + zero steady-state allocation after construction. + + The wrapper supports two construction modes: + - **Owning**: Pass by value to transfer ownership. The wrapper + allocates storage and owns the sink. + - **Reference**: Pass a pointer to wrap without ownership. The + pointed-to sink must outlive this wrapper. + + @par Awaitable Preallocation + The constructor preallocates storage for the type-erased awaitable. + This reserves all virtual address space at server startup + so memory usage can be measured up front, rather than + allocating piecemeal as traffic arrives. + + @par Immediate Completion + Operations complete immediately without suspending when the + buffer sequence is empty, or when the underlying sink's + awaitable reports readiness via `await_ready`. + + @par Thread Safety + Not thread-safe. Concurrent operations on the same wrapper + are undefined behavior. + + @par Example + @code + // Owning - takes ownership of the sink + any_write_sink ws(some_sink{args...}); + + // Reference - wraps without ownership + some_sink sink; + any_write_sink ws(&sink); + + capy::const_buffer buf(data, size); + auto [ec, n] = co_await ws.write(std::span(&buf, 1)); + auto [ec2] = co_await ws.write_eof(); + @endcode + + @see any_write_stream, WriteSink +*/ +class any_write_sink +{ + struct vtable; + struct write_awaitable_ops; + struct eof_awaitable_ops; + + template + struct vtable_for_impl; + + void* sink_ = nullptr; + vtable const* vt_ = nullptr; + void* cached_awaitable_ = nullptr; + void* storage_ = nullptr; + write_awaitable_ops const* active_write_ops_ = nullptr; + eof_awaitable_ops const* active_eof_ops_ = nullptr; + +public: + /** Destructor. + + Destroys the owned sink (if any) and releases the cached + awaitable storage. + */ + ~any_write_sink(); + + /** Construct a default instance. + + Constructs an empty wrapper. Operations on a default-constructed + wrapper result in undefined behavior. + */ + any_write_sink() = default; + + /** Non-copyable. + + The awaitable cache is per-instance and cannot be shared. + */ + any_write_sink(any_write_sink const&) = delete; + any_write_sink& operator=(any_write_sink const&) = delete; + + /** Construct by moving. + + Transfers ownership of the wrapped sink (if owned) and + cached awaitable storage from `other`. After the move, `other` is + in a default-constructed state. + + @param other The wrapper to move from. + */ + any_write_sink(any_write_sink&& other) noexcept + : sink_(std::exchange(other.sink_, nullptr)) + , vt_(std::exchange(other.vt_, nullptr)) + , cached_awaitable_(std::exchange(other.cached_awaitable_, nullptr)) + , storage_(std::exchange(other.storage_, nullptr)) + , active_write_ops_(std::exchange(other.active_write_ops_, nullptr)) + , active_eof_ops_(std::exchange(other.active_eof_ops_, nullptr)) + { + } + + /** Assign by moving. + + Destroys any owned sink and releases existing resources, + then transfers ownership from `other`. + + @param other The wrapper to move from. + @return Reference to this wrapper. + */ + any_write_sink& + operator=(any_write_sink&& other) noexcept; + + /** Construct by taking ownership of a WriteSink. + + Allocates storage and moves the sink into this wrapper. + The wrapper owns the sink and will destroy it. + + @param s The sink to take ownership of. + */ + template + requires (!std::same_as, any_write_sink>) + any_write_sink(S s); + + /** Construct by wrapping a WriteSink without ownership. + + Wraps the given sink by pointer. The sink must remain + valid for the lifetime of this wrapper. + + @param s Pointer to the sink to wrap. + */ + template + any_write_sink(S* s); + + /** Check if the wrapper contains a valid sink. + + @return `true` if wrapping a sink, `false` if default-constructed + or moved-from. + */ + bool + has_value() const noexcept + { + return sink_ != nullptr; + } + + /** Check if the wrapper contains a valid sink. + + @return `true` if wrapping a sink, `false` if default-constructed + or moved-from. + */ + explicit + operator bool() const noexcept + { + return has_value(); + } + + /** Initiate a partial write operation. + + Attempt to write up to `capy::buffer_size( buffers )` bytes from + the provided buffer sequence. May consume less than the + full sequence. + + @param buffers The buffer sequence containing data to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Immediate Completion + The operation completes immediately without suspending + the calling coroutine when: + @li The buffer sequence is empty, returning `{error_code{}, 0}`. + @li The underlying sink's awaitable reports immediate + readiness via `await_ready`. + + @note This is a partial operation and may not process the + entire buffer sequence. Use @ref write for guaranteed + complete transfer. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + template + auto + write_some(CB buffers); + + /** Initiate a complete write operation. + + Writes data from the provided buffer sequence. The operation + completes when all bytes have been consumed, or an error + occurs. Forwards to the underlying sink's `write` operation, + windowed through @ref capy::buffer_param when the sequence exceeds + the per-call buffer limit. + + @param buffers The buffer sequence containing data to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Immediate Completion + The operation completes immediately without suspending + the calling coroutine when: + @li The buffer sequence is empty, returning `{error_code{}, 0}`. + @li Every underlying `write` call completes + immediately (the wrapped sink reports readiness + via `await_ready` on each iteration). + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + template + capy::io_task + write(CB buffers); + + /** Atomically write data and signal end-of-stream. + + Writes all data from the buffer sequence and then signals + end-of-stream. The implementation decides how to partition + the data across calls to the underlying sink's @ref write + and `write_eof`. When the caller's buffer sequence is + non-empty, the final call to the underlying sink is always + `write_eof` with a non-empty buffer sequence. When the + caller's buffer sequence is empty, only `write_eof()` with + no data is called. + + @param buffers The buffer sequence containing data to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Immediate Completion + The operation completes immediately without suspending + the calling coroutine when: + @li The buffer sequence is empty. Only the @ref write_eof() + call is performed. + @li All underlying operations complete immediately (the + wrapped sink reports readiness via `await_ready`). + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + template + capy::io_task + write_eof(CB buffers); + + /** Signal end of data. + + Indicates that no more data will be written to the sink. + The operation completes when the sink is finalized, or + an error occurs. + + @return An awaitable that await-returns `(error_code)`. + + @par Immediate Completion + The operation completes immediately without suspending + the calling coroutine when the underlying sink's awaitable + reports immediate readiness via `await_ready`. + + @par Preconditions + The wrapper must contain a valid sink (`has_value() == true`). + */ + auto + write_eof(); + +protected: + /** Rebind to a new sink after move. + + Updates the internal pointer to reference a new sink object. + Used by owning wrappers after move assignment when the owned + object has moved to a new location. + + @param new_sink The new sink to bind to. Must be the same + type as the original sink. + + @note Terminates if called with a sink of different type + than the original. + */ + template + void + rebind(S& new_sink) noexcept + { + if(vt_ != &vtable_for_impl::value) + std::terminate(); + sink_ = &new_sink; + } + +private: + auto + write_some_(std::span buffers); + + auto + write_(std::span buffers); + + auto + write_eof_buffers_(std::span buffers); +}; + +struct any_write_sink::write_awaitable_ops +{ + bool (*await_ready)(void*); + std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, capy::io_env const*); + capy::io_result (*await_resume)(void*); + void (*destroy)(void*) noexcept; +}; + +struct any_write_sink::eof_awaitable_ops +{ + bool (*await_ready)(void*); + std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, capy::io_env const*); + capy::io_result<> (*await_resume)(void*); + void (*destroy)(void*) noexcept; +}; + +struct any_write_sink::vtable +{ + write_awaitable_ops const* (*construct_write_some_awaitable)( + void* sink, + void* storage, + std::span buffers); + write_awaitable_ops const* (*construct_write_awaitable)( + void* sink, + void* storage, + std::span buffers); + write_awaitable_ops const* (*construct_write_eof_buffers_awaitable)( + void* sink, + void* storage, + std::span buffers); + eof_awaitable_ops const* (*construct_eof_awaitable)( + void* sink, + void* storage); + std::size_t awaitable_size; + std::size_t awaitable_align; + void (*destroy)(void*) noexcept; +}; + +template +struct any_write_sink::vtable_for_impl +{ + using WriteSomeAwaitable = decltype(std::declval().write_some( + std::span{})); + using WriteAwaitable = decltype(std::declval().write( + std::span{})); + using WriteEofBuffersAwaitable = decltype(std::declval().write_eof( + std::span{})); + using EofAwaitable = decltype(std::declval().write_eof()); + + static void + do_destroy_impl(void* sink) noexcept + { + static_cast(sink)->~S(); + } + + static write_awaitable_ops const* + construct_write_some_awaitable_impl( + void* sink, + void* storage, + std::span buffers) + { + auto& s = *static_cast(sink); + ::new(storage) WriteSomeAwaitable(s.write_some(buffers)); + + static constexpr write_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~WriteSomeAwaitable(); + } + }; + return &ops; + } + + static write_awaitable_ops const* + construct_write_awaitable_impl( + void* sink, + void* storage, + std::span buffers) + { + auto& s = *static_cast(sink); + ::new(storage) WriteAwaitable(s.write(buffers)); + + static constexpr write_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~WriteAwaitable(); // LCOV_EXCL_LINE runs via destroy tests; gcov miscounts fn-ptr thunk + } + }; + return &ops; + } + + static write_awaitable_ops const* + construct_write_eof_buffers_awaitable_impl( + void* sink, + void* storage, + std::span buffers) + { + auto& s = *static_cast(sink); + ::new(storage) WriteEofBuffersAwaitable(s.write_eof(buffers)); + + static constexpr write_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~WriteEofBuffersAwaitable(); // LCOV_EXCL_LINE runs via destroy tests; gcov miscounts fn-ptr thunk + } + }; + return &ops; + } + + static eof_awaitable_ops const* + construct_eof_awaitable_impl( + void* sink, + void* storage) + { + auto& s = *static_cast(sink); + ::new(storage) EofAwaitable(s.write_eof()); + + static constexpr eof_awaitable_ops ops = { + +[](void* p) { + return static_cast(p)->await_ready(); + }, + +[](void* p, std::coroutine_handle<> h, capy::io_env const* env) { + return capy::detail::call_await_suspend( + static_cast(p), h, env); + }, + +[](void* p) { + return static_cast(p)->await_resume(); + }, + +[](void* p) noexcept { + static_cast(p)->~EofAwaitable(); + } + }; + return &ops; + } + + static constexpr std::size_t max4( + std::size_t a, std::size_t b, + std::size_t c, std::size_t d) noexcept + { + std::size_t ab = a > b ? a : b; + std::size_t cd = c > d ? c : d; + return ab > cd ? ab : cd; + } + + static constexpr std::size_t max_awaitable_size = + max4(sizeof(WriteSomeAwaitable), + sizeof(WriteAwaitable), + sizeof(WriteEofBuffersAwaitable), + sizeof(EofAwaitable)); + + static constexpr std::size_t max_awaitable_align = + max4(alignof(WriteSomeAwaitable), + alignof(WriteAwaitable), + alignof(WriteEofBuffersAwaitable), + alignof(EofAwaitable)); + + static constexpr vtable value = { + &construct_write_some_awaitable_impl, + &construct_write_awaitable_impl, + &construct_write_eof_buffers_awaitable_impl, + &construct_eof_awaitable_impl, + max_awaitable_size, + max_awaitable_align, + &do_destroy_impl + }; +}; + +inline +any_write_sink::~any_write_sink() +{ + if(storage_) + { + vt_->destroy(sink_); + ::operator delete(storage_); + } + if(cached_awaitable_) + { + if(active_write_ops_) + active_write_ops_->destroy(cached_awaitable_); + else if(active_eof_ops_) + active_eof_ops_->destroy(cached_awaitable_); + ::operator delete(cached_awaitable_); + } +} + +inline any_write_sink& +any_write_sink::operator=(any_write_sink&& other) noexcept +{ + if(this != &other) + { + if(storage_) + { + vt_->destroy(sink_); + ::operator delete(storage_); + } + if(cached_awaitable_) + { + if(active_write_ops_) + active_write_ops_->destroy(cached_awaitable_); + else if(active_eof_ops_) + active_eof_ops_->destroy(cached_awaitable_); + ::operator delete(cached_awaitable_); + } + sink_ = std::exchange(other.sink_, nullptr); + vt_ = std::exchange(other.vt_, nullptr); + cached_awaitable_ = std::exchange(other.cached_awaitable_, nullptr); + storage_ = std::exchange(other.storage_, nullptr); + active_write_ops_ = std::exchange(other.active_write_ops_, nullptr); + active_eof_ops_ = std::exchange(other.active_eof_ops_, nullptr); + } + return *this; +} + +template + requires (!std::same_as, any_write_sink>) +any_write_sink::any_write_sink(S s) + : vt_(&vtable_for_impl::value) +{ + struct guard { + any_write_sink* self; + bool committed = false; + ~guard() { + if(!committed && self->storage_) { + if(self->sink_) + self->vt_->destroy(self->sink_); // LCOV_EXCL_LINE OOM rollback: only when the cached-awaitable allocation throws + ::operator delete(self->storage_); + self->storage_ = nullptr; + self->sink_ = nullptr; + } + } + } g{this}; + + storage_ = ::operator new(sizeof(S)); + sink_ = ::new(storage_) S(std::move(s)); + + // Preallocate the awaitable storage (sized for max of write/eof) + cached_awaitable_ = ::operator new(vt_->awaitable_size); + + g.committed = true; +} + +template +any_write_sink::any_write_sink(S* s) + : sink_(s) + , vt_(&vtable_for_impl::value) +{ + // Preallocate the awaitable storage (sized for max of write/eof) + cached_awaitable_ = ::operator new(vt_->awaitable_size); +} + +inline auto +any_write_sink::write_some_( + std::span buffers) +{ + struct awaitable + { + any_write_sink* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_write_ops_ = self_->vt_->construct_write_some_awaitable( + self_->sink_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_write_ops_->await_ready(self_->cached_awaitable_)) + return h; + + return self_->active_write_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_write_sink* self; + ~guard() { + self->active_write_ops_->destroy(self->cached_awaitable_); + self->active_write_ops_ = nullptr; + } + } g{self_}; + return self_->active_write_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +inline auto +any_write_sink::write_( + std::span buffers) +{ + struct awaitable + { + any_write_sink* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_write_ops_ = self_->vt_->construct_write_awaitable( + self_->sink_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_write_ops_->await_ready(self_->cached_awaitable_)) + return h; + + return self_->active_write_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_write_sink* self; + ~guard() { + self->active_write_ops_->destroy(self->cached_awaitable_); + self->active_write_ops_ = nullptr; + } + } g{self_}; + return self_->active_write_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +inline auto +any_write_sink::write_eof() +{ + struct awaitable + { + any_write_sink* self_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + // Construct the underlying awaitable into cached storage + self_->active_eof_ops_ = self_->vt_->construct_eof_awaitable( + self_->sink_, + self_->cached_awaitable_); + + // Check if underlying is immediately ready + if(self_->active_eof_ops_->await_ready(self_->cached_awaitable_)) + return h; + + // Forward to underlying awaitable + return self_->active_eof_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result<> + await_resume() + { + struct guard { + any_write_sink* self; + ~guard() { + self->active_eof_ops_->destroy(self->cached_awaitable_); + self->active_eof_ops_ = nullptr; + } + } g{self_}; + return self_->active_eof_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this}; +} + +inline auto +any_write_sink::write_eof_buffers_( + std::span buffers) +{ + struct awaitable + { + any_write_sink* self_; + std::span buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_write_ops_ = + self_->vt_->construct_write_eof_buffers_awaitable( + self_->sink_, + self_->cached_awaitable_, + buffers_); + + if(self_->active_write_ops_->await_ready(self_->cached_awaitable_)) + return h; + + return self_->active_write_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + struct guard { + any_write_sink* self; + ~guard() { + self->active_write_ops_->destroy(self->cached_awaitable_); + self->active_write_ops_ = nullptr; + } + } g{self_}; + return self_->active_write_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +template +auto +any_write_sink::write_some(CB buffers) +{ + struct awaitable + { + any_write_sink* self_; + capy::detail::const_buffer_array ba_; + + awaitable( + any_write_sink* self, + CB const& buffers) + : self_(self) + , ba_(buffers) + { + } + + bool + await_ready() const noexcept + { + return ba_.to_span().empty(); + } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + { + self_->active_write_ops_ = self_->vt_->construct_write_some_awaitable( + self_->sink_, + self_->cached_awaitable_, + ba_.to_span()); + + if(self_->active_write_ops_->await_ready(self_->cached_awaitable_)) + return h; + + return self_->active_write_ops_->await_suspend( + self_->cached_awaitable_, h, env); + } + + capy::io_result + await_resume() + { + if(ba_.to_span().empty()) + return {{}, 0}; + + struct guard { + any_write_sink* self; + ~guard() { + self->active_write_ops_->destroy(self->cached_awaitable_); + self->active_write_ops_ = nullptr; + } + } g{self_}; + return self_->active_write_ops_->await_resume( + self_->cached_awaitable_); + } + }; + return awaitable{this, buffers}; +} + +template +capy::io_task +any_write_sink::write(CB buffers) +{ + capy::const_buffer_param bp(buffers); + std::size_t total = 0; + + for(;;) + { + auto bufs = bp.data(); + if(bufs.empty()) + break; + + auto [ec, n] = co_await write_(bufs); + total += n; + if(ec) + co_return {ec, total}; + bp.consume(n); + } + + co_return {{}, total}; +} + +template +capy::io_task +any_write_sink::write_eof(CB buffers) +{ + capy::const_buffer_param bp(buffers); + std::size_t total = 0; + + for(;;) + { + auto bufs = bp.data(); + if(bufs.empty()) + { + auto [ec] = co_await write_eof(); + co_return {ec, total}; + } + + if(! bp.more()) + { + // Last window — send atomically with EOF + auto [ec, n] = co_await write_eof_buffers_(bufs); + total += n; + co_return {ec, total}; + } + + auto [ec, n] = co_await write_(bufs); + total += n; + if(ec) + co_return {ec, total}; + bp.consume(n); + } +} + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/io/pull_from.hpp b/include/boost/http/io/pull_from.hpp new file mode 100644 index 00000000..f2d97c96 --- /dev/null +++ b/include/boost/http/io/pull_from.hpp @@ -0,0 +1,187 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_IO_PULL_FROM_HPP +#define BOOST_HTTP_IO_PULL_FROM_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace boost { +namespace http { + +/** Transfer data from a ReadSource to a BufferSink. + + This function reads data from the source directly into the sink's + internal buffers using the callee-owns-buffers model. The sink + provides writable buffers via `prepare()`, the source reads into + them, and the sink commits the data. When the source signals EOF, + `commit_eof()` is called on the sink to finalize the transfer. + + @tparam Src The source type, must satisfy @ref ReadSource. + @tparam Sink The sink type, must satisfy @ref BufferSink. + + @param source The source to read data from. + @param sink The sink to write data to. + + @return A task that yields `(std::error_code, std::size_t)`. + On success, `ec` is default-constructed (no error) and `n` is + the total number of bytes transferred. On error, `ec` contains + the error code and `n` is the total number of bytes transferred + before the error. + + @par Example + @code + task transfer_body(ReadSource auto& source, BufferSink auto& sink) + { + auto [ec, n] = co_await pull_from(source, sink); + if (ec) + { + // Handle error + } + // n bytes were transferred + } + @endcode + + @see ReadSource, BufferSink, push_to +*/ +template +capy::io_task +pull_from(Src& source, Sink& sink) +{ + capy::mutable_buffer dst_arr[capy::detail::max_iovec_]; + std::size_t total = 0; + + for(;;) + { + auto dst_bufs = sink.prepare(dst_arr); + if(dst_bufs.empty()) + { + // No buffer space available; commit nothing to flush + auto [flush_ec] = co_await sink.commit(0); + if(flush_ec) + co_return {flush_ec, total}; + continue; + } + + auto [ec, n] = co_await source.read( + std::span(dst_bufs)); + + auto [commit_ec] = co_await sink.commit(n); + total += n; + + if(commit_ec) + co_return {commit_ec, total}; + + if(ec == capy::cond::eof) + { + auto [eof_ec] = co_await sink.commit_eof(0); + co_return {eof_ec, total}; + } + + if(ec) + co_return {ec, total}; + } +} + +/** Transfer data from a capy::ReadStream to a BufferSink. + + This function reads data from the stream directly into the sink's + internal buffers using the callee-owns-buffers model. The sink + provides writable buffers via `prepare()`, the stream reads into + them using `read_some()`, and the sink commits the data. When the + stream signals EOF, `commit_eof()` is called on the sink to + finalize the transfer. + + This overload handles partial reads from the stream, committing + data incrementally as it arrives. It loops until EOF is encountered + or an error occurs. + + @tparam Src The stream type, must satisfy @ref capy::ReadStream. + @tparam Sink The sink type, must satisfy @ref BufferSink. + + @param source The stream to read data from. + @param sink The sink to write data to. + + @return A task that yields `(std::error_code, std::size_t)`. + On success, `ec` is default-constructed (no error) and `n` is + the total number of bytes transferred. On error, `ec` contains + the error code and `n` is the total number of bytes transferred + before the error. + + @par Example + @code + task transfer_body(capy::ReadStream auto& stream, BufferSink auto& sink) + { + auto [ec, n] = co_await pull_from(stream, sink); + if (ec) + { + // Handle error + } + // n bytes were transferred + } + @endcode + + @see capy::ReadStream, BufferSink, push_to +*/ +template + requires (!ReadSource) +capy::io_task +pull_from(Src& source, Sink& sink) +{ + capy::mutable_buffer dst_arr[capy::detail::max_iovec_]; + std::size_t total = 0; + + for(;;) + { + // Prepare destination buffers from the sink + auto dst_bufs = sink.prepare(dst_arr); + if(dst_bufs.empty()) + { + // No buffer space available; commit nothing to flush + auto [flush_ec] = co_await sink.commit(0); + if(flush_ec) + co_return {flush_ec, total}; + continue; + } + + // Read data from the stream into the sink's buffers + auto [ec, n] = co_await source.read_some( + std::span(dst_bufs)); + + auto [commit_ec] = co_await sink.commit(n); + total += n; + + if(commit_ec) + co_return {commit_ec, total}; + + if(ec == capy::cond::eof) + { + auto [eof_ec] = co_await sink.commit_eof(0); + co_return {eof_ec, total}; + } + + // Check for other errors + if(ec) + co_return {ec, total}; + } +} + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/io/push_to.hpp b/include/boost/http/io/push_to.hpp new file mode 100644 index 00000000..dc7820a1 --- /dev/null +++ b/include/boost/http/io/push_to.hpp @@ -0,0 +1,152 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +#ifndef BOOST_HTTP_IO_PUSH_TO_HPP +#define BOOST_HTTP_IO_PUSH_TO_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace boost { +namespace http { + +/** Transfer data from a BufferSource to a WriteSink. + + This function pulls data from the source and writes it to the + sink until the source is exhausted or an error occurs. When + the source signals completion, `write_eof()` is called on the + sink to finalize the transfer. + + @tparam Src The source type, must satisfy @ref BufferSource. + @tparam Sink The sink type, must satisfy @ref WriteSink. + + @param source The source to pull data from. + @param sink The sink to write data to. + + @return A task that yields `(std::error_code, std::size_t)`. + On success, `ec` is default-constructed (no error) and `n` is + the total number of bytes transferred. On error, `ec` contains + the error code and `n` is the total number of bytes transferred + before the error. + + @par Example + @code + task transfer_body(BufferSource auto& source, WriteSink auto& sink) + { + auto [ec, n] = co_await push_to(source, sink); + if (ec) + { + // Handle error + } + // n bytes were transferred + } + @endcode + + @see BufferSource, WriteSink +*/ +template +capy::io_task +push_to(Src& source, Sink& sink) +{ + capy::const_buffer arr[capy::detail::max_iovec_]; + std::size_t total = 0; + + for(;;) + { + auto [ec, bufs] = co_await source.pull(arr); + if(ec == capy::cond::eof) + { + auto [eof_ec] = co_await sink.write_eof(); + co_return {eof_ec, total}; + } + if(ec) + co_return {ec, total}; + + auto [write_ec, n] = co_await sink.write(bufs); + total += n; + source.consume(n); + if(write_ec) + co_return {write_ec, total}; + } +} + +/** Transfer data from a BufferSource to a capy::WriteStream. + + This function pulls data from the source and writes it to the + stream until the source is exhausted or an error occurs. The + stream uses `write_some()` which may perform partial writes, + so this function loops until all pulled data is consumed. + + Unlike the WriteSink overload, this function does not signal + EOF explicitly since capy::WriteStream does not provide a write_eof + method. The transfer completes when the source is exhausted. + + @tparam Src The source type, must satisfy @ref BufferSource. + @tparam Stream The stream type, must satisfy @ref capy::WriteStream. + + @param source The source to pull data from. + @param stream The stream to write data to. + + @return A task that yields `(std::error_code, std::size_t)`. + On success, `ec` is default-constructed (no error) and `n` is + the total number of bytes transferred. On error, `ec` contains + the error code and `n` is the total number of bytes transferred + before the error. + + @par Example + @code + task transfer_body(BufferSource auto& source, capy::WriteStream auto& stream) + { + auto [ec, n] = co_await push_to(source, stream); + if (ec) + { + // Handle error + } + // n bytes were transferred + } + @endcode + + @see BufferSource, capy::WriteStream, pull_from +*/ +template + requires (!WriteSink) +capy::io_task +push_to(Src& source, Stream& stream) +{ + capy::const_buffer arr[capy::detail::max_iovec_]; + std::size_t total = 0; + + for(;;) + { + auto [ec, bufs] = co_await source.pull(arr); + if(ec == capy::cond::eof) + co_return {{}, total}; + if(ec) + co_return {ec, total}; + + auto [write_ec, n] = co_await stream.write_some(bufs); + total += n; + source.consume(n); + if(write_ec) + co_return {write_ec, total}; + } +} + +} // namespace http +} // namespace boost + +#endif diff --git a/include/boost/http/json/json_sink.hpp b/include/boost/http/json/json_sink.hpp index ed58683e..cfaa6402 100644 --- a/include/boost/http/json/json_sink.hpp +++ b/include/boost/http/json/json_sink.hpp @@ -25,7 +25,7 @@ namespace http { /** A sink for streaming JSON data to a parser. This class wraps a `boost::json::stream_parser` and satisfies the - @ref capy::WriteSink concept, enabling incremental JSON parsing + @ref http::WriteSink concept, enabling incremental JSON parsing from any data source that produces buffer sequences. Since JSON parsing is synchronous, all operations return @@ -50,7 +50,7 @@ namespace http { Distinct objects: Safe. Shared objects: Unsafe. - @see capy::WriteSink, json::stream_parser + @see http::WriteSink, json::stream_parser */ class json_sink { diff --git a/include/boost/http/parser.hpp b/include/boost/http/parser.hpp index 321c2ab9..fff87d19 100644 --- a/include/boost/http/parser.hpp +++ b/include/boost/http/parser.hpp @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include #include @@ -411,7 +411,7 @@ class parser /** Return a source for reading body data. - The returned source satisfies @ref capy::BufferSource. + The returned source satisfies @ref http::BufferSource. On first pull, headers are automatically parsed if not yet received. @@ -428,9 +428,9 @@ class parser @param stream The stream to read from. - @return A source satisfying @ref capy::BufferSource. + @return A source satisfying @ref http::BufferSource. - @see @ref read_header, @ref capy::BufferSource. + @see @ref read_header, @ref http::BufferSource. */ template source @@ -449,7 +449,7 @@ class parser @see WriteSink. */ - template + template capy::io_task<> read(capy::ReadStream auto& stream, Sink&& sink); @@ -481,7 +481,7 @@ class parser /** A source for reading the message body. - This type satisfies @ref capy::BufferSource. It can be + This type satisfies @ref http::BufferSource. It can be constructed immediately after parser construction; on first pull, headers are automatically parsed if not yet received. @@ -715,7 +715,7 @@ consume(std::size_t n) noexcept pr_->consume_body(n); } -template +template capy::io_task<> parser:: read(capy::ReadStream auto& stream, Sink&& sink) diff --git a/include/boost/http/serializer.hpp b/include/boost/http/serializer.hpp index 42fb7fa5..322223cd 100644 --- a/include/boost/http/serializer.hpp +++ b/include/boost/http/serializer.hpp @@ -16,7 +16,7 @@ #include #include -#include +#include #include #include #include @@ -578,7 +578,7 @@ class serializer } @endcode - @see @ref capy::BufferSink, @ref capy::any_buffer_sink, + @see @ref http::BufferSink, @ref http::any_buffer_sink, @ref serializer. */ template diff --git a/include/boost/http/server/http_worker.hpp b/include/boost/http/server/http_worker.hpp index e5265f13..f5518a2b 100644 --- a/include/boost/http/server/http_worker.hpp +++ b/include/boost/http/server/http_worker.hpp @@ -57,8 +57,8 @@ namespace http { , sock(ctx) { sock.open(); - rp.req_body = capy::any_buffer_source(parser.source_for(sock)); - rp.res_body = capy::any_buffer_sink(serializer.sink_for(sock)); + rp.req_body = http::any_buffer_source(parser.source_for(sock)); + rp.res_body = http::any_buffer_sink(serializer.sink_for(sock)); stream = capy::any_read_stream(&sock); } @@ -105,8 +105,8 @@ class BOOST_HTTP_DECL http_worker , serializer(serializer_cfg) { serializer.set_message(rp.res); - rp.req_body = capy::any_buffer_source(parser.source_for(stream_)); - rp.res_body = capy::any_buffer_sink(serializer.sink_for(stream_)); + rp.req_body = http::any_buffer_source(parser.source_for(stream_)); + rp.res_body = http::any_buffer_sink(serializer.sink_for(stream_)); } /** Handle an HTTP session. diff --git a/include/boost/http/server/route_handler.hpp b/include/boost/http/server/route_handler.hpp index eec7e0ed..b77cf945 100644 --- a/include/boost/http/server/route_handler.hpp +++ b/include/boost/http/server/route_handler.hpp @@ -23,8 +23,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -426,10 +426,10 @@ class BOOST_HTTP_SYMBOL_VISIBLE http::response res; /// Provides access to the request body - capy::any_buffer_source req_body; + http::any_buffer_source req_body; /// Provides access to the response body - capy::any_buffer_sink res_body; + http::any_buffer_sink res_body; /// Arbitrary per-route data http::datastore route_data; diff --git a/include/boost/http/test/buffer_sink.hpp b/include/boost/http/test/buffer_sink.hpp new file mode 100644 index 00000000..60ef6dee --- /dev/null +++ b/include/boost/http/test/buffer_sink.hpp @@ -0,0 +1,274 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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/http +// + +#ifndef BOOST_HTTP_TEST_BUFFER_SINK_HPP +#define BOOST_HTTP_TEST_BUFFER_SINK_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace boost { +namespace http { +namespace test { + +/** A mock buffer sink for testing callee-owns-buffers write operations. + + Use this to verify code that writes data using the callee-owns-buffers + pattern without needing real I/O. Call @ref prepare to get writable + buffers, write into them, then call @ref commit to finalize. The + associated @ref capy::test::fuse enables error injection at controlled points. + + This class satisfies the @ref BufferSink concept by providing + internal storage that callers write into directly. + + @par Thread Safety + Not thread-safe. + + @par Example + @code + capy::test::fuse f; + buffer_sink bs( f ); + + auto r = f.armed( [&]( capy::test::fuse& ) -> task { + capy::mutable_buffer arr[16]; + auto bufs = bs.prepare( arr ); + if( bufs.empty() ) + co_return; + + // Write data into the first prepared buffer + std::memcpy( bufs[0].data(), "Hello", 5 ); + + auto [ec] = co_await bs.commit( 5 ); + if( ec ) + co_return; + + auto [ec2] = co_await bs.commit_eof( 0 ); + // bs.data() returns "Hello" + } ); + @endcode + + @see capy::test::fuse, BufferSink +*/ +class buffer_sink +{ + capy::test::fuse f_; + std::string data_; + std::string prepare_buf_; + std::size_t prepare_size_ = 0; + std::size_t max_prepare_size_; + bool eof_called_ = false; + +public: + /** Construct a buffer sink. + + @param f The capy::test::fuse used to inject errors during commits. + + @param max_prepare_size Maximum bytes available per prepare. + Use to simulate limited buffer space. + */ + explicit buffer_sink( + capy::test::fuse f = {}, + std::size_t max_prepare_size = 4096) noexcept + : f_(std::move(f)) + , max_prepare_size_(max_prepare_size) + { + prepare_buf_.resize(max_prepare_size_); + } + + /// Return the written data as a string view. + std::string_view + data() const noexcept + { + return data_; + } + + /// Return the number of bytes written. + std::size_t + size() const noexcept + { + return data_.size(); + } + + /// Return whether commit_eof has been called. + bool + eof_called() const noexcept + { + return eof_called_; + } + + /// Clear all data and reset state. + void + clear() noexcept + { + data_.clear(); + prepare_size_ = 0; + eof_called_ = false; + } + + /** Prepare writable buffers. + + Fills the provided span with mutable buffer descriptors pointing + to internal storage. The caller writes data into these buffers, + then calls @ref commit to finalize. + + @param dest Span of capy::mutable_buffer to fill. + + @return A span of filled buffers (empty or 1 buffer in this implementation). + */ + std::span + prepare(std::span dest) + { + if(dest.empty()) + return {}; + + prepare_size_ = max_prepare_size_; + dest[0] = capy::make_buffer(prepare_buf_.data(), prepare_size_); + return dest.first(1); + } + + /** Commit bytes written to the prepared buffers. + + Transfers `n` bytes from the prepared buffer to the internal + data buffer. Before committing, the attached @ref capy::test::fuse is + consulted to possibly inject an error for testing fault scenarios. + + @param n The number of bytes to commit. + + @return An awaitable that await-returns `(error_code)`. + + @par Cancellation + If the environment's stop token has been requested, the commit + completes immediately with `capy::error::canceled` and commits no data. + + @see capy::test::fuse + */ + auto + commit(std::size_t n) + { + struct awaitable + { + buffer_sink* self_; + std::size_t n_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // The operation completes synchronously, but await_suspend is + // the only place capy::io_env is delivered (the promise's + // transform_awaiter forwards it here). Returning false means + // the coroutine does not actually suspend; it resumes + // immediately, having observed the stop token. See capy::io_env, + // IoAwaitable. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result<> + await_resume() + { + if(canceled_) + return {capy::error::canceled}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec}; + + std::size_t to_commit = (std::min)(n_, self_->prepare_size_); + self_->data_.append(self_->prepare_buf_.data(), to_commit); + self_->prepare_size_ = 0; + + return {}; + } + }; + return awaitable{this, n}; + } + + /** Commit final bytes and signal end-of-stream. + + Transfers `n` bytes from the prepared buffer to the internal + data buffer and marks the sink as finalized. Before committing, + the attached @ref capy::test::fuse is consulted to possibly inject an error + for testing fault scenarios. + + @param n The number of bytes to commit. + + @return An awaitable that await-returns `(error_code)`. + + @par Cancellation + If the environment's stop token has been requested, the operation + completes immediately with `capy::error::canceled`, commits no data, and + does not signal end-of-stream. + + @see capy::test::fuse + */ + auto + commit_eof(std::size_t n) + { + struct awaitable + { + buffer_sink* self_; + std::size_t n_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // Reads the stop token without suspending; see the comment + // on commit() for details. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result<> + await_resume() + { + if(canceled_) + return {capy::error::canceled}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec}; + + std::size_t to_commit = (std::min)(n_, self_->prepare_size_); + self_->data_.append(self_->prepare_buf_.data(), to_commit); + self_->prepare_size_ = 0; + + self_->eof_called_ = true; + return {}; + } + }; + return awaitable{this, n}; + } +}; + +} // test +} // capy +} // boost + +#endif diff --git a/include/boost/http/test/buffer_source.hpp b/include/boost/http/test/buffer_source.hpp new file mode 100644 index 00000000..5fb6fe4c --- /dev/null +++ b/include/boost/http/test/buffer_source.hpp @@ -0,0 +1,213 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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/http +// + +#ifndef BOOST_HTTP_TEST_BUFFER_SOURCE_HPP +#define BOOST_HTTP_TEST_BUFFER_SOURCE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace boost { +namespace http { +namespace test { + +/** A mock buffer source for testing pull (BufferSource) operations. + + Use this to verify code that transfers data from a buffer source to + a sink without needing real I/O. Call @ref provide to supply data, + then @ref pull to retrieve buffer descriptors. The associated + @ref capy::test::fuse enables error injection at controlled points. + + This class satisfies the @ref BufferSource concept by providing + a pull interface that fills an array of buffer descriptors and + a consume interface to indicate bytes used. + + @par Thread Safety + Not thread-safe. + + @par Example + @code + capy::test::fuse f; + buffer_source bs( f ); + bs.provide( "Hello, " ); + bs.provide( "World!" ); + + auto r = f.armed( [&]( capy::test::fuse& ) -> task { + capy::const_buffer arr[16]; + auto [ec, bufs] = co_await bs.pull( arr ); + if( ec ) + co_return; + // bufs contains buffer descriptors + std::size_t n = capy::buffer_size( bufs ); + bs.consume( n ); + } ); + @endcode + + @see capy::test::fuse, BufferSource +*/ +class buffer_source +{ + capy::test::fuse f_; + std::string data_; + std::size_t pos_ = 0; + std::size_t max_pull_size_; + +public: + /** Construct a buffer source. + + @param f The capy::test::fuse used to inject errors during pulls. + + @param max_pull_size Maximum bytes returned per pull. + Use to simulate chunked delivery. + */ + explicit buffer_source( + capy::test::fuse f = {}, + std::size_t max_pull_size = std::size_t(-1)) noexcept + : f_(std::move(f)) + , max_pull_size_(max_pull_size) + { + } + + /** Append data to be returned by subsequent pulls. + + Multiple calls accumulate data that @ref pull returns. + + @param sv The data to append. + */ + void + provide(std::string_view sv) + { + data_.append(sv); + } + + /// Clear all data and reset the read position. + void + clear() noexcept + { + data_.clear(); + pos_ = 0; + } + + /// Return the number of bytes available for pulling. + std::size_t + available() const noexcept + { + return data_.size() - pos_; + } + + /** Consume bytes from the source. + + Advances the internal read position by the specified number + of bytes. The next call to @ref pull returns data starting + after the consumed bytes. + + @param n The number of bytes to consume. Must not exceed the + total size of buffers returned by the previous @ref pull. + */ + void + consume(std::size_t n) noexcept + { + pos_ += n; + } + + /** Pull buffer data from the source. + + Fills the provided span with buffer descriptors pointing to + internal data starting from the current unconsumed position. + Returns a span of filled buffers. When no data remains, + returns an empty span to signal completion. + + Calling pull multiple times without intervening @ref consume + returns the same data. Use consume to advance past processed + bytes. + + @param dest Span of capy::const_buffer to fill. + + @return An awaitable that await-returns `(error_code,std::span)`. + + @par Cancellation + If the environment's stop token has been requested, the pull + completes immediately with `capy::error::canceled` and an empty span. + + @see consume, capy::test::fuse + */ + auto + pull(std::span dest) + { + struct awaitable + { + buffer_source* self_; + std::span dest_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // The operation completes synchronously, but await_suspend is + // the only place capy::io_env is delivered (the promise's + // transform_awaiter forwards it here). Returning false means + // the coroutine does not actually suspend; it resumes + // immediately, having observed the stop token. See capy::io_env, + // IoAwaitable. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result> + await_resume() + { + if(canceled_) + return {capy::error::canceled, {}}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, {}}; + + if(self_->pos_ >= self_->data_.size()) + return {capy::error::eof, {}}; + + std::size_t avail = self_->data_.size() - self_->pos_; + std::size_t to_return = (std::min)(avail, self_->max_pull_size_); + + if(dest_.empty()) + return {{}, {}}; + + // Fill a single buffer descriptor + dest_[0] = capy::make_buffer( + self_->data_.data() + self_->pos_, + to_return); + + return {{}, dest_.first(1)}; + } + }; + return awaitable{this, dest}; + } +}; + +} // test +} // capy +} // boost + +#endif diff --git a/include/boost/http/test/read_source.hpp b/include/boost/http/test/read_source.hpp new file mode 100644 index 00000000..aea5c840 --- /dev/null +++ b/include/boost/http/test/read_source.hpp @@ -0,0 +1,271 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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/http +// + +#ifndef BOOST_HTTP_TEST_READ_SOURCE_HPP +#define BOOST_HTTP_TEST_READ_SOURCE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace boost { +namespace http { +namespace test { + +/** A mock source for testing read operations. + + Use this to verify code that performs complete reads without needing + real I/O. Call @ref provide to supply data, then @ref read + to consume it. The associated @ref capy::test::fuse enables error injection + at controlled points. + + This class satisfies the @ref ReadSource concept by providing both + partial reads via `read_some` (satisfying @ref ReadStream) and + complete reads via `read` that fill the entire buffer sequence + before returning. + + @par Thread Safety + Not thread-safe. + + @par Example + @code + capy::test::fuse f; + read_source rs( f ); + rs.provide( "Hello, " ); + rs.provide( "World!" ); + + auto r = f.armed( [&]( capy::test::fuse& ) -> task { + char buf[32]; + auto [ec, n] = co_await rs.read( + capy::mutable_buffer( buf, sizeof( buf ) ) ); + if( ec ) + co_return; + // buf contains "Hello, World!" + } ); + @endcode + + @see capy::test::fuse, ReadSource +*/ +class read_source +{ + capy::test::fuse f_; + std::string data_; + std::size_t pos_ = 0; + std::size_t max_read_size_; + +public: + /** Construct a read source. + + @param f The capy::test::fuse used to inject errors during reads. + + @param max_read_size Maximum bytes returned per read. + Use to simulate chunked delivery. + */ + explicit read_source( + capy::test::fuse f = {}, + std::size_t max_read_size = std::size_t(-1)) noexcept + : f_(std::move(f)) + , max_read_size_(max_read_size) + { + } + + /** Append data to be returned by subsequent reads. + + Multiple calls accumulate data that @ref read returns. + + @param sv The data to append. + */ + void + provide(std::string_view sv) + { + data_.append(sv); + } + + /// Clear all data and reset the read position. + void + clear() noexcept + { + data_.clear(); + pos_ = 0; + } + + /// Return the number of bytes available for reading. + std::size_t + available() const noexcept + { + return data_.size() - pos_; + } + + /** Asynchronously read some data from the source. + + Transfers up to `capy::buffer_size( buffers )` bytes from the internal + buffer to the provided mutable buffer sequence. If no data + remains, returns `capy::error::eof`. Before every read, the attached + @ref capy::test::fuse is consulted to possibly inject an error for testing + fault scenarios. + + @param buffers The mutable buffer sequence to receive data. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Cancellation + If the environment's stop token has been requested, the read + completes immediately with `capy::error::canceled` and transfers no + data. An empty buffer sequence is a no-op that completes + successfully regardless of the stop token. + + @see capy::test::fuse + */ + template + auto + read_some(MB buffers) + { + struct awaitable + { + read_source* self_; + MB buffers_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // The operation completes synchronously, but await_suspend is + // the only place capy::io_env is delivered (the promise's + // transform_awaiter forwards it here). Returning false means + // the coroutine does not actually suspend; it resumes + // immediately, having observed the stop token. See capy::io_env, + // IoAwaitable. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result + await_resume() + { + if(capy::buffer_empty(buffers_)) + return {{}, 0}; + + if(canceled_) + return {capy::error::canceled, 0}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, 0}; + + if(self_->pos_ >= self_->data_.size()) + return {capy::error::eof, 0}; + + std::size_t avail = self_->data_.size() - self_->pos_; + if(avail > self_->max_read_size_) + avail = self_->max_read_size_; + auto src = capy::make_buffer(self_->data_.data() + self_->pos_, avail); + std::size_t const n = capy::buffer_copy(buffers_, src); + self_->pos_ += n; + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } + + /** Asynchronously read data from the source. + + Fills the entire buffer sequence from the internal data. + If the available data is less than the buffer size, returns + `capy::error::eof` with the number of bytes transferred. Before + every read, the attached @ref capy::test::fuse is consulted to possibly + inject an error for testing fault scenarios. + + Unlike @ref read_some, this ignores `max_read_size` and + transfers all available data in a single operation, matching + the @ref ReadSource semantic contract. + + @param buffers The mutable buffer sequence to receive data. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Cancellation + If the environment's stop token has been requested, the read + completes immediately with `capy::error::canceled` and transfers no + data. An empty buffer sequence is a no-op that completes + successfully regardless of the stop token. + + @see capy::test::fuse + */ + template + auto + read(MB buffers) + { + struct awaitable + { + read_source* self_; + MB buffers_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // Reads the stop token without suspending; see the comment + // on read_some() for details. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result + await_resume() + { + if(capy::buffer_empty(buffers_)) + return {{}, 0}; + + if(canceled_) + return {capy::error::canceled, 0}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, 0}; + + if(self_->pos_ >= self_->data_.size()) + return {capy::error::eof, 0}; + + std::size_t avail = self_->data_.size() - self_->pos_; + auto src = capy::make_buffer(self_->data_.data() + self_->pos_, avail); + std::size_t const n = capy::buffer_copy(buffers_, src); + self_->pos_ += n; + + if(n < capy::buffer_size(buffers_)) + return {capy::error::eof, n}; + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } +}; + +} // test +} // capy +} // boost + +#endif diff --git a/include/boost/http/test/write_sink.hpp b/include/boost/http/test/write_sink.hpp new file mode 100644 index 00000000..68f2d96d --- /dev/null +++ b/include/boost/http/test/write_sink.hpp @@ -0,0 +1,466 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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/http +// + +#ifndef BOOST_HTTP_TEST_WRITE_SINK_HPP +#define BOOST_HTTP_TEST_WRITE_SINK_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost { +namespace http { +namespace test { + +/** A mock sink for testing write operations. + + Use this to verify code that performs complete writes without needing + real I/O. Call @ref write to write data, then @ref data to retrieve + what was written. The associated @ref capy::test::fuse enables error injection + at controlled points. + + This class satisfies the @ref WriteSink concept by providing partial + writes via `write_some` (satisfying @ref WriteStream), complete + writes via `write`, and EOF signaling via `write_eof`. + + @par Thread Safety + Not thread-safe. + + @par Example + @code + capy::test::fuse f; + write_sink ws( f ); + + auto r = f.armed( [&]( capy::test::fuse& ) -> task { + auto [ec, n] = co_await ws.write( + capy::const_buffer( "Hello", 5 ) ); + if( ec ) + co_return; + auto [ec2] = co_await ws.write_eof(); + if( ec2 ) + co_return; + // ws.data() returns "Hello" + } ); + @endcode + + @see capy::test::fuse, WriteSink +*/ +class write_sink +{ + capy::test::fuse f_; + std::string data_; + std::string expect_; + std::size_t max_write_size_; + bool eof_called_ = false; + + std::error_code + consume_match_() noexcept + { + if(data_.empty() || expect_.empty()) + return {}; + std::size_t const n = (std::min)(data_.size(), expect_.size()); + if(std::string_view(data_.data(), n) != + std::string_view(expect_.data(), n)) + return capy::error::test_failure; + data_.erase(0, n); + expect_.erase(0, n); + return {}; + } + +public: + /** Construct a write sink. + + @param f The capy::test::fuse used to inject errors during writes. + + @param max_write_size Maximum bytes transferred per write. + Use to simulate chunked delivery. + */ + explicit write_sink( + capy::test::fuse f = {}, + std::size_t max_write_size = std::size_t(-1)) noexcept + : f_(std::move(f)) + , max_write_size_(max_write_size) + { + } + + /// Return the written data as a string view. + std::string_view + data() const noexcept + { + return data_; + } + + /** Set the expected data for subsequent writes. + + Stores the expected data and immediately tries to match + against any data already written. Matched data is consumed + from both buffers. + + @param sv The expected data. + + @return An error if existing data does not match. + */ + std::error_code + expect(std::string_view sv) + { + expect_.assign(sv); + return consume_match_(); + } + + /// Return the number of bytes written. + std::size_t + size() const noexcept + { + return data_.size(); + } + + /// Return whether write_eof has been called. + bool + eof_called() const noexcept + { + return eof_called_; + } + + /// Clear all data and reset state. + void + clear() noexcept + { + data_.clear(); + expect_.clear(); + eof_called_ = false; + } + + /** Asynchronously write some data to the sink. + + Transfers up to `capy::buffer_size( buffers )` bytes from the provided + const buffer sequence to the internal buffer. Before every write, + the attached @ref capy::test::fuse is consulted to possibly inject an error. + + @param buffers The const buffer sequence containing data to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Cancellation + If the environment's stop token has been requested, the write + completes immediately with `capy::error::canceled` and transfers no + data. An empty buffer sequence is a no-op that completes + successfully regardless of the stop token. + + @see capy::test::fuse + */ + template + auto + write_some(CB buffers) + { + struct awaitable + { + write_sink* self_; + CB buffers_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // The operation completes synchronously, but await_suspend is + // the only place capy::io_env is delivered (the promise's + // transform_awaiter forwards it here). Returning false means + // the coroutine does not actually suspend; it resumes + // immediately, having observed the stop token. See capy::io_env, + // IoAwaitable. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result + await_resume() + { + if(capy::buffer_empty(buffers_)) + return {{}, 0}; + + if(canceled_) + return {capy::error::canceled, 0}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, 0}; + + std::size_t n = capy::buffer_size(buffers_); + n = (std::min)(n, self_->max_write_size_); + + std::size_t const old_size = self_->data_.size(); + self_->data_.resize(old_size + n); + capy::buffer_copy(capy::make_buffer( + self_->data_.data() + old_size, n), buffers_, n); + + ec = self_->consume_match_(); + if(ec) + { + self_->data_.resize(old_size); + return {ec, 0}; + } + + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } + + /** Asynchronously write data to the sink. + + Transfers all bytes from the provided const buffer sequence + to the internal buffer. Unlike @ref write_some, this ignores + `max_write_size` and writes all available data, matching the + @ref WriteSink semantic contract. + + @par Exception Safety + Injected I/O conditions are reported via the `error_code` + component of the result. Throws `std::system_error` only when + the attached @ref capy::test::fuse is in exception mode and reaches its + failure point; no-throw otherwise. + + @param buffers The const buffer sequence containing data to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @par Cancellation + If the environment's stop token has been requested, the write + completes immediately with `capy::error::canceled` and transfers no + data. + + @throws std::system_error When the attached @ref capy::test::fuse is in + exception mode and reaches its failure point. + + @see capy::test::fuse + */ + template + auto + write(CB buffers) + { + struct awaitable + { + write_sink* self_; + CB buffers_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // Reads the stop token without suspending; see the comment + // on write_some() for details. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result + await_resume() + { + if(canceled_) + return {capy::error::canceled, 0}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, 0}; + + std::size_t n = capy::buffer_size(buffers_); + if(n == 0) + return {{}, 0}; + + std::size_t const old_size = self_->data_.size(); + self_->data_.resize(old_size + n); + capy::buffer_copy(capy::make_buffer( + self_->data_.data() + old_size, n), buffers_); + + ec = self_->consume_match_(); + if(ec) + return {ec, n}; + + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } + + /** Atomically write data and signal end-of-stream. + + Transfers all bytes from the provided const buffer sequence to + the internal buffer and signals end-of-stream. Before the write, + the attached @ref capy::test::fuse is consulted to possibly inject an error + for testing fault scenarios. + + @par Effects + On success, appends the written bytes to the internal buffer + and marks the sink as finalized. + If an error is injected by the capy::test::fuse, the internal buffer remains + unchanged. + + @par Exception Safety + Injected I/O conditions are reported via the `error_code` + component of the result. Throws `std::system_error` only when + the attached @ref capy::test::fuse is in exception mode and reaches its + failure point; no-throw otherwise. + + @par Cancellation + If the environment's stop token has been requested, the operation + completes immediately with `capy::error::canceled`, transfers no data, + and does not signal end-of-stream. + + @param buffers The const buffer sequence containing data to write. + + @return An awaitable that await-returns `(error_code,std::size_t)`. + + @throws std::system_error When the attached @ref capy::test::fuse is in + exception mode and reaches its failure point. + + @see capy::test::fuse + */ + template + auto + write_eof(CB buffers) + { + struct awaitable + { + write_sink* self_; + CB buffers_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // Reads the stop token without suspending; see the comment + // on write_some() for details. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result + await_resume() + { + if(canceled_) + return {capy::error::canceled, 0}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, 0}; + + std::size_t n = capy::buffer_size(buffers_); + if(n > 0) + { + std::size_t const old_size = self_->data_.size(); + self_->data_.resize(old_size + n); + capy::buffer_copy(capy::make_buffer( + self_->data_.data() + old_size, n), buffers_); + + ec = self_->consume_match_(); + if(ec) + return {ec, n}; + } + + self_->eof_called_ = true; + + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } + + /** Signal end-of-stream. + + Marks the sink as finalized, indicating no more data will be + written. Before signaling, the attached @ref capy::test::fuse is consulted + to possibly inject an error for testing fault scenarios. + + @par Effects + On success, marks the sink as finalized. + If an error is injected by the capy::test::fuse, the state remains unchanged. + + @par Exception Safety + Injected I/O conditions are reported via the `error_code` + component of the result. Throws `std::system_error` only when + the attached @ref capy::test::fuse is in exception mode and reaches its + failure point; no-throw otherwise. + + @par Cancellation + If the environment's stop token has been requested, the operation + completes immediately with `capy::error::canceled` and does not signal + end-of-stream. + + @return An awaitable that await-returns `(error_code)`. + + @throws std::system_error When the attached @ref capy::test::fuse is in + exception mode and reaches its failure point. + + @see capy::test::fuse + */ + auto + write_eof() + { + struct awaitable + { + write_sink* self_; + bool canceled_ = false; + + bool await_ready() const noexcept { return false; } + + // Reads the stop token without suspending; see the comment + // on write_some() for details. + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const* env) noexcept + { + canceled_ = env->stop_token.stop_requested(); + return false; + } + + capy::io_result<> + await_resume() + { + if(canceled_) + return {capy::error::canceled}; + + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec}; + + self_->eof_called_ = true; + return {}; + } + }; + return awaitable{this}; + } +}; + +} // test +} // capy +} // boost + +#endif diff --git a/src/json/json_body.cpp b/src/json/json_body.cpp index 621c34d9..5fbfcd86 100644 --- a/src/json/json_body.cpp +++ b/src/json/json_body.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include namespace boost { @@ -35,7 +35,7 @@ operator()(route_params& p) const json_sink sink( options_.storage, options_.parse_opts); - auto [ec, n] = co_await capy::push_to( + auto [ec, n] = co_await http::push_to( p.req_body, sink); if(ec) co_return route_error(ec); diff --git a/test/unit/concept/buffer_sink.cpp b/test/unit/concept/buffer_sink.cpp new file mode 100644 index 00000000..d08cf3af --- /dev/null +++ b/test/unit/concept/buffer_sink.cpp @@ -0,0 +1,266 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +#include + +#include + +#include +#include +#include + +namespace boost { +namespace http { + +namespace { +using namespace capy; + +// Mock IoAwaitable returning (error_code) +struct mock_commit_awaitable +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::tuple + await_resume() const noexcept + { + return {}; + } +}; + +// Mock IoAwaitable returning wrong type (error_code, size_t) +struct mock_commit_awaitable_wrong_type +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::pair + await_resume() const noexcept + { + return {}; + } +}; + +// Mock awaitable missing IoAwaitable protocol +struct mock_commit_awaitable_not_io +{ + bool await_ready() const noexcept { return true; } + + void await_suspend(std::coroutine_handle<>) const noexcept {} + + std::tuple + await_resume() const noexcept + { + return {}; + } +}; + +//---------------------------------------------------------- +// Mock sink types +//---------------------------------------------------------- + +// Valid BufferSink +struct valid_buffer_sink +{ + std::span + prepare(std::span) + { + return {}; + } + + mock_commit_awaitable + commit(std::size_t) + { + return {}; + } + + mock_commit_awaitable + commit_eof(std::size_t) + { + return {}; + } +}; + +// Invalid: commit returns wrong type +struct invalid_buffer_sink_wrong_type +{ + std::span + prepare(std::span) + { + return {}; + } + + mock_commit_awaitable_wrong_type + commit(std::size_t) + { + return {}; + } + + mock_commit_awaitable_wrong_type + commit_eof(std::size_t) + { + return {}; + } +}; + +// Invalid: missing prepare +struct invalid_buffer_sink_no_prepare +{ + mock_commit_awaitable + commit(std::size_t) + { + return {}; + } + + mock_commit_awaitable + commit_eof(std::size_t) + { + return {}; + } +}; + +// Invalid: missing commit +struct invalid_buffer_sink_no_commit +{ + std::span + prepare(std::span) + { + return {}; + } + + mock_commit_awaitable + commit_eof(std::size_t) + { + return {}; + } +}; + +// Invalid: missing commit_eof +struct invalid_buffer_sink_no_commit_eof +{ + std::span + prepare(std::span) + { + return {}; + } + + mock_commit_awaitable + commit(std::size_t) + { + return {}; + } +}; + +// Invalid: commit is not IoAwaitable +struct invalid_buffer_sink_not_io +{ + std::span + prepare(std::span) + { + return {}; + } + + mock_commit_awaitable_not_io + commit(std::size_t) + { + return {}; + } + + mock_commit_awaitable_not_io + commit_eof(std::size_t) + { + return {}; + } +}; + +// Invalid: prepare returns wrong type (size_t instead of span) +struct invalid_buffer_sink_prepare_returns_void +{ + void + prepare(std::span) + { + } + + mock_commit_awaitable + commit(std::size_t) + { + return {}; + } + + mock_commit_awaitable + commit_eof(std::size_t) + { + return {}; + } +}; + +// Invalid: prepare has old signature (ptr + count) +struct invalid_buffer_sink_wrong_sig +{ + std::size_t + prepare(mutable_buffer*, std::size_t) // Old signature + { + return 0; + } + + mock_commit_awaitable + commit(std::size_t) + { + return {}; + } + + mock_commit_awaitable + commit_eof(std::size_t) + { + return {}; + } +}; + +} // namespace + +//---------------------------------------------------------- +// Static assertions +//---------------------------------------------------------- + +// Valid sinks satisfy BufferSink +static_assert(BufferSink); + +// Wrong return types do not satisfy BufferSink +static_assert(!BufferSink); + +// Missing methods do not satisfy BufferSink +static_assert(!BufferSink); +static_assert(!BufferSink); +static_assert(!BufferSink); + +// Non-IoAwaitable does not satisfy BufferSink +static_assert(!BufferSink); + +// Wrong prepare return type does not satisfy BufferSink +static_assert(!BufferSink); + +// Wrong signature does not satisfy BufferSink +static_assert(!BufferSink); + +} // namespace http +} // namespace boost diff --git a/test/unit/concept/buffer_source.cpp b/test/unit/concept/buffer_source.cpp new file mode 100644 index 00000000..83cb11c5 --- /dev/null +++ b/test/unit/concept/buffer_source.cpp @@ -0,0 +1,176 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +#include + +#include + +#include +#include +#include + +namespace boost { +namespace http { + +namespace { +using namespace capy; + +// Mock IoAwaitable returning (error_code, span) +struct mock_source_awaitable +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::pair> + await_resume() const noexcept + { + return {}; + } +}; + +// Mock IoAwaitable returning wrong type (just error_code) +struct mock_source_awaitable_wrong_type +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::error_code + await_resume() const noexcept + { + return {}; + } +}; + +// Mock awaitable missing IoAwaitable protocol +struct mock_source_awaitable_not_io +{ + bool await_ready() const noexcept { return true; } + + void await_suspend(std::coroutine_handle<>) const noexcept {} + + std::pair + await_resume() const noexcept + { + return {}; + } +}; + +//---------------------------------------------------------- +// Mock source types +//---------------------------------------------------------- + +// Valid BufferSource +struct valid_buffer_source +{ + mock_source_awaitable + pull(std::span) + { + return {}; + } + + void consume(std::size_t) noexcept {} +}; + +// Invalid: pull returns wrong type +struct invalid_buffer_source_wrong_type +{ + mock_source_awaitable_wrong_type + pull(std::span) + { + return {}; + } +}; + +// Invalid: missing pull +struct invalid_buffer_source_no_pull +{ + // No pull method +}; + +// Invalid: pull is not IoAwaitable +struct invalid_buffer_source_not_io +{ + mock_source_awaitable_not_io + pull(std::span) + { + return {}; + } +}; + +// Invalid: pull returns non-awaitable +struct invalid_buffer_source_returns_int +{ + int pull(std::span) { return 0; } +}; + +// Invalid: pull has wrong signature (old style with ptr+count) +struct invalid_buffer_source_wrong_sig +{ + mock_source_awaitable + pull(const_buffer*, std::size_t) // Old signature + { + return {}; + } + + void consume(std::size_t) noexcept {} +}; + +// Invalid: missing consume +struct invalid_buffer_source_no_consume +{ + mock_source_awaitable + pull(std::span) + { + return {}; + } +}; + +} // namespace + +//---------------------------------------------------------- +// Static assertions +//---------------------------------------------------------- + +// Valid sources satisfy BufferSource +static_assert(BufferSource); + +// Wrong return types do not satisfy BufferSource +static_assert(!BufferSource); + +// Missing methods do not satisfy BufferSource +static_assert(!BufferSource); + +// Non-IoAwaitable does not satisfy BufferSource +static_assert(!BufferSource); + +// Non-awaitable return does not satisfy BufferSource +static_assert(!BufferSource); + +// Wrong signature does not satisfy BufferSource +static_assert(!BufferSource); + +// Missing consume does not satisfy BufferSource +static_assert(!BufferSource); + +} // namespace http +} // namespace boost diff --git a/test/unit/concept/read_source.cpp b/test/unit/concept/read_source.cpp new file mode 100644 index 00000000..5348c889 --- /dev/null +++ b/test/unit/concept/read_source.cpp @@ -0,0 +1,273 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +#include + +#include + +#include +#include +#include + +namespace boost { +namespace http { + +namespace { +using namespace capy; + +// Mock IoAwaitable returning (error_code, size_t) as pair +struct mock_source_awaitable_pair +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::pair + await_resume() const noexcept + { + return {}; + } +}; + +// Mock IoAwaitable returning (error_code, size_t) as tuple +struct mock_source_awaitable_tuple +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::tuple + await_resume() const noexcept + { + return {}; + } +}; + +// Mock IoAwaitable with wrong return type (just error_code) +struct mock_source_awaitable_wrong_type +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::tuple + await_resume() const noexcept + { + return {}; + } +}; + +// Mock IoAwaitable returning int +struct mock_source_awaitable_int +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + int await_resume() const noexcept { return 0; } +}; + +// Mock awaitable missing IoAwaitable protocol +struct mock_source_awaitable_not_io +{ + bool await_ready() const noexcept { return true; } + + void await_suspend(std::coroutine_handle<>) const noexcept {} + + std::pair + await_resume() const noexcept + { + return {}; + } +}; + +//---------------------------------------------------------- +// Mock source types +//---------------------------------------------------------- + +// Valid ReadSource with both read_some and read (pair) +struct valid_read_source_pair +{ + template + mock_source_awaitable_pair + read_some(MB const&) + { + return {}; + } + + template + mock_source_awaitable_pair + read(MB const&) + { + return {}; + } +}; + +// Valid ReadSource with both read_some and read (tuple) +struct valid_read_source_tuple +{ + template + mock_source_awaitable_tuple + read_some(MB const&) + { + return {}; + } + + template + mock_source_awaitable_tuple + read(MB const&) + { + return {}; + } +}; + +// Valid ReadSource accepting mutable_buffer directly (non-templated) +struct valid_read_source_not_templated +{ + mock_source_awaitable_pair + read_some(mutable_buffer const&) + { + return {}; + } + + mock_source_awaitable_pair + read(mutable_buffer const&) + { + return {}; + } +}; + +// Invalid: has read but no read_some (does not satisfy ReadStream) +struct invalid_read_source_no_read_some +{ + template + mock_source_awaitable_pair + read(MB const&) + { + return {}; + } +}; + +// Invalid: has read_some but no read +struct invalid_read_source_no_read +{ + template + mock_source_awaitable_pair + read_some(MB const&) + { + return {}; + } +}; + +// Invalid: read returns wrong type (just ec instead of ec, size_t) +struct invalid_read_source_wrong_type +{ + template + mock_source_awaitable_pair + read_some(MB const&) + { + return {}; + } + + template + mock_source_awaitable_wrong_type + read(MB const&) + { + return {}; + } +}; + +// Invalid: missing both read_some and read +struct invalid_read_source_empty +{ +}; + +// Invalid: read is not IoAwaitable +struct invalid_read_source_not_io +{ + template + mock_source_awaitable_pair + read_some(MB const&) + { + return {}; + } + + template + mock_source_awaitable_not_io + read(MB const&) + { + return {}; + } +}; + +// Invalid: read returns non-awaitable +struct invalid_read_source_returns_int +{ + template + mock_source_awaitable_pair + read_some(MB const&) + { + return {}; + } + + template + int read(MB const&) { return 0; } +}; + +} // namespace + +//---------------------------------------------------------- +// Static assertions +//---------------------------------------------------------- + +// Valid sources satisfy ReadSource +static_assert(ReadSource); +static_assert(ReadSource); +static_assert(ReadSource); + +// Has read but no read_some: does not satisfy ReadSource +static_assert(!ReadSource); + +// Has read_some but no read: does not satisfy ReadSource +static_assert(!ReadSource); + +// Wrong return type does not satisfy ReadSource +static_assert(!ReadSource); + +// Missing everything does not satisfy ReadSource +static_assert(!ReadSource); + +// Non-IoAwaitable does not satisfy ReadSource +static_assert(!ReadSource); + +// Non-awaitable return does not satisfy ReadSource +static_assert(!ReadSource); + +} // namespace http +} // namespace boost diff --git a/test/unit/concept/write_sink.cpp b/test/unit/concept/write_sink.cpp new file mode 100644 index 00000000..4a2ed92e --- /dev/null +++ b/test/unit/concept/write_sink.cpp @@ -0,0 +1,485 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +#include + +#include + +#include +#include +#include + +namespace boost { +namespace http { + +namespace { +using namespace capy; + +// Mock IoAwaitable returning std::error_code (for write_eof()) +struct mock_sink_awaitable +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::tuple + await_resume() const noexcept + { + return {}; + } +}; + +// Mock IoAwaitable returning (error_code, size_t) +struct mock_sink_awaitable_with_size +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + std::pair + await_resume() const noexcept + { + return {}; + } +}; + +// Mock IoAwaitable returning int +struct mock_sink_awaitable_int +{ + bool await_ready() const noexcept { return true; } + + void await_suspend( + std::coroutine_handle<>, + io_env const*) const noexcept + { + } + + int await_resume() const noexcept { return 0; } +}; + +// Mock awaitable missing IoAwaitable protocol +struct mock_sink_awaitable_not_io +{ + bool await_ready() const noexcept { return true; } + + void await_suspend(std::coroutine_handle<>) const noexcept {} + + std::tuple + await_resume() const noexcept + { + return {}; + } +}; + +// Mock awaitable missing IoAwaitable protocol, returns (ec, size_t) +struct mock_sink_awaitable_with_size_not_io +{ + bool await_ready() const noexcept { return true; } + + void await_suspend(std::coroutine_handle<>) const noexcept {} + + std::pair + await_resume() const noexcept + { + return {}; + } +}; + +//---------------------------------------------------------- +// Mock sink types +//---------------------------------------------------------- + +// Valid WriteSink: write_some + write + write_eof(buffers) + write_eof() +struct valid_write_sink +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Valid WriteSink with non-templated overloads +struct valid_write_sink_not_templated +{ + mock_sink_awaitable_with_size + write_some(const_buffer const&) + { + return {}; + } + + mock_sink_awaitable_with_size + write(const_buffer const&) + { + return {}; + } + + mock_sink_awaitable_with_size + write_eof(const_buffer const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Invalid: missing write_some (does not satisfy WriteStream) +struct invalid_write_sink_no_write_some +{ + template + mock_sink_awaitable_with_size + write(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Invalid: write returns wrong type (ec instead of ec, size_t) +struct invalid_write_sink_wrong_write_type +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable + write(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Invalid: write_eof() returns wrong type (ec,size_t instead of ec) +struct invalid_write_sink_wrong_eof_type +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable_with_size + write_eof() + { + return {}; + } +}; + +// Invalid: missing write +struct invalid_write_sink_no_write +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Invalid: missing write_eof() (bare) +struct invalid_write_sink_no_bare_eof +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } +}; + +// Invalid: missing write_eof(buffers) +struct invalid_write_sink_no_write_eof_buffers +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write(CB const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Invalid: write is not IoAwaitable +struct invalid_write_sink_write_not_io +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size_not_io + write(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Invalid: write_eof() is not IoAwaitable +struct invalid_write_sink_eof_not_io +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable_not_io + write_eof() + { + return {}; + } +}; + +// Invalid: write_eof(buffers) returns wrong type +struct invalid_write_sink_wrong_write_eof_buffers_type +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + mock_sink_awaitable_with_size + write(CB const&) + { + return {}; + } + + template + mock_sink_awaitable + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Invalid: write returns non-awaitable +struct invalid_write_sink_write_returns_int +{ + template + mock_sink_awaitable_with_size + write_some(CB const&) + { + return {}; + } + + template + int write(CB const&) { return 0; } + + template + mock_sink_awaitable_with_size + write_eof(CB const&) + { + return {}; + } + + mock_sink_awaitable + write_eof() + { + return {}; + } +}; + +// Invalid: empty type +struct invalid_write_sink_empty +{ +}; + +} // namespace + +//---------------------------------------------------------- +// Static assertions +//---------------------------------------------------------- + +// Valid sinks satisfy WriteSink +static_assert(WriteSink); +static_assert(WriteSink); + +// Missing write_some does not satisfy WriteSink +static_assert(!WriteSink); + +// Wrong return types do not satisfy WriteSink +static_assert(!WriteSink); +static_assert(!WriteSink); +static_assert(!WriteSink); + +// Missing methods do not satisfy WriteSink +static_assert(!WriteSink); +static_assert(!WriteSink); +static_assert(!WriteSink); +static_assert(!WriteSink); + +// Non-IoAwaitable does not satisfy WriteSink +static_assert(!WriteSink); +static_assert(!WriteSink); + +// Non-awaitable return does not satisfy WriteSink +static_assert(!WriteSink); + +} // namespace http +} // namespace boost diff --git a/test/unit/io/any_buffer_sink.cpp b/test/unit/io/any_buffer_sink.cpp new file mode 100644 index 00000000..bd61f847 --- /dev/null +++ b/test/unit/io/any_buffer_sink.cpp @@ -0,0 +1,1453 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +// Test that pull_from header is self-contained. +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include +#include +#include +#include +#include + +namespace boost { +namespace http { +namespace { +using namespace capy; +using namespace capy::test; + +// Static assert that any_buffer_sink satisfies BufferSink and WriteSink +static_assert(BufferSink); +static_assert(WriteSink); + +//---------------------------------------------------------- +// Mock satisfying both BufferSink and WriteSink. +// Tracks which API was used so tests can verify native +// forwarding vs. synthesized path. + +class buffer_write_sink +{ + capy::test::fuse f_; + std::string data_; + std::string prepare_buf_; + std::size_t prepare_size_ = 0; + std::size_t max_prepare_size_; + bool eof_called_ = false; + bool write_api_used_ = false; + +public: + explicit buffer_write_sink( + capy::test::fuse f = {}, + std::size_t max_prepare_size = 4096) noexcept + : f_(std::move(f)) + , max_prepare_size_(max_prepare_size) + { + prepare_buf_.resize(max_prepare_size_); + } + + std::string_view + data() const noexcept + { + return data_; + } + + bool + eof_called() const noexcept + { + return eof_called_; + } + + /// Return true if the WriteSink API was used. + bool + write_api_used() const noexcept + { + return write_api_used_; + } + + //------------------------------------------------------ + // BufferSink interface + + std::span + prepare(std::span dest) + { + if(dest.empty()) + return {}; + prepare_size_ = max_prepare_size_; + dest[0] = make_buffer(prepare_buf_.data(), prepare_size_); + return dest.first(1); + } + + auto + commit(std::size_t n) + { + struct awaitable + { + buffer_write_sink* self_; + std::size_t n_; + + bool await_ready() const noexcept { return true; } + void await_suspend(std::coroutine_handle<>, io_env const*) const noexcept {} + + io_result<> + await_resume() + { + auto ec = self_->f_.maybe_fail(); + if(ec) return {ec}; + std::size_t to_commit = (std::min)(n_, self_->prepare_size_); + self_->data_.append(self_->prepare_buf_.data(), to_commit); + self_->prepare_size_ = 0; + return {}; + } + }; + return awaitable{this, n}; + } + + auto + commit_eof(std::size_t n) + { + struct awaitable + { + buffer_write_sink* self_; + std::size_t n_; + + bool await_ready() const noexcept { return true; } + void await_suspend(std::coroutine_handle<>, io_env const*) const noexcept {} + + io_result<> + await_resume() + { + auto ec = self_->f_.maybe_fail(); + if(ec) return {ec}; + std::size_t to_commit = (std::min)(n_, self_->prepare_size_); + self_->data_.append(self_->prepare_buf_.data(), to_commit); + self_->prepare_size_ = 0; + self_->eof_called_ = true; + return {}; + } + }; + return awaitable{this, n}; + } + + //------------------------------------------------------ + // WriteSink interface + + template + auto + write_some(CB buffers) + { + struct awaitable + { + buffer_write_sink* 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() + { + self_->write_api_used_ = true; + if(buffer_empty(buffers_)) + return {{}, 0}; + auto ec = self_->f_.maybe_fail(); + if(ec) return {ec, 0}; + + std::size_t n = buffer_size(buffers_); + std::size_t const old_size = self_->data_.size(); + self_->data_.resize(old_size + n); + buffer_copy(make_buffer( + self_->data_.data() + old_size, n), buffers_, n); + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } + + template + auto + write(CB buffers) + { + struct awaitable + { + buffer_write_sink* 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() + { + self_->write_api_used_ = true; + auto ec = self_->f_.maybe_fail(); + if(ec) return {ec, 0}; + + std::size_t n = buffer_size(buffers_); + if(n == 0) return {{}, 0}; + std::size_t const old_size = self_->data_.size(); + self_->data_.resize(old_size + n); + buffer_copy(make_buffer( + self_->data_.data() + old_size, n), buffers_); + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } + + template + auto + write_eof(CB buffers) + { + struct awaitable + { + buffer_write_sink* 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() + { + self_->write_api_used_ = true; + auto ec = self_->f_.maybe_fail(); + if(ec) return {ec, 0}; + + std::size_t n = buffer_size(buffers_); + if(n > 0) + { + std::size_t const old_size = self_->data_.size(); + self_->data_.resize(old_size + n); + buffer_copy(make_buffer( + self_->data_.data() + old_size, n), buffers_); + } + self_->eof_called_ = true; + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } + + auto + write_eof() + { + struct awaitable + { + buffer_write_sink* self_; + + bool await_ready() const noexcept { return true; } + void await_suspend(std::coroutine_handle<>, io_env const*) const noexcept {} + + io_result<> + await_resume() + { + self_->write_api_used_ = true; + auto ec = self_->f_.maybe_fail(); + if(ec) return {ec}; + self_->eof_called_ = true; + return {}; + } + }; + return awaitable{this}; + } +}; + +// Verify concepts at compile time +static_assert(BufferSink); +static_assert(WriteSink); + +// Verify BufferSink-only mock does NOT satisfy WriteSink +static_assert(!WriteSink); + +//---------------------------------------------------------- + +// Suspends, then resumes from await_suspend, to exercise the +// type-erased await_suspend forwarding the always-ready mocks skip. +struct resuming_eof_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result<> await_resume() { return {}; } +}; + +struct resuming_size_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 1}; } +}; + +// Satisfies BufferSink + WriteSink with suspending operations. +struct resuming_buffer_sink +{ + char buf_[8] = {}; + std::span prepare(std::span dest) + { + if(dest.empty()) + return {}; + dest[0] = make_buffer(buf_, sizeof(buf_)); + return dest.first(1); + } + resuming_eof_awaitable commit(std::size_t) { return {}; } + resuming_eof_awaitable commit_eof(std::size_t) { return {}; } + template + resuming_size_awaitable write_some(CB) { return {}; } + template + resuming_size_awaitable write(CB) { return {}; } + template + resuming_size_awaitable write_eof(CB) { return {}; } + resuming_eof_awaitable write_eof() { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the sink is constructed. +struct throwing_move_buffer_sink +{ + int* destroyed_; + explicit throwing_move_buffer_sink(int* d) : destroyed_(d) {} + throwing_move_buffer_sink(throwing_move_buffer_sink&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_buffer_sink() { if(destroyed_) ++(*destroyed_); } + std::span prepare(std::span dest) + { return dest.empty() ? std::span{} : dest.first(0); } + resuming_eof_awaitable commit(std::size_t) { return {}; } + resuming_eof_awaitable commit_eof(std::size_t) { return {}; } + template + resuming_size_awaitable write_some(CB) { return {}; } + template + resuming_size_awaitable write(CB) { return {}; } + template + resuming_size_awaitable write_eof(CB) { return {}; } + resuming_eof_awaitable write_eof() { return {}; } +}; + +//---------------------------------------------------------- + +class any_buffer_sink_test +{ +public: + void + testConstruct() + { + // Default construct + { + any_buffer_sink abs; + BOOST_TEST(!abs.has_value()); + BOOST_TEST(!abs); + } + + // Construct from BufferSink-only (reference) + { + capy::test::fuse f; + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + BOOST_TEST(abs.has_value()); + BOOST_TEST(static_cast(abs)); + } + + // Construct from BufferSink+WriteSink (reference) + { + capy::test::fuse f; + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + BOOST_TEST(abs.has_value()); + BOOST_TEST(static_cast(abs)); + } + } + + void + testConstructOwning() + { + // Owning construct by value (BufferSink-only) + { + capy::test::fuse f; + test::buffer_sink bs(f); + any_buffer_sink abs(std::move(bs)); + BOOST_TEST(abs.has_value()); + } + + // Owning construct by value (BufferSink+WriteSink) + { + capy::test::fuse f; + buffer_write_sink bws(f); + any_buffer_sink abs(std::move(bws)); + BOOST_TEST(abs.has_value()); + } + + // Owning construct, then use + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(std::move(bs)); + + mutable_buffer arr[capy::detail::max_iovec_]; + auto bufs = abs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + + std::memcpy(bufs[0].data(), "owned", 5); + + auto [ec] = co_await abs.commit_eof(5); + if(ec) + co_return; + }); + BOOST_TEST(r.success); + } + } + + void + testMove() + { + capy::test::fuse f; + test::buffer_sink bs(f); + + any_buffer_sink abs1(&bs); + BOOST_TEST(abs1.has_value()); + + // Move construct + any_buffer_sink abs2(std::move(abs1)); + BOOST_TEST(abs2.has_value()); + BOOST_TEST(!abs1.has_value()); + + // Move assign into empty + any_buffer_sink abs3; + abs3 = std::move(abs2); + BOOST_TEST(abs3.has_value()); + BOOST_TEST(!abs2.has_value()); + } + + void + testMoveAssignOverExisting() + { + capy::test::fuse f; + test::buffer_sink bs1(f); + test::buffer_sink bs2(f); + + any_buffer_sink abs1(&bs1); + any_buffer_sink abs2(&bs2); + BOOST_TEST(abs1.has_value()); + BOOST_TEST(abs2.has_value()); + + // Move assign over a wrapper that already holds a sink + abs1 = std::move(abs2); + BOOST_TEST(abs1.has_value()); + BOOST_TEST(!abs2.has_value()); + } + + void + testPrepareCommit() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + mutable_buffer arr[capy::detail::max_iovec_]; + auto bufs = abs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + BOOST_TEST(bufs[0].size() > 0); + + // Write data into the buffer + std::memcpy(bufs[0].data(), "hello", 5); + + auto [ec] = co_await abs.commit(5); + if(ec) + co_return; + + BOOST_TEST_EQ(bs.data(), "hello"); + }); + BOOST_TEST(r.success); + } + + void + testCommitWithEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + mutable_buffer arr[capy::detail::max_iovec_]; + auto bufs = abs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + + std::memcpy(bufs[0].data(), "world", 5); + + auto [ec] = co_await abs.commit_eof(5); + if(ec) + co_return; + + BOOST_TEST_EQ(bs.data(), "world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testCommitEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + mutable_buffer arr[capy::detail::max_iovec_]; + auto bufs = abs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + + std::memcpy(bufs[0].data(), "data", 4); + + auto [ec1] = co_await abs.commit(4); + if(ec1) + co_return; + + auto [ec2] = co_await abs.commit_eof(0); + if(ec2) + co_return; + + BOOST_TEST_EQ(bs.data(), "data"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testMultipleCommits() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + // First write + { + mutable_buffer arr[capy::detail::max_iovec_]; + auto bufs = abs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + + std::memcpy(bufs[0].data(), "hello ", 6); + + auto [ec] = co_await abs.commit(6); + if(ec) + co_return; + } + + // Second write + { + mutable_buffer arr[capy::detail::max_iovec_]; + auto bufs = abs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + + std::memcpy(bufs[0].data(), "world", 5); + + auto [ec] = co_await abs.commit_eof(5); + if(ec) + co_return; + } + + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testEmptyCommit() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto [ec] = co_await abs.commit_eof(0); + if(ec) + co_return; + + BOOST_TEST(bs.data().empty()); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + //------------------------------------------------------ + // Synthesized WriteSink tests (BufferSink-only) + + void + testWriteSome() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto buf = make_buffer("hello", 5); + auto [ec, n] = co_await abs.write_some(buf); + if(ec) + co_return; + + BOOST_TEST(n > 0); + BOOST_TEST(n <= 5u); + BOOST_TEST_EQ(bs.data(), + std::string_view("hello", n)); + }); + BOOST_TEST(r.success); + } + + void + testWrite() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto buf = make_buffer("hello world", 11); + auto [ec, n] = co_await abs.write(buf); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofWithBuffers() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto buf = make_buffer("final", 5); + auto [ec, n] = co_await abs.write_eof(buf); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(bs.data(), "final"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofNoArg() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto [ec] = co_await abs.write_eof(); + if(ec) + co_return; + + BOOST_TEST(bs.data().empty()); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteThenEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto buf = make_buffer("payload", 7); + auto [ec1, n] = co_await abs.write(buf); + if(ec1) + co_return; + + BOOST_TEST_EQ(n, 7u); + BOOST_TEST(!bs.eof_called()); + + auto [ec2] = co_await abs.write_eof(); + if(ec2) + co_return; + + BOOST_TEST_EQ(bs.data(), "payload"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testFuseErrorCommit() + { + int success_count = 0; + int error_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + mutable_buffer arr[capy::detail::max_iovec_]; + auto bufs = abs.prepare(arr); + std::memcpy(bufs[0].data(), "data", 4); + + auto [ec] = co_await abs.commit(4); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testFuseErrorCommitEof() + { + int success_count = 0; + int error_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto [ec] = co_await abs.commit_eof(0); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testWriteSomeEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto [ec, n] = co_await abs.write_some(const_buffer{}); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(bs.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto [ec, n] = co_await abs.write(const_buffer{}); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(bs.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto [ec, n] = co_await abs.write_eof(const_buffer{}); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(bs.data().empty()); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testFuseErrorWriteSome() + { + int success_count = 0; + int error_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto buf = make_buffer("hello", 5); + auto [ec, n] = co_await abs.write_some(buf); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testFuseErrorWrite() + { + int success_count = 0; + int error_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto buf = make_buffer("hello world", 11); + auto [ec, n] = co_await abs.write(buf); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testFuseErrorWriteEofBuffers() + { + int success_count = 0; + int error_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto buf = make_buffer("final", 5); + auto [ec, n] = co_await abs.write_eof(buf); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testFuseErrorWriteEof() + { + int success_count = 0; + int error_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + + auto [ec] = co_await abs.write_eof(); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testMoveOwning() + { + // Move construct from owning + { + capy::test::fuse f; + test::buffer_sink bs(f); + any_buffer_sink abs1(std::move(bs)); + BOOST_TEST(abs1.has_value()); + + any_buffer_sink abs2(std::move(abs1)); + BOOST_TEST(abs2.has_value()); + BOOST_TEST(!abs1.has_value()); + } + + // Move assign from owning into empty + { + capy::test::fuse f; + test::buffer_sink bs(f); + any_buffer_sink abs1(std::move(bs)); + + any_buffer_sink abs2; + abs2 = std::move(abs1); + BOOST_TEST(abs2.has_value()); + BOOST_TEST(!abs1.has_value()); + } + + // Move assign from owning over existing + { + capy::test::fuse f; + test::buffer_sink bs1(f); + test::buffer_sink bs2(f); + any_buffer_sink abs1(std::move(bs1)); + any_buffer_sink abs2(std::move(bs2)); + + abs1 = std::move(abs2); + BOOST_TEST(abs1.has_value()); + BOOST_TEST(!abs2.has_value()); + } + } + + void + testSelfMoveAssign() + { + capy::test::fuse f; + test::buffer_sink bs(f); + any_buffer_sink abs(&bs); + BOOST_TEST(abs.has_value()); + + any_buffer_sink* p = &abs; + any_buffer_sink* q = p; + *p = std::move(*q); + BOOST_TEST(abs.has_value()); + } + + //------------------------------------------------------ + // Native WriteSink forwarding tests (BufferSink+WriteSink) + + void + testNativeWriteSome() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + auto buf = make_buffer("hello", 5); + auto [ec, n] = co_await abs.write_some(buf); + if(ec) + co_return; + + BOOST_TEST(n > 0); + BOOST_TEST(n <= 5u); + BOOST_TEST(bws.write_api_used()); + BOOST_TEST_EQ(bws.data(), + std::string_view("hello", n)); + }); + BOOST_TEST(r.success); + } + + void + testNativeWrite() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + auto buf = make_buffer("hello world", 11); + auto [ec, n] = co_await abs.write(buf); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST(bws.write_api_used()); + BOOST_TEST_EQ(bws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testNativeWriteEofWithBuffers() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + auto buf = make_buffer("final", 5); + auto [ec, n] = co_await abs.write_eof(buf); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(bws.write_api_used()); + BOOST_TEST_EQ(bws.data(), "final"); + BOOST_TEST(bws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testNativeWriteEofNoArg() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + auto [ec] = co_await abs.write_eof(); + if(ec) + co_return; + + BOOST_TEST(bws.write_api_used()); + BOOST_TEST(bws.data().empty()); + BOOST_TEST(bws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testNativeWriteThenEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + auto buf = make_buffer("payload", 7); + auto [ec1, n] = co_await abs.write(buf); + if(ec1) + co_return; + + BOOST_TEST_EQ(n, 7u); + BOOST_TEST(!bws.eof_called()); + + auto [ec2] = co_await abs.write_eof(); + if(ec2) + co_return; + + BOOST_TEST_EQ(bws.data(), "payload"); + BOOST_TEST(bws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testNativeWriteSomeEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + auto [ec, n] = co_await abs.write_some(const_buffer{}); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0u); + }); + BOOST_TEST(r.success); + } + + void + testNativeWriteEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + auto [ec, n] = co_await abs.write(const_buffer{}); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0u); + }); + BOOST_TEST(r.success); + } + + void + testNativeWriteEofEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + auto [ec, n] = co_await abs.write_eof(const_buffer{}); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(bws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testNativeOwning() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(std::move(bws)); + + auto buf = make_buffer("owned", 5); + auto [ec, n] = co_await abs.write_eof(buf); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + }); + BOOST_TEST(r.success); + } + + void + testNativePrepareCommit() + { + // BufferSink API still works when WriteSink is also present + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_write_sink bws(f); + any_buffer_sink abs(&bws); + + mutable_buffer arr[capy::detail::max_iovec_]; + auto bufs = abs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + + std::memcpy(bufs[0].data(), "buf-api", 7); + + auto [ec] = co_await abs.commit(7); + if(ec) + co_return; + + // BufferSink API used, not WriteSink + BOOST_TEST(!bws.write_api_used()); + BOOST_TEST_EQ(bws.data(), "buf-api"); + }); + BOOST_TEST(r.success); + } + + //------------------------------------------------------ + // pull_from tests + + void + testPullFromReadStream() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream src(f); + src.provide("hello world"); + + test::buffer_sink sink(f); + + auto [ec, n] = co_await pull_from(src, sink); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(sink.data(), "hello world"); + BOOST_TEST(sink.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPullFromReadStreamTypeErased() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream src(f); + src.provide("hello world"); + + test::buffer_sink sink(f); + any_buffer_sink abs(&sink); + + auto [ec, n] = co_await pull_from(src, abs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(sink.data(), "hello world"); + BOOST_TEST(sink.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPullFromReadStreamChunked() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream src(f, 5); // max 5 bytes per read + src.provide("hello world"); + + test::buffer_sink sink(f); + + auto [ec, n] = co_await pull_from(src, sink); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(sink.data(), "hello world"); + BOOST_TEST(sink.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPullFromReadStreamEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream src(f); + // No data provided + + test::buffer_sink sink(f); + + auto [ec, n] = co_await pull_from(src, sink); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(sink.data().empty()); + BOOST_TEST(sink.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPullFromReadSource() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source src(f); + src.provide("hello world"); + + test::buffer_sink sink(f); + + auto [ec, n] = co_await pull_from(src, sink); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(sink.data(), "hello world"); + BOOST_TEST(sink.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPullFromReadSourceTypeErased() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source src(f); + src.provide("hello world"); + + test::buffer_sink sink(f); + any_buffer_sink abs(&sink); + + auto [ec, n] = co_await pull_from(src, abs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(sink.data(), "hello world"); + BOOST_TEST(sink.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPullFromReadSourceChunked() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source src(f, 5); // max 5 bytes per read + src.provide("hello world"); + + test::buffer_sink sink(f); + + auto [ec, n] = co_await pull_from(src, sink); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(sink.data(), "hello world"); + BOOST_TEST(sink.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPullFromReadSourceEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source src(f); + // No data provided + + test::buffer_sink sink(f); + + auto [ec, n] = co_await pull_from(src, sink); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(sink.data().empty()); + BOOST_TEST(sink.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testConstructThrows() + { + // Owning construction whose sink move-ctor throws must not run + // the sink destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_buffer_sink(throwing_move_buffer_sink{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testSuspends() + { + // Drive every operation through an awaitable that suspends and + // then resumes, covering the type-erased await_suspend paths. + resuming_buffer_sink sink; + any_buffer_sink abs(&sink); + + auto coro = [&]() -> capy::task { + mutable_buffer arr[capy::detail::max_iovec_]; + abs.prepare(arr); + + auto [ec1] = co_await abs.commit(1); + if(ec1) + co_return 0; + + char const data[] = "x"; + auto [ec2, n2] = co_await abs.write_some(const_buffer(data, 1)); + if(ec2) + co_return 0; + auto [ec3, n3] = co_await abs.write(const_buffer(data, 1)); + if(ec3) + co_return 0; + auto [ec4, n4] = co_await abs.write_eof(const_buffer(data, 1)); + if(ec4) + co_return 0; + + abs.prepare(arr); + auto [ec5] = co_await abs.commit_eof(1); + if(ec5) + co_return 0; + auto [ec6] = co_await abs.write_eof(); + if(ec6) + co_return 0; + + co_return n2 + n3 + n4; + }; + + std::size_t result{}; + capy::test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 3u); + } + + void + run() + { + testConstruct(); + testConstructOwning(); + testConstructThrows(); + testSuspends(); + testMove(); + testMoveAssignOverExisting(); + testPrepareCommit(); + testCommitWithEof(); + testCommitEof(); + testMultipleCommits(); + testEmptyCommit(); + testWriteSome(); + testWrite(); + testWriteEofWithBuffers(); + testWriteEofNoArg(); + testWriteThenEof(); + testFuseErrorCommit(); + testFuseErrorCommitEof(); + testWriteSomeEmpty(); + testWriteEmpty(); + testWriteEofEmpty(); + testFuseErrorWriteSome(); + testFuseErrorWrite(); + testFuseErrorWriteEofBuffers(); + testFuseErrorWriteEof(); + testMoveOwning(); + testSelfMoveAssign(); + testNativeWriteSome(); + testNativeWrite(); + testNativeWriteEofWithBuffers(); + testNativeWriteEofNoArg(); + testNativeWriteThenEof(); + testNativeWriteSomeEmpty(); + testNativeWriteEmpty(); + testNativeWriteEofEmpty(); + testNativeOwning(); + testNativePrepareCommit(); + testPullFromReadStream(); + testPullFromReadStreamTypeErased(); + testPullFromReadStreamChunked(); + testPullFromReadStreamEmpty(); + testPullFromReadSource(); + testPullFromReadSourceTypeErased(); + testPullFromReadSourceChunked(); + testPullFromReadSourceEmpty(); + } +}; + +TEST_SUITE(any_buffer_sink_test, "boost.capy.io.any_buffer_sink"); + +} // namespace +} // namespace http +} // namespace boost diff --git a/test/unit/io/any_buffer_source.cpp b/test/unit/io/any_buffer_source.cpp new file mode 100644 index 00000000..56f41654 --- /dev/null +++ b/test/unit/io/any_buffer_source.cpp @@ -0,0 +1,941 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +// Test that push_to header is self-contained. +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include +#include +#include +#include +#include + +namespace boost { +namespace http { +namespace { +using namespace capy; +using namespace capy::test; + +// Static assert that any_buffer_source satisfies BufferSource +static_assert(BufferSource); +static_assert(ReadSource); + +//---------------------------------------------------------- +// Mock satisfying both BufferSource and ReadSource. +// Tracks which API was used so tests can verify native +// forwarding vs. synthesized path. + +class buffer_read_source +{ + capy::test::fuse f_; + std::string data_; + std::size_t pos_ = 0; + std::size_t max_pull_size_; + bool read_api_used_ = false; + +public: + explicit buffer_read_source( + capy::test::fuse f = {}, + std::size_t max_pull_size = std::size_t(-1)) noexcept + : f_(std::move(f)) + , max_pull_size_(max_pull_size) + { + } + + void + provide(std::string_view sv) + { + data_.append(sv); + } + + std::size_t + available() const noexcept + { + return data_.size() - pos_; + } + + /// Return true if the ReadSource API was used. + bool + read_api_used() const noexcept + { + return read_api_used_; + } + + //------------------------------------------------------ + // BufferSource interface + + void + consume(std::size_t n) noexcept + { + pos_ += n; + } + + auto + pull(std::span dest) + { + struct awaitable + { + buffer_read_source* self_; + std::span dest_; + + bool await_ready() const noexcept { return true; } + void await_suspend(std::coroutine_handle<>, io_env const*) const noexcept {} + + io_result> + await_resume() + { + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, {}}; + + if(self_->pos_ >= self_->data_.size()) + return {capy::error::eof, {}}; + + std::size_t avail = self_->data_.size() - self_->pos_; + std::size_t to_return = (std::min)(avail, self_->max_pull_size_); + + if(dest_.empty()) + return {{}, {}}; + + dest_[0] = make_buffer( + self_->data_.data() + self_->pos_, + to_return); + return {{}, dest_.first(1)}; + } + }; + return awaitable{this, dest}; + } + + //------------------------------------------------------ + // ReadSource interface + + template + auto + read_some(MB buffers) + { + struct awaitable + { + buffer_read_source* 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() + { + self_->read_api_used_ = true; + if(buffer_empty(buffers_)) + return {{}, 0}; + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, 0}; + + if(self_->pos_ >= self_->data_.size()) + return {capy::error::eof, 0}; + + std::size_t avail = self_->data_.size() - self_->pos_; + if(avail > self_->max_pull_size_) + avail = self_->max_pull_size_; + auto src = make_buffer( + self_->data_.data() + self_->pos_, avail); + std::size_t const n = buffer_copy(buffers_, src); + self_->pos_ += n; + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } + + template + auto + read(MB buffers) + { + struct awaitable + { + buffer_read_source* 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() + { + self_->read_api_used_ = true; + if(buffer_empty(buffers_)) + return {{}, 0}; + auto ec = self_->f_.maybe_fail(); + if(ec) + return {ec, 0}; + + if(self_->pos_ >= self_->data_.size()) + return {capy::error::eof, 0}; + + std::size_t avail = self_->data_.size() - self_->pos_; + auto src = make_buffer( + self_->data_.data() + self_->pos_, avail); + std::size_t const n = buffer_copy(buffers_, src); + self_->pos_ += n; + + if(n < buffer_size(buffers_)) + return {capy::error::eof, n}; + return {{}, n}; + } + }; + return awaitable{this, buffers}; + } +}; + +// Suspends, then resumes from await_suspend, to exercise the +// type-erased await_suspend forwarding the always-ready mock skips. +struct resuming_pull_awaitable +{ + std::span dest_; + char const* data_; + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result> + await_resume() + { + if(dest_.empty()) + return {{}, {}}; + dest_[0] = make_buffer(data_, 3); + return {{}, dest_.first(1)}; + } +}; + +struct resuming_io_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 3}; } +}; + +// Satisfies BufferSource + ReadSource with suspending operations. +struct resuming_buffer_source +{ + char data_[4] = "abc"; + resuming_pull_awaitable pull(std::span dest) + { return {dest, data_}; } + void consume(std::size_t) noexcept {} + template + resuming_io_awaitable read_some(MB) { return {}; } + template + resuming_io_awaitable read(MB) { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the source is constructed. +struct throwing_move_buffer_source +{ + int* destroyed_; + explicit throwing_move_buffer_source(int* d) : destroyed_(d) {} + throwing_move_buffer_source(throwing_move_buffer_source&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_buffer_source() { if(destroyed_) ++(*destroyed_); } + resuming_pull_awaitable pull(std::span dest) + { return {dest, nullptr}; } + void consume(std::size_t) noexcept {} + template + resuming_io_awaitable read_some(MB) { return {}; } + template + resuming_io_awaitable read(MB) { return {}; } +}; + +// Verify concepts at compile time +static_assert(BufferSource); +static_assert(ReadSource); + +// Verify BufferSource-only mock does NOT satisfy ReadSource +static_assert(!ReadSource); + +//---------------------------------------------------------- + +class any_buffer_source_test +{ +public: + void + testConstruct() + { + // Default construct + { + any_buffer_source abs; + BOOST_TEST(!abs.has_value()); + BOOST_TEST(!abs); + } + + // Construct from BufferSource-only (reference) + { + capy::test::fuse f; + test::buffer_source bs(f); + any_buffer_source abs(&bs); + BOOST_TEST(abs.has_value()); + BOOST_TEST(static_cast(abs)); + } + + // Construct from BufferSource+ReadSource (reference) + { + capy::test::fuse f; + buffer_read_source brs(f); + any_buffer_source abs(&brs); + BOOST_TEST(abs.has_value()); + } + + // Owning construct from BufferSource+ReadSource + { + capy::test::fuse f; + any_buffer_source abs((buffer_read_source(f))); + BOOST_TEST(abs.has_value()); + } + } + + void + testMove() + { + capy::test::fuse f; + test::buffer_source bs(f); + + any_buffer_source abs1(&bs); + BOOST_TEST(abs1.has_value()); + + // Move construct + any_buffer_source abs2(std::move(abs1)); + BOOST_TEST(abs2.has_value()); + BOOST_TEST(!abs1.has_value()); + + // Move assign + any_buffer_source abs3; + abs3 = std::move(abs2); + BOOST_TEST(abs3.has_value()); + BOOST_TEST(!abs2.has_value()); + } + + void + testMoveNative() + { + capy::test::fuse f; + buffer_read_source brs(f); + + any_buffer_source abs1(&brs); + BOOST_TEST(abs1.has_value()); + + // Move construct + any_buffer_source abs2(std::move(abs1)); + BOOST_TEST(abs2.has_value()); + BOOST_TEST(!abs1.has_value()); + + // Move assign + any_buffer_source abs3; + abs3 = std::move(abs2); + BOOST_TEST(abs3.has_value()); + BOOST_TEST(!abs2.has_value()); + } + + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the storage_ + // teardown branch in operator=. + any_buffer_source a(buffer_read_source{capy::test::fuse{}}); + any_buffer_source b(buffer_read_source{capy::test::fuse{}}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + + void + testConstructThrows() + { + // Owning construction whose source move-ctor throws must not + // run the source destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_buffer_source(throwing_move_buffer_source{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testSuspends() + { + // Drive pull/read/read_some whose awaitables suspend then + // resume, covering the type-erased await_suspend forwarding. + resuming_buffer_source src; + any_buffer_source abs(&src); + + auto coro = [&]() -> capy::task { + const_buffer arr[capy::detail::max_iovec_]; + auto [ec1, bufs] = co_await abs.pull(arr); + if(ec1) + co_return 0; + + char buf[3] = {}; + auto [ec2, n2] = co_await abs.read(make_buffer(buf, 3)); + if(ec2) + co_return 0; + + auto [ec3, n3] = co_await abs.read_some(make_buffer(buf, 3)); + if(ec3) + co_return 0; + + co_return bufs.size() + n2 + n3; + }; + + std::size_t result{}; + capy::test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 7u); + } + + void + testPull() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + + any_buffer_source abs(&bs); + + const_buffer arr[capy::detail::max_iovec_]; + auto [ec, bufs] = co_await abs.pull(arr); + if(ec) + co_return; + + BOOST_TEST_EQ(bufs.size(), 1u); + BOOST_TEST_EQ(bufs[0].size(), 11u); + abs.consume(11); + }); + BOOST_TEST(r.success); + } + + void + testConsume() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + + any_buffer_source abs(&bs); + + const_buffer arr[capy::detail::max_iovec_]; + + // First pull returns all data + auto [ec1, bufs1] = co_await abs.pull(arr); + if(ec1) + co_return; + BOOST_TEST_EQ(bufs1.size(), 1u); + BOOST_TEST_EQ(bufs1[0].size(), 11u); + + // Consume partial (5 bytes = "hello") + abs.consume(5); + + // Second pull returns remaining data + auto [ec2, bufs2] = co_await abs.pull(arr); + if(ec2) + co_return; + BOOST_TEST_EQ(bufs2.size(), 1u); + BOOST_TEST_EQ(bufs2[0].size(), 6u); // " world" + + // Consume rest + abs.consume(6); + + // Third pull returns eof (exhausted) + auto [ec3, bufs3] = co_await abs.pull(arr); + if(ec3 != capy::cond::eof) + co_return; + BOOST_TEST(bufs3.empty()); + }); + BOOST_TEST(r.success); + } + + void + testPullWithoutConsume() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("test"); + + any_buffer_source abs(&bs); + + const_buffer arr[capy::detail::max_iovec_]; + + // Pull returns data + auto [ec1, bufs1] = co_await abs.pull(arr); + if(ec1) + co_return; + BOOST_TEST_EQ(bufs1.size(), 1u); + BOOST_TEST_EQ(bufs1[0].size(), 4u); + + // Pull again without consume returns same data + auto [ec2, bufs2] = co_await abs.pull(arr); + if(ec2) + co_return; + BOOST_TEST_EQ(bufs2.size(), 1u); + BOOST_TEST_EQ(bufs2[0].size(), 4u); + + abs.consume(4); + }); + BOOST_TEST(r.success); + } + + void + testPullMultiple() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f, 5); // max 5 bytes per pull + bs.provide("hello world"); + + any_buffer_source abs(&bs); + + std::size_t total = 0; + for(;;) + { + const_buffer arr[capy::detail::max_iovec_]; + auto [ec, bufs] = co_await abs.pull(arr); + if(ec == capy::cond::eof) + break; + if(ec) + co_return; + for(auto const& buf : bufs) + { + total += buf.size(); + abs.consume(buf.size()); + } + } + + BOOST_TEST_EQ(total, 11u); + }); + BOOST_TEST(r.success); + } + + void + testPullEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + // No data provided + + any_buffer_source abs(&bs); + + const_buffer arr[capy::detail::max_iovec_]; + auto [ec, bufs] = co_await abs.pull(arr); + if(ec != capy::cond::eof) + co_return; + BOOST_TEST(bufs.empty()); + }); + BOOST_TEST(r.success); + } + + //------------------------------------------------------ + // Synthesized ReadSource tests (BufferSource-only mock) + + void + testSynthesizedReadSome() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + + any_buffer_source abs(&bs); + + char buf[64]; + auto [ec, n] = co_await abs.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + + BOOST_TEST(n > 0); + BOOST_TEST(n <= 11u); + BOOST_TEST_EQ( + std::string_view(buf, n), + std::string_view("hello world", n)); + }); + BOOST_TEST(r.success); + } + + void + testSynthesizedRead() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + + any_buffer_source abs(&bs); + + char buf[11]; + auto [ec, n] = co_await abs.read( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ( + std::string_view(buf, n), + "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testSynthesizedReadSomeEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("data"); + + any_buffer_source abs(&bs); + + // Empty buffer returns 0 immediately + auto [ec, n] = co_await abs.read_some( + mutable_buffer(nullptr, 0)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0u); + }); + BOOST_TEST(r.success); + } + + //------------------------------------------------------ + // Native ReadSource tests (BufferSource+ReadSource mock) + + void + testNativeReadSome() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_read_source brs(f); + brs.provide("hello world"); + + any_buffer_source abs(&brs); + + char buf[64]; + auto [ec, n] = co_await abs.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + + BOOST_TEST(n > 0); + BOOST_TEST(n <= 11u); + BOOST_TEST(brs.read_api_used()); + BOOST_TEST_EQ( + std::string_view(buf, n), + std::string_view("hello world", n)); + }); + BOOST_TEST(r.success); + } + + void + testNativeRead() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_read_source brs(f); + brs.provide("hello world"); + + any_buffer_source abs(&brs); + + char buf[11]; + auto [ec, n] = co_await abs.read( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST(brs.read_api_used()); + BOOST_TEST_EQ( + std::string_view(buf, n), + "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testNativeReadSomeEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_read_source brs(f); + brs.provide("data"); + + any_buffer_source abs(&brs); + + // Empty buffer returns 0 immediately + auto [ec, n] = co_await abs.read_some( + mutable_buffer(nullptr, 0)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0u); + // ReadSource API should NOT be called for empty buffers + BOOST_TEST(!brs.read_api_used()); + }); + BOOST_TEST(r.success); + } + + void + testNativePullAndConsume() + { + // Verify that pull/consume still works even when + // the wrapped type satisfies ReadSource + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_read_source brs(f); + brs.provide("hello"); + + any_buffer_source abs(&brs); + + const_buffer arr[capy::detail::max_iovec_]; + auto [ec, bufs] = co_await abs.pull(arr); + if(ec) + co_return; + + BOOST_TEST_EQ(bufs.size(), 1u); + BOOST_TEST_EQ(bufs[0].size(), 5u); + abs.consume(5); + + // Read API should NOT be used for pull/consume + BOOST_TEST(!brs.read_api_used()); + }); + BOOST_TEST(r.success); + } + + void + testNativeOwning() + { + // Verify owning construction forwards native ReadSource + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_read_source brs(f); + brs.provide("hello world"); + + any_buffer_source abs(std::move(brs)); + + char buf[11]; + auto [ec, n] = co_await abs.read( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ( + std::string_view(buf, n), + "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testNativeReadEof() + { + // Verify EOF handling through native path + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_read_source brs(f); + brs.provide("hi"); + + any_buffer_source abs(&brs); + + // Try to read more than available + char buf[64]; + auto [ec, n] = co_await abs.read( + mutable_buffer(buf, sizeof(buf))); + + // Fuse may inject a non-eof error + if(ec && ec != capy::cond::eof) + co_return; + + // Should get partial data + EOF + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 2u); + BOOST_TEST(brs.read_api_used()); + BOOST_TEST_EQ(std::string_view(buf, n), "hi"); + }); + BOOST_TEST(r.success); + } + + void + testNativeReadSomeChunked() + { + // Verify chunked native read_some + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + buffer_read_source brs(f, 3); + brs.provide("hello world"); + + any_buffer_source abs(&brs); + + std::string result; + for(;;) + { + char buf[64]; + auto [ec, n] = co_await abs.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec == capy::cond::eof) + break; + if(ec) + co_return; + result.append(buf, n); + } + + BOOST_TEST(brs.read_api_used()); + BOOST_TEST_EQ(result, "hello world"); + }); + BOOST_TEST(r.success); + } + + //------------------------------------------------------ + // push_to tests + + void + testPushTo() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPushToTypeErased() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + + any_buffer_source abs(&bs); + + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(abs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPushToChunked() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f, 5); // max 5 bytes per pull + bs.provide("hello world"); + + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPushToEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + // No data provided + + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + run() + { + testConstruct(); + testMove(); + testMoveNative(); + testMoveAssignOwning(); + testConstructThrows(); + testSuspends(); + testPull(); + testConsume(); + testPullWithoutConsume(); + testPullMultiple(); + testPullEmpty(); + testSynthesizedReadSome(); + testSynthesizedRead(); + testSynthesizedReadSomeEmpty(); + testNativeReadSome(); + testNativeRead(); + testNativeReadSomeEmpty(); + testNativePullAndConsume(); + testNativeOwning(); + testNativeReadEof(); + testNativeReadSomeChunked(); + testPushTo(); + testPushToTypeErased(); + testPushToChunked(); + testPushToEmpty(); + } +}; + +TEST_SUITE(any_buffer_source_test, "boost.capy.io.any_buffer_source"); + +} // namespace +} // namespace http +} // namespace boost diff --git a/test/unit/io/any_read_source.cpp b/test/unit/io/any_read_source.cpp new file mode 100644 index 00000000..0e3c412b --- /dev/null +++ b/test/unit/io/any_read_source.cpp @@ -0,0 +1,849 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include + +#include +#include +#include + +namespace boost { +namespace http { + +static_assert(ReadSource); + +namespace { +using namespace capy; +using namespace capy::test; + +struct pending_source_awaitable +{ + int* counter_; + pending_source_awaitable(int* c) : counter_(c) {} + pending_source_awaitable(pending_source_awaitable&& o) noexcept + : counter_(std::exchange(o.counter_, nullptr)) {} + ~pending_source_awaitable() { if(counter_) ++(*counter_); } + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> await_suspend(std::coroutine_handle<>, io_env const*) + { return std::noop_coroutine(); } + io_result await_resume() + { return {{}, 0}; } +}; + +struct pending_read_source +{ + int* counter_; + pending_source_awaitable read_some( + MutableBufferSequence auto) + { return pending_source_awaitable{counter_}; } + pending_source_awaitable read( + MutableBufferSequence auto) + { return pending_source_awaitable{counter_}; } +}; + +// Reports not-ready, then resumes from await_suspend, exercising the +// type-erased await_suspend forwarding the always-ready mocks skip. +struct resuming_source_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 5}; } +}; + +struct resuming_read_source +{ + resuming_source_awaitable read_some( + MutableBufferSequence auto) + { return {}; } + resuming_source_awaitable read( + MutableBufferSequence auto) + { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the source is constructed. +struct throwing_move_read_source +{ + int* destroyed_; + explicit throwing_move_read_source(int* d) : destroyed_(d) {} + throwing_move_read_source(throwing_move_read_source&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_read_source() { if(destroyed_) ++(*destroyed_); } + resuming_source_awaitable read_some( + MutableBufferSequence auto) + { return {}; } + resuming_source_awaitable read( + MutableBufferSequence auto) + { return {}; } +}; + +class any_read_source_test +{ +public: + void + testConstruct() + { + // Default construct + { + any_read_source ars; + BOOST_TEST(!ars.has_value()); + BOOST_TEST(!ars); + } + + // Construct from source + { + capy::test::fuse f; + test::read_source rs(f); + any_read_source ars(&rs); + BOOST_TEST(ars.has_value()); + BOOST_TEST(static_cast(ars)); + } + } + + void + testConstructOwning() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("owned"); + + any_read_source ars(std::move(rs)); + BOOST_TEST(ars.has_value()); + BOOST_TEST(static_cast(ars)); + + char buf[5] = {}; + auto [ec, n] = co_await ars.read_some(make_buffer(buf)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(std::string_view(buf, n), "owned"); + }); + BOOST_TEST(r.success); + } + + void + testMove() + { + capy::test::fuse f; + test::read_source rs(f); + + any_read_source ars1(&rs); + BOOST_TEST(ars1.has_value()); + + // Move construct + any_read_source ars2(std::move(ars1)); + BOOST_TEST(ars2.has_value()); + BOOST_TEST(!ars1.has_value()); + + // Move assign to empty + any_read_source ars3; + ars3 = std::move(ars2); + BOOST_TEST(ars3.has_value()); + BOOST_TEST(!ars2.has_value()); + } + + void + testMoveAssignNonEmpty() + { + capy::test::fuse f; + test::read_source rs1(f); + test::read_source rs2(f); + + any_read_source ars1(&rs1); + any_read_source ars2(&rs2); + BOOST_TEST(ars1.has_value()); + BOOST_TEST(ars2.has_value()); + + // Move assign over non-empty target + ars1 = std::move(ars2); + BOOST_TEST(ars1.has_value()); + BOOST_TEST(!ars2.has_value()); + } + + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the storage_ + // teardown branch in operator=. + capy::test::fuse f1; + capy::test::fuse f2; + any_read_source a(test::read_source{f1}); + any_read_source b(test::read_source{f2}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + + void + testConstructThrows() + { + // Owning construction whose source move-ctor throws must not + // run the source destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_read_source(throwing_move_read_source{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testReadSuspends() + { + // Drive a read whose awaitable suspends and then resumes, + // covering the type-erased await_suspend forwarding. + resuming_read_source rs; + any_read_source ars(&rs); + + auto coro = [&]() -> capy::task { + char buf[1]; + auto [ec, n] = co_await ars.read(make_buffer(buf, 1)); + if(ec) + co_return 0; + co_return n; + }; + + std::size_t result{}; + capy::test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 5u); + } + + void + testSelfAssign() + { + capy::test::fuse f; + test::read_source rs(f); + + any_read_source ars(&rs); + BOOST_TEST(ars.has_value()); + + // Indirect self-assignment should be a no-op + auto& ref = ars; + ars = std::move(ref); + BOOST_TEST(ars.has_value()); + } + + void + testReadSome() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hello world"); + + any_read_source ars(&rs); + + char buf[32] = {}; + auto [ec, n] = co_await ars.read_some(make_buffer(buf)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testReadSomePartial() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hello world"); + + any_read_source ars(&rs); + + char buf[5] = {}; + auto [ec, n] = co_await ars.read_some(make_buffer(buf)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello"); + BOOST_TEST_EQ(rs.available(), 6u); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeMultiple() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("abcdefghij"); + + any_read_source ars(&rs); + + char buf[3] = {}; + + auto [ec1, n1] = co_await ars.read_some(make_buffer(buf)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 3u); + BOOST_TEST_EQ(std::string_view(buf, n1), "abc"); + + auto [ec2, n2] = co_await ars.read_some(make_buffer(buf)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 3u); + BOOST_TEST_EQ(std::string_view(buf, n2), "def"); + + auto [ec3, n3] = co_await ars.read_some(make_buffer(buf)); + if(ec3) + co_return; + BOOST_TEST_EQ(n3, 3u); + BOOST_TEST_EQ(std::string_view(buf, n3), "ghi"); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + + any_read_source ars(&rs); + + char buf[32] = {}; + auto [ec, n] = co_await ars.read_some(make_buffer(buf)); + if(ec && ec != cond::eof) + co_return; + + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, 0u); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeBufferSequence() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("helloworld"); + + any_read_source ars(&rs); + + char buf1[5] = {}; + char buf2[5] = {}; + std::array buffers = {{ + make_buffer(buf1), + make_buffer(buf2) + }}; + + auto [ec, n] = co_await ars.read_some(buffers); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(std::string_view(buf1, 5), "hello"); + BOOST_TEST_EQ(std::string_view(buf2, 5), "world"); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeEmptyBuffer() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("data"); + + any_read_source ars(&rs); + + auto [ec, n] = co_await ars.read_some(mutable_buffer()); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST_EQ(rs.available(), 4u); + }); + BOOST_TEST(r.success); + } + + void + testRead() + { + // Buffer exactly matches available data + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hello world"); + + any_read_source ars(&rs); + + char buf[11] = {}; + auto [ec, n] = co_await ars.read(make_buffer(buf, 11)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testReadPartial() + { + // Buffer smaller than available data - fills completely + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hello world"); + + any_read_source ars(&rs); + + char buf[5] = {}; + auto [ec, n] = co_await ars.read(make_buffer(buf)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello"); + BOOST_TEST_EQ(rs.available(), 6u); + }); + BOOST_TEST(r.success); + } + + void + testReadMultiple() + { + // Multiple reads that exactly consume available data + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("abcdefghi"); + + any_read_source ars(&rs); + + char buf[3] = {}; + + auto [ec1, n1] = co_await ars.read(make_buffer(buf)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 3u); + BOOST_TEST_EQ(std::string_view(buf, n1), "abc"); + + auto [ec2, n2] = co_await ars.read(make_buffer(buf)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 3u); + BOOST_TEST_EQ(std::string_view(buf, n2), "def"); + + auto [ec3, n3] = co_await ars.read(make_buffer(buf)); + if(ec3) + co_return; + BOOST_TEST_EQ(n3, 3u); + BOOST_TEST_EQ(std::string_view(buf, n3), "ghi"); + }); + BOOST_TEST(r.success); + } + + void + testReadInsufficientData() + { + // Buffer larger than available data - fails with EOF + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hi"); + + any_read_source ars(&rs); + + char buf[10] = {}; + auto [ec, n] = co_await ars.read(make_buffer(buf)); + // Should fail because buffer can't be filled + if(ec && ec != cond::eof) + co_return; // fuse-injected error + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, 2u); // 2 bytes read before EOF + }); + BOOST_TEST(r.success); + } + + void + testReadEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + // No data provided - should get EOF + + any_read_source ars(&rs); + + char buf[32] = {}; + auto [ec, n] = co_await ars.read(make_buffer(buf)); + if(ec && ec != cond::eof) + co_return; + + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, 0u); + }); + BOOST_TEST(r.success); + } + + void + testReadEofAfterData() + { + // Read exact amount, then get EOF on next read + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("x"); + + any_read_source ars(&rs); + + char buf[1] = {}; + + auto [ec1, n1] = co_await ars.read(make_buffer(buf)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 1u); + + auto [ec2, n2] = co_await ars.read(make_buffer(buf)); + // Should get EOF because no more data + if(ec2 && ec2 != cond::eof) + co_return; // fuse-injected error + BOOST_TEST(ec2 == cond::eof); + BOOST_TEST_EQ(n2, 0u); + }); + BOOST_TEST(r.success); + } + + void + testReadBufferSequence() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("helloworld"); + + any_read_source ars(&rs); + + char buf1[5] = {}; + char buf2[5] = {}; + std::array buffers = {{ + make_buffer(buf1), + make_buffer(buf2) + }}; + + auto [ec, n] = co_await ars.read(buffers); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(std::string_view(buf1, 5), "hello"); + BOOST_TEST_EQ(std::string_view(buf2, 5), "world"); + }); + BOOST_TEST(r.success); + } + + void + testReadSingleBuffer() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hello world"); + + any_read_source ars(&rs); + + char buf[11] = {}; + auto [ec, n] = co_await ars.read(make_buffer(buf)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testReadArray() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("helloworld"); + + any_read_source ars(&rs); + + char buf1[5] = {}; + char buf2[5] = {}; + std::array buffers = {{ + make_buffer(buf1), + make_buffer(buf2) + }}; + + auto [ec, n] = co_await ars.read(buffers); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(std::string_view(buf1, 5), "hello"); + BOOST_TEST_EQ(std::string_view(buf2, 5), "world"); + }); + BOOST_TEST(r.success); + } + + void + testReadEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("data"); + + any_read_source ars(&rs); + + auto [ec, n] = co_await ars.read(mutable_buffer()); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST_EQ(rs.available(), 4u); + }); + BOOST_TEST(r.success); + } + + void + testReadWithMaxReadSize() + { + // Verify read forwards to underlying source's read which + // fills the buffer ignoring max_read_size + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f, 5); + rs.provide("hello world"); + + any_read_source ars(&rs); + + char buf[11] = {}; + auto [ec, n] = co_await ars.read(make_buffer(buf)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testReadWithMaxReadSizeMultiple() + { + // Verify multiple reads forward to underlying source's read + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f, 3); + rs.provide("abcdefghij"); + + any_read_source ars(&rs); + + char buf[5] = {}; + + auto [ec1, n1] = co_await ars.read(make_buffer(buf)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 5u); + BOOST_TEST_EQ(std::string_view(buf, n1), "abcde"); + + auto [ec2, n2] = co_await ars.read(make_buffer(buf)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 5u); + BOOST_TEST_EQ(std::string_view(buf, n2), "fghij"); + }); + BOOST_TEST(r.success); + } + + void + testReadManyBuffers() + { + // Buffer sequence exceeds max_iovec_ -- verifies the + // windowed loop fills every buffer in the sequence. + constexpr unsigned N = capy::detail::max_iovec_ + 4; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + // Build data: "abcd..." repeating, one byte per buffer + std::string data; + for(unsigned i = 0; i < N; ++i) + data.push_back(static_cast('a' + (i % 26))); + + test::read_source rs(f); + rs.provide(data); + + any_read_source ars(&rs); + + char storage[N] = {}; + std::array buffers; + for(unsigned i = 0; i < N; ++i) + buffers[i] = mutable_buffer(&storage[i], 1); + + auto [ec, n] = co_await ars.read(buffers); + if(ec) + co_return; + + BOOST_TEST_EQ(n, std::size_t(N)); + for(unsigned i = 0; i < N; ++i) + BOOST_TEST_EQ(storage[i], data[i]); + }); + BOOST_TEST(r.success); + } + + void + testReadManyBuffersEof() + { + // Buffer sequence exceeds max_iovec_ but data runs out + // mid-way through the second window. + constexpr unsigned N = capy::detail::max_iovec_ + 4; + constexpr unsigned avail = capy::detail::max_iovec_ + 2; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + std::string data; + for(unsigned i = 0; i < avail; ++i) + data.push_back(static_cast('a' + (i % 26))); + + test::read_source rs(f); + rs.provide(data); + + any_read_source ars(&rs); + + char storage[N] = {}; + std::array buffers; + for(unsigned i = 0; i < N; ++i) + buffers[i] = mutable_buffer(&storage[i], 1); + + auto [ec, n] = co_await ars.read(buffers); + if(ec && ec != cond::eof) + co_return; + + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, std::size_t(avail)); + for(unsigned i = 0; i < avail; ++i) + BOOST_TEST_EQ(storage[i], data[i]); + }); + BOOST_TEST(r.success); + } + + void + testDestroyWithActiveAwaitable() + { + // Split vtable: active_ops_ set in await_suspend. + int destroyed = 0; + pending_read_source ps{&destroyed}; + { + any_read_source ars(&ps); + char buf[1]; + auto aw = ars.read_some(mutable_buffer(buf, 1)); + BOOST_TEST(!aw.await_ready()); + + capy::test::blocking_context bctx; + auto ex = bctx.get_executor(); + io_env env{executor_ref(ex), {}}; + aw.await_suspend( + std::noop_coroutine(), &env); + } + BOOST_TEST_EQ(destroyed, 1); + } + + void + testMoveAssignWithActiveAwaitable() + { + int destroyed = 0; + pending_read_source ps{&destroyed}; + { + any_read_source ars(&ps); + char buf[1]; + auto aw = ars.read_some(mutable_buffer(buf, 1)); + BOOST_TEST(!aw.await_ready()); + + capy::test::blocking_context bctx; + auto ex = bctx.get_executor(); + io_env env{executor_ref(ex), {}}; + aw.await_suspend( + std::noop_coroutine(), &env); + + any_read_source empty; + ars = std::move(empty); + BOOST_TEST_EQ(destroyed, 1); + } + } + + void + run() + { + testConstruct(); + testConstructOwning(); + testMove(); + testMoveAssignNonEmpty(); + testMoveAssignOwning(); + testConstructThrows(); + testReadSuspends(); + testSelfAssign(); + testReadSome(); + testReadSomePartial(); + testReadSomeMultiple(); + testReadSomeEof(); + testReadSomeBufferSequence(); + testReadSomeEmptyBuffer(); + testRead(); + testReadPartial(); + testReadMultiple(); + testReadInsufficientData(); + testReadEof(); + testReadEofAfterData(); + testReadBufferSequence(); + testReadSingleBuffer(); + testReadArray(); + testReadEmpty(); + testReadWithMaxReadSize(); + testReadWithMaxReadSizeMultiple(); + testReadManyBuffers(); + testReadManyBuffersEof(); + testDestroyWithActiveAwaitable(); + testMoveAssignWithActiveAwaitable(); + } +}; + +TEST_SUITE(any_read_source_test, "boost.capy.io.any_read_source"); + +} // namespace +} // namespace http +} // namespace boost diff --git a/test/unit/io/any_write_sink.cpp b/test/unit/io/any_write_sink.cpp new file mode 100644 index 00000000..94533dab --- /dev/null +++ b/test/unit/io/any_write_sink.cpp @@ -0,0 +1,807 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include + +#include +#include +#include +#include + +namespace boost { +namespace http { + +static_assert(WriteSink); + +namespace { +using namespace capy; +using namespace capy::test; + +struct pending_sink_awaitable +{ + int* counter_; + pending_sink_awaitable(int* c) : counter_(c) {} + pending_sink_awaitable(pending_sink_awaitable&& o) noexcept + : counter_(std::exchange(o.counter_, nullptr)) {} + ~pending_sink_awaitable() { if(counter_) ++(*counter_); } + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> await_suspend(std::coroutine_handle<>, io_env const*) + { return std::noop_coroutine(); } + io_result await_resume() + { return {{}, 0}; } +}; + +struct pending_sink_eof_awaitable +{ + int* counter_; + pending_sink_eof_awaitable(int* c) : counter_(c) {} + pending_sink_eof_awaitable(pending_sink_eof_awaitable&& o) noexcept + : counter_(std::exchange(o.counter_, nullptr)) {} + ~pending_sink_eof_awaitable() { if(counter_) ++(*counter_); } + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> await_suspend(std::coroutine_handle<>, io_env const*) + { return std::noop_coroutine(); } + io_result<> await_resume() + { return {}; } +}; + +struct pending_write_sink +{ + int* counter_; + pending_sink_awaitable write_some( + ConstBufferSequence auto) + { return pending_sink_awaitable{counter_}; } + pending_sink_awaitable write( + ConstBufferSequence auto) + { return pending_sink_awaitable{counter_}; } + pending_sink_awaitable write_eof( + ConstBufferSequence auto) + { return pending_sink_awaitable{counter_}; } + pending_sink_eof_awaitable write_eof() + { return pending_sink_eof_awaitable{counter_}; } +}; + +// Suspends, then resumes from await_suspend, to exercise the +// type-erased await_suspend forwarding the always-ready mocks skip. +struct resuming_sink_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result await_resume() { return {{}, 1}; } +}; + +struct resuming_sink_eof_awaitable +{ + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> h, io_env const*) noexcept + { return h; } + io_result<> await_resume() { return {}; } +}; + +struct resuming_write_sink +{ + resuming_sink_awaitable write_some(ConstBufferSequence auto) + { return {}; } + resuming_sink_awaitable write(ConstBufferSequence auto) + { return {}; } + resuming_sink_awaitable write_eof(ConstBufferSequence auto) + { return {}; } + resuming_sink_eof_awaitable write_eof() { return {}; } +}; + +// Move constructor throws so owning construction fails after storage +// is allocated but before the sink is constructed. +struct throwing_move_write_sink +{ + int* destroyed_; + explicit throwing_move_write_sink(int* d) : destroyed_(d) {} + throwing_move_write_sink(throwing_move_write_sink&& o) : destroyed_(o.destroyed_) + { throw_test_exception_opaque("move ctor"); } + ~throwing_move_write_sink() { if(destroyed_) ++(*destroyed_); } + resuming_sink_awaitable write_some(ConstBufferSequence auto) + { return {}; } + resuming_sink_awaitable write(ConstBufferSequence auto) + { return {}; } + resuming_sink_awaitable write_eof(ConstBufferSequence auto) + { return {}; } + resuming_sink_eof_awaitable write_eof() { return {}; } +}; + +class any_write_sink_test +{ +public: + void + testConstruct() + { + // Default construct + { + any_write_sink aws; + BOOST_TEST(!aws.has_value()); + BOOST_TEST(!aws); + } + + // Construct from sink + { + capy::test::fuse f; + test::write_sink ws(f); + any_write_sink aws(&ws); + BOOST_TEST(aws.has_value()); + BOOST_TEST(static_cast(aws)); + } + } + + void + testMove() + { + capy::test::fuse f; + test::write_sink ws(f); + + any_write_sink aws1(&ws); + BOOST_TEST(aws1.has_value()); + + // Move construct + any_write_sink aws2(std::move(aws1)); + BOOST_TEST(aws2.has_value()); + BOOST_TEST(!aws1.has_value()); + + // Move assign + any_write_sink aws3; + aws3 = std::move(aws2); + BOOST_TEST(aws3.has_value()); + BOOST_TEST(!aws2.has_value()); + } + + void + testWriteSome() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write_some( + make_buffer("hello world", 11)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testWriteSomePartial() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f, 5); + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write_some( + make_buffer("hello world", 11)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(ws.data(), "hello"); + }); + BOOST_TEST(r.success); + } + + void + testWriteSomeMultiple() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec1, n1] = co_await aws.write_some( + make_buffer("hello", 5)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 5u); + + auto [ec2, n2] = co_await aws.write_some( + make_buffer(" ", 1)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 1u); + + auto [ec3, n3] = co_await aws.write_some( + make_buffer("world", 5)); + if(ec3) + co_return; + BOOST_TEST_EQ(n3, 5u); + + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testWriteSomeEmptyBuffer() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write_some(const_buffer()); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEmptyBuffer() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write(const_buffer()); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testWrite() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f, 5); // max 5 bytes per write + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write( + make_buffer("hello world", 11)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(!ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteMultiple() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec1, n1] = co_await aws.write( + make_buffer("hello", 5)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 5u); + + auto [ec2, n2] = co_await aws.write( + make_buffer(" ", 1)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 1u); + + auto [ec3, n3] = co_await aws.write( + make_buffer("world", 5)); + if(ec3) + co_return; + BOOST_TEST_EQ(n3, 5u); + + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testWriteBufferSequence() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + std::array buffers = {{ + make_buffer("hello", 5), + make_buffer("world", 5) + }}; + + auto [ec, n] = co_await aws.write(buffers); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(ws.data(), "helloworld"); + }); + BOOST_TEST(r.success); + } + + void + testWriteSingleBuffer() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write( + make_buffer("hello world", 11)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofWithBuffers() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write_eof( + make_buffer("hello", 5)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(ws.data(), "hello"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofWithEmptyBuffers() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write_eof(const_buffer()); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec] = co_await aws.write_eof(); + if(ec) + co_return; + + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteThenWriteEof() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + auto [ec1, n] = co_await aws.write( + make_buffer("hello", 5)); + if(ec1) + co_return; + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(!ws.eof_called()); + + auto [ec2] = co_await aws.write_eof(); + if(ec2) + co_return; + BOOST_TEST(ws.eof_called()); + BOOST_TEST_EQ(ws.data(), "hello"); + }); + BOOST_TEST(r.success); + } + + void + testWriteArray() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + + any_write_sink aws(&ws); + + std::array buffers = {{ + make_buffer("hello", 5), + make_buffer("world", 5) + }}; + + auto [ec, n] = co_await aws.write(buffers); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(ws.data(), "helloworld"); + }); + BOOST_TEST(r.success); + } + + void + testWritePartial() + { + // Verify that any_write_sink loops to consume all data + // even when underlying sink has max_write_size + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f, 5); // max 5 bytes per write + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write( + make_buffer("hello world", 11)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofWithBuffersPartial() + { + // Verify that any_write_sink loops to consume all data + // and signals eof even when underlying sink has max_write_size + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f, 5); // max 5 bytes per write + + any_write_sink aws(&ws); + + auto [ec, n] = co_await aws.write_eof( + make_buffer("hello world", 11)); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testConstructOwning() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + any_write_sink aws{std::move(ws)}; + BOOST_TEST(aws.has_value()); + + auto [ec, n] = co_await aws.write_some( + make_buffer("hello", 5)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 5u); + }); + BOOST_TEST(r.success); + } + + void + testWriteManyBuffers() + { + // Buffer sequence exceeds max_iovec_ -- verifies the + // windowed loop writes every buffer in the sequence. + constexpr unsigned N = capy::detail::max_iovec_ + 4; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + any_write_sink aws(&ws); + + std::string expected; + std::vector strings; + std::vector buffers; + for(unsigned i = 0; i < N; ++i) + { + strings.push_back(std::string(1, + static_cast('a' + (i % 26)))); + expected += strings.back(); + } + for(auto const& s : strings) + buffers.emplace_back(s.data(), s.size()); + + auto [ec, n] = co_await aws.write(buffers); + if(ec) + co_return; + + BOOST_TEST_EQ(n, std::size_t(N)); + BOOST_TEST_EQ(ws.data(), expected); + BOOST_TEST(!ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofManyBuffers() + { + // Buffer sequence exceeds max_iovec_ -- verifies the + // last window is sent atomically with EOF via write_eof(buffers). + constexpr unsigned N = capy::detail::max_iovec_ + 4; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::write_sink ws(f); + any_write_sink aws(&ws); + + std::string expected; + std::vector strings; + std::vector buffers; + for(unsigned i = 0; i < N; ++i) + { + strings.push_back(std::string(1, + static_cast('a' + (i % 26)))); + expected += strings.back(); + } + for(auto const& s : strings) + buffers.emplace_back(s.data(), s.size()); + + auto [ec, n] = co_await aws.write_eof(buffers); + if(ec) + co_return; + + BOOST_TEST_EQ(n, std::size_t(N)); + BOOST_TEST_EQ(ws.data(), expected); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testDestroyWithActiveWriteAwaitable() + { + // Split vtable: active_write_ops_ set in await_suspend. + int destroyed = 0; + pending_write_sink ps{&destroyed}; + { + any_write_sink aws(&ps); + char const data[] = "x"; + auto aw = aws.write_some(const_buffer(data, 1)); + BOOST_TEST(!aw.await_ready()); + + capy::test::blocking_context bctx; + auto ex = bctx.get_executor(); + io_env env{executor_ref(ex), {}}; + aw.await_suspend(std::noop_coroutine(), &env); + } + BOOST_TEST_EQ(destroyed, 1); + } + + void + testDestroyWithActiveEofAwaitable() + { + // Split vtable: active_eof_ops_ set in await_suspend. + int destroyed = 0; + pending_write_sink ps{&destroyed}; + { + any_write_sink aws(&ps); + auto aw = aws.write_eof(); + BOOST_TEST(!aw.await_ready()); + + capy::test::blocking_context bctx; + auto ex = bctx.get_executor(); + io_env env{executor_ref(ex), {}}; + aw.await_suspend( + std::noop_coroutine(), &env); + } + BOOST_TEST_EQ(destroyed, 1); + } + + void + testMoveAssignWithActiveAwaitable() + { + int destroyed = 0; + pending_write_sink ps{&destroyed}; + { + any_write_sink aws(&ps); + char const data[] = "x"; + auto aw = aws.write_some(const_buffer(data, 1)); + BOOST_TEST(!aw.await_ready()); + + capy::test::blocking_context bctx; + auto ex = bctx.get_executor(); + io_env env{executor_ref(ex), {}}; + aw.await_suspend( + std::noop_coroutine(), &env); + + any_write_sink empty; + aws = std::move(empty); + BOOST_TEST_EQ(destroyed, 1); + } + } + + void + testMoveAssignWithActiveEofAwaitable() + { + // Move-assign while an eof awaitable is active exercises the + // active_eof_ops_ destroy branch in operator=. + int destroyed = 0; + pending_write_sink ps{&destroyed}; + { + any_write_sink aws(&ps); + auto aw = aws.write_eof(); + BOOST_TEST(!aw.await_ready()); + + capy::test::blocking_context bctx; + auto ex = bctx.get_executor(); + io_env env{executor_ref(ex), {}}; + aw.await_suspend(std::noop_coroutine(), &env); + + any_write_sink empty; + aws = std::move(empty); + BOOST_TEST_EQ(destroyed, 1); + } + } + + void + testMoveAssignOwning() + { + // Move-assign over an owning wrapper to exercise the storage_ + // teardown branch in operator=. + capy::test::fuse f1; + capy::test::fuse f2; + any_write_sink a(test::write_sink{f1}); + any_write_sink b(test::write_sink{f2}); + BOOST_TEST(a.has_value()); + + a = std::move(b); + BOOST_TEST(a.has_value()); + BOOST_TEST(!b.has_value()); + } + + void + testConstructThrows() + { + // Owning construction whose sink move-ctor throws must not run + // the sink destructor on a null pointer. + int destroyed = 0; + BOOST_TEST_THROWS( + any_write_sink(throwing_move_write_sink{&destroyed}), + test_exception); + BOOST_TEST_EQ(destroyed, 1); + } + + void + testSuspends() + { + // Drive write/write_some/write_eof whose awaitables suspend + // then resume, covering the type-erased await_suspend paths. + resuming_write_sink sink; + any_write_sink aws(&sink); + + auto coro = [&]() -> capy::task { + char const data[] = "x"; + auto [ec1, n1] = co_await aws.write_some(const_buffer(data, 1)); + if(ec1) + co_return 0; + auto [ec2, n2] = co_await aws.write(const_buffer(data, 1)); + if(ec2) + co_return 0; + auto [ec3, n3] = co_await aws.write_eof(const_buffer(data, 1)); + if(ec3) + co_return 0; + auto [ec4] = co_await aws.write_eof(); + if(ec4) + co_return 0; + co_return n1 + n2 + n3; + }; + + std::size_t result{}; + capy::test::run_blocking([&](std::size_t v) { result = v; })(coro()); + BOOST_TEST_EQ(result, 3u); + } + + void + run() + { + testConstruct(); + testConstructOwning(); + testMove(); + testWriteSome(); + testWriteSomePartial(); + testWriteSomeMultiple(); + testWriteSomeEmptyBuffer(); + testWriteEmptyBuffer(); + testWrite(); + testWriteMultiple(); + testWriteBufferSequence(); + testWriteSingleBuffer(); + testWriteManyBuffers(); + testWriteEofWithBuffers(); + testWriteEofWithEmptyBuffers(); + testWriteEof(); + testWriteThenWriteEof(); + testWriteArray(); + testWritePartial(); + testWriteEofWithBuffersPartial(); + testWriteEofManyBuffers(); + testDestroyWithActiveWriteAwaitable(); + testDestroyWithActiveEofAwaitable(); + testMoveAssignWithActiveAwaitable(); + testMoveAssignWithActiveEofAwaitable(); + testMoveAssignOwning(); + testConstructThrows(); + testSuspends(); + } +}; + +TEST_SUITE(any_write_sink_test, "boost.capy.io.any_write_sink"); + +} // namespace +} // namespace http +} // namespace boost diff --git a/test/unit/io/pull_from.cpp b/test/unit/io/pull_from.cpp new file mode 100644 index 00000000..6f310bd8 --- /dev/null +++ b/test/unit/io/pull_from.cpp @@ -0,0 +1,438 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +#include +#include +#include + +#include "test_helpers.hpp" + +#include + +namespace boost { +namespace http { +namespace { +using namespace capy; +using namespace capy::test; + +class pull_from_test +{ +public: + //------------------------------------------------------------------- + // ReadSource → BufferSink tests + //------------------------------------------------------------------- + + void + testReadSourceToBufferSinkEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + // ReadSource returns capy::error::eof when empty, but pull_from handles it + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(bs.data().empty()); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadSourceToBufferSinkSingle() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hello world"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadSourceToBufferSinkMultiple() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hello"); + rs.provide(" "); + rs.provide("world"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadSourceToBufferSinkPartialRead() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f, 5); // max 5 bytes per read + rs.provide("hello world"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadSourceToBufferSinkLarge() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + std::string large_data(10000, 'x'); + rs.provide(large_data); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10000u); + BOOST_TEST_EQ(bs.size(), 10000u); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadSourceToBufferSinkSmallSinkBuffer() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("hello world"); + test::buffer_sink bs(f, 5); // small buffer + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadSourceToBufferSinkSourceError() + { + int error_count = 0; + int success_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("test data"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testReadSourceToBufferSinkSinkError() + { + int error_count = 0; + int success_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::read_source rs(f); + rs.provide("test data"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + //------------------------------------------------------------------- + // ReadStream → BufferSink tests + //------------------------------------------------------------------- + + void + testReadStreamToBufferSinkEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(bs.data().empty()); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadStreamToBufferSinkSingle() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f); + rs.provide("hello world"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadStreamToBufferSinkMultiple() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f); + rs.provide("hello"); + rs.provide(" "); + rs.provide("world"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadStreamToBufferSinkPartialRead() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f, 3); // max 3 bytes per read + rs.provide("hello world"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadStreamToBufferSinkLarge() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f); + std::string large_data(10000, 'y'); + rs.provide(large_data); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10000u); + BOOST_TEST_EQ(bs.size(), 10000u); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadStreamToBufferSinkSmallSinkBuffer() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f); + rs.provide("hello world"); + test::buffer_sink bs(f, 4); // small buffer + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadStreamToBufferSinkChunked() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f, 7); // max 7 bytes per read + rs.provide("hello world test data"); + test::buffer_sink bs(f, 5); // max 5 bytes per prepare + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 21u); + BOOST_TEST_EQ(bs.data(), "hello world test data"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testReadStreamToBufferSinkStreamError() + { + int error_count = 0; + int success_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f); + rs.provide("test data"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testReadStreamToBufferSinkSinkError() + { + int error_count = 0; + int success_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + capy::test::read_stream rs(f); + rs.provide("test data"); + test::buffer_sink bs(f); + + auto [ec, n] = co_await pull_from(rs, bs); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + run() + { + // ReadSource → BufferSink tests + testReadSourceToBufferSinkEmpty(); + testReadSourceToBufferSinkSingle(); + testReadSourceToBufferSinkMultiple(); + testReadSourceToBufferSinkPartialRead(); + testReadSourceToBufferSinkLarge(); + testReadSourceToBufferSinkSmallSinkBuffer(); + testReadSourceToBufferSinkSourceError(); + testReadSourceToBufferSinkSinkError(); + + // ReadStream → BufferSink tests + testReadStreamToBufferSinkEmpty(); + testReadStreamToBufferSinkSingle(); + testReadStreamToBufferSinkMultiple(); + testReadStreamToBufferSinkPartialRead(); + testReadStreamToBufferSinkLarge(); + testReadStreamToBufferSinkSmallSinkBuffer(); + testReadStreamToBufferSinkChunked(); + testReadStreamToBufferSinkStreamError(); + testReadStreamToBufferSinkSinkError(); + } +}; + +TEST_SUITE(pull_from_test, "boost.capy.io.pull_from"); + +} // namespace +} // capy +} // boost diff --git a/test/unit/io/push_to.cpp b/test/unit/io/push_to.cpp new file mode 100644 index 00000000..f62cd09a --- /dev/null +++ b/test/unit/io/push_to.cpp @@ -0,0 +1,389 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include +#include + +#include +#include +#include + +#include "test_helpers.hpp" + +#include + +namespace boost { +namespace http { +namespace { +using namespace capy; +using namespace capy::test; + +class push_to_test +{ +public: + //------------------------------------------------------------------- + // BufferSource → WriteSink tests + //------------------------------------------------------------------- + + void + testBufferSourceToWriteSinkEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteSinkSingle() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteSinkMultiple() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello"); + bs.provide(" "); + bs.provide("world"); + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteSinkChunked() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f, 5); // max 5 bytes per pull + bs.provide("hello world"); + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteSinkLarge() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + std::string large_data(10000, 'x'); + bs.provide(large_data); + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10000u); + BOOST_TEST_EQ(ws.size(), 10000u); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteSinkSourceError() + { + int error_count = 0; + int success_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("test data"); + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testBufferSourceToWriteSinkSinkError() + { + int error_count = 0; + int success_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("test data"); + test::write_sink ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + //------------------------------------------------------------------- + // BufferSource → WriteStream tests + //------------------------------------------------------------------- + + void + testBufferSourceToWriteStreamEmpty() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + capy::test::write_stream ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteStreamSingle() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + capy::test::write_stream ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteStreamMultiple() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello"); + bs.provide(" "); + bs.provide("world"); + capy::test::write_stream ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteStreamPartialWrite() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("hello world"); + capy::test::write_stream ws(f, 3); // max 3 bytes per write + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteStreamLarge() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + std::string large_data(10000, 'y'); + bs.provide(large_data); + capy::test::write_stream ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 10000u); + BOOST_TEST_EQ(ws.size(), 10000u); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteStreamChunked() + { + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f, 7); // max 7 bytes per pull + bs.provide("hello world test data"); + capy::test::write_stream ws(f, 5); // max 5 bytes per write + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + co_return; + + BOOST_TEST_EQ(n, 21u); + BOOST_TEST_EQ(ws.data(), "hello world test data"); + }); + BOOST_TEST(r.success); + } + + void + testBufferSourceToWriteStreamSourceError() + { + int error_count = 0; + int success_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("test data"); + capy::test::write_stream ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + testBufferSourceToWriteStreamStreamError() + { + int error_count = 0; + int success_count = 0; + + capy::test::fuse f; + auto r = f.armed([&](capy::test::fuse&) -> capy::task<> { + test::buffer_source bs(f); + bs.provide("test data"); + capy::test::write_stream ws(f); + + auto [ec, n] = co_await push_to(bs, ws); + if(ec) + { + ++error_count; + co_return; + } + ++success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(error_count > 0); + BOOST_TEST(success_count > 0); + } + + void + run() + { + // BufferSource → WriteSink tests + testBufferSourceToWriteSinkEmpty(); + testBufferSourceToWriteSinkSingle(); + testBufferSourceToWriteSinkMultiple(); + testBufferSourceToWriteSinkChunked(); + testBufferSourceToWriteSinkLarge(); + testBufferSourceToWriteSinkSourceError(); + testBufferSourceToWriteSinkSinkError(); + + // BufferSource → WriteStream tests + testBufferSourceToWriteStreamEmpty(); + testBufferSourceToWriteStreamSingle(); + testBufferSourceToWriteStreamMultiple(); + testBufferSourceToWriteStreamPartialWrite(); + testBufferSourceToWriteStreamLarge(); + testBufferSourceToWriteStreamChunked(); + testBufferSourceToWriteStreamSourceError(); + testBufferSourceToWriteStreamStreamError(); + } +}; + +TEST_SUITE(push_to_test, "boost.capy.io.push_to"); + +} // namespace +} // capy +} // boost diff --git a/test/unit/json/json_sink.cpp b/test/unit/json/json_sink.cpp index ec09cff1..409af475 100644 --- a/test/unit/json/json_sink.cpp +++ b/test/unit/json/json_sink.cpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include #include @@ -116,7 +116,7 @@ static_assert(capy::Executor); struct json_sink_test { // Verify json_sink satisfies WriteSink concept - static_assert(capy::WriteSink); + static_assert(http::WriteSink); //------------------------------------------------------ // Basic functionality tests diff --git a/test/unit/parser.cpp b/test/unit/parser.cpp index af7c3f74..70b8c244 100644 --- a/test/unit/parser.cpp +++ b/test/unit/parser.cpp @@ -18,7 +18,7 @@ #include #include #include -#include +#include #include #include "test_helpers.hpp" @@ -1972,7 +1972,7 @@ struct parser_coro_test if(hdr_ec) co_return; - capy::test::write_sink ws(f); + http::test::write_sink ws(f); auto [ec] = co_await pr.read(rs, ws); if(ec) co_return; @@ -2007,7 +2007,7 @@ struct parser_coro_test if(hdr_ec) co_return; - capy::test::write_sink ws(f); + http::test::write_sink ws(f); auto [ec] = co_await pr.read(rs, ws); if(ec) co_return; diff --git a/test/unit/serializer.cpp b/test/unit/serializer.cpp index 750ce4c5..76fce169 100644 --- a/test/unit/serializer.cpp +++ b/test/unit/serializer.cpp @@ -15,8 +15,8 @@ #include #include #include -#include -#include +#include +#include #include #include #include @@ -796,7 +796,7 @@ struct serializer_test //-------------------------------------------- // Verify serializer::sink satisfies BufferSink concept - static_assert(capy::BufferSink< + static_assert(http::BufferSink< serializer::sink>); void @@ -1227,7 +1227,7 @@ struct serializer_test sr.set_message(res); auto sink = sr.sink_for(ws); - capy::any_buffer_sink abs(sink); + http::any_buffer_sink abs(sink); std::string_view body = "Hello, World!"; auto [ec, n] = co_await abs.write( @@ -1262,7 +1262,7 @@ struct serializer_test sr.set_message(res); auto sink = sr.sink_for(ws); - capy::any_buffer_sink abs(sink); + http::any_buffer_sink abs(sink); std::string_view body = "hello"; auto [ec, n] = co_await abs.write_eof( diff --git a/test/unit/test/buffer_sink.cpp b/test/unit/test/buffer_sink.cpp new file mode 100644 index 00000000..04cc433a --- /dev/null +++ b/test/unit/test/buffer_sink.cpp @@ -0,0 +1,358 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include +#include +#include +#include + +namespace boost { +namespace http { +namespace test { +using namespace capy; +using namespace capy::test; + +static_assert(BufferSink); + +class buffer_sink_test +{ +public: + void + testConstruct() + { + fuse f; + auto r = f.armed([&](fuse&) { + buffer_sink bs(f); + BOOST_TEST_EQ(bs.size(), 0u); + BOOST_TEST(bs.data().empty()); + BOOST_TEST(! bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testPrepareCommit() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f); + + mutable_buffer arr[16]; + auto bufs = bs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + BOOST_TEST(bufs[0].size() > 0); + + std::memcpy(bufs[0].data(), "hello", 5); + + auto [ec] = co_await bs.commit(5); + if(ec) + co_return; + + BOOST_TEST_EQ(bs.data(), "hello"); + BOOST_TEST_EQ(bs.size(), 5u); + }); + BOOST_TEST(r.success); + } + + void + testPrepareEmpty() + { + fuse f; + auto r = f.armed([&](fuse&) { + buffer_sink bs(f); + + std::span empty_span; + auto bufs = bs.prepare(empty_span); + BOOST_TEST(bufs.empty()); + }); + BOOST_TEST(r.success); + } + + void + testMultipleCommits() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f); + + { + mutable_buffer arr[16]; + auto bufs = bs.prepare(arr); + std::memcpy(bufs[0].data(), "hello ", 6); + auto [ec] = co_await bs.commit(6); + if(ec) + co_return; + } + + { + mutable_buffer arr[16]; + auto bufs = bs.prepare(arr); + std::memcpy(bufs[0].data(), "world", 5); + auto [ec] = co_await bs.commit(5); + if(ec) + co_return; + } + + BOOST_TEST_EQ(bs.data(), "hello world"); + BOOST_TEST_EQ(bs.size(), 11u); + }); + BOOST_TEST(r.success); + } + + void + testCommitWithEof() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f); + + mutable_buffer arr[16]; + auto bufs = bs.prepare(arr); + std::memcpy(bufs[0].data(), "data", 4); + + auto [ec] = co_await bs.commit_eof(4); + if(ec) + co_return; + + BOOST_TEST_EQ(bs.data(), "data"); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testCommitEof() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f); + + auto [ec] = co_await bs.commit_eof(0); + if(ec) + co_return; + + BOOST_TEST(bs.data().empty()); + BOOST_TEST(bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testCommitThenEof() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f); + + mutable_buffer arr[16]; + auto bufs = bs.prepare(arr); + std::memcpy(bufs[0].data(), "hello", 5); + + auto [ec1] = co_await bs.commit(5); + if(ec1) + co_return; + BOOST_TEST(! bs.eof_called()); + + auto [ec2] = co_await bs.commit_eof(0); + if(ec2) + co_return; + BOOST_TEST(bs.eof_called()); + BOOST_TEST_EQ(bs.data(), "hello"); + }); + BOOST_TEST(r.success); + } + + void + testMaxPrepareSize() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f, 8); + + mutable_buffer arr[16]; + auto bufs = bs.prepare(arr); + BOOST_TEST_EQ(bufs.size(), 1u); + BOOST_TEST_EQ(bufs[0].size(), 8u); + + std::memcpy(bufs[0].data(), "12345678", 8); + + auto [ec] = co_await bs.commit(8); + if(ec) + co_return; + + BOOST_TEST_EQ(bs.data(), "12345678"); + }); + BOOST_TEST(r.success); + } + + void + testClear() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f); + + mutable_buffer arr[16]; + auto bufs = bs.prepare(arr); + std::memcpy(bufs[0].data(), "hello", 5); + + auto [ec1] = co_await bs.commit(5); + if(ec1) + co_return; + + auto [ec2] = co_await bs.commit_eof(0); + if(ec2) + co_return; + + BOOST_TEST_EQ(bs.data(), "hello"); + BOOST_TEST(bs.eof_called()); + + bs.clear(); + + BOOST_TEST(bs.data().empty()); + BOOST_TEST_EQ(bs.size(), 0u); + BOOST_TEST(! bs.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testFuseErrorInjectionCommit() + { + int commit_success_count = 0; + int commit_error_count = 0; + + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f); + + mutable_buffer arr[16]; + auto bufs = bs.prepare(arr); + std::memcpy(bufs[0].data(), "data", 4); + + auto [ec] = co_await bs.commit(4); + if(ec) + { + ++commit_error_count; + co_return; + } + ++commit_success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(commit_error_count > 0); + BOOST_TEST(commit_success_count > 0); + } + + void + testFuseErrorInjectionCommitEof() + { + int eof_success_count = 0; + int eof_error_count = 0; + + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_sink bs(f); + + auto [ec] = co_await bs.commit_eof(0); + if(ec) + { + ++eof_error_count; + co_return; + } + ++eof_success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(eof_error_count > 0); + BOOST_TEST(eof_success_count > 0); + } + + void + testCommitCanceled() + { + // commit awaited with an already-requested stop token returns + // capy::error::canceled and commits nothing. + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + buffer_sink bs; + mutable_buffer arr[4]; + auto bufs = bs.prepare(arr); + std::memcpy(bufs[0].data(), "hello", 5); + + auto [ec] = co_await bs.commit(5); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST_EQ(bs.size(), 0u); + }()); + BOOST_TEST(ran); + } + + void + testCommitEofCanceled() + { + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + buffer_sink bs; + mutable_buffer arr[4]; + auto bufs = bs.prepare(arr); + std::memcpy(bufs[0].data(), "hello", 5); + + auto [ec] = co_await bs.commit_eof(5); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST_EQ(bs.size(), 0u); + BOOST_TEST(! bs.eof_called()); + }()); + BOOST_TEST(ran); + } + + void + run() + { + testConstruct(); + testPrepareCommit(); + testPrepareEmpty(); + testMultipleCommits(); + testCommitWithEof(); + testCommitEof(); + testCommitThenEof(); + testMaxPrepareSize(); + testClear(); + testFuseErrorInjectionCommit(); + testFuseErrorInjectionCommitEof(); + testCommitCanceled(); + testCommitEofCanceled(); + } +}; + +TEST_SUITE(buffer_sink_test, "boost.capy.test.buffer_sink"); + +} // test +} // capy +} // boost diff --git a/test/unit/test/buffer_source.cpp b/test/unit/test/buffer_source.cpp new file mode 100644 index 00000000..a374d860 --- /dev/null +++ b/test/unit/test/buffer_source.cpp @@ -0,0 +1,348 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include +#include +#include + +namespace boost { +namespace http { +namespace test { +using namespace capy; +using namespace capy::test; + +static_assert(BufferSource); + +class buffer_source_test +{ +public: + void + testConstruct() + { + fuse f; + auto r = f.armed([&](fuse&) { + buffer_source bs(f); + BOOST_TEST_EQ(bs.available(), 0u); + }); + BOOST_TEST(r.success); + } + + void + testProvide() + { + fuse f; + auto r = f.armed([&](fuse&) { + buffer_source bs(f); + bs.provide("hello"); + BOOST_TEST_EQ(bs.available(), 5u); + + bs.provide(" world"); + BOOST_TEST_EQ(bs.available(), 11u); + }); + BOOST_TEST(r.success); + } + + void + testClear() + { + fuse f; + auto r = f.armed([&](fuse&) { + buffer_source bs(f); + bs.provide("data"); + BOOST_TEST_EQ(bs.available(), 4u); + + bs.clear(); + BOOST_TEST_EQ(bs.available(), 0u); + }); + BOOST_TEST(r.success); + } + + void + testPull() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f); + bs.provide("hello world"); + + const_buffer arr[16]; + auto [ec, bufs] = co_await bs.pull(arr); + if(ec) + co_return; + + BOOST_TEST_EQ(bufs.size(), 1u); + BOOST_TEST_EQ(bufs[0].size(), 11u); + BOOST_TEST_EQ( + buffer_to_string(bufs), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testConsume() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f); + bs.provide("hello world"); + + const_buffer arr[16]; + + auto [ec1, bufs1] = co_await bs.pull(arr); + if(ec1) + co_return; + BOOST_TEST_EQ(bufs1.size(), 1u); + BOOST_TEST_EQ(bufs1[0].size(), 11u); + + bs.consume(5); + BOOST_TEST_EQ(bs.available(), 6u); + + auto [ec2, bufs2] = co_await bs.pull(arr); + if(ec2) + co_return; + BOOST_TEST_EQ(bufs2.size(), 1u); + BOOST_TEST_EQ(bufs2[0].size(), 6u); + BOOST_TEST_EQ( + buffer_to_string(bufs2), " world"); + + bs.consume(6); + + auto [ec3, bufs3] = co_await bs.pull(arr); + if(ec3 != cond::eof) + co_return; + BOOST_TEST(bufs3.empty()); + }); + BOOST_TEST(r.success); + } + + void + testPullWithoutConsume() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f); + bs.provide("test"); + + const_buffer arr[16]; + + auto [ec1, bufs1] = co_await bs.pull(arr); + if(ec1) + co_return; + BOOST_TEST_EQ(bufs1.size(), 1u); + BOOST_TEST_EQ(bufs1[0].size(), 4u); + + auto [ec2, bufs2] = co_await bs.pull(arr); + if(ec2) + co_return; + BOOST_TEST_EQ(bufs2.size(), 1u); + BOOST_TEST_EQ(bufs2[0].size(), 4u); + + bs.consume(4); + }); + BOOST_TEST(r.success); + } + + void + testPullEmpty() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f); + + const_buffer arr[16]; + auto [ec, bufs] = co_await bs.pull(arr); + if(ec != cond::eof) + co_return; + BOOST_TEST(bufs.empty()); + }); + BOOST_TEST(r.success); + } + + void + testPullEmptyDest() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f); + bs.provide("data"); + + std::span empty_span; + auto [ec, bufs] = co_await bs.pull(empty_span); + if(ec) + co_return; + BOOST_TEST(bufs.empty()); + BOOST_TEST_EQ(bs.available(), 4u); + }); + BOOST_TEST(r.success); + } + + void + testMaxPullSize() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f, 5); + bs.provide("hello world"); + + const_buffer arr[16]; + auto [ec, bufs] = co_await bs.pull(arr); + if(ec) + co_return; + + BOOST_TEST_EQ(bufs.size(), 1u); + BOOST_TEST_EQ(bufs[0].size(), 5u); + BOOST_TEST_EQ( + buffer_to_string(bufs), "hello"); + }); + BOOST_TEST(r.success); + } + + void + testMaxPullSizeMultiple() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f, 5); + bs.provide("hello world"); + + std::size_t total = 0; + for(;;) + { + const_buffer arr[16]; + auto [ec, bufs] = co_await bs.pull(arr); + if(ec == cond::eof) + break; + if(ec) + co_return; + for(auto const& buf : bufs) + { + total += buf.size(); + bs.consume(buf.size()); + } + } + + BOOST_TEST_EQ(total, 11u); + }); + BOOST_TEST(r.success); + } + + void + testFuseErrorInjection() + { + int pull_success_count = 0; + int pull_error_count = 0; + + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f); + bs.provide("test data"); + + const_buffer arr[16]; + auto [ec, bufs] = co_await bs.pull(arr); + if(ec) + { + ++pull_error_count; + co_return; + } + ++pull_success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(pull_error_count > 0); + BOOST_TEST(pull_success_count > 0); + } + + void + testClearAndReuse() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + buffer_source bs(f); + bs.provide("first"); + + const_buffer arr[16]; + + auto [ec1, bufs1] = co_await bs.pull(arr); + if(ec1) + co_return; + BOOST_TEST_EQ( + buffer_to_string(bufs1), "first"); + + bs.consume(5); + bs.clear(); + bs.provide("second"); + + auto [ec2, bufs2] = co_await bs.pull(arr); + if(ec2) + co_return; + BOOST_TEST_EQ( + buffer_to_string(bufs2), "second"); + }); + BOOST_TEST(r.success); + } + + void + testPullCanceled() + { + // pull awaited with an already-requested stop token returns + // capy::error::canceled and an empty buffer span. + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + buffer_source bs; + bs.provide("hello world"); + + const_buffer arr[4]; + auto [ec, bufs] = co_await bs.pull(arr); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST(bufs.empty()); + }()); + BOOST_TEST(ran); + } + + void + run() + { + testConstruct(); + testProvide(); + testClear(); + testPull(); + testConsume(); + testPullWithoutConsume(); + testPullEmpty(); + testPullEmptyDest(); + testMaxPullSize(); + testMaxPullSizeMultiple(); + testFuseErrorInjection(); + testClearAndReuse(); + testPullCanceled(); + } +}; + +TEST_SUITE(buffer_source_test, "boost.capy.test.buffer_source"); + +} // test +} // capy +} // boost diff --git a/test/unit/test/read_source.cpp b/test/unit/test/read_source.cpp new file mode 100644 index 00000000..c3bac6da --- /dev/null +++ b/test/unit/test/read_source.cpp @@ -0,0 +1,584 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include +#include +#include + +namespace boost { +namespace http { +namespace test { +using namespace capy; +using namespace capy::test; + +static_assert(ReadSource); + +class read_source_test +{ +public: + void + testConstruct() + { + fuse f; + auto r = f.armed([&](fuse&) { + read_source rs(f); + BOOST_TEST(rs.available() == 0); + }); + BOOST_TEST(r.success); + } + + void + testProvide() + { + fuse f; + auto r = f.armed([&](fuse&) { + read_source rs(f); + rs.provide("hello"); + BOOST_TEST_EQ(rs.available(), 5u); + + rs.provide(" world"); + BOOST_TEST_EQ(rs.available(), 11u); + }); + BOOST_TEST(r.success); + } + + void + testClear() + { + fuse f; + auto r = f.armed([&](fuse&) { + read_source rs(f); + rs.provide("data"); + BOOST_TEST_EQ(rs.available(), 4u); + + rs.clear(); + BOOST_TEST_EQ(rs.available(), 0u); + }); + BOOST_TEST(r.success); + } + + void + testRead() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("hello world"); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read(make_buffer(buf)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testReadPartial() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("hello world"); + + char buf[5] = {}; + auto [ec, n] = co_await rs.read(make_buffer(buf)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello"); + BOOST_TEST_EQ(rs.available(), 6u); + }); + BOOST_TEST(r.success); + } + + void + testReadMultiple() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("abcdefghij"); + + char buf[3] = {}; + + auto [ec1, n1] = co_await rs.read(make_buffer(buf)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 3u); + BOOST_TEST_EQ(std::string_view(buf, n1), "abc"); + + auto [ec2, n2] = co_await rs.read(make_buffer(buf)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 3u); + BOOST_TEST_EQ(std::string_view(buf, n2), "def"); + + auto [ec3, n3] = co_await rs.read(make_buffer(buf)); + if(ec3) + co_return; + BOOST_TEST_EQ(n3, 3u); + BOOST_TEST_EQ(std::string_view(buf, n3), "ghi"); + + // Last byte: read returns EOF with partial transfer + auto [ec4, n4] = co_await rs.read(make_buffer(buf)); + if(ec4 && ec4 != cond::eof) + co_return; + BOOST_TEST(ec4 == cond::eof); + BOOST_TEST_EQ(n4, 1u); + BOOST_TEST_EQ(std::string_view(buf, n4), "j"); + }); + BOOST_TEST(r.success); + } + + void + testReadEof() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read(make_buffer(buf)); + if(ec && ec != cond::eof) + co_return; + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, 0u); + }); + BOOST_TEST(r.success); + } + + void + testReadEofAfterData() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("x"); + + char buf[32] = {}; + + auto [ec1, n1] = co_await rs.read(make_buffer(buf)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 1u); + + auto [ec2, n2] = co_await rs.read(make_buffer(buf)); + if(ec2 && ec2 != cond::eof) + co_return; + BOOST_TEST(ec2 == cond::eof); + BOOST_TEST_EQ(n2, 0u); + }); + BOOST_TEST(r.success); + } + + void + testReadBufferSequence() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("helloworld"); + + char buf1[5] = {}; + char buf2[5] = {}; + std::array buffers = {{ + make_buffer(buf1), + make_buffer(buf2) + }}; + + auto [ec, n] = co_await rs.read(buffers); + if(ec) + co_return; + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(std::string_view(buf1, 5), "hello"); + BOOST_TEST_EQ(std::string_view(buf2, 5), "world"); + }); + BOOST_TEST(r.success); + } + + void + testReadEmpty() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("data"); + + auto [ec, n] = co_await rs.read(mutable_buffer()); + if(ec) + co_return; + BOOST_TEST_EQ(n, 0u); + BOOST_TEST_EQ(rs.available(), 4u); + }); + BOOST_TEST(r.success); + } + + void + testFuseErrorInjection() + { + int read_success_count = 0; + int read_error_count = 0; + + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("test data"); + + char buf[9] = {}; + auto [ec, n] = co_await rs.read(make_buffer(buf)); + if(ec) + { + ++read_error_count; + co_return; + } + ++read_success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(read_error_count > 0); + BOOST_TEST(read_success_count > 0); + } + + void + testClearAndReuse() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("first"); + + char buf[32] = {}; + + auto [ec1, n1] = co_await rs.read(make_buffer(buf)); + if(ec1) + co_return; + BOOST_TEST_EQ(std::string_view(buf, n1), "first"); + + rs.clear(); + rs.provide("second"); + + auto [ec2, n2] = co_await rs.read(make_buffer(buf)); + if(ec2) + co_return; + BOOST_TEST_EQ(std::string_view(buf, n2), "second"); + }); + BOOST_TEST(r.success); + } + + void + testMaxReadSize() + { + // max_read_size only affects read_some, not read + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f, 5); + rs.provide("hello world"); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello"); + BOOST_TEST_EQ(rs.available(), 6u); + }); + BOOST_TEST(r.success); + } + + void + testMaxReadSizeMultiple() + { + // max_read_size only affects read_some, not read + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f, 3); + rs.provide("abcdefgh"); + + char buf[32] = {}; + + auto [ec1, n1] = co_await rs.read_some(make_buffer(buf)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 3u); + BOOST_TEST_EQ(std::string_view(buf, n1), "abc"); + + auto [ec2, n2] = co_await rs.read_some(make_buffer(buf)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 3u); + BOOST_TEST_EQ(std::string_view(buf, n2), "def"); + + auto [ec3, n3] = co_await rs.read_some(make_buffer(buf)); + if(ec3) + co_return; + BOOST_TEST_EQ(n3, 2u); + BOOST_TEST_EQ(std::string_view(buf, n3), "gh"); + }); + BOOST_TEST(r.success); + } + + //-------------------------------------------- + // + // read_some tests (ReadStream refinement) + // + //-------------------------------------------- + + void + testReadSome() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("hello world"); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testReadSomePartial() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("hello world"); + + char buf[5] = {}; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(std::string_view(buf, n), "hello"); + BOOST_TEST_EQ(rs.available(), 6u); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeEof() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec && ec != cond::eof) + co_return; + BOOST_TEST(ec == cond::eof); + BOOST_TEST_EQ(n, 0u); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeEmpty() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("data"); + + auto [ec, n] = co_await rs.read_some(mutable_buffer()); + if(ec) + co_return; + BOOST_TEST_EQ(n, 0u); + BOOST_TEST_EQ(rs.available(), 4u); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeEmptyExhausted() + { + // Empty buffers should succeed even when source is exhausted + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + + auto [ec, n] = co_await rs.read_some(mutable_buffer()); + if(ec) + co_return; + BOOST_TEST_EQ(n, 0u); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeMaxReadSize() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f, 3); + rs.provide("hello world"); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 3u); + BOOST_TEST_EQ(std::string_view(buf, n), "hel"); + BOOST_TEST_EQ(rs.available(), 8u); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeBufferSequence() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("helloworld"); + + char buf1[5] = {}; + char buf2[5] = {}; + std::array buffers = {{ + make_buffer(buf1), + make_buffer(buf2) + }}; + + auto [ec, n] = co_await rs.read_some(buffers); + if(ec) + co_return; + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(std::string_view(buf1, 5), "hello"); + BOOST_TEST_EQ(std::string_view(buf2, 5), "world"); + }); + BOOST_TEST(r.success); + } + + void + testReadSomeFuseErrorInjection() + { + int read_success_count = 0; + int read_error_count = 0; + + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + read_source rs(f); + rs.provide("test data"); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + { + ++read_error_count; + co_return; + } + ++read_success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(read_error_count > 0); + BOOST_TEST(read_success_count > 0); + } + + void + testReadCanceled() + { + // read awaited with an already-requested stop token returns + // capy::error::canceled instead of data. + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + read_source rs; + rs.provide("hello world"); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read(make_buffer(buf)); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST_EQ(n, 0u); + }()); + BOOST_TEST(ran); + } + + void + testReadSomeCanceled() + { + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + read_source rs; + rs.provide("hello world"); + + char buf[32] = {}; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST_EQ(n, 0u); + }()); + BOOST_TEST(ran); + } + + void + run() + { + testConstruct(); + testProvide(); + testClear(); + testRead(); + testReadPartial(); + testReadMultiple(); + testReadEof(); + testReadEofAfterData(); + testReadBufferSequence(); + testReadEmpty(); + testFuseErrorInjection(); + testClearAndReuse(); + testMaxReadSize(); + testMaxReadSizeMultiple(); + + // read_some tests (ReadStream refinement) + testReadSome(); + testReadSomePartial(); + testReadSomeEof(); + testReadSomeEmpty(); + testReadSomeEmptyExhausted(); + testReadSomeMaxReadSize(); + testReadSomeBufferSequence(); + testReadSomeFuseErrorInjection(); + + testReadCanceled(); + testReadSomeCanceled(); + } +}; + +TEST_SUITE(read_source_test, "boost.capy.test.read_source"); + +} // test +} // capy +} // boost diff --git a/test/unit/test/write_sink.cpp b/test/unit/test/write_sink.cpp new file mode 100644 index 00000000..5f06539f --- /dev/null +++ b/test/unit/test/write_sink.cpp @@ -0,0 +1,650 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// +// 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/http +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include +#include +#include + +namespace boost { +namespace http { +namespace test { +using namespace capy; +using namespace capy::test; + +static_assert(WriteSink); + +class write_sink_test +{ +public: + void + testConstruct() + { + fuse f; + auto r = f.armed([&](fuse&) { + write_sink ws(f); + BOOST_TEST(ws.size() == 0); + BOOST_TEST(ws.data().empty()); + BOOST_TEST(! ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWrite() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write( + make_buffer("hello world", 11)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(! ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteMultiple() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec1, n1] = co_await ws.write( + make_buffer("hello", 5)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 5u); + + auto [ec2, n2] = co_await ws.write( + make_buffer(" ", 1)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 1u); + + auto [ec3, n3] = co_await ws.write( + make_buffer("world", 5)); + if(ec3) + co_return; + BOOST_TEST_EQ(n3, 5u); + + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST_EQ(ws.size(), 11u); + }); + BOOST_TEST(r.success); + } + + void + testWriteBufferSequence() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + std::array buffers = {{ + make_buffer("hello", 5), + make_buffer("world", 5) + }}; + + auto [ec, n] = co_await ws.write(buffers); + if(ec) + co_return; + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(ws.data(), "helloworld"); + }); + BOOST_TEST(r.success); + } + + void + testWriteEmpty() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write(const_buffer()); + if(ec) + co_return; + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofWithBuffers() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write_eof( + make_buffer("hello", 5)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(ws.data(), "hello"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofWithEmptyBuffers() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write_eof(const_buffer()); + if(ec) + co_return; + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteEof() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec] = co_await ws.write_eof(); + if(ec) + co_return; + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + void + testWriteThenWriteEof() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec1, n] = co_await ws.write( + make_buffer("hello", 5)); + if(ec1) + co_return; + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(! ws.eof_called()); + + auto [ec2] = co_await ws.write_eof(); + if(ec2) + co_return; + BOOST_TEST(ws.eof_called()); + BOOST_TEST_EQ(ws.data(), "hello"); + }); + BOOST_TEST(r.success); + } + + void + testFuseErrorInjection() + { + int write_success_count = 0; + int write_error_count = 0; + + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write( + make_buffer("test data", 9)); + if(ec) + { + ++write_error_count; + co_return; + } + ++write_success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(write_error_count > 0); + BOOST_TEST(write_success_count > 0); + } + + void + testFuseErrorInjectionWriteEof() + { + int eof_success_count = 0; + int eof_error_count = 0; + + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec] = co_await ws.write_eof(); + if(ec) + { + ++eof_error_count; + co_return; + } + ++eof_success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(eof_error_count > 0); + BOOST_TEST(eof_success_count > 0); + } + + void + testExpect() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + auto ec = ws.expect("hello"); + BOOST_TEST(! ec); + + auto [ec2, n] = co_await ws.write( + make_buffer("hello", 5)); + if(ec2) + co_return; + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(ws.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testExpectMismatch() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + auto ec = ws.expect("hello"); + BOOST_TEST(! ec); + + auto [ec2, n] = co_await ws.write( + make_buffer("world", 5)); + if(! ec2) + co_return; + BOOST_TEST(ec2 == capy::error::test_failure); + }); + BOOST_TEST(r.success); + } + + void + testExpectWithExistingData() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write( + make_buffer("hello", 5)); + if(ec) + co_return; + BOOST_TEST_EQ(ws.data(), "hello"); + + auto ec2 = ws.expect("hello"); + BOOST_TEST(! ec2); + BOOST_TEST(ws.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testExpectMismatchWithExistingData() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write( + make_buffer("hello", 5)); + if(ec) + co_return; + + auto ec2 = ws.expect("world"); + BOOST_TEST(ec2 == capy::error::test_failure); + }); + BOOST_TEST(r.success); + } + + void + testClear() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec1, n1] = co_await ws.write( + make_buffer("hello", 5)); + if(ec1) + co_return; + + auto [ec2] = co_await ws.write_eof(); + if(ec2) + co_return; + + BOOST_TEST_EQ(ws.data(), "hello"); + BOOST_TEST(ws.eof_called()); + + ws.clear(); + + BOOST_TEST(ws.data().empty()); + BOOST_TEST(! ws.eof_called()); + + auto [ec3, n2] = co_await ws.write( + make_buffer("world", 5)); + if(ec3) + co_return; + BOOST_TEST_EQ(ws.data(), "world"); + }); + BOOST_TEST(r.success); + } + + void + testWritePartial() + { + // write() ignores max_write_size and writes all data + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f, 5); + + auto [ec, n] = co_await ws.write( + make_buffer("hello world", 11)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testWriteEofWithBuffersPartial() + { + // write_eof(buffers) ignores max_write_size and writes all data + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f, 5); + + auto [ec, n] = co_await ws.write_eof( + make_buffer("hello world", 11)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + BOOST_TEST(ws.eof_called()); + }); + BOOST_TEST(r.success); + } + + //-------------------------------------------- + // + // write_some tests (WriteStream refinement) + // + //-------------------------------------------- + + void + testWriteSome() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write_some( + make_buffer("hello world", 11)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 11u); + BOOST_TEST_EQ(ws.data(), "hello world"); + }); + BOOST_TEST(r.success); + } + + void + testWriteSomePartial() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f, 5); + + auto [ec, n] = co_await ws.write_some( + make_buffer("hello world", 11)); + if(ec) + co_return; + BOOST_TEST_EQ(n, 5u); + BOOST_TEST_EQ(ws.data(), "hello"); + }); + BOOST_TEST(r.success); + } + + void + testWriteSomeEmpty() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write_some(const_buffer()); + if(ec) + co_return; + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(ws.data().empty()); + }); + BOOST_TEST(r.success); + } + + void + testWriteSomeBufferSequence() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + std::array buffers = {{ + make_buffer("hello", 5), + make_buffer("world", 5) + }}; + + auto [ec, n] = co_await ws.write_some(buffers); + if(ec) + co_return; + BOOST_TEST_EQ(n, 10u); + BOOST_TEST_EQ(ws.data(), "helloworld"); + }); + BOOST_TEST(r.success); + } + + void + testWriteSomeMaxWriteSize() + { + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f, 3); + + auto [ec1, n1] = co_await ws.write_some( + make_buffer("abcdefgh", 8)); + if(ec1) + co_return; + BOOST_TEST_EQ(n1, 3u); + BOOST_TEST_EQ(ws.data(), "abc"); + + auto [ec2, n2] = co_await ws.write_some( + make_buffer("defgh", 5)); + if(ec2) + co_return; + BOOST_TEST_EQ(n2, 3u); + BOOST_TEST_EQ(ws.data(), "abcdef"); + }); + BOOST_TEST(r.success); + } + + void + testWriteSomeFuseErrorInjection() + { + int write_success_count = 0; + int write_error_count = 0; + + fuse f; + auto r = f.armed([&](fuse&) -> capy::task<> { + write_sink ws(f); + + auto [ec, n] = co_await ws.write_some( + make_buffer("test data", 9)); + if(ec) + { + ++write_error_count; + co_return; + } + ++write_success_count; + }); + + BOOST_TEST(r.success); + BOOST_TEST(write_error_count > 0); + BOOST_TEST(write_success_count > 0); + } + + void + testWriteCanceled() + { + // Each write op awaited with an already-requested stop token + // returns capy::error::canceled and has no effect. + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + write_sink ws; + auto [ec, n] = co_await ws.write( + const_buffer("hello", 5)); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST_EQ(n, 0u); + BOOST_TEST_EQ(ws.size(), 0u); + }()); + BOOST_TEST(ran); + } + + void + testWriteSomeCanceled() + { + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + write_sink ws; + auto [ec, n] = co_await ws.write_some( + const_buffer("hello", 5)); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST_EQ(n, 0u); + BOOST_TEST_EQ(ws.size(), 0u); + }()); + BOOST_TEST(ran); + } + + void + testWriteEofWithBuffersCanceled() + { + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + write_sink ws; + auto [ec, n] = co_await ws.write_eof( + const_buffer("hello", 5)); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST_EQ(n, 0u); + BOOST_TEST_EQ(ws.size(), 0u); + BOOST_TEST(! ws.eof_called()); + }()); + BOOST_TEST(ran); + } + + void + testWriteEofCanceled() + { + std::stop_source ss; + ss.request_stop(); + bool ran = false; + run_blocking(ss.get_token())( + [&]() -> capy::task<> + { + write_sink ws; + auto [ec] = co_await ws.write_eof(); + ran = true; + BOOST_TEST(ec == cond::canceled); + BOOST_TEST(! ws.eof_called()); + }()); + BOOST_TEST(ran); + } + + void + run() + { + testConstruct(); + testWrite(); + testWriteMultiple(); + testWriteBufferSequence(); + testWriteEmpty(); + testWriteEofWithBuffers(); + testWriteEofWithEmptyBuffers(); + testWriteEof(); + testWriteThenWriteEof(); + testFuseErrorInjection(); + testFuseErrorInjectionWriteEof(); + testExpect(); + testExpectMismatch(); + testExpectWithExistingData(); + testExpectMismatchWithExistingData(); + testClear(); + testWritePartial(); + testWriteEofWithBuffersPartial(); + + // write_some tests (WriteStream refinement) + testWriteSome(); + testWriteSomePartial(); + testWriteSomeEmpty(); + testWriteSomeBufferSequence(); + testWriteSomeMaxWriteSize(); + testWriteSomeFuseErrorInjection(); + + testWriteCanceled(); + testWriteSomeCanceled(); + testWriteEofWithBuffersCanceled(); + testWriteEofCanceled(); + } +}; + +TEST_SUITE(write_sink_test, "boost.capy.test.write_sink"); + +} // test +} // capy +} // boost diff --git a/test/unit/test_helpers.hpp b/test/unit/test_helpers.hpp index 0cb918db..d2ca5420 100644 --- a/test/unit/test_helpers.hpp +++ b/test/unit/test_helpers.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// 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) @@ -21,11 +22,31 @@ #include "test_suite.hpp" #include +#include #include namespace boost { namespace http { +// Thrown by tests that exercise exception paths. +struct test_exception : std::runtime_error +{ + explicit test_exception(char const* msg) + : std::runtime_error(msg) + { + } +}; + +// Throw test_exception in a way the optimizer cannot prove non-returning +// (so code after a placement-new is not flagged unreachable; see MSVC C4702). +inline void +throw_test_exception_opaque(char const* msg) +{ + volatile bool always = true; + if(always) + throw test_exception(msg); +} + inline std::string const& test_pattern()