diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 573ebb261..8c4350781 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -29,15 +29,10 @@ * xref:6.streams/6.intro.adoc[Stream Concepts] ** xref:6.streams/6a.overview.adoc[Overview] ** xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] -** xref:6.streams/6c.sources-sinks.adoc[Sources and Sinks (Complete I/O)] -** xref:6.streams/6d.buffer-concepts.adoc[Buffer Sources and Sinks] -** xref:6.streams/6e.algorithms.adoc[Transfer Algorithms] ** xref:6.streams/6f.isolation.adoc[Physical Isolation] * xref:7.testing/7.intro.adoc[Testing] ** xref:7.testing/7a.drivers.adoc[Driving Tests] ** xref:7.testing/7b.mock-streams.adoc[Mock Streams] -** xref:7.testing/7c.mock-sources-sinks.adoc[Mock Sources and Sinks] -** xref:7.testing/7d.mock-buffer-concepts.adoc[Mock Buffer Sources and Sinks] ** xref:7.testing/7e.buffer-inspection.adoc[Buffer Inspection] * xref:8.examples/8.intro.adoc[Example Programs] ** xref:8.examples/8a.hello-task.adoc[Hello Task] @@ -48,7 +43,6 @@ ** xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] ** xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] ** xref:8.examples/8i.echo-server-corosio.adoc[Echo Server with Corosio] -** xref:8.examples/8j.stream-pipeline.adoc[Stream Pipeline] ** xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] ** xref:8.examples/8l.async-mutex.adoc[Async Mutex] ** xref:8.examples/8m.parallel-tasks.adoc[Parallel Tasks] @@ -59,13 +53,8 @@ ** xref:9.design/9a.CapyLayering.adoc[Layered Abstractions] ** xref:9.design/9b.Separation.adoc[Why Capy Is Separate] ** xref:9.design/9c.ReadStream.adoc[ReadStream] -** xref:9.design/9d.ReadSource.adoc[ReadSource] -** xref:9.design/9e.BufferSource.adoc[BufferSource] ** xref:9.design/9f.WriteStream.adoc[WriteStream] -** xref:9.design/9g.WriteSink.adoc[WriteSink] -** xref:9.design/9h.BufferSink.adoc[BufferSink] ** xref:9.design/9i.TypeEraseAwaitable.adoc[Type-Erasing Awaitables] -** xref:9.design/9j.any_buffer_sink.adoc[AnyBufferSink] ** xref:9.design/9k.Executor.adoc[Executor] ** xref:9.design/9l.RunApi.adoc[Run API] ** xref:9.design/9m.WhyNotCobalt.adoc[Why Not Cobalt?] diff --git a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc index 53c50208d..ec03da646 100644 --- a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc +++ b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc @@ -6,9 +6,9 @@ This section introduces Capy's stream concepts—the abstractions that enable da * Understanding of buffer sequences -== Six Concepts for Data Flow +== Three Concepts for Data Flow -Capy defines six concepts for I/O operations, organized in three pairs: +Capy defines three concepts for I/O operations: [cols="1,1,2"] |=== @@ -22,26 +22,12 @@ Capy defines six concepts for I/O operations, organized in three pairs: | Write | Partial writes—writes as much as possible -| `ReadSource` -| Read -| Complete reads—fills buffer or signals EOF - -| `WriteSink` -| Write -| Complete writes with explicit EOF signaling - -| `BufferSource` -| Read -| Callee-owns-buffers read pattern - -| `BufferSink` -| Write -| Callee-owns-buffers write pattern +| `Stream` +| Read + Write +| A connected pair—satisfies both `ReadStream` and `WriteStream` |=== -== Streams vs Sources/Sinks - -=== Streams: Partial I/O +== Streams: Partial I/O Stream operations transfer *some* data and return. They do not guarantee a specific amount: @@ -58,47 +44,6 @@ auto [ec, n] = co_await stream.write_some(buffers); This matches raw OS behavior—syscalls return when data is available, not when buffers are full. -=== Sources/Sinks: Complete I/O - -Source/sink operations complete fully or signal completion: - -[source,cpp] ----- -// ReadSource: fills buffer completely, or returns EOF/error with partial -auto [ec, n] = co_await source.read(buffer); -// n == buffer_size(buffer), or ec indicates why not - -// WriteSink: writes all data, with explicit EOF -co_await sink.write_eof(buffers); // atomic write + EOF signal ----- - -These are higher-level abstractions built on streams. - -== Buffer Sources/Sinks: Callee-Owns-Buffers - -The third pair inverts buffer ownership: - -* With streams/sources/sinks, the caller provides buffers -* With buffer sources/sinks, the callee provides buffers - -[source,cpp] ----- -// BufferSource: callee provides read-only buffers -const_buffer arr[8]; -auto [ec, bufs] = co_await source.pull(arr); -// bufs is a span pointing into source's internal data; -// ec == cond::eof with an empty span signals exhaustion -source.consume(n); // mark n bytes consumed before the next pull - -// BufferSink: callee provides writable buffers -mutable_buffer arr[8]; -auto bufs = sink.prepare(arr); // returns a span of writable buffers -// Write into bufs, then commit -co_await sink.commit(bytes_written); ----- - -This pattern enables zero-copy I/O: data never moves through intermediate buffers. - == Type-Erasing Wrappers Each concept has a corresponding type-erasing wrapper: @@ -115,18 +60,6 @@ Each concept has a corresponding type-erasing wrapper: | (Both) | `any_stream` - -| `ReadSource` -| `any_read_source` - -| `WriteSink` -| `any_write_sink` - -| `BufferSource` -| `any_buffer_source` - -| `BufferSink` -| `any_buffer_sink` |=== These wrappers enable: @@ -143,18 +76,6 @@ These wrappers enable: * You're implementing a protocol that processes data incrementally * Performance is critical and you want minimal abstraction -=== Use Sources/Sinks When: - -* You need complete data units (messages, records, frames) -* EOF signaling is part of your protocol -* You're composing transformations (compression, encryption) - -=== Use Buffer Sources/Sinks When: - -* Zero-copy is essential -* The source/sink owns the memory (memory-mapped files, hardware buffers) -* You're implementing a processing pipeline - == The Value Proposition Type-erased wrappers let you write transport-agnostic code: diff --git a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc index 6bc5f6312..de55fefff 100644 --- a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc +++ b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc @@ -256,4 +256,4 @@ The implementation doesn't know the concrete stream type. It compiles once and w | Type-erased bidirectional stream wrapper |=== -You have now learned the stream concepts for partial I/O. Continue to xref:6.streams/6c.sources-sinks.adoc[Sources and Sinks] to learn about complete I/O with EOF signaling. +You have now learned the stream concepts for partial I/O. Continue to xref:6.streams/6f.isolation.adoc[Physical Isolation]. diff --git a/doc/modules/ROOT/pages/6.streams/6c.sources-sinks.adoc b/doc/modules/ROOT/pages/6.streams/6c.sources-sinks.adoc deleted file mode 100644 index 132e66f3e..000000000 --- a/doc/modules/ROOT/pages/6.streams/6c.sources-sinks.adoc +++ /dev/null @@ -1,249 +0,0 @@ -= Sources and Sinks (Complete I/O) - -This section explains the `ReadSource` and `WriteSink` concepts for complete I/O operations with EOF signaling. - -== Prerequisites - -* Completed xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] -* Understanding of partial I/O with `ReadStream` and `WriteStream` - -== ReadSource - -A `ReadSource` provides complete read operations that fill buffers entirely or signal EOF: - -[source,cpp] ----- -template -concept ReadSource = - ReadStream && - requires(T& source, mutable_buffer_archetype buffers) { - { source.read(buffers) } -> IoAwaitable; - }; ----- - -=== read Semantics - -[source,cpp] ----- -template -IoAwaitable auto read(Buffers buffers); ----- - -Await-returns `(error_code, std::size_t)`: - -* On success: `!ec`, and `n == buffer_size(buffers)` (buffer completely filled) -* On EOF: `ec == cond::eof`, and `n` is bytes read before EOF (partial read) -* On error: `ec`, and `n` is bytes read before error - -If `buffer_empty(buffers)` is true, the operation completes immediately with `!ec` and `n` equal to 0. - -The key difference from `ReadStream`: a successful read fills the buffer completely. - -=== Use Cases - -* Reading fixed-size records -* Reading message frames with known sizes -* Filling buffers for batch processing - -=== Example - -[source,cpp] ----- -template -task> read_message(Source& source) -{ - // Read fixed-size header - message_header header; - auto [ec, n] = co_await source.read( - mutable_buffer(&header, sizeof(header))); - - if (ec == cond::eof && n == 0) - co_return std::nullopt; // Clean EOF - - if (ec) - throw std::system_error(ec); - - // Read variable-size body - std::vector body(header.body_size); - auto [ec2, n2] = co_await source.read(make_buffer(body)); - - if (ec2) - throw std::system_error(ec2); - - co_return message{header, std::move(body)}; -} ----- - -== WriteSink - -A `WriteSink` provides complete write operations with explicit EOF signaling: - -[source,cpp] ----- -template -concept WriteSink = - WriteStream && - requires(T& sink, const_buffer_archetype buffers) { - { sink.write(buffers) } -> IoAwaitable; - { sink.write_eof(buffers) } -> IoAwaitable; - { sink.write_eof() } -> IoAwaitable; - }; ----- - -=== write Semantics - -[source,cpp] ----- -// Partial write (inherited from WriteStream) -template -IoAwaitable auto write_some(Buffers buffers); - -// Complete write -template -IoAwaitable auto write(Buffers buffers); - -// Atomic final write + EOF signal -template -IoAwaitable auto write_eof(Buffers buffers); - -// Signal EOF without data -IoAwaitable auto write_eof(); ----- - -`write` consumes the entire buffer sequence before returning. `write_eof(buffers)` atomically writes the buffer sequence and signals end-of-stream in a single operation, enabling protocol-level optimizations (e.g., HTTP chunked encoding terminal, compression trailers). - -After calling `write_eof(buffers)` or `write_eof()`, no further writes are permitted. - -=== Use Cases - -* Writing complete messages -* HTTP body transmission (content-length or chunked) -* Protocol framing with explicit termination - -=== Example - -[source,cpp] ----- -template -task<> send_response(Sink& sink, response const& resp) -{ - // Write headers - auto headers = format_headers(resp); - auto [ec, n] = co_await sink.write(make_buffer(headers)); - if (ec) - co_return; - - // Write body and signal EOF atomically - auto [ec2, n2] = co_await sink.write_eof(make_buffer(resp.body)); - if (ec2) - co_return; -} ----- - -== Type-Erasing Wrappers - -=== any_read_source - -[source,cpp] ----- -#include - -template -any_read_source(S source); // owning: takes ownership of a moved-in source - -template -any_read_source(S* source); // reference: wraps by pointer, source must outlive the wrapper ----- - -=== any_write_sink - -[source,cpp] ----- -#include - -template -any_write_sink(S sink); // owning: takes ownership of a moved-in sink - -template -any_write_sink(S* sink); // reference: wraps by pointer, sink must outlive the wrapper ----- - -== Example: HTTP Body Handler - -The HTTP library uses `any_write_sink` for body transmission: - -[source,cpp] ----- -// HTTP response handler doesn't know the underlying transport -task<> send_body(any_write_sink& body, std::string_view data) -{ - // Works whether body is: - // - Direct socket write (content-length) - // - Chunked encoding wrapper - // - Compressed stream - // - Test mock - - co_await body.write_eof(make_buffer(data)); -} ----- - -The caller decides the concrete implementation: - -[source,cpp] ----- -// Content-length mode -content_length_sink cl_sink(socket, data.size()); -any_write_sink body{&cl_sink}; // references cl_sink -send_body(body, data); - -// Chunked mode -chunked_sink ch_sink(socket); -any_write_sink body{&ch_sink}; // references ch_sink -send_body(body, data); ----- - -Same `send_body` function, different transfer encodings: the library handles the difference. - -== Streams vs Sources/Sinks - -[cols="1,1,1"] -|=== -| Aspect | Streams | Sources/Sinks - -| Transfer amount -| Partial (whatever is available) -| Complete (fill buffer or EOF) - -| EOF handling -| Implicit (read returns 0) -| Explicit (`write_eof()`, `write_eof(buffers)`) - -| Use case -| Raw I/O, incremental processing -| Message-oriented protocols - -| Abstraction level -| Lower (closer to OS) -| Higher (application-friendly) -|=== - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| ReadSource concept definition - -| `` -| WriteSink concept definition - -| `` -| Type-erased read source wrapper - -| `` -| Type-erased write sink wrapper -|=== - -You have now learned about sources and sinks for complete I/O. Continue to xref:6.streams/6d.buffer-concepts.adoc[Buffer Sources and Sinks] to learn about the callee-owns-buffers pattern. diff --git a/doc/modules/ROOT/pages/6.streams/6d.buffer-concepts.adoc b/doc/modules/ROOT/pages/6.streams/6d.buffer-concepts.adoc deleted file mode 100644 index f18bfc78c..000000000 --- a/doc/modules/ROOT/pages/6.streams/6d.buffer-concepts.adoc +++ /dev/null @@ -1,298 +0,0 @@ -= Buffer Sources and Sinks - -This section explains the `BufferSource` and `BufferSink` concepts for zero-copy I/O where the callee owns the buffers. - -== Prerequisites - -* Completed xref:6.streams/6c.sources-sinks.adoc[Sources and Sinks] -* Understanding of caller-owns-buffers patterns - -== Callee-Owns-Buffers Pattern - -With streams and sources/sinks, the *caller* provides buffers: - -[source,cpp] ----- -// Caller owns the buffer -char my_buffer[1024]; -co_await stream.read_some(make_buffer(my_buffer)); ----- - -Data flows: source → caller's buffer → processing - -With buffer sources/sinks, the *callee* provides buffers: - -[source,cpp] ----- -// Callee owns the buffers -const_buffer arr[8]; -auto [ec, bufs] = co_await source.pull(arr); -// bufs is a span pointing into source's internal storage ----- - -Data flows: source's internal buffer → processing (no copy!) - -== BufferSource - -A `BufferSource` provides read-only buffers from its internal storage: - -[source,cpp] ----- -template -concept BufferSource = - requires(T& src, std::span dest, std::size_t n) { - { src.pull(dest) } -> IoAwaitable; - // pull await-returns (error_code, std::span) - src.consume(n); - }; ----- - -=== pull Semantics - -[source,cpp] ----- -IoAwaitable auto pull(std::span dest); -void consume(std::size_t n); ----- - -`pull` await-returns `(error_code, std::span)`. The source fills `dest` with descriptors pointing into its internal storage and returns a span over the filled prefix: - -* On success: `!ec`, the returned span describes the available data -* On exhaustion: `ec == cond::eof` with an empty span -* On error: `ec` set to the error condition - -The returned buffers point into the source's internal storage and remain valid until you call `consume`. Calling `pull` again *without* an intervening `consume` returns the same unconsumed data. Call `consume(n)` to mark `n` bytes as consumed and advance the source. - -=== Example - -[source,cpp] ----- -template -task<> process_source(Source& source) -{ - const_buffer arr[8]; - - for (;;) - { - auto [ec, bufs] = co_await source.pull(arr); - - if (ec == cond::eof) - break; // Source exhausted - - if (ec) - throw std::system_error(ec); - - // Process buffers (zero-copy!) - std::size_t total = 0; - for (auto const& b : bufs) - { - process_data(b.data(), b.size()); - total += b.size(); - } - - source.consume(total); - } -} ----- - -== BufferSink - -A `BufferSink` provides writable buffers for direct write access: - -[source,cpp] ----- -template -concept BufferSink = - requires(T& sink, std::span dest, std::size_t n) { - { sink.prepare(dest) } -> std::same_as>; - { sink.commit(n) } -> IoAwaitable; // await-returns (error_code) - { sink.commit_eof(n) } -> IoAwaitable; // await-returns (error_code) - }; ----- - -=== prepare Semantics - -[source,cpp] ----- -std::span prepare(std::span dest); ----- - -Synchronous operation. The sink fills `dest` with writable buffer descriptors from its internal storage and returns a span over the filled prefix (which may be shorter than `dest`). - -=== commit Semantics - -[source,cpp] ----- -IoAwaitable auto commit(std::size_t n); -IoAwaitable auto commit_eof(std::size_t n); ----- - -`commit(n)` finalizes `n` bytes of prepared data and await-returns `(error_code)`. `commit_eof(n)` finalizes `n` final bytes and signals end-of-stream; pass `0` to signal end-of-stream with no further data. - -=== Example - -[source,cpp] ----- -template -task<> write_to_sink(Sink& sink, std::span data) -{ - std::size_t written = 0; - - while (written < data.size()) - { - mutable_buffer arr[8]; - auto bufs = sink.prepare(arr); - - if (bufs.empty()) - throw std::runtime_error("sink full"); - - // Copy into sink's buffers - std::size_t copied = 0; - for (auto& b : bufs) - { - if (written >= data.size()) - break; - std::size_t chunk = (std::min)( - b.size(), - data.size() - written); - std::memcpy(b.data(), data.data() + written, chunk); - written += chunk; - copied += chunk; - } - - if (written == data.size()) - co_await sink.commit_eof(copied); - else - co_await sink.commit(copied); - } -} ----- - -== Zero-Copy Benefits - -Buffer sources/sinks enable true zero-copy I/O: - -=== Memory-Mapped Files - -A `BufferSource` is a concept, not a base class, so a type models it -simply by providing the required members: - -[source,cpp] ----- -class mmap_source // models BufferSource -{ - void* mapped_region_; - std::size_t size_; - std::size_t offset_ = 0; - -public: - io_task> pull(std::span dest) - { - if (offset_ >= size_) - co_return {error::eof, {}}; // Exhausted - - // Return pointer into mapped memory: no copy! - dest[0] = const_buffer( - static_cast(mapped_region_) + offset_, - size_ - offset_); - - co_return {{}, dest.subspan(0, 1)}; - } - - void consume(std::size_t n) - { - offset_ += n; - } -}; ----- - -=== Hardware Buffers - -DMA buffers, GPU memory, network card ring buffers—all can be exposed through `BufferSource`/`BufferSink` without intermediate copying. - -== Type-Erasing Wrappers - -=== any_buffer_source - -[source,cpp] ----- -#include - -// Owning: takes ownership of the source -template -any_buffer_source(S source); - -// Reference: wraps without ownership, source must outlive the wrapper -template -any_buffer_source(S* source); ----- - -=== any_buffer_sink - -[source,cpp] ----- -#include - -// Owning: takes ownership of the sink -template -any_buffer_sink(S sink); - -// Reference: wraps without ownership, sink must outlive the wrapper -template -any_buffer_sink(S* sink); ----- - -== Example: Compression Pipeline - -[source,cpp] ----- -// Compressor provides compressed data via BufferSource -// Decompressor consumes compressed data via BufferSink - -task<> decompress_stream(any_buffer_source& compressed, any_write_sink& output) -{ - const_buffer arr[8]; - - for (;;) - { - auto [ec, bufs] = co_await compressed.pull(arr); - if (ec == cond::eof) - break; - if (ec) - throw std::system_error(ec); - - std::size_t total = 0; - for (auto const& b : bufs) - { - auto decompressed = decompress_block(b); - co_await output.write(make_buffer(decompressed)); - total += b.size(); - } - - compressed.consume(total); - } - - co_await output.write_eof(); -} ----- - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| BufferSource concept definition - -| `` -| BufferSink concept definition - -| `` -| Type-erased buffer source wrapper - -| `` -| Type-erased buffer sink wrapper -|=== - -You have now learned about buffer sources and sinks for zero-copy I/O. Continue to xref:6.streams/6e.algorithms.adoc[Transfer Algorithms] to learn about composed read/write operations. diff --git a/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc b/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc deleted file mode 100644 index 96d3dbb2e..000000000 --- a/doc/modules/ROOT/pages/6.streams/6e.algorithms.adoc +++ /dev/null @@ -1,329 +0,0 @@ -= Transfer Algorithms - -This section explains the composed read/write operations and transfer algorithms. - -== Prerequisites - -* Completed xref:6.streams/6d.buffer-concepts.adoc[Buffer Sources and Sinks] -* Understanding of all six stream concepts - -== Composed Read/Write - -The partial operations (`read_some`, `write_some`) often require looping. Capy provides composed operations that handle the loops for you. - -=== read - -Fills a buffer completely by looping `read_some`: - -[source,cpp] ----- -#include - -template -io_task -read(Stream& stream, Buffers buffers); ----- - -Await-returns `io_result`, destructuring as `[ec, n]`. Keeps reading until: - -* Buffer is full (`n == buffer_size(buffers)`) -* The underlying `read_some` reports a condition before the buffer is full (the condition is propagated with the partial count) - -Example: - -[source,cpp] ----- -char buf[1024]; -auto [ec, n] = co_await read(stream, make_buffer(buf)); -// n == 1024, or ec indicates why not ----- - -=== read_at_least - -A straightforward extension of `read`: instead of filling `buffers` -completely, it stops as soon as at least `n` bytes have been read: - -[source,cpp] ----- -#include - -template -io_task -read_at_least(Stream& stream, Buffers buffers, std::size_t n); ----- - -The required amount `n` must be met or exceeded; the remaining capacity of -`buffers` is optional, so a single `read_some` that delivers more than `n` -bytes keeps the extra without looping again. Keeps reading until: - -* At least `n` bytes have been read (`n \<= return value \<= buffer_size(buffers)`) -* The underlying `read_some` reports a condition before `n` bytes are read (the condition is propagated with the partial count) - -If `n` exceeds `buffer_size(buffers)` the request is impossible to satisfy and -the operation fails immediately with `std::errc::invalid_argument` and a count -of `0`, without reading. - -Example: - -[source,cpp] ----- -char buf[4096]; -// Require 16 bytes; opportunistically take whatever else is ready. -auto [ec, n] = co_await read_at_least(stream, make_buffer(buf), 16); -// n >= 16 on success, possibly up to 4096 ----- - -=== write - -Writes all data by looping `write_some`: - -[source,cpp] ----- -#include - -template -io_task -write(Stream& stream, Buffers buffers); ----- - -Keeps writing until: - -* All data is written (`n == buffer_size(buffers)`) -* Error occurs (returns error with partial count) - -Example: - -[source,cpp] ----- -co_await write(stream, make_buffer("Hello, World!")); ----- - -=== write_at_least - -The mirror of `read_at_least`, provided for symmetry: it stops as soon as at -least `n` bytes have been written, rather than draining `buffers` entirely: - -[source,cpp] ----- -#include - -template -io_task -write_at_least(Stream& stream, Buffers buffers, std::size_t n); ----- - -Keeps writing until: - -* At least `n` bytes have been written (`n \<= return value \<= buffer_size(buffers)`) -* The underlying `write_some` reports a condition before `n` bytes are written (the condition is propagated with the partial count) - -If `n` exceeds `buffer_size(buffers)` the operation fails immediately with -`std::errc::invalid_argument` and a count of `0`, without writing. - -=== write_now - -`write_now` eagerly writes a complete buffer sequence, attempting to finish -synchronously. If every underlying `write_some` completes without suspending, -the whole operation completes in `await_ready` with no coroutine suspension. -It caches a single coroutine frame and reuses it across calls, avoiding -repeated allocation on a hot write path: - -[source,cpp] ----- -#include - -template -class write_now; ----- - -Construct it from a stream, then call it like a function. Only one operation -may be outstanding at a time: - -[source,cpp] ----- -write_now wn(stream); -auto [ec, n] = co_await wn(make_buffer("hello")); -if (ec) - throw std::system_error(ec); ----- - -== Transfer Algorithms - -Transfer algorithms move data between sources/sinks and streams. - -=== push_to - -Transfers data from a `BufferSource` to a destination: - -[source,cpp] ----- -#include - -// To WriteSink (with EOF propagation) -template -io_task -push_to(Source& source, Sink& sink); - -// To WriteStream (streaming, no EOF) -template -io_task -push_to(Source& source, Stream& stream); ----- - -The source provides buffers via `pull()`. Data is pushed to the destination. Buffer ownership stays with the source: no intermediate copying when possible. - -Example: - -[source,cpp] ----- -// Transfer file to network -mmap_source file("large_file.bin"); -co_await push_to(file, socket); ----- - -=== pull_from - -Transfers data from a source to a `BufferSink`: - -[source,cpp] ----- -#include - -// From ReadSource -template -io_task -pull_from(Source& source, Sink& sink); - -// From ReadStream (streaming) -template -io_task -pull_from(Stream& stream, Sink& sink); ----- - -The sink provides writable buffers via `prepare()`. Data is pulled from the source directly into the sink's buffers. - -Example: - -[source,cpp] ----- -// Receive network data into compression buffer -compression_sink compressor; -co_await pull_from(socket, compressor); ----- - -=== Why No buffer-to-buffer? - -There is no `push_to(BufferSource, BufferSink)` because it would require redundant copying. The source owns read-only buffers; the sink owns writable buffers. Transferring between them would need an intermediate copy, defeating the zero-copy purpose. - -Instead, compose with an intermediate stage: - -[source,cpp] ----- -// Transform: BufferSource -> processing -> BufferSink -task<> process_pipeline(any_buffer_source& source, any_buffer_sink& sink) -{ - const_buffer src_arr[8]; - - while (true) - { - auto [ec, src_bufs] = co_await source.pull(src_arr); - if (ec == cond::eof) - break; - - std::size_t consumed = 0; - for (auto const& b : src_bufs) - { - auto processed = transform(b); - - // Write processed data to sink - mutable_buffer dst_arr[8]; - auto dst_bufs = sink.prepare(dst_arr); - - std::size_t copied = buffer_copy( - dst_bufs, - make_buffer(processed)); - - co_await sink.commit(copied); - consumed += b.size(); - } - - source.consume(consumed); - } - - co_await sink.commit_eof(0); -} ----- - -== Naming Convention - -The algorithm names reflect buffer ownership: - -[cols="1,2"] -|=== -| Name | Meaning - -| `push_to` -| Source provides buffers → push data to destination - -| `pull_from` -| Sink provides buffers → pull data from source -|=== - -The preposition indicates the direction of buffer provision, not data flow. - -== Error Handling - -All transfer algorithms return `(error_code, std::size_t)`: - -* `error_code` — Success, EOF, or error condition -* `std::size_t` — Total bytes transferred before return - -On error, partial transfer may have occurred. The returned count indicates how much was transferred. - -Example: - -[source,cpp] ----- -auto [ec, total] = co_await push_to(source, sink); - -if (ec == cond::eof) -{ - // Normal completion - std::cout << "Transferred " << total << " bytes\n"; -} -else if (ec) -{ - // Error occurred - std::cerr << "Error after " << total << " bytes: " << ec.message() << "\n"; -} ----- - -== Reference - -[cols="1,3"] -|=== -| Header | Description - -| `` -| Composed read operations - -| `` -| Read at least a minimum number of bytes - -| `` -| Composed write operations - -| `` -| Write at least a minimum number of bytes - -| `` -| Eager write with frame caching - -| `` -| BufferSource → WriteSink/WriteStream transfer - -| `` -| ReadSource/ReadStream → BufferSink transfer -|=== - -You have now learned about transfer algorithms. Continue to xref:6.streams/6f.isolation.adoc[Physical Isolation] to learn how type erasure enables compilation firewalls. diff --git a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc index c885e6ad5..004a391c1 100644 --- a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc +++ b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc @@ -4,7 +4,7 @@ This section explains how type-erased wrappers enable compilation firewalls and == Prerequisites -* Completed xref:6.streams/6e.algorithms.adoc[Transfer Algorithms] +* Completed xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] * Understanding of type-erased wrappers == The Compilation Firewall Pattern @@ -90,10 +90,10 @@ Type erasure decouples your code from specific transport implementations: [source,cpp] ---- // Your library code -task<> send_message(any_write_sink& sink, message const& msg) +task<> send_message(any_write_stream& stream, message const& msg) { - co_await sink.write(make_buffer(msg.header)); - co_await sink.write_eof(make_buffer(msg.body)); + co_await write(stream, make_buffer(msg.header)); + co_await write(stream, make_buffer(msg.body)); } ---- @@ -103,23 +103,18 @@ Callers provide any conforming implementation: ---- // TCP socket tcp::socket socket; -any_write_sink sink{&socket}; // references socket -send_message(sink, msg); +any_write_stream stream{&socket}; // references socket +send_message(stream, msg); // TLS stream -tls::stream stream; -any_write_sink sink{&stream}; // references stream -send_message(sink, msg); - -// HTTP chunked encoding -chunked_sink chunked{underlying}; -any_write_sink sink{&chunked}; // references chunked -send_message(sink, msg); +tls::stream tls; +any_write_stream stream{&tls}; // references tls +send_message(stream, msg); // Test mock -test::write_sink mock; -any_write_sink sink{&mock}; // references mock -send_message(sink, msg); +test::write_stream mock; +any_write_stream stream{&mock}; // references mock +send_message(stream, msg); ---- Same `send_message` function, different transports: compile once, use everywhere. @@ -171,8 +166,7 @@ Returning type-erased values forces heap allocation. Return concrete types when ---- // http_client.hpp #pragma once -#include -#include +#include struct http_request { @@ -185,7 +179,7 @@ struct http_response { int status_code; std::map headers; - any_read_source body; // Body is a source, not a buffer + any_read_stream body; // Body is read as a stream }; // Send request, receive response @@ -210,7 +204,7 @@ auto response = co_await send_request(conn, { // Read body through type-erased source char storage[4096]; mutable_buffer buf(storage, sizeof(storage)); -auto [ec, n] = co_await response.body.read(buf); +auto [ec, n] = co_await response.body.read_some(buf); ---- The HTTP library is isolated from transport details. It compiles once. Users bring their own transport. @@ -236,7 +230,5 @@ Type-erased wrappers are in ``: * `any_stream` * `any_read_stream`, `any_write_stream` -* `any_read_source`, `any_write_sink` -* `any_buffer_source`, `any_buffer_sink` -You have now completed the Stream Concepts section. These abstractions—streams, sources, sinks, and their type-erased wrappers—form the foundation for Capy's I/O model. Continue to xref:../8.examples/8a.hello-task.adoc[Example Programs] to see complete working examples. +You have now completed the Stream Concepts section. These abstractions—streams and their type-erased wrappers—form the foundation for Capy's I/O model. Continue to xref:../8.examples/8a.hello-task.adoc[Example Programs] to see complete working examples. diff --git a/doc/modules/ROOT/pages/7.testing/7.intro.adoc b/doc/modules/ROOT/pages/7.testing/7.intro.adoc index 5be820318..95cdd87bb 100644 --- a/doc/modules/ROOT/pages/7.testing/7.intro.adoc +++ b/doc/modules/ROOT/pages/7.testing/7.intro.adoc @@ -35,19 +35,6 @@ type of the stream or source you pass in. protocol logic that calls `read_some` and `write_some` without touching a socket. -* xref:7.testing/7c.mock-sources-sinks.adoc[Mock Sources and Sinks] -- - `read_source` and `write_sink` implement the complete-I/O concepts from - xref:6.streams/6c.sources-sinks.adoc[Sources and Sinks]. Unlike the - stream mocks, they loop internally until the buffer is fully filled or - drained, and `write_sink` accepts an explicit EOF signal. - -* xref:7.testing/7d.mock-buffer-concepts.adoc[Mock Buffer Sources and Sinks] - -- `buffer_source` and `buffer_sink` implement the buffer concepts from - xref:6.streams/6d.buffer-concepts.adoc[Buffer Sources and Sinks]. - `buffer_source` exposes staged bytes via a pull interface; - `buffer_sink` provides callee-owned storage that the algorithm writes - into directly. - * xref:7.testing/7e.buffer-inspection.adoc[Buffer Inspection] -- `bufgrind` iterates every split point of a buffer sequence, exercising every chunk-boundary condition; `buffer_to_string` concatenates buffer sequences diff --git a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc index dd3ed912c..046fe9021 100644 --- a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc +++ b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc @@ -425,4 +425,4 @@ void test_read_line() | Connected bidirectional pair for client/server tests. |=== -Continue to xref:7.testing/7c.mock-sources-sinks.adoc[Mock Sources and Sinks]. +Continue to xref:7.testing/7e.buffer-inspection.adoc[Buffer Inspection]. diff --git a/doc/modules/ROOT/pages/7.testing/7c.mock-sources-sinks.adoc b/doc/modules/ROOT/pages/7.testing/7c.mock-sources-sinks.adoc deleted file mode 100644 index f66e62d2c..000000000 --- a/doc/modules/ROOT/pages/7.testing/7c.mock-sources-sinks.adoc +++ /dev/null @@ -1,293 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -= Mock Sources and Sinks - -Concept-conforming test doubles for the complete-I/O concepts in -xref:6.streams/6c.sources-sinks.adoc[Sources and Sinks]. Sources fill the -buffer completely (looping internally if needed); sinks accept all bytes -and report EOF. - -== read_source - -`read_source` implements the `ReadSource` concept. Test code stages bytes -via `provide()`, then the system under test calls `read()` and receives the -entire requested length back (or an error or EOF). The attached `fuse` -injects errors at every read call, exercising the caller's error-handling -paths. Because `fuse` copies share state (see -xref:7.testing/7a.drivers.adoc#_shared_state_across_copies[Shared State Across Copies]), -constructing `read_source rs(f)` by value still ties `rs` to the same -fail-point machinery as `f`. - -[source,cpp] ----- -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_read_source() -{ - fuse f; - read_source rs(f); - rs.provide("Hello, "); - rs.provide("World!"); - - auto r = f.armed([&](fuse&) -> task { - char buf[32]; - auto [ec, n] = co_await rs.read( - mutable_buffer(buf, sizeof(buf))); - if(ec) - co_return; - BOOST_TEST(std::string_view(buf, n) == "Hello, World!"); - }); - BOOST_TEST(r.success); -} ----- - -=== Complete vs. Partial Reads - -`read_source` exposes both `read()` and `read_some()`. The distinction -matters: - -`read_some()` is a partial read, inherited from `ReadStream`. It returns -up to `max_read_size` bytes per call and may return fewer bytes than the -buffer can hold. Callers must loop to fill a buffer. - -`read()` is a complete read, satisfying `ReadSource`. It transfers all -available data in a single operation, ignoring the `max_read_size` limit. -On success `n` equals `buffer_size(buffers)`. If available data runs out -before the buffer is filled, `read()` returns `cond::eof` with `n` -set to however many bytes were transferred. Callers do not need to loop. - -This is the key behavioral difference from `read_stream::read_some()`, -which always returns a partial result and never fills the buffer on its -own. - -[cols="1,2"] -|=== -| Member | Description - -| `explicit read_source(fuse f = {}, std::size_t max_read_size = std::size_t(-1))` -| Construct with an optional shared `fuse` and an optional per-read byte limit. - When omitted, the fuse is inert and reads return all available data at once. - Set `max_read_size` to simulate chunked delivery; the limit applies to - `read_some()` only -- `read()` ignores it. - -| `provide(std::string_view sv)` -| Append bytes to the internal buffer for subsequent reads. Multiple - calls accumulate data. - -| `read(MutableBufferSequence buffers)` -| Complete read. Transfers all available data in a single step, ignoring - `max_read_size`. Returns `cond::eof` with partial `n` if data runs - out before the buffer is filled. Consults the fuse before every call. - -| `read_some(MutableBufferSequence buffers)` -| Partial read. Returns up to `max_read_size` bytes (or all available if - no limit was set). Returns `cond::eof` when the buffer is drained. - Consults the fuse before every call. - -| `available() -> std::size_t` -| Return the number of bytes remaining to be read. - -| `clear()` -| Clear all data and reset the read position. -|=== - -== write_sink - -`write_sink` implements the `WriteSink` concept. The system under test -calls `write()` and `write_eof()` while the test inspects what was written -via `data()` and checks whether EOF was signaled via `eof_called()`. -Test code may also call `expect()` to register the data it anticipates; -any mismatch between written bytes and that prefix causes the next write -(`write`, `write_some`, or `write_eof(buffers)`) to return `error::test_failure`. Because `fuse` copies share state (see -xref:7.testing/7a.drivers.adoc#_shared_state_across_copies[Shared State Across Copies]), -constructing `write_sink ws(f)` by value still ties `ws` to the same -fail-point machinery as `f`. - -[source,cpp] ----- -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_write_sink() -{ - fuse f; - write_sink ws(f); - - auto r = f.armed([&](fuse&) -> task { - auto [ec, n] = co_await ws.write( - const_buffer("Hello", 5)); - if(ec) - co_return; - auto [ec2] = co_await ws.write_eof(); - if(ec2) - co_return; - }); - BOOST_TEST(r.success); - BOOST_TEST(ws.data() == "Hello"); - BOOST_TEST(ws.eof_called()); -} ----- - -=== EOF Signal - -`write_eof()` is the explicit end-of-stream marker, with no analog in -`write_stream`. Some protocols treat connection close as the end-of-body -signal (HTTP/1.0 without `Content-Length` is one example), so the sink -needs a way to capture that event separately from the data transfer. - -`write_sink` provides two forms of the signal: - -* `write_eof()` -- signal EOF without data. -* `write_eof(buffers)` -- atomically write the last chunk and signal EOF - in a single awaitable. This form lets protocol code optimize the final - send so data and the termination marker travel together. - -After either form succeeds, `eof_called()` returns `true`. The fuse is -consulted before the operation, so both forms participate in error -injection. - -[cols="1,2"] -|=== -| Member | Description - -| `explicit write_sink(fuse f = {}, std::size_t max_write_size = std::size_t(-1))` -| Construct with an optional shared `fuse` and an optional per-write byte limit. - When omitted, the fuse is inert and writes accept all bytes at once. - Set `max_write_size` to simulate chunked delivery; the limit applies to - `write_some()` only -- `write()` and `write_eof(buffers)` ignore it. - -| `write(ConstBufferSequence buffers)` -| Complete write. Transfers all bytes from `buffers` to the internal - buffer, ignoring `max_write_size`. Checks against expected data after - appending; on mismatch returns `(error::test_failure, n)` with the - appended bytes left in place. Consults the fuse before every call. - -| `write_some(ConstBufferSequence buffers)` -| Partial write. Appends up to `max_write_size` bytes to the internal - buffer, then checks against the expected prefix. On mismatch, rolls - back the appended bytes and returns `(error::test_failure, 0)` -- in - contrast to `write()`, which leaves the partial write in place. - Consults the fuse before every call. - -| `write_eof(ConstBufferSequence buffers)` -| Atomically write remaining bytes and signal end-of-stream. Sets - `eof_called()` to `true` on success. Consults the fuse before the call. - Await-returns `(error_code, n)`. - -| `write_eof()` -| Signal end-of-stream without writing data. Sets `eof_called()` to - `true` on success. Consults the fuse before the call. - Await-returns `(error_code)`. - -| `data() -> std::string_view` -| Return bytes written but not yet matched by `expect()`. - -| `size() -> std::size_t` -| Return the number of bytes written. - -| `eof_called() -> bool` -| Return `true` if `write_eof()` or `write_eof(buffers)` has succeeded. - -| `expect(std::string_view sv) -> std::error_code` -| Register expected data and immediately check any already-written bytes. - Matched bytes are consumed from both sides. Returns an error if existing - data does not match. - -| `clear()` -| Clear all data, expected data, and reset `eof_called` to `false`. -|=== - -== Putting It Together - -The following snippet tests a request-handler coroutine that reads a -fixed-size request from a `ReadSource`, processes it, and writes the -response to a `WriteSink`. The `fuse.armed()` loop exercises every -error site in both the read and write paths. - -[source,cpp] ----- -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -// Function under test: echo the request back as the response -template -task -handle_request(Source& source, Sink& sink) -{ - char buf[64]; - auto [ec, n] = co_await source.read( - mutable_buffer(buf, sizeof(buf))); - if(ec && ec != cond::eof) - co_return ec; - - auto [ec2, n2] = co_await sink.write( - const_buffer(buf, n)); - if(ec2) - co_return ec2; - - auto [ec3] = co_await sink.write_eof(); - if(ec3) - co_return ec3; - - co_return std::error_code{}; -} - -void test_handle_request() -{ - fuse f; - auto r = f.armed([&](fuse&) -> task { - read_source rs(f); - write_sink ws(f); - rs.provide("ping"); - - auto ec = co_await handle_request(rs, ws); - if(ec) - co_return; // fuse injected an error; exit gracefully - BOOST_TEST(ws.data() == "ping"); - BOOST_TEST(ws.eof_called()); - }); - BOOST_TEST(r.success); -} ----- - -== Reference - -[cols="1,3"] -|=== -| Header | Contents - -| `` -| Mock ReadSource with complete reads. - -| `` -| Mock WriteSink with complete writes and explicit EOF. -|=== - -Continue to xref:7.testing/7d.mock-buffer-concepts.adoc[Mock Buffer Sources and Sinks]. diff --git a/doc/modules/ROOT/pages/7.testing/7d.mock-buffer-concepts.adoc b/doc/modules/ROOT/pages/7.testing/7d.mock-buffer-concepts.adoc deleted file mode 100644 index 028203b73..000000000 --- a/doc/modules/ROOT/pages/7.testing/7d.mock-buffer-concepts.adoc +++ /dev/null @@ -1,343 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -= Mock Buffer Sources and Sinks - -Concept-conforming test doubles for the buffer concepts in -xref:6.streams/6d.buffer-concepts.adoc[Buffer Sources and Sinks]. These -mocks let you test code that consumes via a `BufferSource` or produces via -a `BufferSink` without wiring up a real dynamic buffer. - -== buffer_source - -`buffer_source` implements the `BufferSource` concept. Test code stages -bytes via `provide()`, and the system under test pulls them through the -`pull()`/`consume()` interface that `BufferSource` requires. Pulled buffers -point directly into the source's internal storage, so no copy occurs. The -attached `fuse` injects errors at every `pull()` call, exercising the -caller's error-handling paths. Because `fuse` copies share state (see -xref:7.testing/7a.drivers.adoc#_shared_state_across_copies[Shared State Across Copies]), -constructing `buffer_source bs(f)` by value still ties `bs` to the same -fail-point machinery as `f`. - -[source,cpp] ----- -#include -#include -#include -#include -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_buffer_source() -{ - fuse f; - buffer_source bs(f); - bs.provide("Hello, "); - bs.provide("World!"); - - auto r = f.armed([&](fuse&) -> task { - const_buffer arr[16]; - auto [ec, bufs] = co_await bs.pull(arr); - if(ec) - co_return; - BOOST_TEST(buffer_to_string(bufs) == "Hello, World!"); - bs.consume(buffer_size(bufs)); - }); - BOOST_TEST(r.success); -} ----- - -=== Staging Data - -Call `provide()` one or more times before the system under test runs. -Each call appends bytes to the internal buffer; the next `pull()` returns -a span covering all accumulated unconsumed data, up to `max_pull_size` if -a limit was set. - -[source,cpp] ----- -buffer_source bs(f); -bs.provide("part one "); -bs.provide("part two"); // total: "part one part two" ----- - -=== Consume Loop - -`pull()` returns the same data on repeated calls until `consume()` advances -the read position. A typical consumer loops until `pull()` returns -`cond::eof`, consuming the returned bytes each time: - -[source,cpp] ----- -const_buffer arr[16]; -for(;;) -{ - auto [ec, bufs] = co_await bs.pull(arr); - if(ec == cond::eof) - break; - if(ec) - co_return; // fuse injected error, or real failure - // process bufs ... - bs.consume(buffer_size(bufs)); -} ----- - -=== Chunked Delivery - -The second constructor parameter caps the bytes returned per `pull()`, -simulating a source that delivers data in small pieces: - -[source,cpp] ----- -buffer_source bs(f, 5); // at most 5 bytes per pull -bs.provide("hello world"); -// first pull returns "hello"; second returns " worl"; etc. ----- - -[cols="1,2"] -|=== -| Member | Description - -| `explicit buffer_source(fuse f = {}, std::size_t max_pull_size = std::size_t(-1))` -| Construct with an optional shared `fuse` and an optional per-pull byte ceiling. - When omitted, the fuse is inert and each pull returns all remaining data. - Set `max_pull_size` to simulate chunked delivery. - -| `provide(std::string_view sv)` -| Append bytes to the internal buffer for subsequent pulls. Multiple - calls accumulate data. - -| `pull(std::span dest)` -| Fills `dest` with buffer descriptors pointing into internal storage. - Await-returns `(error_code, std::span)`. Returns `cond::eof` - when no data remains. Consults the fuse before every call. Repeated - calls without `consume()` return the same data. - -| `consume(std::size_t n)` -| Advance the read position by `n` bytes. The next `pull()` returns data - starting after the consumed bytes. - -| `available() -> std::size_t` -| Return the number of bytes not yet consumed. - -| `clear()` -| Clear all data and reset the read position. -|=== - -== buffer_sink - -`buffer_sink` implements the `BufferSink` concept. The system under test -follows the callee-owns-buffers pattern: it calls `prepare()` to get -writable buffer space from the sink, writes directly into those buffers, -then calls `commit()` or `commit_eof()` to finalize the bytes. The test -then inspects what was captured via `data()` and checks whether the -end-of-stream was signaled via `eof_called()`. The attached `fuse` -injects errors at every async step. Because `fuse` copies share state (see -xref:7.testing/7a.drivers.adoc#_shared_state_across_copies[Shared State Across Copies]), -constructing `buffer_sink bs(f)` by value still ties `bs` to the same -fail-point machinery as `f`. - -[source,cpp] ----- -#include -#include -#include - -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -void test_buffer_sink() -{ - fuse f; - auto r = f.armed([&](fuse&) -> task { - buffer_sink bs(f); - - mutable_buffer arr[16]; - auto bufs = bs.prepare(arr); - - 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); - if(ec2) - co_return; - - BOOST_TEST(bs.data() == "Hello"); - BOOST_TEST(bs.eof_called()); - }); - BOOST_TEST(r.success); -} ----- - -=== Reading What Was Written - -After the coroutine completes, `data()` returns a `string_view` of all -committed bytes. `size()` gives the byte count. `eof_called()` returns -`true` if `commit_eof()` succeeded during the run. - -[source,cpp] ----- -BOOST_TEST(bs.data() == "expected output"); -BOOST_TEST(bs.size() == 15u); -BOOST_TEST(bs.eof_called()); ----- - -Call these accessors inside the `f.armed()` lambda after the system -under test completes successfully. They are the primary mechanism for -asserting what the system under test produced. - -=== The prepare/commit Protocol - -`prepare()` is synchronous. It fills the provided span with one writable -buffer descriptor pointing into the sink's internal storage. The caller -writes data into those buffers, then calls `commit(n)` to finalize `n` -bytes, or `commit_eof(n)` to finalize `n` bytes and signal end-of-stream -in a single step. Passing `n = 0` to `commit_eof` signals EOF without -writing additional bytes. - -=== Limited Buffer Space - -The second constructor parameter caps the bytes available per `prepare()`, -simulating a sink with constrained internal space: - -[source,cpp] ----- -buffer_sink bs(f, 8); // prepare returns at most 8 bytes at a time ----- - -[cols="1,2"] -|=== -| Member | Description - -| `explicit buffer_sink(fuse f = {}, std::size_t max_prepare_size = 4096)` -| Construct with an optional shared `fuse` and an optional per-prepare byte ceiling. - When omitted, the fuse is inert and `prepare()` exposes `4096` bytes of buffer - space. Set `max_prepare_size` to simulate limited buffer space. - -| `prepare(std::span dest)` -| Synchronously fills `dest` with writable buffer descriptors into - internal storage. Returns the filled span (one buffer in this - implementation, or empty if `dest` is empty). Does not consult - the fuse. - -| `commit(std::size_t n)` -| Finalize `n` bytes written to the most recent `prepare()` buffers. - Await-returns `(error_code)`. Consults the fuse before committing. - -| `commit_eof(std::size_t n)` -| Finalize `n` bytes and signal end-of-stream. Await-returns - `(error_code)`. Sets `eof_called()` to `true` on success. Consults - the fuse before committing. Pass `n = 0` to signal EOF without - additional data. - -| `data() -> std::string_view` -| Return all bytes committed so far. - -| `size() -> std::size_t` -| Return the number of bytes committed. - -| `eof_called() -> bool` -| Return `true` if `commit_eof()` has succeeded. - -| `clear()` -| Clear all committed data and reset `eof_called` to `false`. -|=== - -== Putting It Together - -The following snippet tests a copy algorithm that pulls from a -`BufferSource` and writes into a `BufferSink`. The `fuse.armed()` loop -exercises every error site in both the pull and commit paths. - -[source,cpp] ----- -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace boost::capy; -using namespace boost::capy::test; - -// Function under test: copy all bytes from source into sink -template -task -copy_all(Source& source, Sink& sink) -{ - const_buffer src_arr[16]; - mutable_buffer dst_arr[16]; - - 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; - } - if(ec1) - co_return ec1; - - 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; - - source.consume(n); - } -} - -void test_copy_all() -{ - fuse f; - auto r = f.armed([&](fuse&) -> task { - buffer_source src(f); - buffer_sink dst(f); - src.provide("ping"); - - auto ec = co_await copy_all(src, dst); - if(ec) - co_return; // fuse injected an error; exit gracefully - BOOST_TEST(dst.data() == "ping"); - BOOST_TEST(dst.eof_called()); - }); - BOOST_TEST(r.success); -} ----- - -== Reference - -[cols="1,3"] -|=== -| Header | Contents - -| `` -| Mock BufferSource for callee-owns-buffers pull tests. - -| `` -| Mock BufferSink for callee-owns-buffers write tests. -|=== - -Continue to xref:7.testing/7e.buffer-inspection.adoc[Buffer Inspection]. diff --git a/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc b/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc index a6ca865fd..ac00d7b3f 100644 --- a/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc +++ b/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc @@ -187,4 +187,4 @@ Connection from 127.0.0.1:54321 == Next Steps -* xref:8.examples/8j.stream-pipeline.adoc[Stream Pipeline] -- Data transformation chains +* xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] -- Erased streams behind a uniform interface diff --git a/doc/modules/ROOT/pages/8.examples/8j.stream-pipeline.adoc b/doc/modules/ROOT/pages/8.examples/8j.stream-pipeline.adoc deleted file mode 100644 index acdab691d..000000000 --- a/doc/modules/ROOT/pages/8.examples/8j.stream-pipeline.adoc +++ /dev/null @@ -1,438 +0,0 @@ -= Stream Pipeline - -Data transformation through a pipeline of sources and sinks. - -== What You Will Learn - -* Building processing pipelines -* Using `BufferSource` and `BufferSink` concepts -* Chaining transformations - -== Prerequisites - -* Completed xref:8.examples/8i.echo-server-corosio.adoc[Echo Server with Corosio] -* Understanding of buffer sources/sinks from xref:../6.streams/6d.buffer-concepts.adoc[Buffer Concepts] - -== Source Code - -[source,cpp] ----- -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -//------------------------------------------------------------------------------ -// -// Transform: uppercase_transform -// -// A BufferSource that pulls from an upstream source and converts all -// characters to uppercase. Demonstrates a simple byte-by-byte transform. -// -//------------------------------------------------------------------------------ - -class uppercase_transform -{ - capy::any_buffer_source* source_; // any_buffer_source* - std::vector buffer_; // std::vector - transformed data - std::size_t consumed_ = 0; // std::size_t - bytes consumed by downstream - bool exhausted_ = false; // bool - upstream exhausted - -public: - explicit uppercase_transform(capy::any_buffer_source& source) - : source_(&source) - { - } - - // BufferSource::consume - advance past processed bytes - void - consume(std::size_t n) noexcept - { - consumed_ += n; - // Compact buffer when fully consumed - if (consumed_ >= buffer_.size()) - { - buffer_.clear(); - consumed_ = 0; - } - } - - // BufferSource::pull - returns task<> to enable co_await on upstream - capy::io_task> - pull(std::span dest) - { - // Already have unconsumed data? - if (consumed_ < buffer_.size()) - { - if (dest.empty()) - co_return {std::error_code{}, std::span{}}; - - dest[0] = capy::const_buffer( - buffer_.data() + consumed_, - buffer_.size() - consumed_); - co_return {std::error_code{}, dest.first(1)}; - } - - // Upstream exhausted? - if (exhausted_) - co_return {capy::error::eof, std::span{}}; - - // Pull from upstream - buffer_.clear(); - consumed_ = 0; - - capy::const_buffer upstream[8]; // const_buffer[8] - // ec: std::error_code, bufs: std::span - auto [ec, bufs] = co_await source_->pull(upstream); - - if (ec == capy::cond::eof) - { - exhausted_ = true; - co_return {capy::error::eof, std::span{}}; - } - - if (ec) - co_return {ec, std::span{}}; - - // Transform: uppercase each byte - for (auto const& buf : bufs) // const_buffer const& - { - auto const* data = static_cast(buf.data()); // char const* - auto size = buf.size(); // std::size_t - - for (std::size_t i = 0; i < size; ++i) - { - buffer_.push_back(static_cast( - std::toupper(static_cast(data[i])))); - } - } - - // Consume from upstream - source_->consume(capy::buffer_size(bufs)); - - // Return transformed data - if (dest.empty() || buffer_.empty()) - co_return {std::error_code{}, std::span{}}; - - dest[0] = capy::const_buffer(buffer_.data(), buffer_.size()); - co_return {std::error_code{}, dest.first(1)}; - } -}; - -//------------------------------------------------------------------------------ -// -// Transform: line_numbering_transform -// -// A BufferSource that pulls from an upstream source and prepends line -// numbers to each line. Demonstrates a transform that changes data size. -// -//------------------------------------------------------------------------------ - -class line_numbering_transform -{ - capy::any_buffer_source* source_; // any_buffer_source* - std::string buffer_; // std::string - transformed data - std::size_t consumed_ = 0; // std::size_t - bytes consumed by downstream - std::size_t line_num_ = 1; // std::size_t - current line number - bool at_line_start_ = true; // bool - are we at start of a line? - bool exhausted_ = false; // bool - upstream exhausted - -public: - explicit line_numbering_transform(capy::any_buffer_source& source) - : source_(&source) - { - } - - // BufferSource::consume - advance past processed bytes - void - consume(std::size_t n) noexcept - { - consumed_ += n; - // Compact buffer when fully consumed - if (consumed_ >= buffer_.size()) - { - buffer_.clear(); - consumed_ = 0; - } - } - - // BufferSource::pull - returns task<> to enable co_await on upstream - capy::io_task> - pull(std::span dest) - { - // Already have unconsumed data? - if (consumed_ < buffer_.size()) - { - if (dest.empty()) - co_return {std::error_code{}, std::span{}}; - - dest[0] = capy::const_buffer( - buffer_.data() + consumed_, - buffer_.size() - consumed_); - co_return {std::error_code{}, dest.first(1)}; - } - - // Upstream exhausted? - if (exhausted_) - co_return {capy::error::eof, std::span{}}; - - // Pull from upstream - buffer_.clear(); - consumed_ = 0; - - capy::const_buffer upstream[8]; // const_buffer[8] - // ec: std::error_code, bufs: std::span - auto [ec, bufs] = co_await source_->pull(upstream); - - if (ec == capy::cond::eof) - { - exhausted_ = true; - co_return {capy::error::eof, std::span{}}; - } - - if (ec) - co_return {ec, std::span{}}; - - // Transform: add line numbers - for (auto const& buf : bufs) // const_buffer const& - { - auto const* data = static_cast(buf.data()); // char const* - auto size = buf.size(); // std::size_t - - for (std::size_t i = 0; i < size; ++i) - { - if (at_line_start_) - { - buffer_ += std::to_string(line_num_++) + ": "; - at_line_start_ = false; - } - buffer_ += data[i]; - if (data[i] == '\n') - at_line_start_ = true; - } - } - - // Consume from upstream - source_->consume(capy::buffer_size(bufs)); - - // Return transformed data - if (dest.empty() || buffer_.empty()) - co_return {std::error_code{}, std::span{}}; - - dest[0] = capy::const_buffer(buffer_.data(), buffer_.size()); - co_return {std::error_code{}, dest.first(1)}; - } -}; - -//------------------------------------------------------------------------------ -// -// transfer: Pull from source and write to sink until exhausted -// -//------------------------------------------------------------------------------ - -capy::task transfer(capy::any_buffer_source& source, capy::any_write_sink& sink) -{ - std::size_t total = 0; // std::size_t - capy::const_buffer bufs[8]; // const_buffer[8] - - for (;;) - { - // ec: std::error_code, spans: std::span - auto [ec, spans] = co_await source.pull(bufs); - - if (ec == capy::cond::eof) - break; - - if (ec) - throw std::system_error(ec); - - // Write each buffer to sink - for (auto const& buf : spans) // const_buffer const& - { - // wec: std::error_code, n: std::size_t - auto [wec, n] = co_await sink.write(buf); - if (wec) - throw std::system_error(wec); - total += n; - } - - // Consume what we read - source.consume(capy::buffer_size(spans)); - } - - capy::io_result<> eof_result = co_await sink.write_eof(); - if (eof_result.ec) - throw std::system_error(eof_result.ec); - - co_return total; -} - -//------------------------------------------------------------------------------ -// -// demo_pipeline: Demonstrate chained transforms -// -//------------------------------------------------------------------------------ - -void demo_pipeline() -{ - std::cout << "=== Stream Pipeline Demo ===\n\n"; - - // Input data - three lines - std::string input = "hello world\nthis is a test\nof the pipeline\n"; - std::cout << "Input:\n" << input << "\n"; - - // Create mock source with input data - capy::test::fuse f; // test::fuse - capy::test::buffer_source source(f); // test::buffer_source - source.provide(input); - - // Build the pipeline using type-erased buffer sources: - // source -> [uppercase] -> [line_numbering] -> sink - - // Stage 1: Wrap raw source as any_buffer_source. - // Using pointer construction (&source) for reference semantics - the - // wrapper does not take ownership, so source must outlive src. - capy::any_buffer_source src{&source}; // any_buffer_source - - // Stage 2: Uppercase transform wraps src. - // Again using pointer construction so upper_src references upper - // without taking ownership. - uppercase_transform upper{src}; // uppercase_transform - capy::any_buffer_source upper_src{&upper}; // any_buffer_source - - // Stage 3: Line numbering transform wraps upper_src. - line_numbering_transform numbered{upper_src}; // line_numbering_transform - capy::any_buffer_source numbered_src{&numbered}; // any_buffer_source - - // Create sink to collect output. - // Pointer construction ensures sink outlives dst. - capy::test::write_sink sink(f); // test::write_sink - capy::any_write_sink dst{&sink}; // any_write_sink - - // Run the pipeline - std::size_t bytes = 0; // std::size_t - capy::test::run_blocking([&](std::size_t n) { bytes = n; })( - transfer(numbered_src, dst)); - - std::cout << "Output (" << bytes << " bytes):\n"; - std::cout << sink.data() << "\n"; - - // Expected output: - // 1: HELLO WORLD - // 2: THIS IS A TEST - // 3: OF THE PIPELINE -} - -int main() -{ - try - { - demo_pipeline(); - } - catch (std::system_error const& e) - { - std::cerr << "Pipeline error: " << e.what() << "\n"; - return 1; - } - return 0; -} ----- - -== Build - -[source,cmake] ----- -add_executable(stream_pipeline stream_pipeline.cpp) -target_link_libraries(stream_pipeline PRIVATE capy) ----- - -== Walkthrough - -=== Pipeline Structure - ----- -Source → Uppercase → LineNumbering → Sink ----- - -Data flows through the pipeline: - -1. Source provides raw input -2. Uppercase transforms to uppercase -3. LineNumbering adds line numbers -4. Sink collects output - -=== BufferSource Implementation - -[source,cpp] ----- -capy::io_task> -pull(std::span dest) -{ - // Pull from upstream - // ec: std::error_code, bufs: std::span - auto [ec, bufs] = co_await source_->pull(upstream); - - // Transform data... - - // Consume from upstream - source_->consume(capy::buffer_size(bufs)); - - // Return transformed buffer - dest[0] = capy::const_buffer(buffer_.data(), buffer_.size()); - co_return {std::error_code{}, dest.first(1)}; -} ----- - -Each stage: - -1. Pulls buffers from upstream using `co_await` -2. Transforms the data -3. Calls `consume()` on upstream to indicate bytes processed -4. Returns transformed buffers - -=== Type Erasure with Pointer Construction - -[source,cpp] ----- -// Using pointer construction (&source) for reference semantics -capy::any_buffer_source src{&source}; // any_buffer_source - -uppercase_transform upper{src}; // uppercase_transform -capy::any_buffer_source upper_src{&upper}; // any_buffer_source ----- - -`any_buffer_source` wraps each stage using pointer construction, allowing uniform composition while preserving the lifetime of the underlying objects. - -== Output - ----- -=== Stream Pipeline Demo === - -Input: -hello world -this is a test -of the pipeline - -Output (52 bytes): -1: HELLO WORLD -2: THIS IS A TEST -3: OF THE PIPELINE ----- - -== Exercises - -1. Add a compression/decompression stage -2. Implement a ROT13 transform -3. Create a filtering stage that drops lines matching a pattern - -== Next Steps - -* xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] -- Lock-free shared state with strands diff --git a/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc b/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc index 1d8700334..ab6137fb1 100644 --- a/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc +++ b/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc @@ -20,13 +20,13 @@ One abstraction level cannot serve all of these needs simultaneously. The insigh Capy offers three layers. They coexist. They interoperate. Users pick the one that matches their constraints. -The first layer is concepts. These are the template-based interfaces: `ReadStream`, `WriteStream`, `BufferSink`, `BufferSource`. Algorithms written against concepts get full optimization. The compiler sees through everything. There is no indirection, no vtable, no allocation overhead. This is what you use for hot inner loops, for protocol parsing, for any path where performance dominates: +The first layer is concepts. These are the template-based interfaces: `ReadStream`, `WriteStream`, `Stream`. Algorithms written against concepts get full optimization. The compiler sees through everything. There is no indirection, no vtable, no allocation overhead. This is what you use for hot inner loops, for protocol parsing, for any path where performance dominates: [source,cpp] ---- -template +template io_task -push_to(Src& source, Sink& sink); +write(S& stream, CB buffers); ---- The cost is that templates propagate. Every caller sees the full implementation. Compile times grow. You cannot hide this behind a `.cpp` file. @@ -80,30 +80,6 @@ This has a direct implication for how you structure code. Use concepts for tight The user chooses where the boundary falls. Not the library. -== Zero-Copy as a First-Class Concern - -Buffer sink and buffer source invert the traditional ownership model. Instead of the caller allocating a buffer, filling it with data, and handing it to the library, the library exposes its own internal storage and the caller fills it in place. Zero copies. The data goes directly where it needs to be. - -The `BufferSink` concept formalizes this with three operations. `prepare()` returns writable buffers from the sink's internal storage. The caller writes data into those buffers. `commit()` tells the sink how many bytes were written, and `commit_eof()` commits the final bytes and signals end-of-stream: - -[source,cpp] ----- -concept BufferSink = - requires(T& sink, std::span dest, - std::size_t n) - { - { sink.prepare(dest) } - -> std::same_as>; - { sink.commit(n) } -> IoAwaitable; - { sink.commit_eof(n) } -> IoAwaitable; - }; ----- - -This matters at the protocol level. The HTTP parser's internal buffer is the buffer you write into. The serializer's internal buffer is the buffer you read from. There is no intermediate copy between the network and the parser, and no intermediate copy between the serializer and the network. - -The key detail is that `commit()` returns an `IoAwaitable`. When the sink is backed by a socket, `commit()` suspends and performs the actual write. When the sink is an in-memory buffer - a string, a parser, a test fixture - `commit()` completes synchronously without creating a coroutine frame. Same code, same API, no overhead for the synchronous case. This is what makes the buffer abstractions practical for both production I/O and testing. - - == Symmetric Transfer and the Pump Symmetric transfer is the mechanism that allows Corosio to match or beat ASIO callback throughput. When one coroutine completes and its continuation is ready to run, symmetric transfer reuses the same coroutine frame without allocation and bypasses the global work queue entirely. ASIO callbacks always go through the queue. Symmetric transfer skips that step. @@ -142,12 +118,12 @@ A database library built this way can express protocol parsing with zero-copy bu The question that matters is: can a library author look at their problem and immediately see which layer to use? If the answer is yes, the design is working. If they have to think about it, something is wrong. -**Protocol parsing:** use `BufferSink` and `BufferSource` concepts as template parameters. Zero copy, zero overhead. Call member functions whose awaitables do all the work, with no coroutine frame allocation. The compiler optimizes everything. +**Protocol parsing:** use the `ReadStream` and `WriteStream` concepts as template parameters. Zero overhead. Call member functions whose awaitables do all the work, with no coroutine frame allocation. The compiler optimizes everything. **Connection management:** use concrete types like `tcp_socket`. These give you `connect()` and `shutdown()` - the operations that are transport-specific. But the concrete type is derived from `io_stream`, a class that models `capy::Stream`, so you can pass `io_stream&` to a non-template function for the business logic that sits on top of the connection. **Full transport abstraction across a library boundary:** use `any_stream`. Complete type erasure, but you lose connection management - there is no `connect()` on an `any_stream`. This means you have to carefully arrange your code so it genuinely requires a physical separation in the Lakos sense. The protocol logic and the connection logic live in separate components, and the type-erased boundary sits between them. -The layers compose. An algorithm written against a `BufferSource` concept can be called from inside a coroutine that is type-erased behind a `task<>`, which is dispatched through a virtual function on a base class that holds an `any_stream&`. Each layer handles its part. Nothing leaks through the boundaries unless you want it to. +The layers compose. An algorithm written against a `ReadStream` concept can be called from inside a coroutine that is type-erased behind a `task<>`, which is dispatched through a virtual function on a base class that holds an `any_stream&`. Each layer handles its part. Nothing leaks through the boundaries unless you want it to. This is what it means when we say the user chooses. Capy provides the tools. The user decides where the boundaries go based on what they know about their performance requirements, their compilation budget, and their architecture. The library does not impose a single answer because there is not one. diff --git a/doc/modules/ROOT/pages/9.design/9b.Separation.adoc b/doc/modules/ROOT/pages/9.design/9b.Separation.adoc index dcd397de4..4f4d3846c 100644 --- a/doc/modules/ROOT/pages/9.design/9b.Separation.adoc +++ b/doc/modules/ROOT/pages/9.design/9b.Separation.adoc @@ -95,7 +95,7 @@ The best modules are those that provide powerful functionality, but have a simple interface. ____ -Capy is a deep module. Its public surface is narrow: a handful of concepts (`ReadStream`, `WriteStream`, `BufferSource`, `BufferSink`), a task type, an executor model, and buffer utilities. Behind that surface lives a substantial implementation: coroutine frame allocation, forward propagation of executors and stop tokens, type-erased stream machinery, and composition primitives. +Capy is a deep module. Its public surface is narrow: a handful of concepts (`ReadStream`, `WriteStream`, `Stream`), a task type, an executor model, and buffer utilities. Behind that surface lives a substantial implementation: coroutine frame allocation, forward propagation of executors and stop tokens, type-erased stream machinery, and composition primitives. Corosio is also a deep module, but a different one. It hides platform-specific event loop complexity (IOCP, epoll, kqueue, select) behind a uniform socket and timer interface. diff --git a/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc b/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc deleted file mode 100644 index 1071f5d4e..000000000 --- a/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc +++ /dev/null @@ -1,339 +0,0 @@ -= ReadSource Concept Design - -== Overview - -This document describes the design of the `ReadSource` concept: a refinement of `ReadStream` that adds a complete-read primitive. It explains how `ReadSource` relates to `ReadStream`, why the refinement hierarchy mirrors the write side, and the use cases each serves. - -== Definition - -[source,cpp] ----- -template -concept ReadSource = - ReadStream && - requires(T& source, mutable_buffer_archetype buffers) - { - { source.read(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(source.read(buffers)), - std::error_code, std::size_t>; - }; ----- - -`ReadSource` refines `ReadStream`. Every `ReadSource` is a `ReadStream`. A `ReadSource` provides two operations: - -=== `read_some(buffers)` -- Partial Read (inherited from `ReadStream`) - -Attempts to read up to `buffer_size(buffers)` bytes from the source into the buffer sequence. Returns `(error_code, std::size_t)` where `n` is the number of bytes read. May return fewer bytes than the buffer can hold. - -==== Semantics - -If `buffer_size(buffers) > 0`: - -- If `!ec`, then `n >= 1 && n \<= buffer_size(buffers)`. `n` bytes were read into the buffer sequence. -- If `ec`, then `n >= 0 && n \< buffer_size(buffers)`. `n` is the number of bytes read before the I/O condition arose. - -Equivalently, `n == buffer_size(buffers)` implies `!ec`: a completion that fills the buffer sequence is a success, even when the underlying operation also signals a condition such as end-of-stream. That condition is reported on a subsequent read. - -If `buffer_empty(buffers)` is true, `n` is 0. The empty buffer is not itself a cause for error, but `ec` may reflect the state of the source. - -Once `read_some` returns an error (including EOF), the caller must not call `read_some` again. The stream is done. Not all implementations can reproduce a prior error on subsequent calls, so the behavior after an error is undefined. - -=== `read(buffers)` -- Complete Read - -Reads data into the buffer sequence. Either fills the entire buffer or returns an error. Returns `(error_code, std::size_t)` where `n` is the number of bytes read. - -==== Semantics - -- On success: `!ec`, `n == buffer_size(buffers)`. The buffer is completely filled. -- On EOF: `ec == cond::eof`, `n` is the number of bytes read before EOF was reached (may be less than `buffer_size(buffers)`). -- On error: `ec`, `n` is the number of bytes read before the error. -- If `buffer_empty(buffers)`: completes immediately, `!ec`, `n == 0`. - -Successful partial reads are not permitted. Either the entire buffer is filled, or the operation returns with an error. This is the defining property of a complete-read primitive. - -Once `read` returns an error (including EOF), the caller must not call `read` or `read_some` again. The source is done. Not all implementations can reproduce a prior error on subsequent calls, so the behavior after an error is undefined. - -When the buffer sequence contains multiple buffers, each buffer is filled completely before proceeding to the next. - -==== Buffer Lifetime - -The caller must ensure that the memory referenced by `buffers` remains valid until the `co_await` expression returns. - -==== Conforming Signatures - -[source,cpp] ----- -template -IoAwaitable auto read_some(Buffers buffers); - -template -IoAwaitable auto read(Buffers buffers); ----- - -== Concept Hierarchy - ----- -ReadStream { read_some } - | - v -ReadSource { read_some, read } ----- - -This mirrors the write side: - ----- -WriteStream { write_some } - | - v -WriteSink { write_some, write, write_eof(buffers), write_eof() } ----- - -Algorithms constrained on `ReadStream` accept both raw streams and sources. Algorithms that need the complete-read guarantee constrain on `ReadSource`. - -== Why ReadSource Refines ReadStream - -Every concrete `ReadSource` type has a natural `read_some`: - -- **HTTP content-length body**: `read_some` returns `min(available_from_network, remaining_content_length)` bytes. It is the underlying stream's `read_some` capped by the body's limit. -- **HTTP chunked body**: `read_some` delivers whatever unchunked data is available from the current chunk. -- **Decompression source** (inflate, zstd): `read_some` does one decompression pass -- feeds available compressed input to the decompressor and returns whatever output is produced. This is how `zlib::inflate()` naturally works. -- **File source**: `read_some` is a single `read()` syscall. It is the OS primitive. -- **Memory source**: `read_some` returns `min(requested, remaining)`. - -No concrete source type lacks a meaningful `read_some`. The claim that "many sources can't meaningfully offer `read_some`" does not hold up under scrutiny. - -=== The Relay Argument - -If `ReadSource` were disjoint from `ReadStream`, generic relay code would need two separate implementations: - -[source,cpp] ----- -// One for ReadStream sources -template -task<> relay(Src& src, Dest& dest); - -// A different one for ReadSource sources -template -task<> relay(Src& src, Dest& dest); ----- - -With the refinement relationship, one function handles both: - -[source,cpp] ----- -// Works for TCP sockets, HTTP bodies, decompressors, files -template -task<> relay(Src& src, Dest& dest); ----- - -This is the same argument that justified `WriteSink` refining `WriteStream`. - -=== The Latency Argument - -With only `read` (complete read), a relay must wait for the entire buffer to fill before forwarding any data: - -[source,cpp] ----- -// Must fill 64KB before sending -- high latency -auto [ec, n] = co_await src.read(mutable_buffer(buf, 65536)); -co_await dest.write_some(const_buffer(buf, n)); ----- - -With `read_some`, data is forwarded as it becomes available: - -[source,cpp] ----- -// Returns with 1KB if that's what's available -- low latency -auto [ec, n] = co_await src.read_some(mutable_buffer(buf, 65536)); -co_await dest.write_some(const_buffer(buf, n)); ----- - -For a decompressor backed by a slow network connection, `read_some` lets you decompress and forward whatever is available instead of blocking until the entire buffer is filled. - -== Member Function Comparison - -[cols="1,1"] -|=== -| `read_some` | `read` - -| Returns whatever is available (at least 1 byte) -| Fills the entire buffer or errors - -| Low latency: forward data immediately -| Higher latency: waits for full buffer - -| Caller loops for complete reads -| Source guarantees completeness - -| Natural for relays and streaming -| Natural for fixed-size records and structured data -|=== - -== Use Cases - -=== Reading an HTTP Body - -An HTTP body with a known content length is a `ReadSource`. The caller reads into a buffer, and the source ensures exactly the right number of bytes are delivered. - -[source,cpp] ----- -template -task read_body(Source& body, std::size_t content_length) -{ - std::string result(content_length, '\0'); - auto [ec, n] = co_await body.read( - mutable_buffer(result.data(), result.size())); - if(ec) - { - result.resize(n); - co_return result; - } - co_return result; -} ----- - -=== Reading Fixed-Size Records from a Source - -When a source produces structured records of known size, `read` guarantees each record is completely filled. - -[source,cpp] ----- -struct record -{ - uint32_t id; - char data[256]; -}; - -template -task<> process_records(Source& source) -{ - for(;;) - { - record rec; - auto [ec, n] = co_await source.read( - mutable_buffer(&rec, sizeof(rec))); - if(ec == cond::eof) - co_return; - if(ec) - co_return; - - handle_record(rec); - } -} ----- - -=== Decompression with Low-Latency Relay - -A decompression source wraps a `ReadStream` and produces decompressed data. Using `read_some` (inherited from `ReadStream`), a relay can forward decompressed data as it becomes available instead of waiting for a full buffer. - -[source,cpp] ----- -template -task<> relay_decompressed(Source& inflater, Sink& dest) -{ - char buf[8192]; - for(;;) - { - // read_some: decompress whatever is available - auto [ec, n] = co_await inflater.read_some( - make_buffer(buf)); - if(ec == cond::eof) - { - auto [wec] = co_await dest.write_eof(); - co_return; - } - if(ec) - co_return; - - auto [wec, nw] = co_await dest.write( - const_buffer(buf, n)); - if(wec) - co_return; - } -} ----- - -=== Relaying from ReadSource to WriteSink - -When connecting a source to a sink, `read_some` provides low-latency forwarding. The final chunk uses `write_eof` for atomic delivery plus EOF signaling. - -[source,cpp] ----- -template -task<> relay(Src& src, Sink& dest) -{ - char buf[8192]; - for(;;) - { - auto [ec, n] = co_await src.read_some( - make_buffer(buf)); - - auto [wec, nw] = co_await dest.write( - const_buffer(buf, n)); - - if(wec) - co_return; - - if(ec == cond::eof) - { - auto [wec] = co_await dest.write_eof(); - co_return; - } - if(ec) - co_return; - } -} ----- - -Because `ReadSource` refines `ReadStream`, this relay accepts `ReadSource` types (HTTP bodies, decompressors, files) as well as raw `ReadStream` types (TCP sockets, TLS streams). - -=== Type-Erased Source - -The `any_read_source` wrapper type-erases a `ReadSource` behind a virtual interface. This is useful when the concrete source type is not known at compile time. - -== Conforming Types - -Examples of types that satisfy `ReadSource`: - -- **HTTP content-length body**: `read_some` returns available bytes capped by remaining length. `read` fills the buffer, enforcing the content length limit. -- **HTTP chunked body**: `read_some` delivers available unchunked data. `read` decodes chunk framing and fills the buffer. -- **Decompression source** (inflate, zstd): `read_some` does one decompression pass. `read` loops decompression until the buffer is filled. -- **File source**: `read_some` is a single `read()` syscall. `read` loops until the buffer is filled or EOF. -- **Memory source**: `read_some` returns available bytes. `read` fills the buffer from the memory region. - -== A Full Buffer Is Always Success - -The `read_some` contract (inherited from `ReadStream`) treats a completion that fills the buffer sequence as a success: `n == buffer_size(buffers)` implies `!ec`. An error is reported only when the transfer was incomplete, in which case `n \< buffer_size(buffers)`. A pending condition such as end-of-stream is deferred to the next read. See xref:9.design/9c.ReadStream.adoc#_design_foundations_why_a_full_buffer_is_always_success[ReadStream: Why a Full Buffer Is Always Success] for the full rationale. The key points: - -- A nonzero `ec` describes only a transfer that fell short of the requested size; it never qualifies a complete transfer. -- A condition observed exactly as the buffer fills is held for the next call, where `n` is necessarily less than the buffer size, rather than delivered alongside good bytes. -- Consumers test `n == buffer_size(buffers)` alone to know the read succeeded, and generic algorithms such as `when_all` and `when_any` distinguish a completed transfer from a failure on that basis. -- A layered stream (TLS, compression) keeps the deferred-condition bookkeeping internal, where it has the context to manage it. - -The `read` operation is a different primitive and uses the complete-read semantics described above: it either fills the entire buffer with `!ec`, or returns `ec` (including `cond::eof`) with `n` indicating the bytes transferred before the condition arose. - -== Summary - -`ReadSource` refines `ReadStream` by adding `read` for complete-read semantics. The refinement relationship enables: - -- Generic algorithms constrained on `ReadStream` work on both raw streams and sources. -- `read_some` provides low-latency forwarding in relays. -- `read` provides the complete-fill guarantee for structured data. - -[cols="1,2,2"] -|=== -| Function | Contract | Use Case - -| `ReadSource::read_some` -| Attempts to read up to `buffer_size(buffers)` bytes. May fill less than the buffer. -| Relays, low-latency forwarding, incremental processing. - -| `ReadSource::read` -| Fills the entire buffer or returns an error with partial count. -| HTTP bodies, decompression, file I/O, structured records. - -| `read` composed (on `ReadStream`) -| Loops `read_some` until the buffer is filled. -| Fixed-size headers, known-length messages over raw streams. -|=== diff --git a/doc/modules/ROOT/pages/9.design/9e.BufferSource.adoc b/doc/modules/ROOT/pages/9.design/9e.BufferSource.adoc deleted file mode 100644 index a9793ea91..000000000 --- a/doc/modules/ROOT/pages/9.design/9e.BufferSource.adoc +++ /dev/null @@ -1,388 +0,0 @@ -= BufferSource Concept Design - -== Overview - -This document describes the design of the `BufferSource` concept, the rationale behind each member function, and the relationship between `BufferSource`, `ReadSource`, and the `push_to` algorithm. `BufferSource` models the "callee owns buffers" pattern on the read side: the source exposes its internal storage as read-only buffers and the caller consumes data directly from them, enabling zero-copy data transfer. - -Where `ReadSource` requires the caller to supply mutable buffers for the source to fill, `BufferSource` inverts the ownership: the source provides read-only views into its own memory and the caller reads from them in place. The two concepts are independent -- neither refines the other -- but the type-erased wrapper `any_buffer_source` satisfies both, bridging the two patterns behind a single runtime interface. - -== Concept Definition - -[source,cpp] ----- -template -concept BufferSource = - requires(T& src, std::span dest, std::size_t n) - { - { src.pull(dest) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(src.pull(dest)), - std::error_code, std::span>; - src.consume(n); - }; ----- - -`BufferSource` is a standalone concept. It does not refine `ReadSource` or `ReadStream`. The two concept families model different ownership patterns and can coexist on the same concrete type. - -== Caller vs Callee Buffer Ownership - -The library provides two concept families for reading data: - -[cols="1,2,2"] -|=== -| Aspect | ReadSource (caller owns) | BufferSource (callee owns) - -| Buffer origin -| Caller allocates mutable buffers; source fills them. -| Source exposes its internal storage as read-only buffers; caller reads - from them. - -| Copy cost -| One copy: source's internal storage -> caller's buffer. -| Zero copies when the caller can process data in place (e.g., scanning, - hashing, forwarding to a `write_some` call). - -| API shape -| `read_some(buffers)`, `read(buffers)` -| `pull(dest)`, `consume(n)` - -| Natural for -| Callers that need to accumulate data into their own buffer (e.g., - parsing a fixed-size header into a struct). -| Sources backed by pre-existing memory (ring buffers, memory-mapped - files, decompression output buffers, kernel receive buffers). -|=== - -Both patterns are necessary. A memory-mapped file source naturally owns the mapped region; the caller reads directly from the mapped pages without copying. Conversely, an application that needs to fill a fixed-size header struct naturally provides its own mutable buffer for the source to fill. - -== Member Functions - -=== `pull(dest)` -- Expose Readable Buffers - -Fills the provided span with const buffer descriptors pointing to the source's internal storage. This operation is asynchronous because the source may need to perform I/O to produce data (e.g., reading from a socket, decompressing a block). - -==== Signature - -[source,cpp] ----- -IoAwaitable auto pull(std::span dest); ----- - -Returns `(error_code, std::span)`. - -==== Semantics - -- **Data available**: `!ec` and `bufs.size() > 0`. The returned span contains buffer descriptors pointing to readable data in the source's internal storage. -- **Source exhausted**: `ec == cond::eof` and `bufs.empty()`. No more data is available; the transfer is complete. -- **Error**: `ec` is `true` and `ec != cond::eof`. An error occurred. - -Calling `pull` multiple times without an intervening `consume` returns the same unconsumed data. This idempotency lets the caller inspect the data, decide how much to process, and then advance the position with `consume`. - -==== Why Asynchronous - -Unlike `BufferSink::prepare`, which is synchronous, `pull` is asynchronous. The asymmetry exists because the two operations have fundamentally different costs: - -- `prepare` returns pointers to _empty_ memory the sink already owns. No data movement is involved; it is pure bookkeeping. -- `pull` may need to _produce_ data before it can return buffer descriptors. A file source reads from disk. A decompression source feeds compressed input to the decompressor. A network source waits for data to arrive on a socket. These operations require I/O. - -Making `pull` synchronous would force the source to pre-buffer all data before the caller can begin consuming it, defeating the streaming model. - -==== Why a Span Parameter - -The caller provides the output span rather than the source returning a fixed-size container. This lets the caller control the stack allocation and avoids heap allocation for the buffer descriptor array: - -[source,cpp] ----- -const_buffer arr[16]; -auto [ec, bufs] = co_await source.pull(arr); ----- - -The source fills as many descriptors as it can (up to `dest.size()`) and returns the populated subspan. - -=== `consume(n)` -- Advance the Read Position - -Advances the source's internal read position by `n` bytes. The next call to `pull` returns data starting after the consumed bytes. This operation is synchronous. - -==== Signature - -[source,cpp] ----- -void consume(std::size_t n) noexcept; ----- - -==== Semantics - -- Advances the read position by `n` bytes. -- `n` must not exceed the total size of the buffers returned by the most recent `pull`. -- After `consume`, the buffers returned by the prior `pull` are invalidated. The caller must call `pull` again to obtain new buffer descriptors. - -==== Why Synchronous - -`consume` is synchronous because it is pure bookkeeping: advancing an offset or releasing a reference. No I/O is involved. The asynchronous work (producing data, performing I/O) happens in `pull`. - -==== Why Separate from `pull` - -Separating `consume` from `pull` gives the caller explicit control over how much data to process before advancing: - -[source,cpp] ----- -const_buffer arr[16]; -auto [ec, bufs] = co_await source.pull(arr); -if(!ec) -{ - // Process some of the data - auto n = process(bufs); - source.consume(n); - // Remaining data returned by next pull -} ----- - -This is essential for partial processing. A parser may examine the pulled data, find that it contains an incomplete message, and consume only the complete portion. The next `pull` returns the remainder prepended to any newly available data. - -If `pull` automatically consumed all returned data, the caller would need to buffer unconsumed bytes itself, defeating the zero-copy benefit. - -== The Pull/Consume Protocol - -The `pull` and `consume` functions form a two-phase read protocol: - -1. **Pull**: the source provides data (async, may involve I/O). -2. **Inspect**: the caller examines the returned buffers. -3. **Consume**: the caller indicates how many bytes were used (sync). -4. **Repeat**: the next `pull` returns data starting after the consumed bytes. - -This protocol enables several patterns that a single-call interface cannot: - -- **Partial consumption**: consume less than what was pulled. The remainder is returned by the next `pull`. -- **Peek**: call `pull` to inspect data without consuming it. Call `pull` again (without `consume`) to get the same data. -- **Scatter writes**: pull once, write the returned buffers to multiple destinations (e.g., `write_some` to a socket), and consume only the bytes that were successfully written. - -== Relationship to `push_to` - -`push_to` is a composed algorithm that transfers data from a `BufferSource` to a `WriteSink` (or `WriteStream`). It is the callee-owns-buffers counterpart to `pull_from`, which transfers from a `ReadSource` (or `ReadStream`) to a `BufferSink`. - -[source,cpp] ----- -template -io_task -push_to(Src& source, Sink& sink); - -template -io_task -push_to(Src& source, Stream& stream); ----- - -The algorithm loops: - -1. Call `source.pull(arr)` to get readable buffers. -2. Write the data to the sink via `sink.write(bufs)` or `stream.write_some(bufs)`. -3. Call `source.consume(n)` to advance past the written bytes. -4. When `pull` signals EOF, call `sink.write_eof()` to finalize the sink (WriteSink overload only). - -The two `push_to` overloads differ in how they write to the destination: - -[cols="1,2"] -|=== -| Overload | Behavior - -| `push_to(BufferSource, WriteSink)` -| Uses `sink.write(bufs)` for complete writes. Each iteration delivers - all pulled data. On EOF, calls `sink.write_eof()` to finalize. - -| `push_to(BufferSource, WriteStream)` -| Uses `stream.write_some(bufs)` for partial writes. Consumes only the - bytes that were actually written, providing backpressure. Does not - signal EOF (WriteStream has no EOF mechanism). -|=== - -`push_to` is the right tool when the data source satisfies `BufferSource` and the destination satisfies `WriteSink` or `WriteStream`. The source's internal buffers are passed directly to the write call, avoiding any intermediate caller-owned buffer. - -== Relationship to `ReadSource` - -`BufferSource` and `ReadSource` are independent concepts serving different ownership models. A concrete type may satisfy one, the other, or both. - -The type-erased wrapper `any_buffer_source` satisfies both concepts. When the wrapped type satisfies only `BufferSource`, the `ReadSource` operations (`read_some`, `read`) are synthesized from `pull` and `consume` with a `buffer_copy` step: the wrapper pulls data from the underlying source, copies it into the caller's mutable buffers, and consumes the copied bytes. - -When the wrapped type satisfies both `BufferSource` and `ReadSource`, the native `read_some` and `read` implementations are forwarded directly across the type-erased boundary, avoiding the extra copy. This dispatch is determined at compile time when the vtable is constructed; at runtime the wrapper checks a single nullable function pointer to select the forwarding path. - -This dual-concept bridge lets algorithms constrained on `ReadSource` work with any `BufferSource` through `any_buffer_source`, and lets algorithms constrained on `BufferSource` work natively with the callee-owns-buffers pattern. - -=== Transfer Algorithm Matrix - -[cols="1,1,1"] -|=== -| Source | Sink | Algorithm - -| `BufferSource` -| `WriteSink` -| `push_to` -- pulls from source, writes to sink, signals EOF - -| `BufferSource` -| `WriteStream` -| `push_to` -- pulls from source, writes partial to stream - -| `ReadSource` -| `BufferSink` -| `pull_from` -- prepares sink buffers, reads into them - -| `ReadStream` -| `BufferSink` -| `pull_from` -- prepares sink buffers, reads partial into them -|=== - -== Use Cases - -=== Zero-Copy Transfer to a Socket - -When the source's internal storage already contains the data to send, `push_to` passes the source's buffers directly to the socket's `write_some`, avoiding any intermediate copy. - -[source,cpp] ----- -template -task<> send_all(Source& source, Stream& socket) -{ - auto [ec, total] = co_await push_to(source, socket); - if(ec) - co_return; - // total bytes sent directly from source's internal buffers -} ----- - -=== Memory-Mapped File Source - -A memory-mapped file is a natural `BufferSource`. The `pull` operation returns buffer descriptors pointing directly into the mapped region. No data is copied until the consumer explicitly copies it. - -[source,cpp] ----- -template -task<> serve_static_file(Source& mmap_source, Sink& response) -{ - auto [ec, total] = co_await push_to(mmap_source, response); - if(ec) - co_return; - // File served via zero-copy from mapped pages -} ----- - -=== Partial Consumption with a Parser - -A protocol parser pulls data, parses as much as it can, and consumes only the parsed portion. The next `pull` returns the unparsed remainder plus any newly arrived data. - -[source,cpp] ----- -template -task parse_message(Source& source) -{ - const_buffer arr[16]; - message msg; - - for(;;) - { - auto [ec, bufs] = co_await source.pull(arr); - if(ec) - co_return msg; - - auto [parsed, complete] = msg.parse(bufs); - source.consume(parsed); - - if(complete) - co_return msg; - } -} ----- - -The parser consumes only the bytes it understood. If a message spans two `pull` calls, the unconsumed tail from the first call is returned at the start of the second. - -=== HTTP Request Body Source - -An HTTP request body can be exposed through a `BufferSource` interface. The concrete implementation handles transfer encoding (content-length, chunked, compressed) behind the abstraction. - -[source,cpp] ----- -task<> handle_request( - any_buffer_source& body, - WriteSink auto& response) -{ - auto [ec, total] = co_await push_to(body, response); - if(ec) - co_return; - // Request body forwarded to response sink -} ----- - -The caller does not know whether the body uses content-length, chunked encoding, or compression. The `BufferSource` interface handles the difference. - -=== Bridging to ReadSource via `any_buffer_source` - -When a function is constrained on `ReadSource` but the concrete type satisfies only `BufferSource`, `any_buffer_source` bridges the gap. - -[source,cpp] ----- -template -task read_all(Source& source); - -// Concrete type satisfies BufferSource only -my_ring_buffer ring; -any_buffer_source abs(ring); - -// Works: any_buffer_source satisfies ReadSource -auto data = co_await read_all(abs); ----- - -The `read_some` and `read` methods pull data internally, copy it into the caller's mutable buffers, and consume the copied bytes. This incurs one buffer copy compared to using `pull` and `consume` directly. - -== Alternatives Considered - -=== Single `pull` That Auto-Consumes - -An earlier design had `pull` automatically consume all returned data, eliminating the separate `consume` call. This was rejected because: - -- Partial consumption becomes impossible. A parser that finds an incomplete message at the end of a pull would need to buffer the remainder itself, negating the zero-copy benefit. -- Peek semantics (inspecting data without consuming it) require the source to maintain a separate undo mechanism. -- The `WriteStream::write_some` pattern naturally consumes only `n` bytes, so the remaining pulled data must survive for the next `write_some` call. Without `consume`, the source would need to track how much of its own returned data was actually used. - -=== `pull` Returning an Owned Container - -A design where `pull` returned a `std::vector` or similar owned container was considered. This was rejected because: - -- Heap allocation on every pull is unacceptable for high-throughput I/O paths. -- The span-based interface lets the caller control storage: a stack-allocated array for the common case, or a heap-allocated array for unusual situations. -- Returning a subspan of the caller's span is zero-overhead and composes naturally with existing buffer algorithm interfaces. - -=== Synchronous `pull` - -Making `pull` synchronous (like `BufferSink::prepare`) was considered. This was rejected because: - -- A source may need to perform I/O to produce data. A file source reads from disk. A decompression source feeds compressed input to the decompressor. A network source waits for data to arrive. -- Forcing synchronous `pull` would require the source to pre-buffer all data before the caller starts consuming, breaking the streaming model and inflating memory usage. -- The asymmetry with `prepare` is intentional: `prepare` returns pointers to empty memory (no I/O needed), while `pull` returns pointers to data that may need to be produced first. - -=== `BufferSource` Refining `ReadSource` - -A design where `BufferSource` refined `ReadSource` (requiring all types to implement `read_some` and `read`) was considered. This was rejected because: - -- Many natural `BufferSource` types (memory-mapped files, ring buffers, DMA receive descriptors) have no meaningful `read_some` primitive. Their data path is pull-then-consume, not read-into-caller-buffer. -- Requiring `read_some` and `read` on every `BufferSource` would force implementations to synthesize these operations even when they are never called. -- The `any_buffer_source` wrapper provides the bridge when needed, without burdening every concrete type. - -=== Combined Pull-and-Consume - -A design with a single `read(dest) -> (error_code, span)` that both pulled and advanced the position was considered. This is equivalent to the auto-consume alternative above and was rejected for the same reasons: it prevents partial consumption and peek semantics. - -== Summary - -[cols="1,2,2"] -|=== -| Function | Contract | Use Case - -| `pull(dest)` -| Async. Fills span with readable buffer descriptors from the source's - internal storage. Returns EOF when exhausted. -| Every read iteration: obtain data to process or forward. - -| `consume(n)` -| Sync. Advances the read position by `n` bytes. Invalidates prior - buffers. -| After processing or forwarding data: indicate how much was used. -|=== - -`BufferSource` is the callee-owns-buffers counterpart to `ReadSource`. The `push_to` algorithm transfers data from a `BufferSource` to a `WriteSink` or `WriteStream`, and `any_buffer_source` bridges the two patterns by satisfying both `BufferSource` and `ReadSource` behind a single type-erased interface. diff --git a/doc/modules/ROOT/pages/9.design/9g.WriteSink.adoc b/doc/modules/ROOT/pages/9.design/9g.WriteSink.adoc deleted file mode 100644 index db8fefd1b..000000000 --- a/doc/modules/ROOT/pages/9.design/9g.WriteSink.adoc +++ /dev/null @@ -1,376 +0,0 @@ -= WriteSink Concept Design - -== Overview - -This document describes the design of the `WriteSink` concept, the rationale behind each member function, and the relationship between `WriteSink`, `WriteStream`, and the `write_now` algorithm. The design was arrived at through deliberation over several alternative approaches, each of which is discussed here with its trade-offs. - -== Concept Hierarchy - -The write-side concept hierarchy consists of two concepts: - -[source,cpp] ----- -// Partial write primitive -template -concept WriteStream = - requires(T& stream, const_buffer_archetype buffers) - { - { stream.write_some(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(stream.write_some(buffers)), - std::error_code, std::size_t>; - }; - -// Complete write with EOF signaling -template -concept WriteSink = - WriteStream && - requires(T& sink, const_buffer_archetype buffers) - { - { sink.write(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.write(buffers)), - std::error_code, std::size_t>; - { sink.write_eof(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.write_eof(buffers)), - std::error_code, std::size_t>; - { sink.write_eof() } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.write_eof()), - std::error_code>; - }; ----- - -`WriteSink` refines `WriteStream`. Every `WriteSink` is a `WriteStream`. Algorithms constrained on `WriteStream` accept both raw streams and sinks. - -== Member Functions - -=== `write_some(buffers)` -- Partial Write - -Attempts to write up to `buffer_size(buffers)` bytes from the buffer sequence to the stream. May consume less than the full sequence. Returns `(error_code, std::size_t)` where `n` is the number of bytes written. - -This is the low-level primitive inherited from `WriteStream`. It is appropriate when the caller manages its own consumption loop or when forwarding data incrementally without needing a complete-write guarantee. - -==== Semantics - -If `buffer_size(buffers) > 0`: - -- If `!ec`, then `n >= 1 && n \<= buffer_size(buffers)`. `n` bytes were written from the buffer sequence. -- If `ec`, then `n >= 0 && n \< buffer_size(buffers)`. `n` is the number of bytes written before the I/O condition arose. - -Equivalently, `n == buffer_size(buffers)` implies `!ec`: a completion that writes the entire buffer sequence is a success, even when the underlying operation also signals a condition. That condition is reported on a subsequent write. - -If `buffer_empty(buffers)` is true, `n` is 0. The empty buffer is not itself a cause for error, but `ec` may reflect the state of the stream. - -==== When to Use - -- Relay interiors: forwarding chunks of data as they arrive without waiting for the entire payload to be consumed. -- Backpressure-aware pipelines: writing what the destination can accept and returning control to the caller. -- Implementing `write` or `write_now` on top of the primitive. - -=== `write(buffers)` -- Complete Write - -Writes the entire buffer sequence. All bytes are consumed before the operation completes. Returns `(error_code, std::size_t)` where `n` is the number of bytes written. - -==== Semantics - -- On success: `!ec`, `n == buffer_size(buffers)`. -- On error: `ec`, `n` is the number of bytes written before the error occurred. -- If `buffer_empty(buffers)`: completes immediately, `!ec`, `n == 0`. - -==== When to Use - -- Writing complete protocol messages or frames. -- Serializing structured data where each fragment must be fully delivered before producing the next. -- Any context where partial delivery is not meaningful. - -==== Why `write` Belongs in the Concept - -For many concrete types, `write` is the natural primitive, not a loop over `write_some`: - -- **File sinks**: the OS `write` call is the primitive. `write_some` would simply delegate to `write`. -- **Buffered writers**: `write` is a `memcpy` into the circular buffer (or drain-then-copy). It is not a loop over `write_some`. -- **Compression sinks** (deflate, zstd): `write` feeds data to the compressor and flushes the output. The internal operation is a single compression call, not iterated partial writes. - -Requiring `write` in the concept lets each type implement the operation in the way that is natural and efficient for that type. - -=== `write_eof(buffers)` -- Atomic Final Write - -Writes the entire buffer sequence and then signals end-of-stream, as a single atomic operation. Returns `(error_code, std::size_t)` where `n` is the number of bytes written. - -After a successful call, no further writes or EOF signals are permitted. - -==== Semantics - -- On success: `!ec`, `n == buffer_size(buffers)`. The sink is finalized. -- On error: `ec`, `n` is bytes written before the error. The sink state is unspecified. - -==== Why Atomicity Matters - -Combining the final write with the EOF signal in a single operation enables optimizations that two separate calls cannot: - -- **HTTP chunked encoding**: `write_eof(data)` can emit the data chunk followed by the terminal `0\r\n\r\n` in a single system call. Calling `write(data)` then `write_eof()` separately forces two calls and may result in two TCP segments. -- **Compression (deflate)**: `write_eof(data)` can pass `Z_FINISH` to the final `deflate()` call, producing the compressed data and the stream trailer together. Separate `write` + `write_eof` would require an extra flush. -- **TLS close-notify**: `write_eof(data)` can coalesce the final application data with the TLS close-notify alert. - -This optimization cannot be achieved by splitting the operation into `write(data)` followed by `write_eof()`. - -=== `write_eof()` -- Bare EOF Signal - -Signals end-of-stream without writing any data. Returns `(error_code)`. - -After a successful call, no further writes or EOF signals are permitted. - -==== Semantics - -- On success: `!ec`. The sink is finalized. -- On error: `ec`. - -==== When to Use - -When the final data has already been written via `write` or `write_some` and only the EOF signal remains. This is less common than `write_eof(buffers)` but necessary when the data and EOF are produced at different times. - -== Relationship to `write_now` - -`write_now` is a composed algorithm that operates on any `WriteStream`. It loops `write_some` until the entire buffer sequence is consumed. It has two properties that a plain `write_some` loop does not: - -1. **Eager completion**: if every `write_some` call completes synchronously (returns `true` from `await_ready`), the entire `write_now` operation completes in `await_ready` with zero coroutine suspensions. -2. **Frame caching**: the internal coroutine frame is cached and reused across calls, eliminating repeated allocation. - -`write_now` is the right tool for code constrained on `WriteStream` alone (for example, writing to a raw TCP socket). Code constrained on `WriteSink` should use `write` directly, because the concrete type's `write` may be more efficient than looping `write_some`, and because `write_now` cannot replicate the atomic `write_eof(buffers)` operation. - -== Use Cases - -=== Serializing Structured Data - -When producing output fragment by fragment (e.g., JSON serialization), each fragment must be fully consumed before the next is produced. The final fragment signals EOF. - -[source,cpp] ----- -template -task<> serialize_json(Sink& sink, json::value const& jv) -{ - auto [ec1, n1] = co_await sink.write(make_buffer("{")); - if(ec1) - co_return; - - auto body = serialize_fields(jv); - auto [ec2, n2] = co_await sink.write(make_buffer(body)); - if(ec2) - co_return; - - auto [ec3, n3] = co_await sink.write_eof(make_buffer("}")); - if(ec3) - co_return; -} ----- - -Here `write` guarantees each fragment is fully delivered, and `write_eof` atomically writes the closing brace and finalizes the sink. - -=== Relaying a Streaming Body - -When forwarding data from a source to a sink, the interior chunks use `write_some` for incremental progress. The final chunk uses `write_eof` for atomic delivery plus EOF. - -[source,cpp] ----- -template -task<> relay(Source& src, Sink& dest) -{ - char buf[8192]; - for(;;) - { - auto [ec, n] = co_await src.read_some( - make_buffer(buf)); - - // Forward whatever arrived before checking the error - std::size_t written = 0; - while(written < n) - { - auto [ec2, n2] = co_await dest.write_some( - const_buffer(buf + written, n - written)); - if(ec2) - co_return; - written += n2; - } - - if(ec == cond::eof) - { - auto [ec2] = co_await dest.write_eof(); - co_return; - } - if(ec) - co_return; - } -} ----- - -The interior loop uses `write_some` because the relay does not need complete-write guarantees for intermediate data. When `read_some` returns EOF, any partial bytes are forwarded first, then the relay signals EOF via `write_eof()` with no data. - -=== Writing Complete Messages - -When sending discrete messages where each must be fully delivered, `write` is the natural choice. - -[source,cpp] ----- -template -task<> send_messages(Sink& sink, std::span messages) -{ - for(auto const& msg : messages) - { - auto [ec, n] = co_await sink.write(make_buffer(msg)); - if(ec) - co_return; - } - auto [ec] = co_await sink.write_eof(); - if(ec) - co_return; -} ----- - -=== HTTP Response Body - -An HTTP response handler writes the body through a type-erased sink. The concrete implementation handles transfer encoding (content-length, chunked, compressed) behind the `WriteSink` interface. - -[source,cpp] ----- -task<> send_response(any_write_sink& body, response const& resp) -{ - // Write headers portion of body - auto headers = format_headers(resp); - auto [ec1, n1] = co_await body.write(make_buffer(headers)); - if(ec1) - co_return; - - // Write body with EOF - auto [ec2, n2] = co_await body.write_eof( - make_buffer(resp.body)); - if(ec2) - co_return; -} ----- - -The caller does not know whether the body is content-length, chunked, or compressed. The `WriteSink` interface handles the difference. - -=== Compression Pipeline - -A deflate sink wraps an underlying `WriteStream` and compresses data on the fly. `write_eof` sets `Z_FINISH` on the final deflate call. - -[source,cpp] ----- -template -task<> compress_and_send(Sink& sink, std::string_view data) -{ - // Write compressed data - auto [ec, n] = co_await sink.write_eof(make_buffer(data)); - if(ec) - co_return; - // sink.write_eof triggered Z_FINISH internally, - // flushing the final compressed block and trailer -} ----- - -=== Buffered Writer - -A buffered writer interposes a buffer between the caller and the underlying stream. `write_some` appends to the buffer without draining. `write` ensures all data is buffered (draining if necessary). `write_eof` flushes the buffer and signals EOF to the underlying stream. - -[source,cpp] ----- -template -task<> buffered_output(Sink& sink) -{ - // Small writes accumulate in the buffer - auto [ec1, n1] = co_await sink.write(make_buffer("key: ")); - if(ec1) - co_return; - - auto [ec2, n2] = co_await sink.write(make_buffer("value\r\n")); - if(ec2) - co_return; - - // Final write flushes buffer + signals EOF - auto [ec3, n3] = co_await sink.write_eof( - make_buffer("end\r\n")); - if(ec3) - co_return; -} ----- - -=== Raw Stream with `write_now` - -When only a `WriteStream` is available (no EOF signaling needed), the `write_now` algorithm provides complete-write behavior with eager completion and frame caching. - -[source,cpp] ----- -template -task<> send_data(Stream& stream) -{ - write_now wn(stream); - - auto [ec1, n1] = co_await wn(make_buffer("hello")); - if(ec1) - co_return; - - // Frame is cached; no allocation on second call - auto [ec2, n2] = co_await wn(make_buffer("world")); - if(ec2) - co_return; -} ----- - -Because `WriteSink` refines `WriteStream`, `write_now` also works on sinks. This can be useful when a function is generic over `WriteStream` and does not need EOF signaling. - -== Alternatives Considered - -=== WriteSink with Only `write` and `write_eof` - -The initial design had `WriteSink` require only `write(buffers)`, `write(buffers, bool eof)`, and `write_eof()`, with no `write_some`. This made `WriteSink` disjoint from `WriteStream`: a function constrained on `WriteStream` (using `write_some`) could not accept a `WriteSink`, and vice versa. - -This was rejected because it prevents generic algorithms from working across both streams and sinks. The refinement relationship (`WriteSink` refines `WriteStream`) is strictly more useful. - -=== WriteSink with Only `write_some` and `write_eof` - -A minimal design was considered where `WriteSink` required only `write_some` and `write_eof`, with callers using `write_now` for complete-write behavior. This approach has three problems: - -1. **No atomic final write**: `write_now` over `write_some` followed by `write_eof()` is two operations. This prevents concrete types from coalescing the final data with the EOF signal (chunked encoding, compression trailers, TLS close-notify). - -2. **`write` is the natural primitive for many types**: files, buffered writers, and compression sinks implement `write` directly, not as a loop over `write_some`. Forcing these types to express complete-write semantics through a function called `write_some` is semantically misleading. - -3. **Implementation burden on callers**: every caller that needs complete-write behavior must construct a `write_now` object and manage it, rather than calling `sink.write(buffers)` directly. - -=== `write(buffers, bool eof)` Instead of `write_eof(buffers)` - -An earlier version used `write(buffers, bool eof)` to combine data writing with optional EOF signaling. This was replaced by `write_eof(buffers)` because: - -- Boolean parameters are opaque at the call site. `write(data, true)` does not convey intent as clearly as `write_eof(data)`. -- `write_eof` is self-documenting: the name states that EOF is signaled. -- No risk of accidentally passing the wrong boolean value. - -=== Three-Concept Hierarchy (`WriteStream` / `WriteCloser` / `WriteSink`) - -A three-level hierarchy was considered, with an intermediate concept (`WriteCloser` or similar) requiring `write_some` + `write_eof` but not `write`. This was rejected because the intermediate concept serves no practical purpose: any concrete type that has `write_some` and `write_eof` can and should provide `write`. There is no use case where a type offers partial writes and EOF signaling but cannot offer complete writes. - -== Summary - -[cols="1,2,2"] -|=== -| Function | Contract | Use Case - -| `write_some(buffers)` -| Attempts to write up to `buffer_size(buffers)` bytes. May consume less than the full sequence. -| Relay interiors, backpressure, implementing composed algorithms. - -| `write(buffers)` -| Writes the entire buffer sequence. -| Protocol messages, serialization, structured output. - -| `write_eof(buffers)` -| Writes the entire buffer sequence and signals EOF atomically. -| Final chunk of a relay, last fragment of serialized output. - -| `write_eof()` -| Signals EOF without data. -| When the final data was already written separately. -|=== - -`WriteSink` refines `WriteStream`. The `write_now` algorithm operates on any `WriteStream` and provides complete-write behavior with eager completion and frame caching, but it cannot replicate the atomic `write_eof(buffers)` that `WriteSink` enables. diff --git a/doc/modules/ROOT/pages/9.design/9h.BufferSink.adoc b/doc/modules/ROOT/pages/9.design/9h.BufferSink.adoc deleted file mode 100644 index 5eb26abc4..000000000 --- a/doc/modules/ROOT/pages/9.design/9h.BufferSink.adoc +++ /dev/null @@ -1,434 +0,0 @@ -= BufferSink Concept Design - -== Overview - -This document describes the design of the `BufferSink` concept, the rationale behind each member function, and the relationship between `BufferSink`, `WriteSink`, and the `pull_from` algorithm. `BufferSink` models the "callee owns buffers" pattern: the sink provides writable memory and the caller writes directly into it, enabling zero-copy data transfer. - -Where `WriteSink` requires the caller to supply buffer sequences containing the data to be written, `BufferSink` inverts the ownership: the sink exposes its internal storage and the caller fills it in place. The two concepts are independent -- neither refines the other -- but the type-erased wrapper `any_buffer_sink` satisfies both, bridging the two patterns behind a single runtime interface. - -== Concept Definition - -[source,cpp] ----- -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) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.commit(n)), - std::error_code>; - - // Async: commit n final bytes and signal end of data - { sink.commit_eof(n) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.commit_eof(n)), - std::error_code>; - }; ----- - -`BufferSink` is a standalone concept. It does not refine `WriteSink` or `WriteStream`. The two concept families model different ownership patterns and can coexist on the same concrete type. - -== Caller vs Callee Buffer Ownership - -The library provides two concept families for writing data: - -[cols="1,2,2"] -|=== -| Aspect | WriteSink (caller owns) | BufferSink (callee owns) - -| Buffer origin -| Caller allocates and fills buffers, then passes them to the sink. -| Sink exposes its internal storage; caller writes into it. - -| Copy cost -| One copy: caller's buffer -> sink's internal storage (or I/O submission). -| Zero copies when the sink's internal storage is the final destination - (e.g., a ring buffer, kernel page, or DMA region). - -| API shape -| `write_some(buffers)`, `write(buffers)`, `write_eof(buffers)` -| `prepare(dest)`, `commit(n)`, `commit_eof(n)` - -| Natural for -| Protocol serializers that produce data into their own buffers, then hand - it off. -| Sinks backed by pre-allocated memory (ring buffers, memory-mapped files, - hardware DMA descriptors). -|=== - -Both patterns are necessary. A compression sink, for example, naturally owns the output buffer where compressed data lands; the caller feeds uncompressed data and the compressor writes results directly into the ring buffer. Conversely, an HTTP serializer naturally produces header bytes into its own scratch space and then hands the buffer sequence to a `WriteSink`. - -== Member Functions - -=== `prepare(dest)` -- Expose Writable Buffers - -Fills the provided span with mutable buffer descriptors pointing to the sink's internal storage. This operation is synchronous. - -==== Signature - -[source,cpp] ----- -std::span prepare(std::span dest); ----- - -==== Semantics - -- Returns a (possibly empty) subspan of `dest` populated with buffer descriptors. Each descriptor points to a writable region of the sink's internal storage. -- If the returned span is empty, the sink has no available space. The caller should call `commit` (possibly with `n == 0`) to flush buffered data and then retry `prepare`. -- The returned buffers remain valid until the next call to `prepare`, `commit`, `commit_eof`, or until the sink is destroyed. - -==== Why Synchronous - -`prepare` is synchronous because it is a bookkeeping operation: the sink returns pointers into memory it already owns. No I/O or blocking is involved. Making `prepare` asynchronous would force a coroutine suspension on every iteration of the write loop, adding overhead with no benefit. - -When the sink has no available space, the correct response is to `commit` the pending data (which _is_ asynchronous, as it may trigger I/O), then call `prepare` again. This keeps the synchronous fast path free of unnecessary suspensions. - -==== Why a Span Parameter - -The caller provides the output span rather than the sink returning a fixed-size container. This lets the caller control the stack allocation and avoids heap allocation for the buffer descriptor array: - -[source,cpp] ----- -mutable_buffer arr[16]; -auto bufs = sink.prepare(arr); ----- - -The sink fills as many descriptors as it can (up to `dest.size()`) and returns the populated subspan. - -=== `commit(n)` -- Finalize Written Data - -Commits `n` bytes that the caller wrote into the buffers returned by the most recent `prepare`. Returns `(error_code)`. - -==== Semantics - -- On success: `!ec`. -- On error: `ec`. -- May trigger underlying I/O (flush to socket, compression pass, etc.). -- After `commit`, the buffers returned by the prior `prepare` are invalidated. The caller must call `prepare` again before writing more data. - -==== When to Use - -- After writing data into prepared buffers and needing to continue the transfer. -- To flush when `prepare` returns an empty span (call `commit(0)` to drain the sink's internal buffer and free space). - -=== `commit_eof(n)` -- Commit Final Data and Signal EOF - -Commits `n` bytes written to the most recent `prepare` buffers and signals end-of-stream. Returns `(error_code)`. - -After a successful call, no further `prepare`, `commit`, or `commit_eof` operations are permitted. - -==== Semantics - -- On success: `!ec`. The sink is finalized. -- On error: `ec`. The sink state is unspecified. - -==== Why `commit_eof` Takes a Byte Count - -Combining the final commit with the EOF signal in a single operation enables the same optimizations that motivate `write_eof(buffers)` on the `WriteSink` side: - -- **HTTP chunked encoding**: `commit_eof(n)` can emit the data chunk followed by the terminal `0\r\n\r\n` in a single system call. -- **Compression (deflate)**: `commit_eof(n)` can pass `Z_FINISH` to the final `deflate()` call, producing the compressed data and the stream trailer together. -- **TLS close-notify**: `commit_eof(n)` can coalesce the final application data with the TLS close-notify alert. - -A separate `commit(n)` followed by `commit_eof(0)` would prevent these optimizations because the sink cannot know during `commit` that no more data will follow. - -== Relationship to `pull_from` - -`pull_from` is a composed algorithm that transfers data from a `ReadSource` (or `ReadStream`) into a `BufferSink`. It is the callee-owns-buffers counterpart to `push_to`, which transfers from a `BufferSource` to a `WriteSink`. - -[source,cpp] ----- -template -io_task -pull_from(Src& source, Sink& sink); - -template -io_task -pull_from(Src& source, Sink& sink); ----- - -The algorithm loops: - -1. Call `sink.prepare(arr)` to get writable buffers. -2. Call `source.read(bufs)` (or `source.read_some(bufs)`) to fill them. -3. Call `sink.commit(n)` to finalize the data. -4. When the source signals EOF, call `sink.commit_eof(0)` to finalize the sink. - -`pull_from` is the right tool when the data source satisfies `ReadSource` or `ReadStream` and the destination satisfies `BufferSink`. It avoids the intermediate caller-owned buffer that a `WriteSink`-based transfer would require. - -The two `pull_from` overloads differ in how they read from the source: - -[cols="1,2"] -|=== -| Overload | Behavior - -| `pull_from(ReadSource, BufferSink)` -| Uses `source.read(bufs)` for complete reads. Each iteration fills the - prepared buffers entirely (or returns EOF/error). - -| `pull_from(ReadStream, BufferSink)` -| Uses `source.read_some(bufs)` for partial reads. Each iteration - commits whatever data was available, providing lower latency. -|=== - -== Relationship to `WriteSink` - -`BufferSink` and `WriteSink` are independent concepts serving different ownership models. A concrete type may satisfy one, the other, or both. - -The type-erased wrapper `any_buffer_sink` satisfies both concepts. When the wrapped type satisfies only `BufferSink`, the `WriteSink` operations (`write_some`, `write`, `write_eof`) are synthesized from `prepare` and `commit` with a `buffer_copy` step. When the wrapped type satisfies both `BufferSink` and `WriteSink`, the native write operations are forwarded directly through the virtual boundary with no extra copy. - -This dual-concept bridge lets algorithms constrained on `WriteSink` work with any `BufferSink` through `any_buffer_sink`, and lets algorithms constrained on `BufferSink` work natively with the callee-owns-buffers pattern. - -=== Transfer Algorithm Matrix - -[cols="1,1,1"] -|=== -| Source | Sink | Algorithm - -| `BufferSource` -| `WriteSink` -| `push_to` -- pulls from source, writes to sink - -| `BufferSource` -| `WriteStream` -| `push_to` -- pulls from source, writes partial to stream - -| `ReadSource` -| `BufferSink` -| `pull_from` -- prepares sink buffers, reads into them - -| `ReadStream` -| `BufferSink` -| `pull_from` -- prepares sink buffers, reads partial into them -|=== - -== Use Cases - -=== Zero-Copy Transfer - -When the sink's internal storage is the final destination (a ring buffer, a kernel page, a DMA region), the caller writes directly into it with no intermediate copy. - -[source,cpp] ----- -template -task<> fill_sink(Sink& sink, std::string_view data) -{ - std::size_t written = 0; - while(written < data.size()) - { - mutable_buffer arr[16]; - auto bufs = sink.prepare(arr); - if(bufs.empty()) - { - auto [ec] = co_await sink.commit(0); - if(ec) - co_return; - continue; - } - - std::size_t n = buffer_copy( - bufs, - const_buffer( - data.data() + written, - data.size() - written)); - written += n; - - if(written == data.size()) - { - auto [ec] = co_await sink.commit_eof(n); - if(ec) - co_return; - } - else - { - auto [ec] = co_await sink.commit(n); - if(ec) - co_return; - } - } -} ----- - -=== Transferring from a ReadSource - -The `pull_from` algorithm reads data directly into the sink's buffers, avoiding a caller-owned intermediate buffer entirely. - -[source,cpp] ----- -template -task<> transfer(Source& source, Sink& sink) -{ - auto [ec, total] = co_await pull_from(source, sink); - if(ec) - co_return; - // total bytes transferred with zero intermediate copies -} ----- - -Compare with the `WriteSink` approach, which requires an intermediate buffer: - -[source,cpp] ----- -template -task<> transfer(Source& source, Sink& sink) -{ - char buf[8192]; // intermediate buffer - for(;;) - { - auto [ec, n] = co_await source.read_some( - make_buffer(buf)); - if(ec == cond::eof) - { - auto [wec] = co_await sink.write_eof(); - co_return; - } - if(ec) - co_return; - auto [wec, nw] = co_await sink.write( - const_buffer(buf, n)); - if(wec) - co_return; - } -} ----- - -The `BufferSink` path eliminates the `buf[8192]` intermediate buffer. - -=== HTTP Response Body Sink - -An HTTP response body can be consumed through a `BufferSink` interface. The concrete implementation handles transfer encoding behind the abstraction. - -[source,cpp] ----- -task<> receive_body( - any_buffer_sink& body, - ReadSource auto& source) -{ - auto [ec, n] = co_await pull_from(source, body); - if(ec) - co_return; - // Body fully received and committed -} ----- - -The caller does not know whether the body uses content-length, chunked encoding, or compression. The `BufferSink` interface handles the difference. - -=== Compression Pipeline - -A compression sink owns an output ring buffer where compressed data lands. The caller writes uncompressed data into prepared buffers, and `commit` triggers a compression pass. - -[source,cpp] ----- -template -task<> compress_input(Sink& sink, std::span input) -{ - std::size_t pos = 0; - while(pos < input.size()) - { - mutable_buffer arr[16]; - auto bufs = sink.prepare(arr); - if(bufs.empty()) - { - auto [ec] = co_await sink.commit(0); - if(ec) - co_return; - continue; - } - - std::size_t n = buffer_copy( - bufs, - const_buffer(input.data() + pos, - input.size() - pos)); - pos += n; - - auto [ec] = co_await sink.commit(n); - if(ec) - co_return; - } - auto [ec] = co_await sink.commit_eof(0); - if(ec) - co_return; -} ----- - -The `commit_eof(0)` call lets the compression sink pass `Z_FINISH` to the final deflate call, flushing the compressed stream trailer. - -=== Bridging to WriteSink via `any_buffer_sink` - -When a function is constrained on `WriteSink` but the concrete type satisfies only `BufferSink`, `any_buffer_sink` bridges the gap. - -[source,cpp] ----- -template -task<> send_message(Sink& sink, std::string_view msg); - -// Concrete type satisfies BufferSink only -my_ring_buffer ring; -any_buffer_sink abs(ring); - -// Works: any_buffer_sink satisfies WriteSink -co_await send_message(abs, "hello"); ----- - -When the wrapped type also satisfies `WriteSink`, `any_buffer_sink` forwards the native write operations directly, avoiding the synthesized `prepare` + `buffer_copy` + `commit` path. - -== Alternatives Considered - -=== Combined Prepare-and-Commit - -An alternative design combined the prepare and commit steps into a single asynchronous operation: `write(dest) -> (error_code, span)`, where the sink returns writable buffers and the commit happens on the next call. This was rejected because: - -- The synchronous `prepare` is a pure bookkeeping operation. Making it asynchronous forces a coroutine suspension on every iteration, even when the sink has space available. -- Separating `prepare` from `commit` lets the caller fill multiple prepared buffers before incurring the cost of an asynchronous commit. -- The two-step protocol makes the buffer lifetime explicit: buffers from `prepare` are valid until `commit` or `commit_eof`. - -=== `prepare` Returning a Count Instead of a Span - -An earlier design had `prepare` fill a raw pointer array and return a count (`std::size_t prepare(mutable_buffer* arr, std::size_t max)`). This was replaced by the span-based interface because: - -- `std::span` is self-describing: it carries both the pointer and the size, eliminating a class of off-by-one errors. -- Returning a subspan of the input span is idiomatic {cpp} and composes well with range-based code. -- The raw-pointer interface required two parameters (pointer + count) where the span interface requires one. - -=== Separate `flush` Operation - -A design with an explicit `flush` method (distinct from `commit`) was considered, where `commit` would only buffer data and `flush` would trigger I/O. This was rejected because: - -- It adds a fourth operation to the concept without clear benefit. The `commit` operation already serves both roles: it finalizes the caller's data and may trigger I/O at the sink's discretion. -- A sink that wants to defer I/O can do so internally by accumulating committed data and flushing when its buffer is full. The caller does not need to know when physical I/O occurs. -- Adding `flush` would complicate the `pull_from` algorithm, which would need to decide when to call `flush` versus `commit`. - -=== `BufferSink` Refining `WriteSink` - -A design where `BufferSink` refined `WriteSink` (requiring all types to implement both interfaces) was considered. This was rejected because: - -- Many natural `BufferSink` types (ring buffers, DMA descriptors) have no meaningful `write_some` primitive. Their data path is prepare-then-commit, not write-from-caller-buffer. -- Requiring `write_some`, `write`, and `write_eof` on every `BufferSink` would force implementations to synthesize these operations even when they are never called. -- The `any_buffer_sink` wrapper provides the bridge when needed, without burdening every concrete type. - -== Summary - -[cols="1,2,2"] -|=== -| Function | Contract | Use Case - -| `prepare(dest)` -| Synchronous. Fills span with writable buffer descriptors from the - sink's internal storage. Returns empty span if no space is available. -| Every write iteration: obtain writable memory before filling it. - -| `commit(n)` -| Async. Commits `n` bytes to the sink. May trigger I/O. -| Interior iterations of a transfer loop. - -| `commit_eof(n)` -| Async. Commits `n` bytes and signals end-of-stream. Finalizes the sink. -| Final iteration: deliver last data and close the stream. -|=== - -`BufferSink` is the callee-owns-buffers counterpart to `WriteSink`. The `pull_from` algorithm transfers data from a `ReadSource` or `ReadStream` into a `BufferSink`, and `any_buffer_sink` bridges the two patterns by satisfying both `BufferSink` and `WriteSink` behind a single type-erased interface. diff --git a/doc/modules/ROOT/pages/9.design/9j.any_buffer_sink.adoc b/doc/modules/ROOT/pages/9.design/9j.any_buffer_sink.adoc deleted file mode 100644 index 48e756573..000000000 --- a/doc/modules/ROOT/pages/9.design/9j.any_buffer_sink.adoc +++ /dev/null @@ -1,263 +0,0 @@ -= any_buffer_sink Design - -== Overview - -This document describes the design of `any_buffer_sink`, a type-erased wrapper that satisfies both `BufferSink` and `WriteSink`. The central design goal is to serve two fundamentally different data-production patterns through a single runtime interface, with no performance compromise for either. - -Data producers fall into two categories: - -- **Generators** produce data on demand. They do not hold the data in advance; they compute or serialize it into memory that someone else provides. An HTTP header serializer, a JSON encoder, and a compression engine are generators. - -- **Buffered sources** already have data sitting in buffers. A memory-mapped file, a ring buffer that received data from a socket, and a pre-serialized response body are buffered sources. - -These two patterns require different buffer ownership models. Generators need writable memory from the sink (the `BufferSink` pattern). Buffered sources need to hand their existing buffers to the sink (the `WriteSink` pattern). Forcing either pattern through the other's interface introduces an unnecessary copy. - -`any_buffer_sink` exposes both interfaces. The caller chooses the one that matches how its data is produced. The wrapper dispatches to the underlying concrete sink through the optimal path, achieving zero-copy when the concrete type supports it and falling back to a synthesized path when it does not. - -== The Two Interfaces - -=== BufferSink: Callee-Owned Buffers - -The `BufferSink` interface (`prepare`, `commit`, `commit_eof`) is designed for generators. The sink owns the memory. The generator asks for writable space, fills it, and commits: - -[source,cpp] ----- -any_buffer_sink abs(concrete_sink{}); - -mutable_buffer arr[16]; -auto bufs = abs.prepare(arr); -// serialize directly into bufs -auto [ec] = co_await abs.commit(bytes_written); ----- - -The data lands in the sink's internal storage with no intermediate copy. If the concrete sink is backed by a kernel page, a DMA descriptor, or a ring buffer, the bytes go directly to their final destination. - -=== WriteSink: Caller-Owned Buffers - -The `WriteSink` interface (`write_some`, `write`, `write_eof`) is designed for buffered sources. The caller already has the data in buffers and passes them to the sink: - -[source,cpp] ----- -any_buffer_sink abs(concrete_sink{}); - -// Data already in buffers -- pass them directly -auto [ec, n] = co_await abs.write(existing_buffers); - -// Or atomically write and signal EOF -auto [ec2, n2] = co_await abs.write_eof(final_buffers); ----- - -When the concrete sink natively supports `WriteSink`, the caller's buffers propagate directly through the type-erased boundary. The sink receives the original buffer descriptors pointing to the caller's memory. No data is copied into an intermediate staging area. - -== Dispatch Strategy - -The vtable records whether the wrapped concrete type satisfies `WriteSink` in addition to `BufferSink`. This determination is made at compile time when the vtable is constructed. At runtime, each `WriteSink` operation checks a single nullable function pointer to select its path. - -=== Native Forwarding (BufferSink + WriteSink) - -When the concrete type satisfies both concepts, the `WriteSink` vtable slots are populated with functions that construct the concrete type's own `write_some`, `write`, `write_eof(buffers)`, and `write_eof()` awaitables in the cached storage. The caller's buffer descriptors pass straight through: - ----- -caller buffers → vtable → concrete write(buffers) → I/O ----- - -No `prepare`, no `buffer_copy`, no `commit`. The concrete type receives the caller's buffers and can submit them directly to the operating system, the compression library, or the next pipeline stage. - -This is the zero-copy path for buffered sources writing to a sink that natively accepts caller-owned buffers. - -=== Synthesized Path (BufferSink Only) - -When the concrete type satisfies only `BufferSink`, the `WriteSink` vtable slots are null. The wrapper synthesizes the `WriteSink` operations from the `BufferSink` primitives: - ----- -caller buffers → prepare → buffer_copy → commit → I/O ----- - -For `write_some`: - -1. Call `prepare` to get writable space from the sink. -2. Copy data from the caller's buffers into the prepared space with `buffer_copy`. -3. Call `commit` to finalize. - -For `write` and `write_eof`: the same loop, repeated until all data is consumed. `write_eof` finishes with `commit_eof` to signal end-of-stream. - -This path incurs one buffer copy, which is unavoidable: the concrete sink only knows how to accept data through its own `prepare`/`commit` protocol, so the caller's buffers must be copied into the sink's internal storage. - -== Why This Matters - -=== No Compromise - -A naive design would pick one interface and synthesize the other unconditionally. If the wrapper only exposed `BufferSink`, every buffered source would pay a copy to move its data into the sink's prepared buffers. If the wrapper only exposed `WriteSink`, every generator would need to allocate its own intermediate buffer, fill it, then hand it to the sink -- paying a copy that the `BufferSink` path avoids. - -`any_buffer_sink` avoids both penalties. Each data-production pattern uses the interface designed for it. The only copy that occurs is the one that is structurally unavoidable: when a `WriteSink` operation targets a concrete type that only speaks `BufferSink`. - -=== True Zero-Copy for Buffered Sources - -Consider an HTTP server where the response body is a memory-mapped file. The file's pages are already in memory. Through the `WriteSink` interface, those pages can propagate directly to the underlying transport: - -[source,cpp] ----- -// body_source is a BufferSource backed by mmap pages -// response_sink wraps a concrete type satisfying both concepts - -any_buffer_sink response_sink(&concrete); - -const_buffer arr[16]; -for(;;) -{ - auto [ec, bufs] = co_await body_source.pull(arr); - if(ec == cond::eof) - { - auto [ec2] = co_await response_sink.write_eof(); - break; - } - if(ec) - break; - // bufs point directly into mmap pages - // write() propagates them through the vtable to the concrete sink - auto [ec2, n] = co_await response_sink.write(bufs); - if(ec2) - break; - body_source.consume(n); -} ----- - -The mapped pages flow from `body_source.pull` through `response_sink.write` to the concrete transport with no intermediate copy. If the concrete sink can scatter-gather those buffers into a `writev` system call, the data moves from the page cache to the network card without touching user-space memory a second time. - -=== Generators Write In-Place - -An HTTP header serializer generates bytes on the fly. It does not hold the output in advance. Through the `BufferSink` interface, it writes directly into whatever memory the concrete sink provides: - -[source,cpp] ----- -task<> serialize_headers( - any_buffer_sink& sink, - response const& resp) -{ - mutable_buffer arr[16]; - - for(auto const& field : resp.fields()) - { - auto bufs = sink.prepare(arr); - // serialize field directly into bufs - std::size_t n = format_field(bufs, field); - auto [ec] = co_await sink.commit(n); - if(ec) - co_return; - } - // headers done; body follows through the same sink -} ----- - -The serializer never allocates a scratch buffer for the formatted output. The bytes land directly in the sink's internal storage, which might be a chunked-encoding buffer, a TLS record buffer, or a circular buffer feeding a socket. - -== Awaitable Caching - -`any_buffer_sink` uses the split vtable pattern described in xref:9.design/9i.TypeEraseAwaitable.adoc[Type-Erasing Awaitables]. Multiple async operations (`commit`, `commit_eof`, plus the four `WriteSink` operations when the concrete type supports them) share a single cached awaitable storage region. - -The constructor computes the maximum size and alignment across all awaitable types that the concrete type can produce and allocates that storage once. This reserves all virtual address space at construction time, so memory usage is measurable at server startup rather than growing piecemeal as requests arrive. - -Two separate `awaitable_ops` structs are used: - -- `awaitable_ops` for operations yielding `io_result<>` (`commit`, `commit_eof`, `write_eof()`) -- `write_awaitable_ops` for operations yielding `io_result` (`write_some`, `write`, `write_eof(buffers)`) - -Each `construct_*` function in the vtable creates the concrete awaitable in the cached storage and returns a pointer to the matching `static constexpr` ops table. The wrapper stores this pointer as `active_ops_` or `active_write_ops_` and uses it for `await_ready`, `await_suspend`, `await_resume`, and destruction. - -== Ownership Modes - -=== Owning - -[source,cpp] ----- -any_buffer_sink abs(my_concrete_sink{args...}); ----- - -The wrapper allocates storage for the concrete sink and moves it in. The wrapper owns the sink and destroys it in its destructor. The awaitable cache is allocated separately. - -If either allocation fails, the constructor cleans up via an internal guard and propagates the exception. - -=== Non-Owning (Reference) - -[source,cpp] ----- -my_concrete_sink sink; -any_buffer_sink abs(&sink); ----- - -The wrapper stores a pointer without allocating storage for the sink. The concrete sink must outlive the wrapper. Only the awaitable cache is allocated. - -This mode is useful when the concrete sink is managed by a higher-level object (e.g., an HTTP connection that owns the transport) and the wrapper is a short-lived handle passed to a body-production function. - -== Relationship to any_buffer_source - -`any_buffer_source` is the read-side counterpart, satisfying both `BufferSource` and `ReadSource`. The same dual-interface principle applies in mirror image: - -[cols="1,1,1"] -|=== -| Direction | Primary concept | Secondary concept - -| Writing (any_buffer_sink) -| `BufferSink` (callee-owned) -| `WriteSink` (caller-owned) - -| Reading (any_buffer_source) -| `BufferSource` (callee-owned) -| `ReadSource` (caller-owned) -|=== - -Both wrappers enable the same design philosophy: the caller chooses the interface that matches its data-production or data-consumption pattern, and the wrapper dispatches optimally. - -== Alternatives Considered - -=== WriteSink-Only Wrapper - -A design where the type-erased wrapper satisfied only `WriteSink` was considered. Generators would allocate their own scratch buffer, serialize into it, and call `write`. This was rejected because: - -- Every generator pays a buffer copy that the `BufferSink` path avoids. For high-throughput paths (HTTP header serialization, compression output), this copy is measurable. -- Generators must manage scratch buffer lifetime and sizing. The `prepare`/`commit` protocol pushes this responsibility to the sink, which knows its own buffer topology. -- The `commit_eof(n)` optimization (coalescing final data with stream termination) is lost. A generator calling `write` cannot signal that its last write is the final one without a separate `write_eof()` call, preventing the sink from combining them. - -=== BufferSink-Only Wrapper - -A design where the wrapper satisfied only `BufferSink` was considered. Buffered sources would copy their data into the sink's prepared buffers via `prepare` + `buffer_copy` + `commit`. This was rejected because: - -- Every buffered source pays a copy that native `WriteSink` forwarding avoids. When the source is a memory-mapped file and the sink is a socket, this eliminates the zero-copy path entirely. -- The `buffer_copy` step becomes the bottleneck for large transfers, dominating what would otherwise be a pure I/O operation. -- Buffered sources that produce scatter-gather buffer sequences (multiple non-contiguous regions) must copy each region individually into prepared buffers, losing the ability to pass the entire scatter-gather list to a `writev` system call. - -=== Separate Wrapper Types - -A design with two distinct wrappers (`any_buffer_sink` satisfying only `BufferSink` and `any_write_sink` satisfying only `WriteSink`) was considered. The caller would choose which wrapper to construct based on its data-production pattern. This was rejected because: - -- The caller and the sink are often decoupled. An HTTP server framework provides the sink; the user provides the body producer. The framework cannot know at compile time whether the user will call `prepare`/`commit` or `write`/`write_eof`. -- Requiring two wrapper types forces the framework to either pick one (losing the other pattern) or expose both (complicating the API). -- A single wrapper that satisfies both concepts lets the framework hand one object to the body producer, which uses whichever interface is natural. No choice is imposed on the framework or the user. - -=== Always Synthesizing WriteSink - -A design where the `WriteSink` operations were always synthesized from `prepare` + `buffer_copy` + `commit`, even when the concrete type natively supports `WriteSink`, was considered. This would simplify the vtable by removing the nullable write-forwarding slots. This was rejected because: - -- The buffer copy is measurable. For a concrete type that can accept caller-owned buffers directly (e.g., a socket wrapper with `writev` support), the synthesized path adds a copy that native forwarding avoids. -- The `write_eof(buffers)` atomicity guarantee is lost. The synthesized path must decompose it into `prepare` + `buffer_copy` + `commit_eof`, which the concrete type cannot distinguish from a non-final commit followed by an empty `commit_eof`. This prevents optimizations like coalescing the last data chunk with a chunked-encoding terminator. - -== Summary - -`any_buffer_sink` satisfies both `BufferSink` and `WriteSink` behind a single type-erased interface. The dual API lets each data-production pattern use the interface designed for it: - -[cols="1,2,2"] -|=== -| Producer type | Interface | Data path - -| Generator (produces on demand) -| `prepare` / `commit` / `commit_eof` -| Writes directly into sink's internal storage. Zero copy. - -| Buffered source (data already in memory) -| `write_some` / `write` / `write_eof` -| Buffers propagate through the vtable. Zero copy when the concrete - type natively supports `WriteSink`. One copy (synthesized) when - it does not. -|=== - -The dispatch is determined at construction time through nullable vtable slots. At runtime, a single pointer check selects the native or synthesized path. Neither pattern pays for the other's existence. diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 5a9864fb7..2aaad8fd3 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -28,7 +28,7 @@ Capy is _not_ an all-purpose coroutine framework, and it is _not_ an implementat * *Lazy coroutine tasks* — `task` with forward-propagating stop tokens and automatic cancellation * *Buffer sequences* — taken straight from Asio and improved -* *Stream concepts* — seven coroutine stream concepts: `ReadStream`, `WriteStream`, `Stream`, `ReadSource`, `WriteSink`, `BufferSource`, `BufferSink` +* *Stream concepts* — three coroutine stream concepts: `ReadStream`, `WriteStream`, `Stream` * *Type-erased streams* — `any_stream`, `any_read_stream`, `any_write_stream` for fast compilation * *Concurrency facilities* — executors, strands, thread pools, `when_all`, `when_any` * *Test utilities* — mock streams, mock sources/sinks, error injection diff --git a/doc/modules/ROOT/pages/why-capy.adoc b/doc/modules/ROOT/pages/why-capy.adoc index 98e11c402..48e013c98 100644 --- a/doc/modules/ROOT/pages/why-capy.adoc +++ b/doc/modules/ROOT/pages/why-capy.adoc @@ -19,8 +19,6 @@ Asio's stream concepts are hybrid by design. Capy's are coroutine-only, which is === What Capy Offers * `ReadStream`, `WriteStream`, `Stream` — partial I/O (returns what's available) -* `ReadSource`, `WriteSink` — complete I/O with EOF signaling -* `BufferSource`, `BufferSink` — zero-copy callee-owns-buffers pattern === Comparison @@ -37,17 +35,6 @@ Asio's stream concepts are hybrid by design. Capy's are coroutine-only, which is | `Stream` ^| - -| `ReadSource` -^| - - -| `WriteSink` -^| - - -| `BufferSource` -^| - - -| `BufferSink` -^| - |=== *Asio's concepts are hybrid (callbacks/futures/coroutines), not coroutine-only @@ -67,9 +54,7 @@ Write `any_stream&` and accept any stream. Your function compiles once. It links === What Capy Offers * `any_read_stream`, `any_write_stream`, `any_stream` — type-erased partial I/O -* `any_read_source`, `any_write_sink` — type-erased complete I/O -* `any_buffer_source`, `any_buffer_sink` — type-erased zero-copy -* `read`, `write`, `push_to`, `pull_from` — algorithms that work with erased or concrete streams +* `read`, `write` — algorithms that work with erased or concrete streams === Comparison @@ -86,29 +71,12 @@ Write `any_stream&` and accept any stream. Your function compiles once. It links | `any_stream` ^| - -| `any_read_source` -^| - - -| `any_write_sink` -^| - - -| `any_buffer_source` -^| - - -| `any_buffer_sink` -^| - - | `read` | `async_read`* | `write` | `async_write`* -| `push_to` -^| - - -| `pull_from` -^| - |=== *Asio's algorithms only support `AsyncReadStream` and `AsyncWriteStream` diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 79312d723..ad09af8bd 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -18,7 +18,6 @@ add_subdirectory(parallel-tasks) add_subdirectory(producer-consumer) add_subdirectory(quitter-shutdown) add_subdirectory(strand-serialization) -add_subdirectory(stream-pipeline) add_subdirectory(timeout-cancellation) add_subdirectory(type-erased-echo) add_subdirectory(when-any-cancellation) @@ -30,10 +29,10 @@ endif() if(BOOST_CAPY_BUILD_CUDA_EXAMPLES) add_subdirectory(cuda/datamovement) add_subdirectory(cuda/notification-strategies) + add_subdirectory(cuda/pipeline) endif() if(BOOST_CAPY_BUILD_NVEXEC_EXAMPLES) - add_subdirectory(cuda/pipeline) endif() add_subdirectory(fabrics) diff --git a/example/Jamfile b/example/Jamfile index fe198337b..8efdae902 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -14,6 +14,5 @@ build-project mock-stream-testing ; build-project type-erased-echo ; build-project timeout-cancellation ; build-project parallel-fetch ; -build-project stream-pipeline ; build-project when-any-cancellation ; build-project asio ; diff --git a/example/cuda/pipeline/README.md b/example/cuda/pipeline/README.md index 719b48ad5..6a465a759 100644 --- a/example/cuda/pipeline/README.md +++ b/example/cuda/pipeline/README.md @@ -25,9 +25,9 @@ built but not run (P4251R0): peer to resume. 3. **Scene 3 (P4251R0), built but not run.** `handle_request` shows the - inference-handler shape: a type-erased `any_read_source` read, GPU + inference-handler shape: a type-erased `any_read_stream` read, GPU dispatch via `await_sender` over a real nvexec kernel, and a type-erased - `any_write_sink` write. It is compiled but not executed (`main` does not + `any_write_stream` write. It is compiled but not executed (`main` does not call it). The paper's listing runs a host `run_model()` under a device-side `then()`, which does not compile on nvexec (host call from device); this mirrors Scene 1's pattern instead, dispatching a real diff --git a/example/cuda/pipeline/cuda_pipeline.cu b/example/cuda/pipeline/cuda_pipeline.cu index 1071bd340..20dff38fd 100644 --- a/example/cuda/pipeline/cuda_pipeline.cu +++ b/example/cuda/pipeline/cuda_pipeline.cu @@ -23,8 +23,8 @@ #include "sender_awaitable.hpp" #include -#include -#include +#include +#include #include #include @@ -130,15 +130,15 @@ scene1(nvexec::stream_scheduler gpu, } // Scene 3 (P4251R0): the inference-handler shape. Network I/O uses -// type-erased coroutine streams (any_read_source / any_write_sink); GPU +// type-erased coroutine streams (any_read_stream / any_write_stream); GPU // dispatch uses a sender bridged with await_sender. The paper's // listing runs a host run_model() under a device-side then(), which does // not compile on nvexec; this mirrors Scene 1 instead, dispatching a real // kernel and hopping continues_on(cpu) before the host-only bridge. [[maybe_unused]] capy::task<> handle_request( - capy::any_read_source& client, - capy::any_write_sink& response, + capy::any_read_stream& client, + capy::any_write_stream& response, nvexec::stream_context& gpu_ctx, exec::static_thread_pool::scheduler cpu) { diff --git a/example/stream-pipeline/CMakeLists.txt b/example/stream-pipeline/CMakeLists.txt deleted file mode 100644 index 6fd38c3ad..000000000 --- a/example/stream-pipeline/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -# -# Copyright (c) 2026 Mungo Gill -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -# Official repository: https://github.com/cppalliance/capy -# - -file(GLOB_RECURSE PFILES CONFIGURE_DEPENDS *.cpp *.hpp - CMakeLists.txt - Jamfile) - -source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "" FILES ${PFILES}) - -add_executable(capy_example_stream_pipeline ${PFILES}) - -set_property(TARGET capy_example_stream_pipeline - PROPERTY FOLDER "examples") - -target_link_libraries(capy_example_stream_pipeline - Boost::capy) diff --git a/example/stream-pipeline/Jamfile b/example/stream-pipeline/Jamfile deleted file mode 100644 index 92d494231..000000000 --- a/example/stream-pipeline/Jamfile +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright (c) 2026 Mungo Gill -# -# Distributed under the Boost Software License, Version 1.0. (See accompanying -# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -# -# Official repository: https://github.com/cppalliance/capy -# - -project - : requirements - /boost/capy//boost_capy - . - ; - -exe stream_pipeline : - [ glob *.cpp ] - ; diff --git a/example/stream-pipeline/stream_pipeline.cpp b/example/stream-pipeline/stream_pipeline.cpp deleted file mode 100644 index a81d1b1a3..000000000 --- a/example/stream-pipeline/stream_pipeline.cpp +++ /dev/null @@ -1,351 +0,0 @@ -// -// Copyright (c) 2026 Mungo Gill -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -// -// Stream Pipeline Example -// -// This example demonstrates chaining buffer sources to create a data -// processing pipeline. Data flows through transform stages: -// -// input -> uppercase_transform -> line_numbering_transform -> output -// -// Each transform is a BufferSource that wraps an upstream any_buffer_source, -// enabling type-erased composition of arbitrary transform chains. -// -// The transforms use task<> coroutines for their pull() methods, allowing -// them to properly co_await the upstream source. -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace capy = boost::capy; - -//------------------------------------------------------------------------------ -// -// Transform: uppercase_transform -// -// A BufferSource that pulls from an upstream source and converts all -// characters to uppercase. Demonstrates a simple byte-by-byte transform. -// -//------------------------------------------------------------------------------ - -class uppercase_transform -{ - capy::any_buffer_source* source_; // any_buffer_source* - std::vector buffer_; // std::vector - transformed data - std::size_t consumed_ = 0; // std::size_t - bytes consumed by downstream - bool exhausted_ = false; // bool - upstream exhausted - -public: - explicit uppercase_transform(capy::any_buffer_source& source) - : source_(&source) - { - } - - // BufferSource::consume - advance past processed bytes - void - consume(std::size_t n) noexcept - { - consumed_ += n; - // Compact buffer when fully consumed - if (consumed_ >= buffer_.size()) - { - buffer_.clear(); - consumed_ = 0; - } - } - - // BufferSource::pull - returns task<> to enable co_await on upstream - capy::io_task> - pull(std::span dest) - { - // Already have unconsumed data? - if (consumed_ < buffer_.size()) - { - if (dest.empty()) - co_return {std::error_code{}, std::span{}}; - - dest[0] = capy::const_buffer( - buffer_.data() + consumed_, - buffer_.size() - consumed_); - co_return {std::error_code{}, dest.first(1)}; - } - - // Upstream exhausted? - if (exhausted_) - co_return {capy::error::eof, std::span{}}; - - // Pull from upstream - buffer_.clear(); - consumed_ = 0; - - capy::const_buffer upstream[8]; // const_buffer[8] - // ec: std::error_code, bufs: std::span - auto [ec, bufs] = co_await source_->pull(upstream); - - if (ec == capy::cond::eof) - { - exhausted_ = true; - co_return {capy::error::eof, std::span{}}; - } - - if (ec) - co_return {ec, std::span{}}; - - // Transform: uppercase each byte - for (auto const& buf : bufs) // const_buffer const& - { - auto const* data = static_cast(buf.data()); // char const* - auto size = buf.size(); // std::size_t - - for (std::size_t i = 0; i < size; ++i) - { - buffer_.push_back(static_cast( - std::toupper(static_cast(data[i])))); - } - } - - // Consume from upstream - source_->consume(capy::buffer_size(bufs)); - - // Return transformed data - if (dest.empty() || buffer_.empty()) - co_return {std::error_code{}, std::span{}}; - - dest[0] = capy::const_buffer(buffer_.data(), buffer_.size()); - co_return {std::error_code{}, dest.first(1)}; - } -}; - -//------------------------------------------------------------------------------ -// -// Transform: line_numbering_transform -// -// A BufferSource that pulls from an upstream source and prepends line -// numbers to each line. Demonstrates a transform that changes data size. -// -//------------------------------------------------------------------------------ - -class line_numbering_transform -{ - capy::any_buffer_source* source_; // any_buffer_source* - std::string buffer_; // std::string - transformed data - std::size_t consumed_ = 0; // std::size_t - bytes consumed by downstream - std::size_t line_num_ = 1; // std::size_t - current line number - bool at_line_start_ = true; // bool - are we at start of a line? - bool exhausted_ = false; // bool - upstream exhausted - -public: - explicit line_numbering_transform(capy::any_buffer_source& source) - : source_(&source) - { - } - - // BufferSource::consume - advance past processed bytes - void - consume(std::size_t n) noexcept - { - consumed_ += n; - // Compact buffer when fully consumed - if (consumed_ >= buffer_.size()) - { - buffer_.clear(); - consumed_ = 0; - } - } - - // BufferSource::pull - returns task<> to enable co_await on upstream - capy::io_task> - pull(std::span dest) - { - // Already have unconsumed data? - if (consumed_ < buffer_.size()) - { - if (dest.empty()) - co_return {std::error_code{}, std::span{}}; - - dest[0] = capy::const_buffer( - buffer_.data() + consumed_, - buffer_.size() - consumed_); - co_return {std::error_code{}, dest.first(1)}; - } - - // Upstream exhausted? - if (exhausted_) - co_return {capy::error::eof, std::span{}}; - - // Pull from upstream - buffer_.clear(); - consumed_ = 0; - - capy::const_buffer upstream[8]; // const_buffer[8] - // ec: std::error_code, bufs: std::span - auto [ec, bufs] = co_await source_->pull(upstream); - - if (ec == capy::cond::eof) - { - exhausted_ = true; - co_return {capy::error::eof, std::span{}}; - } - - if (ec) - co_return {ec, std::span{}}; - - // Transform: add line numbers - for (auto const& buf : bufs) // const_buffer const& - { - auto const* data = static_cast(buf.data()); // char const* - auto size = buf.size(); // std::size_t - - for (std::size_t i = 0; i < size; ++i) - { - if (at_line_start_) - { - buffer_ += std::to_string(line_num_++) + ": "; - at_line_start_ = false; - } - buffer_ += data[i]; - if (data[i] == '\n') - at_line_start_ = true; - } - } - - // Consume from upstream - source_->consume(capy::buffer_size(bufs)); - - // Return transformed data - if (dest.empty() || buffer_.empty()) - co_return {std::error_code{}, std::span{}}; - - dest[0] = capy::const_buffer(buffer_.data(), buffer_.size()); - co_return {std::error_code{}, dest.first(1)}; - } -}; - -//------------------------------------------------------------------------------ -// -// transfer: Pull from source and write to sink until exhausted -// -//------------------------------------------------------------------------------ - -capy::task transfer(capy::any_buffer_source& source, capy::any_write_sink& sink) -{ - std::size_t total = 0; // std::size_t - capy::const_buffer bufs[8]; // const_buffer[8] - - for (;;) - { - // ec: std::error_code, spans: std::span - auto [ec, spans] = co_await source.pull(bufs); - - if (ec == capy::cond::eof) - break; - - if (ec) - throw std::system_error(ec); - - // Write each buffer to sink - for (auto const& buf : spans) // const_buffer const& - { - // wec: std::error_code, n: std::size_t - auto [wec, n] = co_await sink.write(buf); - if (wec) - throw std::system_error(wec); - total += n; - } - - // Consume what we read - source.consume(capy::buffer_size(spans)); - } - - capy::io_result<> eof_result = co_await sink.write_eof(); - if (eof_result.ec) - throw std::system_error(eof_result.ec); - - co_return total; -} - -//------------------------------------------------------------------------------ -// -// demo_pipeline: Demonstrate chained transforms -// -//------------------------------------------------------------------------------ - -void demo_pipeline() -{ - std::cout << "=== Stream Pipeline Demo ===\n\n"; - - // Input data - three lines - std::string input = "hello world\nthis is a test\nof the pipeline\n"; - std::cout << "Input:\n" << input << "\n"; - - // Create mock source with input data - capy::test::fuse f; // test::fuse - capy::test::buffer_source source(f); // test::buffer_source - source.provide(input); - - // Build the pipeline using type-erased buffer sources: - // source -> [uppercase] -> [line_numbering] -> sink - - // Stage 1: Wrap raw source as any_buffer_source. - // Using pointer construction (&source) for reference semantics - the - // wrapper does not take ownership, so source must outlive src. - capy::any_buffer_source src{&source}; // any_buffer_source - - // Stage 2: Uppercase transform wraps src. - // Again using pointer construction so upper_src references upper - // without taking ownership. - uppercase_transform upper{src}; // uppercase_transform - capy::any_buffer_source upper_src{&upper}; // any_buffer_source - - // Stage 3: Line numbering transform wraps upper_src. - line_numbering_transform numbered{upper_src}; // line_numbering_transform - capy::any_buffer_source numbered_src{&numbered}; // any_buffer_source - - // Create sink to collect output. - // Pointer construction ensures sink outlives dst. - capy::test::write_sink sink(f); // test::write_sink - capy::any_write_sink dst{&sink}; // any_write_sink - - // Run the pipeline - std::size_t bytes = 0; // std::size_t - capy::test::run_blocking([&](std::size_t n) { bytes = n; })( - transfer(numbered_src, dst)); - - std::cout << "Output (" << bytes << " bytes):\n"; - std::cout << sink.data() << "\n"; - - // Expected output: - // 1: HELLO WORLD - // 2: THIS IS A TEST - // 3: OF THE PIPELINE -} - -int main() -{ - try - { - demo_pipeline(); - } - catch (std::system_error const& e) - { - std::cerr << "Pipeline error: " << e.what() << "\n"; - return 1; - } - return 0; -} diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index 9a26eafc2..a385b4f48 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -44,8 +44,6 @@ // Concepts #include -#include -#include #include #include #include @@ -53,10 +51,8 @@ #include #include #include -#include #include #include -#include #include // Execution @@ -79,14 +75,8 @@ #include // Type-erased I/O -#include -#include -#include #include #include -#include #include -#include -#include #endif diff --git a/include/boost/capy/concept/buffer_sink.hpp b/include/boost/capy/concept/buffer_sink.hpp deleted file mode 100644 index cc64d058f..000000000 --- a/include/boost/capy/concept/buffer_sink.hpp +++ /dev/null @@ -1,145 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_CONCEPT_BUFFER_SINK_HPP -#define BOOST_CAPY_CONCEPT_BUFFER_SINK_HPP - -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace boost { -namespace capy { - -/** 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 IoAwaitable that - decomposes to `(error_code)` - @li `T` must provide `commit_eof(n)` returning an @ref 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 ); - - IoAwaitable auto commit( std::size_t n ); - IoAwaitable auto commit_eof( std::size_t n ); - @endcode - - @par Example - - @code - template - io_task transfer( Source& source, Sink& sink ) - { - const_buffer src_arr[16]; - 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, IoAwaitable, 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) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.commit(n)), - std::error_code>; - - // Async: commit n final bytes and signal end of data - { sink.commit_eof(n) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.commit_eof(n)), - std::error_code>; - }; - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/concept/buffer_source.hpp b/include/boost/capy/concept/buffer_source.hpp deleted file mode 100644 index 5ec0cc522..000000000 --- a/include/boost/capy/concept/buffer_source.hpp +++ /dev/null @@ -1,121 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_CONCEPT_BUFFER_SOURCE_HPP -#define BOOST_CAPY_CONCEPT_BUFFER_SOURCE_HPP - -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace boost { -namespace capy { - -/** 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 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 ) - { - 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 IoAwaitable, WriteSink, awaitable_decomposes_to -*/ -template -concept BufferSource = - requires(T& src, std::span dest, std::size_t n) - { - { src.pull(dest) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(src.pull(dest)), - std::error_code, std::span>; - src.consume(n); - }; - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/concept/read_source.hpp b/include/boost/capy/concept/read_source.hpp deleted file mode 100644 index d1d6c08e5..000000000 --- a/include/boost/capy/concept/read_source.hpp +++ /dev/null @@ -1,136 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_CONCEPT_READ_SOURCE_HPP -#define BOOST_CAPY_CONCEPT_READ_SOURCE_HPP - -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace boost { -namespace capy { - -/** Concept for types providing complete reads from a data source. - - A type satisfies `ReadSource` if it satisfies @ref ReadStream - and additionally provides a `read` member function that accepts - any @ref MutableBufferSequence and await-returns - `(error_code, std::size_t)`. - - `ReadSource` refines `ReadStream`. Every `ReadSource` is a - `ReadStream`. Algorithms constrained on `ReadStream` accept both - raw streams and sources. - - @tparam T The source type. - - @par Syntactic Requirements - - @li `T` must satisfy @ref 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 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 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 > - IoAwaitable auto read_some( MB buffers ); // inherited from ReadStream - - template< MutableBufferSequence MB > - 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( - mutable_buffer( header ) ); - if( ec ) - co_return; - // header contains exactly 16 bytes - } - @endcode - - @see ReadStream, IoAwaitable, MutableBufferSequence, - awaitable_decomposes_to -*/ -template -concept ReadSource = - ReadStream && - requires(T& source, mutable_buffer_archetype buffers) - { - { source.read(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(source.read(buffers)), - std::error_code, std::size_t>; - }; - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/concept/write_sink.hpp b/include/boost/capy/concept/write_sink.hpp deleted file mode 100644 index 0324a3601..000000000 --- a/include/boost/capy/concept/write_sink.hpp +++ /dev/null @@ -1,165 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_CONCEPT_WRITE_SINK_HPP -#define BOOST_CAPY_CONCEPT_WRITE_SINK_HPP - -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace boost { -namespace capy { - -/** Concept for types providing complete writes with EOF signaling. - - A type satisfies `WriteSink` if it satisfies @ref WriteStream - and additionally provides `write`, `write_eof(buffers)`, and - `write_eof()` member functions that await-return - `(error_code, std::size_t)`. - - `WriteSink` refines `WriteStream`. Every `WriteSink` is a - `WriteStream`. Algorithms constrained on `WriteStream` accept - both raw streams and sinks. - - @tparam T The sink type. - - @par Syntactic Requirements - - @li `T` must satisfy @ref 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 IoAwaitable - - @par Semantic Requirements - - The inherited `write_some` operation attempts to write up to - `buffer_size( buffers )` bytes (partial write). See @ref 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 > - IoAwaitable auto write_some( Buffers buffers ); // inherited - - template< ConstBufferSequence Buffers > - IoAwaitable auto write( Buffers buffers ); - - template< ConstBufferSequence Buffers > - IoAwaitable auto write_eof( Buffers buffers ); - - 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 WriteStream, IoAwaitable, ConstBufferSequence, - awaitable_decomposes_to -*/ -template -concept WriteSink = - WriteStream && - requires(T& sink, const_buffer_archetype buffers) - { - { sink.write(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.write(buffers)), - std::error_code, std::size_t>; - { sink.write_eof(buffers) } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.write_eof(buffers)), - std::error_code, std::size_t>; - { sink.write_eof() } -> IoAwaitable; - requires awaitable_decomposes_to< - decltype(sink.write_eof()), - std::error_code>; - }; - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/ex/immediate.hpp b/include/boost/capy/ex/immediate.hpp index b3c7c1fc1..7f315a96a 100644 --- a/include/boost/capy/ex/immediate.hpp +++ b/include/boost/capy/ex/immediate.hpp @@ -47,7 +47,7 @@ namespace capy { } @endcode - @par Satisfying WriteSink with sync operations + @par Building synchronous I/O operations @code struct my_sync_sink { diff --git a/include/boost/capy/io/any_buffer_sink.hpp b/include/boost/capy/io/any_buffer_sink.hpp deleted file mode 100644 index 6c0935e97..000000000 --- a/include/boost/capy/io/any_buffer_sink.hpp +++ /dev/null @@ -1,1258 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_IO_ANY_BUFFER_SINK_HPP -#define BOOST_CAPY_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 capy { - -/** 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 - 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 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 `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 - 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 - 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 - 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 `io_result<>`. */ -struct any_buffer_sink::awaitable_ops -{ - bool (*await_ready)(void*); - std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, io_env const*); - io_result<> (*await_resume)(void*); - void (*destroy)(void*) noexcept; -}; - -/** Type-erased ops for awaitables that await-return `io_result`. */ -struct any_buffer_sink::write_awaitable_ops -{ - bool (*await_ready)(void*); - std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, io_env const*); - 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) - { - return self_->active_ops_->await_suspend( - self_->cached_awaitable_, h, env); - } - - 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, io_env const* env) - { - return self_->active_ops_->await_suspend( - self_->cached_awaitable_, h, env); - } - - 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, 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); - } - - 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, 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); - } - - 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, 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); - } - - 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 -io_task -any_buffer_sink::write_some(CB buffers) -{ - 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 + buffer_copy + commit - mutable_buffer arr[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 = buffer_copy(dst_bufs, src); - auto [ec] = co_await commit(n); - if(ec) - co_return {ec, 0}; - co_return {{}, n}; -} - -template -io_task -any_buffer_sink::write(CB buffers) -{ - 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 + buffer_copy + commit - for(;;) - { - auto src = bp.data(); - if(src.empty()) - break; - - mutable_buffer arr[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 = 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, io_env const* env) - { - return self_->active_ops_->await_suspend( - self_->cached_awaitable_, h, env); - } - - 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 -io_task -any_buffer_sink::write_eof(CB buffers) -{ - // Native WriteSink path - if(vt_->construct_write_eof_buffers_awaitable) - { - 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 + buffer_copy + commit + commit_eof - buffer_param bp(buffers); - std::size_t total = 0; - - for(;;) - { - auto src = bp.data(); - if(src.empty()) - break; - - mutable_buffer arr[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 = 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 capy -} // namespace boost - -#endif diff --git a/include/boost/capy/io/any_buffer_source.hpp b/include/boost/capy/io/any_buffer_source.hpp deleted file mode 100644 index fea79949d..000000000 --- a/include/boost/capy/io/any_buffer_source.hpp +++ /dev/null @@ -1,834 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_IO_ANY_BUFFER_SOURCE_HPP -#define BOOST_CAPY_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 capy { - -/** 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); - - 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(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 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 == 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 `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 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 - 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 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 == 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 - 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 `io_result>`. */ -struct any_buffer_source::awaitable_ops -{ - bool (*await_ready)(void*); - std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, io_env const*); - io_result> (*await_resume)(void*); - void (*destroy)(void*) noexcept; -}; - -/** Type-erased ops for awaitables that await-return `io_result`. */ -struct any_buffer_source::read_awaitable_ops -{ - bool (*await_ready)(void*); - std::coroutine_handle<> (*await_suspend)(void*, std::coroutine_handle<>, io_env const*); - 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) - { - return self_->active_ops_->await_suspend( - self_->cached_awaitable_, h, env); - } - - 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, 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); - } - - 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, 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); - } - - 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 -io_task -any_buffer_source::read_some(MB buffers) -{ - 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 + buffer_copy + consume - const_buffer arr[detail::max_iovec_]; - auto [ec, bufs] = co_await pull(arr); - if(ec) - co_return {ec, 0}; - - auto n = buffer_copy(dest, bufs); - consume(n); - co_return {{}, n}; -} - -template -io_task -any_buffer_source::read(MB buffers) -{ - 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 + buffer_copy + consume - for(;;) - { - auto dest = bp.data(); - if(dest.empty()) - break; - - const_buffer arr[detail::max_iovec_]; - auto [ec, bufs] = co_await pull(arr); - - if(ec) - co_return {ec, total}; - - auto n = buffer_copy(dest, bufs); - consume(n); - total += n; - bp.consume(n); - } - - co_return {{}, total}; -} - -static_assert(BufferSource); -static_assert(ReadSource); - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/io/any_read_source.hpp b/include/boost/capy/io/any_read_source.hpp deleted file mode 100644 index 969dba7b7..000000000 --- a/include/boost/capy/io/any_read_source.hpp +++ /dev/null @@ -1,597 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_IO_ANY_READ_SOURCE_HPP -#define BOOST_CAPY_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 capy { - -/** 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); - - 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 `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 == 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 - 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<>, io_env const*); - 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, io_env const* env) { - return 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, io_env const* env) { - return 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_; - 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, 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); - } - - 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, 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); - } - - 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 -io_task -any_read_source::read(MB buffers) -{ - 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 capy -} // namespace boost - -#endif diff --git a/include/boost/capy/io/any_write_sink.hpp b/include/boost/capy/io/any_write_sink.hpp deleted file mode 100644 index 39900c6bd..000000000 --- a/include/boost/capy/io/any_write_sink.hpp +++ /dev/null @@ -1,911 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_IO_ANY_WRITE_SINK_HPP -#define BOOST_CAPY_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 capy { - -/** 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); - - 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 `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 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 - 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 - 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<>, io_env const*); - 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<>, io_env const*); - 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) { - return 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, io_env const* env) { - return 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, 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); - } - - 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, 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); - } - - 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, 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); - } - - 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, 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); - } - - 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_; - 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, 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); - } - - 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 -io_task -any_write_sink::write(CB buffers) -{ - 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 -io_task -any_write_sink::write_eof(CB buffers) -{ - 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 capy -} // namespace boost - -#endif diff --git a/include/boost/capy/io/pull_from.hpp b/include/boost/capy/io/pull_from.hpp deleted file mode 100644 index 36579bfa4..000000000 --- a/include/boost/capy/io/pull_from.hpp +++ /dev/null @@ -1,187 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_IO_PULL_FROM_HPP -#define BOOST_CAPY_IO_PULL_FROM_HPP - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace boost { -namespace capy { - -/** 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 -io_task -pull_from(Src& source, Sink& sink) -{ - mutable_buffer dst_arr[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 == 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 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 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(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 ReadStream, BufferSink, push_to -*/ -template - requires (!ReadSource) -io_task -pull_from(Src& source, Sink& sink) -{ - mutable_buffer dst_arr[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 == 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 capy -} // namespace boost - -#endif diff --git a/include/boost/capy/io/push_to.hpp b/include/boost/capy/io/push_to.hpp deleted file mode 100644 index d772d4b4c..000000000 --- a/include/boost/capy/io/push_to.hpp +++ /dev/null @@ -1,152 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_IO_PUSH_TO_HPP -#define BOOST_CAPY_IO_PUSH_TO_HPP - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace boost { -namespace capy { - -/** 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 -io_task -push_to(Src& source, Sink& sink) -{ - const_buffer arr[detail::max_iovec_]; - std::size_t total = 0; - - for(;;) - { - auto [ec, bufs] = co_await source.pull(arr); - if(ec == 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 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 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 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, WriteStream auto& stream) - { - auto [ec, n] = co_await push_to(source, stream); - if (ec) - { - // Handle error - } - // n bytes were transferred - } - @endcode - - @see BufferSource, WriteStream, pull_from -*/ -template - requires (!WriteSink) -io_task -push_to(Src& source, Stream& stream) -{ - const_buffer arr[detail::max_iovec_]; - 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); - total += n; - source.consume(n); - if(write_ec) - co_return {write_ec, total}; - } -} - -} // namespace capy -} // namespace boost - -#endif diff --git a/include/boost/capy/test/buffer_sink.hpp b/include/boost/capy/test/buffer_sink.hpp deleted file mode 100644 index 31996615e..000000000 --- a/include/boost/capy/test/buffer_sink.hpp +++ /dev/null @@ -1,274 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_TEST_BUFFER_SINK_HPP -#define BOOST_CAPY_TEST_BUFFER_SINK_HPP - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace boost { -namespace capy { -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 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 - fuse f; - buffer_sink bs( f ); - - auto r = f.armed( [&]( fuse& ) -> task { - 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 fuse, BufferSink -*/ -class buffer_sink -{ - 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 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( - 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 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] = 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 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 `error::canceled` and commits no data. - - @see 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 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 io_env, - // IoAwaitable. - bool - await_suspend( - std::coroutine_handle<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result<> - await_resume() - { - if(canceled_) - return {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 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 `error::canceled`, commits no data, and - does not signal end-of-stream. - - @see 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<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result<> - await_resume() - { - if(canceled_) - return {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/capy/test/buffer_source.hpp b/include/boost/capy/test/buffer_source.hpp deleted file mode 100644 index b666561ad..000000000 --- a/include/boost/capy/test/buffer_source.hpp +++ /dev/null @@ -1,213 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_TEST_BUFFER_SOURCE_HPP -#define BOOST_CAPY_TEST_BUFFER_SOURCE_HPP - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace boost { -namespace capy { -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 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 - fuse f; - buffer_source bs( f ); - bs.provide( "Hello, " ); - bs.provide( "World!" ); - - auto r = f.armed( [&]( fuse& ) -> task { - const_buffer arr[16]; - auto [ec, bufs] = co_await bs.pull( arr ); - if( ec ) - co_return; - // bufs contains buffer descriptors - std::size_t n = buffer_size( bufs ); - bs.consume( n ); - } ); - @endcode - - @see fuse, BufferSource -*/ -class buffer_source -{ - fuse f_; - std::string data_; - std::size_t pos_ = 0; - std::size_t max_pull_size_; - -public: - /** Construct a buffer source. - - @param f The fuse used to inject errors during pulls. - - @param max_pull_size Maximum bytes returned per pull. - Use to simulate chunked delivery. - */ - explicit buffer_source( - 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 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 `error::canceled` and an empty span. - - @see consume, 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 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 io_env, - // IoAwaitable. - bool - await_suspend( - std::coroutine_handle<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result> - await_resume() - { - if(canceled_) - return {error::canceled, {}}; - - auto ec = self_->f_.maybe_fail(); - if(ec) - return {ec, {}}; - - if(self_->pos_ >= self_->data_.size()) - return {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] = 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/capy/test/read_source.hpp b/include/boost/capy/test/read_source.hpp deleted file mode 100644 index 1fd19ce0e..000000000 --- a/include/boost/capy/test/read_source.hpp +++ /dev/null @@ -1,271 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_TEST_READ_SOURCE_HPP -#define BOOST_CAPY_TEST_READ_SOURCE_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace boost { -namespace capy { -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 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 - fuse f; - read_source rs( f ); - rs.provide( "Hello, " ); - rs.provide( "World!" ); - - auto r = f.armed( [&]( fuse& ) -> task { - char buf[32]; - auto [ec, n] = co_await rs.read( - mutable_buffer( buf, sizeof( buf ) ) ); - if( ec ) - co_return; - // buf contains "Hello, World!" - } ); - @endcode - - @see fuse, ReadSource -*/ -class read_source -{ - fuse f_; - std::string data_; - std::size_t pos_ = 0; - std::size_t max_read_size_; - -public: - /** Construct a read source. - - @param f The fuse used to inject errors during reads. - - @param max_read_size Maximum bytes returned per read. - Use to simulate chunked delivery. - */ - explicit read_source( - 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 `buffer_size( buffers )` bytes from the internal - buffer to the provided mutable buffer sequence. If no data - remains, returns `error::eof`. Before every read, the attached - @ref 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 `error::canceled` and transfers no - data. An empty buffer sequence is a no-op that completes - successfully regardless of the stop token. - - @see 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 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 io_env, - // IoAwaitable. - bool - await_suspend( - std::coroutine_handle<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result - await_resume() - { - if(buffer_empty(buffers_)) - return {{}, 0}; - - if(canceled_) - return {error::canceled, 0}; - - auto ec = self_->f_.maybe_fail(); - if(ec) - return {ec, 0}; - - if(self_->pos_ >= self_->data_.size()) - return {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 = 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}; - } - - /** 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 - `error::eof` with the number of bytes transferred. Before - every read, the attached @ref 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 `error::canceled` and transfers no - data. An empty buffer sequence is a no-op that completes - successfully regardless of the stop token. - - @see 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<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result - await_resume() - { - if(buffer_empty(buffers_)) - return {{}, 0}; - - if(canceled_) - return {error::canceled, 0}; - - auto ec = self_->f_.maybe_fail(); - if(ec) - return {ec, 0}; - - if(self_->pos_ >= self_->data_.size()) - return {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 {error::eof, n}; - return {{}, n}; - } - }; - return awaitable{this, buffers}; - } -}; - -} // test -} // capy -} // boost - -#endif diff --git a/include/boost/capy/test/write_sink.hpp b/include/boost/capy/test/write_sink.hpp deleted file mode 100644 index daf912238..000000000 --- a/include/boost/capy/test/write_sink.hpp +++ /dev/null @@ -1,466 +0,0 @@ -// -// 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/capy -// - -#ifndef BOOST_CAPY_TEST_WRITE_SINK_HPP -#define BOOST_CAPY_TEST_WRITE_SINK_HPP - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace boost { -namespace capy { -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 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 - fuse f; - write_sink ws( f ); - - auto r = f.armed( [&]( fuse& ) -> task { - auto [ec, n] = co_await ws.write( - 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 fuse, WriteSink -*/ -class write_sink -{ - 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 error::test_failure; - data_.erase(0, n); - expect_.erase(0, n); - return {}; - } - -public: - /** Construct a write sink. - - @param f The fuse used to inject errors during writes. - - @param max_write_size Maximum bytes transferred per write. - Use to simulate chunked delivery. - */ - explicit write_sink( - 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 `buffer_size( buffers )` bytes from the provided - const buffer sequence to the internal buffer. Before every write, - the attached @ref 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 `error::canceled` and transfers no - data. An empty buffer sequence is a no-op that completes - successfully regardless of the stop token. - - @see 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 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 io_env, - // IoAwaitable. - bool - await_suspend( - std::coroutine_handle<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result - await_resume() - { - if(buffer_empty(buffers_)) - return {{}, 0}; - - if(canceled_) - return {error::canceled, 0}; - - auto ec = self_->f_.maybe_fail(); - if(ec) - return {ec, 0}; - - std::size_t n = 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); - buffer_copy(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 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 `error::canceled` and transfers no - data. - - @throws std::system_error When the attached @ref fuse is in - exception mode and reaches its failure point. - - @see 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<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result - await_resume() - { - if(canceled_) - return {error::canceled, 0}; - - 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_); - - 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 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 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 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 `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 fuse is in - exception mode and reaches its failure point. - - @see 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<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result - await_resume() - { - if(canceled_) - return {error::canceled, 0}; - - 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_); - - 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 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 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 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 `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 fuse is in - exception mode and reaches its failure point. - - @see 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<>, - io_env const* env) noexcept - { - canceled_ = env->stop_token.stop_requested(); - return false; - } - - io_result<> - await_resume() - { - if(canceled_) - return {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/test/unit/concept/buffer_sink.cpp b/test/unit/concept/buffer_sink.cpp deleted file mode 100644 index 50eda03e6..000000000 --- a/test/unit/concept/buffer_sink.cpp +++ /dev/null @@ -1,264 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include - -#include - -#include -#include -#include - -namespace boost { -namespace capy { - -namespace { - -// 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 capy -} // namespace boost diff --git a/test/unit/concept/buffer_source.cpp b/test/unit/concept/buffer_source.cpp deleted file mode 100644 index 7720978e1..000000000 --- a/test/unit/concept/buffer_source.cpp +++ /dev/null @@ -1,174 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include - -#include - -#include -#include -#include - -namespace boost { -namespace capy { - -namespace { - -// 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 capy -} // namespace boost diff --git a/test/unit/concept/read_source.cpp b/test/unit/concept/read_source.cpp deleted file mode 100644 index 0d323101f..000000000 --- a/test/unit/concept/read_source.cpp +++ /dev/null @@ -1,271 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include - -#include - -#include -#include -#include - -namespace boost { -namespace capy { - -namespace { - -// 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 capy -} // namespace boost diff --git a/test/unit/concept/write_sink.cpp b/test/unit/concept/write_sink.cpp deleted file mode 100644 index fc6ad08e9..000000000 --- a/test/unit/concept/write_sink.cpp +++ /dev/null @@ -1,483 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include - -#include - -#include -#include -#include - -namespace boost { -namespace capy { - -namespace { - -// 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 capy -} // namespace boost diff --git a/test/unit/io/any_buffer_sink.cpp b/test/unit/io/any_buffer_sink.cpp deleted file mode 100644 index 8159d8513..000000000 --- a/test/unit/io/any_buffer_sink.cpp +++ /dev/null @@ -1,1450 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -// Test that pull_from header is self-contained. -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include -#include -#include -#include -#include - -namespace boost { -namespace capy { -namespace { - -// 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 -{ - 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( - 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) - { - 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) - { - 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) - { - 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) - { - test::fuse f; - buffer_write_sink bws(f); - any_buffer_sink abs(std::move(bws)); - BOOST_TEST(abs.has_value()); - } - - // Owning construct, then use - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_sink bs(f); - any_buffer_sink abs(std::move(bs)); - - mutable_buffer arr[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() - { - 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() - { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_sink bs(f); - any_buffer_sink abs(&bs); - - mutable_buffer arr[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_sink bs(f); - any_buffer_sink abs(&bs); - - mutable_buffer arr[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_sink bs(f); - any_buffer_sink abs(&bs); - - mutable_buffer arr[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_sink bs(f); - any_buffer_sink abs(&bs); - - // First write - { - mutable_buffer arr[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[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_sink bs(f); - any_buffer_sink abs(&bs); - - mutable_buffer arr[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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - { - 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 - { - 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 - { - 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() - { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - buffer_write_sink bws(f); - any_buffer_sink abs(&bws); - - mutable_buffer arr[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 = [&]() -> task { - mutable_buffer arr[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{}; - 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 capy -} // namespace boost diff --git a/test/unit/io/any_buffer_source.cpp b/test/unit/io/any_buffer_source.cpp deleted file mode 100644 index f752bdb69..000000000 --- a/test/unit/io/any_buffer_source.cpp +++ /dev/null @@ -1,938 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -// Test that push_to header is self-contained. -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include -#include -#include -#include -#include - -namespace boost { -namespace capy { -namespace { - -// 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 -{ - 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( - 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 {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 {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 {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 {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) - { - 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) - { - test::fuse f; - buffer_read_source brs(f); - any_buffer_source abs(&brs); - BOOST_TEST(abs.has_value()); - } - - // Owning construct from BufferSource+ReadSource - { - test::fuse f; - any_buffer_source abs((buffer_read_source(f))); - BOOST_TEST(abs.has_value()); - } - } - - void - testMove() - { - 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() - { - 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{test::fuse{}}); - any_buffer_source b(buffer_read_source{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 = [&]() -> task { - const_buffer arr[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{}; - test::run_blocking([&](std::size_t v) { result = v; })(coro()); - BOOST_TEST_EQ(result, 7u); - } - - void - testPull() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - bs.provide("hello world"); - - any_buffer_source abs(&bs); - - const_buffer arr[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - bs.provide("hello world"); - - any_buffer_source abs(&bs); - - const_buffer arr[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - bs.provide("test"); - - any_buffer_source abs(&bs); - - const_buffer arr[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - // No data provided - - any_buffer_source abs(&bs); - - const_buffer arr[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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - buffer_read_source brs(f); - brs.provide("hello"); - - any_buffer_source abs(&brs); - - const_buffer arr[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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 capy -} // namespace boost diff --git a/test/unit/io/any_read_source.cpp b/test/unit/io/any_read_source.cpp deleted file mode 100644 index 247d0d9e7..000000000 --- a/test/unit/io/any_read_source.cpp +++ /dev/null @@ -1,846 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include - -#include -#include -#include - -namespace boost { -namespace capy { - -static_assert(ReadSource); - -namespace { - -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 - { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - 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() - { - 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=. - test::fuse f1; - 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 = [&]() -> 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{}; - test::run_blocking([&](std::size_t v) { result = v; })(coro()); - BOOST_TEST_EQ(result, 5u); - } - - void - testSelfAssign() - { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 = detail::max_iovec_ + 4; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 = detail::max_iovec_ + 4; - constexpr unsigned avail = detail::max_iovec_ + 2; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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()); - - 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()); - - 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 capy -} // namespace boost diff --git a/test/unit/io/any_write_sink.cpp b/test/unit/io/any_write_sink.cpp deleted file mode 100644 index a57cac86f..000000000 --- a/test/unit/io/any_write_sink.cpp +++ /dev/null @@ -1,826 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include - -#include -#include -#include -#include - -namespace boost { -namespace capy { - -static_assert(WriteSink); - -namespace { - -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 - { - 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() - { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - testWriteMutableSequence() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::write_sink ws(f); - - any_write_sink aws(&ws); - - std::string body = "hello world"; - auto [ec, n] = co_await aws.write(make_buffer(body)); - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 = detail::max_iovec_ + 4; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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 = detail::max_iovec_ + 4; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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()); - - 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()); - - 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()); - - 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()); - - 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=. - test::fuse f1; - 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 = [&]() -> 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{}; - 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(); - testWriteMutableSequence(); - 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 capy -} // namespace boost diff --git a/test/unit/io/pull_from.cpp b/test/unit/io/pull_from.cpp deleted file mode 100644 index b36cbfe04..000000000 --- a/test/unit/io/pull_from.cpp +++ /dev/null @@ -1,435 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include - -namespace boost { -namespace capy { -namespace { - -class pull_from_test -{ -public: - //------------------------------------------------------------------- - // ReadSource → BufferSink tests - //------------------------------------------------------------------- - - void - testReadSourceToBufferSinkEmpty() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::read_source rs(f); - test::buffer_sink bs(f); - - auto [ec, n] = co_await pull_from(rs, bs); - // ReadSource returns 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - 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 deleted file mode 100644 index 8737e4c2e..000000000 --- a/test/unit/io/push_to.cpp +++ /dev/null @@ -1,386 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include - -namespace boost { -namespace capy { -namespace { - -class push_to_test -{ -public: - //------------------------------------------------------------------- - // BufferSource → WriteSink tests - //------------------------------------------------------------------- - - void - testBufferSourceToWriteSinkEmpty() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - bs.provide("hello world"); - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - bs.provide("hello"); - bs.provide(" "); - bs.provide("world"); - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - bs.provide("hello world"); - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - std::string large_data(10000, 'y'); - bs.provide(large_data); - 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() - { - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f, 7); // max 7 bytes per pull - bs.provide("hello world test data"); - 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - bs.provide("test data"); - 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; - - test::fuse f; - auto r = f.armed([&](test::fuse&) -> task<> { - test::buffer_source bs(f); - bs.provide("test data"); - 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/test/buffer_sink.cpp b/test/unit/test/buffer_sink.cpp deleted file mode 100644 index 34cc1c7ae..000000000 --- a/test/unit/test/buffer_sink.cpp +++ /dev/null @@ -1,355 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include -#include -#include -#include - -namespace boost { -namespace capy { -namespace 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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 - // error::canceled and commits nothing. - std::stop_source ss; - ss.request_stop(); - bool ran = false; - run_blocking(ss.get_token())( - [&]() -> 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())( - [&]() -> 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 deleted file mode 100644 index 86fba9166..000000000 --- a/test/unit/test/buffer_source.cpp +++ /dev/null @@ -1,345 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include -#include -#include - -namespace boost { -namespace capy { -namespace 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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 - // error::canceled and an empty buffer span. - std::stop_source ss; - ss.request_stop(); - bool ran = false; - run_blocking(ss.get_token())( - [&]() -> 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 deleted file mode 100644 index b161a4b21..000000000 --- a/test/unit/test/read_source.cpp +++ /dev/null @@ -1,581 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include -#include -#include - -namespace boost { -namespace capy { -namespace 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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 - // error::canceled instead of data. - std::stop_source ss; - ss.request_stop(); - bool ran = false; - run_blocking(ss.get_token())( - [&]() -> 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())( - [&]() -> 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/read_stream.cpp b/test/unit/test/read_stream.cpp index b3b59ddec..f15462923 100644 --- a/test/unit/test/read_stream.cpp +++ b/test/unit/test/read_stream.cpp @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -27,7 +26,6 @@ namespace capy { namespace test { static_assert(ReadStream); -static_assert(!ReadSource); class read_stream_test { diff --git a/test/unit/test/write_sink.cpp b/test/unit/test/write_sink.cpp deleted file mode 100644 index e1102f596..000000000 --- a/test/unit/test/write_sink.cpp +++ /dev/null @@ -1,647 +0,0 @@ -// -// 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/capy -// - -// Test that header file is self-contained. -#include - -#include -#include -#include -#include - -#include "test/unit/test_helpers.hpp" - -#include -#include -#include - -namespace boost { -namespace capy { -namespace 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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 == error::test_failure); - }); - BOOST_TEST(r.success); - } - - void - testExpectWithExistingData() - { - fuse f; - auto r = f.armed([&](fuse&) -> 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&) -> 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 == error::test_failure); - }); - BOOST_TEST(r.success); - } - - void - testClear() - { - fuse f; - auto r = f.armed([&](fuse&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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&) -> 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 error::canceled and has no effect. - std::stop_source ss; - ss.request_stop(); - bool ran = false; - run_blocking(ss.get_token())( - [&]() -> 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())( - [&]() -> 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())( - [&]() -> 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())( - [&]() -> 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/write_stream.cpp b/test/unit/test/write_stream.cpp index a426661d5..42987f756 100644 --- a/test/unit/test/write_stream.cpp +++ b/test/unit/test/write_stream.cpp @@ -11,7 +11,6 @@ #include #include -#include #include #include #include @@ -27,7 +26,6 @@ namespace capy { namespace test { static_assert(WriteStream); -static_assert(!WriteSink); class write_stream_test {