diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 6f070d251..7cd9d363a 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -34,35 +34,41 @@ ** 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.examples/7.intro.adoc[Example Programs] -** xref:7.examples/7a.hello-task.adoc[Hello Task] -** xref:7.examples/7b.producer-consumer.adoc[Producer-Consumer] -** xref:7.examples/7c.buffer-composition.adoc[Buffer Composition] -** xref:7.examples/7d.mock-stream-testing.adoc[Mock Stream Testing] -** xref:7.examples/7e.type-erased-echo.adoc[Type-Erased Echo] -** xref:7.examples/7f.timeout-cancellation.adoc[Timeout with Cancellation] -** xref:7.examples/7g.parallel-fetch.adoc[Parallel Fetch] -** xref:7.examples/7h.custom-dynamic-buffer.adoc[Custom Dynamic Buffer] -** xref:7.examples/7i.echo-server-corosio.adoc[Echo Server with Corosio] -** xref:7.examples/7j.stream-pipeline.adoc[Stream Pipeline] -** xref:7.examples/7k.strand-serialization.adoc[Strand Serialization] -** xref:7.examples/7l.async-mutex.adoc[Async Mutex] -** xref:7.examples/7m.parallel-tasks.adoc[Parallel Tasks] -** xref:7.examples/7n.custom-executor.adoc[Custom Executor] -* xref:8.design/8.intro.adoc[Design] -** xref:8.design/8a.CapyLayering.adoc[Layered Abstractions] -** xref:8.design/8b.Separation.adoc[Why Capy Is Separate] -** xref:8.design/8c.ReadStream.adoc[ReadStream] -** xref:8.design/8d.ReadSource.adoc[ReadSource] -** xref:8.design/8e.BufferSource.adoc[BufferSource] -** xref:8.design/8f.WriteStream.adoc[WriteStream] -** xref:8.design/8g.WriteSink.adoc[WriteSink] -** xref:8.design/8h.BufferSink.adoc[BufferSink] -** xref:8.design/8i.TypeEraseAwaitable.adoc[Type-Erasing Awaitables] -** xref:8.design/8j.any_buffer_sink.adoc[AnyBufferSink] -** xref:8.design/8k.Executor.adoc[Executor] -** xref:8.design/8l.RunApi.adoc[Run API] -** xref:8.design/8m.WhyNotCobalt.adoc[Why Not Cobalt?] -** xref:8.design/8n.WhyNotCobaltConcepts.adoc[Why Not Cobalt Concepts?] -** xref:8.design/8o.WhyNotTMC.adoc[Why Not TooManyCooks?] +* 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] +** xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] +** xref:8.examples/8c.buffer-composition.adoc[Buffer Composition] +** xref:8.examples/8d.mock-stream-testing.adoc[Mock Stream Testing] +** xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] +** xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] +** xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] +** xref:8.examples/8h.custom-dynamic-buffer.adoc[Custom Dynamic Buffer] +** 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] +** xref:8.examples/8n.custom-executor.adoc[Custom Executor] +* xref:9.design/9.intro.adoc[Design] +** 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?] +** xref:9.design/9n.WhyNotCobaltConcepts.adoc[Why Not Cobalt Concepts?] +** xref:9.design/9o.WhyNotTMC.adoc[Why Not TooManyCooks?] * xref:reference:boost/capy.adoc[Reference] diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc index d55441b5d..b0cdeadac 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.allocators.adoc @@ -53,7 +53,7 @@ To prevent this, any code that calls `.resume()` on a coroutine handle must use capy::safe_resume(h); // saves and restores TLS around h.resume() ---- -`safe_resume` saves the current thread-local allocator, calls `h.resume()`, then restores the saved value. This makes TLS behave like a stack: nested resumes cannot spoil the outer value. All of Capy's built-in executors (`thread_pool`, strands, `blocking_context`) use `safe_resume` internally. Custom executor event loops must do the same -- see xref:7.examples/7n.custom-executor.adoc[Custom Executor] for an example. +`safe_resume` saves the current thread-local allocator, calls `h.resume()`, then restores the saved value. This makes TLS behave like a stack: nested resumes cannot spoil the outer value. All of Capy's built-in executors (`thread_pool`, strands, `blocking_context`) use `safe_resume` internally. Custom executor event loops must do the same -- see xref:8.examples/8n.custom-executor.adoc[Custom Executor] for an example. == The FrameAllocator Concept diff --git a/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc b/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc index 921de559f..fdf0dfc35 100644 --- a/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5a.overview.adoc @@ -99,23 +99,6 @@ This single signature accepts: * A custom composite type * *Any composition of the above—without allocation* -== Zero-Allocation Composition - -With concepts, composition creates views, not copies: - -[source,cpp] ----- -HeaderBuffers headers = /* ... */; -BodyBuffers body = /* ... */; - -// cat() creates a view that iterates both sequences -auto combined = cat(headers, body); // No allocation! - -write_data(combined); // Works because combined satisfies ConstBufferSequence ----- - -The `cat` function returns a lightweight object that, when iterated, first yields buffers from `headers`, then from `body`. The buffers themselves are not copied—only iterators are composed. - == STL Parallel This design follows Stepanov's insight from the STL: algorithms parameterized on concepts (iterators), not concrete types (containers), enable composition that concrete types forbid. diff --git a/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc b/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc index c7c23be20..b2bdb58c9 100644 --- a/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5c.sequences.adoc @@ -133,23 +133,6 @@ task read_all(Stream& stream, Buffers buffers) * Iteration over unconsumed buffers * `buffer_size()` of remaining bytes -== Zero-Allocation Composition - -The `cat()` function composes buffer sequences without allocation: - -[source,cpp] ----- -auto headers = std::array{header_buf1, header_buf2}; -auto body = body_buffer; - -auto combined = cat(headers, body); // No allocation - -// combined satisfies ConstBufferSequence -// Iteration yields: header_buf1, header_buf2, body_buffer ----- - -The returned object stores references (or small copies for single buffers) and iterates through the composed sequence on demand. - == Why Bidirectional? The concepts require bidirectional ranges (not just forward ranges) for two reasons: diff --git a/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc b/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc index 416cb5b01..b10f8c3b4 100644 --- a/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5e.algorithms.adoc @@ -202,7 +202,8 @@ std::string header = build_header(); std::vector body = load_body(); // No copying—header and body are written directly -co_await write(stream, cat(make_buffer(header), make_buffer(body))); +std::array buffers = {make_buffer(header), make_buffer(body)}; +co_await write(stream, buffers); ---- === Scatter/Gather Operations diff --git a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc index 30ec5ba28..ff2218db2 100644 --- a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc +++ b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc @@ -237,4 +237,4 @@ Type-erased wrappers are in ``: * `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:../7.examples/7a.hello-task.adoc[Example Programs] to see complete working examples. +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. diff --git a/doc/modules/ROOT/pages/7.testing/7.intro.adoc b/doc/modules/ROOT/pages/7.testing/7.intro.adoc new file mode 100644 index 000000000..5be820318 --- /dev/null +++ b/doc/modules/ROOT/pages/7.testing/7.intro.adoc @@ -0,0 +1,66 @@ +// +// 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 +// + += Testing + +Real I/O is a poor foundation for unit tests. Network operations are slow, +non-deterministic, and do not fail on demand -- so error-handling paths go +untested until production breaks them. Capy ships a self-contained toolkit +that replaces the transport with in-memory mocks, drives coroutines to +completion on the calling thread, and injects failures at every +`maybe_fail()` site so that every error branch is exercised automatically. +Because each mock satisfies the same concept as its production counterpart, +test code reads the same as production code -- the only difference is the +type of the stream or source you pass in. + +== What This Section Covers + +* xref:7.testing/7a.drivers.adoc[Driving Tests] -- `run_blocking` drives a + coroutine to completion on the calling thread without a real executor; + `fuse` runs the test body repeatedly, injecting an error at each + `maybe_fail()` site in turn until every failure path has been covered; + and the `thread_name` header's `set_current_thread_name` function labels + worker threads so that failures in multi-threaded tests are easier to + attribute. + +* xref:7.testing/7b.mock-streams.adoc[Mock Streams] -- `read_stream`, + `write_stream`, and `stream` (a connected pair) implement the partial-I/O + concepts from xref:6.streams/6b.streams.adoc[Streams]. Use them to test + 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 + into a `std::string` for easy assertion. + +== How the Pieces Fit + +A typical test constructs one or more mocks, arms a `fuse`, and hands the +mocks to the code under test inside a `run_blocking` call. The `fuse` +repeats the test body automatically -- once for each failure site and once +in exception mode -- while `run_blocking` keeps the whole thing on the +calling thread. Buffer utilities such as `bufgrind` and `buffer_to_string` +wrap the mock data for assertions, letting you verify that every split of +an input buffer produces the same correct output. + +Continue to xref:7.testing/7a.drivers.adoc[Driving Tests] to begin. diff --git a/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc b/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc new file mode 100644 index 000000000..bd440f257 --- /dev/null +++ b/doc/modules/ROOT/pages/7.testing/7a.drivers.adoc @@ -0,0 +1,463 @@ +// +// 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 +// + += Driving Tests + +Three utilities work together to run capy code synchronously inside a unit +test: `run_blocking` drives a coroutine to completion on the calling thread, +`fuse` injects errors at controlled points and reruns the test body until +every failure path is covered, and `set_current_thread_name` labels worker +threads so that multi-threaded test output is readable. + +== Prerequisites + +* xref:4.coroutines/4.intro.adoc[Coroutines in Capy] +* xref:6.streams/6.intro.adoc[Stream Concepts] + +== run_blocking + +`run_blocking` bridges async coroutine code into a synchronous test body. +It creates a single-threaded event loop on the calling thread, launches the +coroutine through it, and blocks until the coroutine finishes or throws. +No real executor or thread pool is involved. + +[source,cpp] +---- +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +task compute(int x) +{ + co_return x * 2; +} + +void test_compute() +{ + int result = 0; + run_blocking([&](int v) { result = v; })(compute(21)); + BOOST_TEST(result == 42); +} +---- + +=== Result Capture + +Call `run_blocking` with a lambda to capture the result. The lambda +receives the coroutine's return value on success. Separate lambdas can +handle the success and error cases independently: + +[source,cpp] +---- +// Discard result; rethrow on exception +run_blocking()(my_task()); + +// Capture result; rethrow on exception +int out = 0; +run_blocking([&](int v) { out = v; })(compute(21)); + +// Capture result; handle exception separately +run_blocking( + [&](int v) { out = v; }, + [](std::exception_ptr ep) { std::rethrow_exception(ep); } +)(compute(21)); + +// With a stop token (discards result) +std::stop_source src; +run_blocking(src.get_token())(my_task()); + +// With a stop token and a result handler +run_blocking(src.get_token(), [&](int v) { out = v; })(compute(21)); + +// With a stop token and separate handlers +run_blocking( + src.get_token(), + [&](int v) { out = v; }, + [](std::exception_ptr ep) { std::rethrow_exception(ep); } +)(compute(21)); +---- + +=== How It Works + +`run_blocking` creates a `blocking_context`, an internal single-threaded +execution context. Work posted to it is queued and processed on the calling +thread until the coroutine signals completion, then control returns to the +caller. The inline executor performs symmetric transfer for `dispatch` calls +so that the coroutine chain runs without unnecessary context switches. +Use this only in test code. Production code should use a real execution +context such as a thread pool. + +[cols="1,2"] +|=== +| Overload | Behavior + +| `run_blocking()` +| Discard result. Rethrows captured exceptions. + +| `run_blocking(on_value)` +| Invoke `on_value(v)` on success. Rethrows exceptions if `on_value` + does not accept `std::exception_ptr`. + +| `run_blocking(on_value, on_error)` +| Invoke `on_value(v)` on success or `on_error(ep)` with + `std::exception_ptr` on failure. + +| `run_blocking(stop_token)` +| Drive with an external stop token; discard result. + +| `run_blocking(stop_token, on_value)` +| Drive with an external stop token; invoke `on_value(v)` on success. + +| `run_blocking(stop_token, on_value, on_error)` +| Drive with an external stop token; invoke `on_value(v)` on success or + `on_error(ep)` on failure. +|=== + +== fuse + +`fuse` tests every error-handling path in a coroutine by injecting failures +systematically. Each call to `maybe_fail()` is a potential failure point. +The returned `result` converts to `bool` and, on failure, carries the source +location of the failing call. + +[source,cpp] +---- +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_with_fuse() +{ + fuse f; + auto r = f.armed([](fuse& f) { + auto ec = f.maybe_fail(); + if(ec) + return; // injected error: exit gracefully + + ec = f.maybe_fail(); + if(ec) + return; + }); + BOOST_TEST(r.success); +} +---- + +=== armed() vs. inert() + +`armed()` runs the test body in two full passes (error-code mode, then +exception mode) and is the normal choice for exhaustive error coverage. + +`inert()` runs the test body exactly once with no injection. Calls to +`maybe_fail()` always return an empty error code and never throw. + +Use `inert()` for happy-path verification ("does this work when nothing +fails?"). Use `armed()` for fault-tolerance verification ("does this +handle a failure at every async step?"). A typical test suite pairs +both -- `inert()` confirms the function works at all, then `armed()` +confirms it handles every error site: + +[source,cpp] +---- +fuse f; + +// Smoke test: happy path +auto r1 = f.inert([&](fuse&) -> task { + read_stream rs(f); + rs.provide("hello"); + + char buf[8]; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST(std::string_view(buf, n) == "hello"); +}); +BOOST_TEST(r1.success); + +// Fault coverage: every error site +auto r2 = f.armed([&](fuse&) -> task { + read_stream rs(f); + rs.provide("hello"); + + char buf[8]; + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + co_return; // fuse injected an error; exit gracefully + BOOST_TEST(std::string_view(buf, n) == "hello"); +}); +BOOST_TEST(r2.success); +---- + +The only difference is the `if(ec) co_return;` guard. In `inert()`, +that guard is dead code (`maybe_fail()` never returns an error); in +`armed()`, it is essential. + +The only way to signal a test failure under `inert()` is to call +`f.fail()` from inside the body: + +[source,cpp] +---- +fuse f; +auto r = f.inert([](fuse& f) { + auto ec = f.maybe_fail(); // always returns {} + assert(!ec); + + if(some_condition_failed) + f.fail(); // the only way to signal failure in inert mode +}); +BOOST_TEST(r.success); +---- + +=== The Early-Return Pattern + +When `armed()` injects an error, the test body receives it from +`maybe_fail()`. The body must exit immediately rather than continuing +as though the operation succeeded. The mock streams also call `maybe_fail()` +internally, so this pattern applies to all I/O calls inside an armed test. + +[source,cpp] +---- +// Correct: early return on injected error +auto [ec, n] = co_await rs.read_some(buf); +if(ec) + co_return; // fuse injected an error -- exit gracefully + +// Wrong: asserting success unconditionally +auto [ec, n] = co_await rs.read_some(buf); +BOOST_TEST(!ec); // fails when fuse injects an error +---- + +=== Coroutine Support + +`armed()` detects when the test lambda returns an `IoRunnable` (such as +`task`) and drives it to completion via `run_blocking` internally. +You do not need to call `run_blocking` yourself: + +[source,cpp] +---- +fuse f; +auto r = f.armed([&](fuse&) -> task { + auto ec = f.maybe_fail(); + if(ec) + co_return; + + auto [ec2, n] = co_await rs.read_some(buf); + if(ec2) + co_return; +}); +BOOST_TEST(r.success); +---- + +=== Custom Fail Points + +A type that holds a `fuse` reference can call `maybe_fail()` from its own +methods to declare additional fail points beyond those built into the +mocks. Outside `armed()` or `inert()` the call is a no-op (returns an +empty error code immediately); inside `armed()` it participates in +fault injection alongside every other site. + +[source,cpp] +---- +class widget +{ + fuse& f_; +public: + explicit widget(fuse& f) : f_(f) {} + + std::error_code process() + { + auto ec = f_.maybe_fail(); + if(ec) + return ec; + // ... actual work ... + return {}; + } +}; + +fuse f; +widget w(f); +w.process(); // maybe_fail() returns {} + +auto r = f.armed([&](fuse&) { w.process(); }); // both branches exercised +BOOST_TEST(r.success); +---- + +=== Custom Error Code + +The default injected code is `error::test_failure`. Pass any +`std::error_code` to the constructor to change it: + +[source,cpp] +---- +fuse f(std::make_error_code(std::errc::operation_canceled)); +auto r = f.armed([](fuse& f) { + auto ec = f.maybe_fail(); + if(ec) + { + assert(ec == std::errc::operation_canceled); + return; + } +}); +BOOST_TEST(r.success); +---- + +[cols="1,2"] +|=== +| Member | Description + +| `fuse()` +| Construct with the default error code (`error::test_failure`). + +| `explicit fuse(std::error_code ec)` +| Construct with a custom error code delivered by `maybe_fail()`. + +| `armed(fn) -> result` +| Run `fn` repeatedly in error-code mode then exception mode, failing + at successive `maybe_fail()` sites. Accepts plain lambdas and coroutine + lambdas returning `IoRunnable`. + +| `inert(fn) -> result` +| Run `fn` once with no injection. `maybe_fail()` always returns `{}`. + Accepts plain lambdas and coroutine lambdas returning `IoRunnable`. + +| `operator()(fn) -> result` +| Alias for `armed(fn)`. + +| `maybe_fail() -> std::error_code` +| Return the injected error code at the active failure point, or `{}` + otherwise. In exception mode, throws `std::system_error` instead of + returning an error. Outside `armed`/`inert`, always returns `{}`. + +| `fail()` +| Signal an explicit test failure and stop execution. Records the call + site in `result::loc`. + +| `fail(std::exception_ptr)` +| Signal a test failure with an associated exception. Stored in + `result::ep`. + +| `result::success` +| `true` if the run completed without any failure. + +| `result::loc` +| Source location of the last `maybe_fail()` or `fail()` call on failure. + +| `result::ep` +| Exception pointer captured from a `fail(ep)` call, or `nullptr`. + +| `result::operator bool()` +| Returns `result::success`. +|=== + +== thread_name + +`set_current_thread_name` names the calling thread so that debuggers, +`htop`, and core dumps show a recognizable label instead of a generic thread +ID. This is most useful when a test failure occurs inside a thread pool +worker and you need to identify which worker was involved. The function is a +no-op on platforms without thread-naming support. + +Platform limits on the name length: + +* Linux, FreeBSD, NetBSD: 15 characters +* macOS: 63 characters +* Windows: no practical limit + +[source,cpp] +---- +#include +#include +#include +#include + +using namespace boost::capy; + +thread_pool pool(4); +run_async(pool.get_executor())([]() -> task { + set_current_thread_name("test-worker-0"); + // ... test work runs here; name appears in gdb thread list + co_return; +}()); +pool.join(); +---- + +Note that `set_current_thread_name` lives in namespace `boost::capy`, not +`boost::capy::test`, because the function is useful in any context, not only +tests. + +[cols="1,2"] +|=== +| Function | Description + +| `set_current_thread_name(char const* name)` +| Set the OS thread name for the calling thread. Truncated to the + platform limit. No-op on unsupported platforms. +|=== + +== Putting It Together + +The canonical test skeleton combines a small coroutine and `fuse.armed()`. +The coroutine overload of `armed()` drives the task itself via `run_blocking` +internally, so the test body uses `co_await` directly: + +[source,cpp] +---- +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +task add(int a, int b) +{ + co_return a + b; +} + +void test_add() +{ + fuse f; + auto r = f.armed([&](fuse&) -> task { + auto sum = co_await add(3, 4); + BOOST_TEST(sum == 7); + }); + BOOST_TEST(r.success); +} +---- + +=== Shared State Across Copies + +`fuse` is a value type backed by a `std::shared_ptr`. Every copy +of a `fuse` object shares the same internal state, so all copies respond +to the same `armed()` or `inert()` call. This is what makes the canonical +pattern work: pass a copy of `f` to each mock at construction time, then +call `f.armed(...)` once -- the injection machinery reaches every mock +because they all hold a copy pointing to the same shared state. + +For tests that need mocks, replace `add` with a function that takes a +`read_stream`, `write_stream`, or other mock, and construct those mocks +with the same `fuse f`. The armed loop will then exercise every I/O +failure path through both error-code and exception modes automatically. + +== Reference + +[cols="1,3"] +|=== +| Header | Contents + +| `` +| Synchronous coroutine driver. + +| `` +| Systematic error injection. + +| `` +| Thread naming for diagnostics. +|=== + +Continue to xref:7.testing/7b.mock-streams.adoc[Mock Streams]. diff --git a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc new file mode 100644 index 000000000..dd3ed912c --- /dev/null +++ b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc @@ -0,0 +1,428 @@ +// +// 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 Streams + +Concept-conforming test doubles for the partial-I/O concepts in +xref:6.streams/6b.streams.adoc[Streams]. Use them to drive protocol +code without real network I/O, with optional chunking to exercise +partial-transfer paths. + +== read_stream + +`read_stream` implements the `ReadStream` concept. Test code stages bytes +via `provide()`, then the system under test (or the test body) calls +`read_some()` to consume them. 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_stream 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_stream() +{ + fuse f; + read_stream rs(f); + rs.provide("Hello, "); + rs.provide("World!"); + + auto r = f.armed([&](fuse&) -> task { + char buf[32]; + auto [ec, n] = co_await rs.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + BOOST_TEST(std::string_view(buf, n) == "Hello, World!"); + }); + BOOST_TEST(r.success); +} +---- + +=== Chunked Delivery + +Passing a `max_read_size` to the constructor limits how many bytes +`read_some` returns per call. Use this to simulate a network that +delivers data in small pieces and verify your protocol code loops +correctly on partial reads. + +[source,cpp] +---- +// At most 4 bytes per read_some call +fuse f; +read_stream rs(f, 4); +rs.provide("Hello, World!"); + +auto r = f.armed([&](fuse&) -> task { + char buf[32]; + auto [ec, n] = co_await rs.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec) + co_return; + BOOST_TEST(n == 4); // "Hell" +}); +BOOST_TEST(r.success); +---- + +=== EOF Behavior + +When all provided data has been consumed, `read_some` returns +`cond::eof` with a byte count of zero. The stream does not +suspend; the result is available immediately. + +[source,cpp] +---- +fuse f; +read_stream rs(f); +rs.provide("hi"); + +auto r = f.inert([&](fuse&) -> task { + char buf[8]; + // First read: consumes "hi" + auto [ec, n] = co_await rs.read_some( + mutable_buffer(buf, sizeof(buf))); + BOOST_TEST(!ec); + BOOST_TEST(std::string_view(buf, n) == "hi"); + + // Second read: EOF + auto [ec2, n2] = co_await rs.read_some( + mutable_buffer(buf, sizeof(buf))); + BOOST_TEST(ec2 == cond::eof); + BOOST_TEST(n2 == 0); +}); +BOOST_TEST(r.success); +---- + +[cols="1,2"] +|=== +| Member | Description + +| `explicit read_stream(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 network delivery. + +| `provide(std::string_view sv)` +| Append bytes to the internal buffer for subsequent reads. Multiple + calls accumulate data. + +| `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 read. + +| `available() -> std::size_t` +| Return the number of bytes remaining to be read. + +| `clear()` +| Clear all data and reset the read position. +|=== + +== write_stream + +`write_stream` implements the `WriteStream` concept. The system under +test calls `write_some()` and the test inspects what was written via +`data()`. Test code may also call `expect()` to register the data it +anticipates; any mismatch between written bytes and that prefix causes +`write_some()` to return `error::test_failure` directly. The fuse is a +separate concern used only for error injection. Because `fuse` copies +share state (see +xref:7.testing/7a.drivers.adoc#_shared_state_across_copies[Shared State Across Copies]), +constructing `write_stream 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_stream() +{ + fuse f; + write_stream ws(f); + + auto r = f.armed([&](fuse&) -> task { + auto [ec, n] = co_await ws.write_some( + const_buffer("Hello", 5)); + if(ec) + co_return; + BOOST_TEST(ws.data() == "Hello"); + }); + BOOST_TEST(r.success); +} +---- + +=== Chunked Writes + +Passing a `max_write_size` to the constructor limits how many bytes +`write_some` accepts per call, simulating a slow consumer. Use this +to verify that your code loops correctly until all data is transferred. + +[source,cpp] +---- +fuse f; +write_stream ws(f, 4); // accept at most 4 bytes per call + +auto r = f.inert([&](fuse&) -> task { + auto [ec, n] = co_await ws.write_some( + const_buffer("Hello", 5)); + BOOST_TEST(!ec); + BOOST_TEST(n == 4); // only "Hell" was accepted +}); +BOOST_TEST(r.success); +---- + +=== Expected Data Verification + +Call `expect()` before or after writes to assert that the written data +matches a prefix. Matched bytes are consumed from both sides. If written +data does not match the expected prefix, the next `write_some` call +returns `error::test_failure`. + +[source,cpp] +---- +fuse f; +write_stream ws(f); +ws.expect("Hello World"); + +auto r = f.inert([&](fuse&) -> task { + // Writing matching data succeeds + auto [ec, n] = co_await ws.write_some( + const_buffer("Hello World", 11)); + BOOST_TEST(!ec); +}); +BOOST_TEST(r.success); +---- + +[cols="1,2"] +|=== +| Member | Description + +| `explicit write_stream(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 network delivery. + +| `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)`. + Consults the fuse before every write. + +| `data() -> std::string_view` +| Return bytes written but not yet matched by `expect()`. + +| `size() -> std::size_t` +| Return the number of bytes written. + +| `expect(std::string_view sv) -> std::error_code` +| Register expected data and immediately check any already-written + bytes. Returns an error if existing data does not match. +|=== + +== stream + +`stream` is a connected bidirectional test double. Create a pair with +`make_stream_pair(f)`. Bytes written to one end become readable on the +other. If `read_some` is called on an end with no buffered data, the +calling coroutine suspends until the peer calls `write_some`. This +makes `stream` useful for testing client/server code without real +sockets. + +Both `stream` ends satisfy `ReadStream` and `WriteStream`. + +[source,cpp] +---- +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_stream_pair() +{ + fuse f; + auto [a, b] = make_stream_pair(f); + + auto r = f.armed([&](fuse&) -> task { + auto [ec, n] = co_await a.write_some( + const_buffer("hello", 5)); + if(ec) + co_return; + + char buf[32]; + auto [ec2, n2] = co_await b.read_some( + mutable_buffer(buf, sizeof(buf))); + if(ec2) + co_return; + BOOST_TEST(std::string_view(buf, n2) == "hello"); + }); + BOOST_TEST(r.success); +} +---- + +=== Connected Semantics + +Data written to `a` goes into `b`'s incoming buffer, and vice versa. +`write_some` completes immediately and posts any suspended peer reader +before returning. If `b.read_some()` is called when `a` has not yet +written anything, the coroutine suspends; it resumes the moment `a` +calls `write_some`. + +The `provide()` member is a shortcut that injects bytes directly into +the peer's incoming buffer, bypassing the fuse. Use it during test +setup when you want to pre-populate data without going through an +operation under test. + +=== EOF and Cross-End Closure + +Calling `close()` on one end signals EOF to the peer. The peer drains +any buffered data first; once the buffer is empty, subsequent +`read_some` calls on the peer return `cond::eof`. The peer may still +call `write_some` after receiving EOF. + +When the fuse injects an error during `read_some` or `write_some`, the +pair is automatically closed: the calling end returns the injected +error, any suspended reader on the other end is resumed with +`cond::eof`, and all subsequent operations on both ends return +`cond::eof`. + +=== Thread Safety + +Single-threaded only. Both ends of the pair must be accessed from the +same thread. Concurrent access from multiple threads or multiple +concurrent coroutines is undefined behavior. + +[cols="1,2"] +|=== +| Function / Member | Description + +| `make_stream_pair(fuse f = {}) -> std::pair` +| Create a connected pair sharing the supplied fuse. + +| `read_some(MutableBufferSequence buffers)` +| Partial read from the peer's outgoing data. Suspends if no data is + available. Returns `cond::eof` when the stream is closed or the peer + called `close()`. Consults the fuse before every read (unless + draining after `close()`). + +| `write_some(ConstBufferSequence buffers)` +| Partial write into the peer's incoming buffer. Resumes a suspended + peer reader if any. Returns `cond::eof` if the stream is closed. + Consults the fuse before every write. + +| `close()` +| Signal EOF to the peer's reads. Buffered data is drained first. + Writes from the peer are unaffected. + +| `set_max_read_size(std::size_t n)` +| Limit bytes returned per `read_some` call on this end, simulating + chunked network delivery (applies to this end only; the peer end has + its own independent limit). + +| `provide(std::string_view sv)` +| Inject bytes into this stream for reading, bypassing the fuse. + Resumes a suspended `read_some` if any. + +| `expect(std::string_view expected) -> std::pair` +| Read exactly `expected.size()` bytes and compare. Returns the error + code and whether the data matched. + +| `data() -> std::string_view` +| Return a view of the unread bytes buffered in this stream. +|=== + +== Putting It Together + +The following snippet tests a function that reads a single line +terminated by `'\n'` from a `ReadStream`. The `fuse.armed()` loop +runs the coroutine repeatedly, failing at every `read_some` call in +turn, then reruns in exception mode. Each injected failure exercises +a different error-handling branch inside `read_line`. + +[source,cpp] +---- +#include +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +// Function under test: read until '\n' or EOF +template +task> +read_line(S& stream) +{ + std::string line; + char ch; + for(;;) + { + auto [ec, n] = co_await stream.read_some( + mutable_buffer(&ch, 1)); + if(ec) + co_return {ec, std::move(line)}; + if(ch == '\n') + break; + line += ch; + } + co_return {std::error_code{}, std::move(line)}; +} + +void test_read_line() +{ + fuse f; + auto r = f.armed([&](fuse&) -> task { + read_stream rs(f); + rs.provide("hello\n"); + + auto [ec, line] = co_await read_line(rs); + if(ec) + co_return; // fuse injected an error; exit gracefully + BOOST_TEST(line == "hello"); + }); + BOOST_TEST(r.success); +} +---- + +== Reference + +[cols="1,3"] +|=== +| Header | Contents + +| `` +| Mock ReadStream with controllable partial reads. + +| `` +| Mock WriteStream with controllable partial writes and expectations. + +| `` +| Connected bidirectional pair for client/server tests. +|=== + +Continue to xref:7.testing/7c.mock-sources-sinks.adoc[Mock Sources and Sinks]. 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 new file mode 100644 index 000000000..d3308f125 --- /dev/null +++ b/doc/modules/ROOT/pages/7.testing/7c.mock-sources-sinks.adoc @@ -0,0 +1,291 @@ +// +// 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 `write_some()` +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. + +| `write_eof()` +| Signal end-of-stream without writing data. Sets `eof_called()` to + `true` on success. Consults the fuse before the call. + +| `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 new file mode 100644 index 000000000..028203b73 --- /dev/null +++ b/doc/modules/ROOT/pages/7.testing/7d.mock-buffer-concepts.adoc @@ -0,0 +1,343 @@ +// +// 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/7.testing/7e.buffer-inspection.adoc b/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc new file mode 100644 index 000000000..5b8aa3481 --- /dev/null +++ b/doc/modules/ROOT/pages/7.testing/7e.buffer-inspection.adoc @@ -0,0 +1,271 @@ +// +// 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 +// + += Buffer Inspection + +Two small utilities round out the toolkit. `bufgrind` iterates every split +point of a buffer sequence, exercising every chunk-boundary condition in the +system under test. `buffer_to_string` concatenates buffer sequences into a +`std::string` for assertion. + +== bufgrind + +`bufgrind` is a test utility that iterates through every way to split a buffer +sequence into two contiguous pieces. For an N-byte input it produces N+1 split +positions: `(0, N)`, `(1, N-1)`, ..., `(N, 0)`. The two pieces at each +position concatenate back to the original sequence. Any code that processes +a buffer in chunks is exercised at every possible chunk boundary with a single +`while` loop. + +`bufgrind` does not perform I/O and does not consult a fuse, so the snippets +on this page drive it under `f.inert(...)`: a single pass is sufficient to +visit every split position, and there are no async failure sites to inject. + +[source,cpp] +---- +#include +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_all_splits() +{ + std::string data = "hello"; + auto cb = make_buffer(data); + + fuse f; + auto r = f.inert([&](fuse&) -> task<> { + bufgrind bg(cb); + while(bg) + { + auto [b1, b2] = co_await bg.next(); + BOOST_TEST_EQ(buffer_to_string(b1, b2), data); + } + }); + BOOST_TEST(r.success); +} +---- + +=== Iteration Pattern + +For a 5-byte input `"hello"`, `bufgrind` yields six positions: + +---- +pos=0: b1="" b2="hello" +pos=1: b1="h" b2="ello" +pos=2: b1="he" b2="llo" +pos=3: b1="hel" b2="lo" +pos=4: b1="hell" b2="o" +pos=5: b1="hello" b2="" +---- + +An empty buffer sequence yields one position where both pieces are empty, +so the loop body always executes at least once. + +=== Step Size + +When the input is large, visiting every byte boundary is expensive. Pass a +`step` parameter to skip positions. The final position (equal to the total +size) is always visited regardless of step alignment. + +[source,cpp] +---- +std::string data = "0123456789"; // 10 bytes +auto cb = make_buffer(data); + +bufgrind bg(cb, 3); +// Visits positions: 0, 3, 6, 9, 10 +while(bg) +{ + auto [b1, b2] = co_await bg.next(); + // exercise parser at each split point +} +---- + +A step of 0 is treated as 1. A step larger than the total size reduces to +two positions: 0 and size. + +=== Mutability Preservation + +`bufgrind` is templated on a `ConstBufferSequence` but the split type it +produces follows the mutability of the input. Passing a `mutable_buffer` +yields `mutable_buffer` slices; passing a `const_buffer` yields +`const_buffer` slices. This matters for tests that need to write into the +produced buffers rather than only read from them. + +[source,cpp] +---- +char data[] = "hello"; +mutable_buffer mb(data, 5); + +bufgrind bg(mb); +while(bg) +{ + auto [b1, b2] = co_await bg.next(); + // b1 and b2 are mutable_buffer; callers may write into them + static_assert(std::is_same_v); +} +---- + +[cols="1,2"] +|=== +| Member | Description + +| `bufgrind(BS const& bs, std::size_t step = 1)` +| Construct over a buffer sequence. `step` controls how many bytes to + advance on each call to `next()`. A step of 0 is treated as 1. + The final split at `buffer_size(bs)` is always included. + +| `operator bool() const` +| Return `true` while more split positions remain. + +| `next()` +| Advance to the next split position. Returns an awaitable that yields + `split_type`, a `std::pair` of `slice_type` values representing + the two pieces at the current position. +|=== + +== buffer_to_string + +`buffer_to_string` concatenates one or more buffer sequences into a +`std::string`. With a single argument it converts that buffer sequence; +with multiple arguments it concatenates them in order. The most common +use is asserting the combined content of a `bufgrind` split. + +[source,cpp] +---- +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +void test_buffer_to_string() +{ + // Single buffer sequence + const_buffer cb(make_buffer(std::string_view("hello"))); + BOOST_TEST_EQ(buffer_to_string(cb), "hello"); + + // Multiple buffer sequences concatenated in order + const_buffer b1(make_buffer(std::string_view("hello"))); + const_buffer b2(make_buffer(std::string_view(" world"))); + BOOST_TEST_EQ(buffer_to_string(b1, b2), "hello world"); +} +---- + +=== Use With bufgrind + +The typical pattern passes both halves of a `bufgrind` split directly to +`buffer_to_string` to verify that each split reconstructs the original +input: + +[source,cpp] +---- +std::string original = "hello world"; +auto cb = make_buffer(original); + +fuse f; +auto r = f.inert([&](fuse&) -> task<> { + bufgrind bg(cb); + while(bg) + { + auto [b1, b2] = co_await bg.next(); + BOOST_TEST_EQ(buffer_to_string(b1, b2), original); + } +}); +BOOST_TEST(r.success); +---- + +[cols="1,2"] +|=== +| Function | Description + +| `buffer_to_string(Buffers const&... bufs) -> std::string` +| Concatenate one or more `ConstBufferSequence` arguments into a single + `std::string`. Arguments are appended in the order given. +|=== + +== Putting It Together + +The following snippet tests a hypothetical parser that reads from a +`read_stream`. `bufgrind` exercises every split of the input so the parser +is run against every possible chunk boundary; `buffer_to_string` verifies +the output at each split: + +[source,cpp] +---- +#include +#include +#include +#include +#include +#include + +using namespace boost::capy; +using namespace boost::capy::test; + +// Hypothetical parser: reads all bytes from a ReadStream +task read_all(read_stream& rs) +{ + std::string out; + std::array buf; + for(;;) + { + auto [ec, n] = co_await rs.read_some(make_buffer(buf)); + if(ec) + co_return out; + out.append(buf.data(), n); + } +} + +void test_parser_all_splits() +{ + std::string input = "GET / HTTP/1.1\r\n"; + auto cb = make_buffer(input); + + fuse f; + auto r = f.inert([&](fuse&) -> task<> { + bufgrind bg(cb); + while(bg) + { + auto [b1, b2] = co_await bg.next(); + + // Feed the split as two discrete reads + read_stream rs(f); + rs.provide(buffer_to_string(b1)); + rs.provide(buffer_to_string(b2)); + + std::string got = co_await read_all(rs); + BOOST_TEST_EQ(got, input); + } + }); + BOOST_TEST(r.success); +} +---- + +== Reference + +[cols="1,3"] +|=== +| Header | Contents + +| `` +| Exhaustive buffer split-point iterator. + +| `` +| Buffer-sequence to string helper. +|=== + +You have reached the end of the Testing section. Continue +to xref:8.examples/8.intro.adoc[Example Programs] for end-to-end usage +or xref:reference:boost/capy.adoc[Reference] for the API browser. diff --git a/doc/modules/ROOT/pages/7.examples/7.intro.adoc b/doc/modules/ROOT/pages/8.examples/8.intro.adoc similarity index 100% rename from doc/modules/ROOT/pages/7.examples/7.intro.adoc rename to doc/modules/ROOT/pages/8.examples/8.intro.adoc diff --git a/doc/modules/ROOT/pages/7.examples/7a.hello-task.adoc b/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc similarity index 97% rename from doc/modules/ROOT/pages/7.examples/7a.hello-task.adoc rename to doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc index f0d544160..4be42e699 100644 --- a/doc/modules/ROOT/pages/7.examples/7a.hello-task.adoc +++ b/doc/modules/ROOT/pages/8.examples/8a.hello-task.adoc @@ -100,4 +100,4 @@ Hello from Capy! == Next Steps -* xref:7.examples/7b.producer-consumer.adoc[Producer-Consumer] — Multiple tasks communicating +* xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] — Multiple tasks communicating diff --git a/doc/modules/ROOT/pages/7.examples/7b.producer-consumer.adoc b/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc similarity index 97% rename from doc/modules/ROOT/pages/7.examples/7b.producer-consumer.adoc rename to doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc index 00bf16419..935b264b8 100644 --- a/doc/modules/ROOT/pages/7.examples/7b.producer-consumer.adoc +++ b/doc/modules/ROOT/pages/8.examples/8b.producer-consumer.adoc @@ -11,7 +11,7 @@ Two tasks communicating via an async event, with strand serialization. == Prerequisites -* Completed xref:7.examples/7a.hello-task.adoc[Hello Task] +* Completed xref:8.examples/8a.hello-task.adoc[Hello Task] * Understanding of basic task creation and launching == Source Code @@ -173,4 +173,4 @@ Consumer: received value 42 == Next Steps -* xref:7.examples/7c.buffer-composition.adoc[Buffer Composition] — Zero-allocation buffer composition +* xref:8.examples/8c.buffer-composition.adoc[Buffer Composition] — Zero-allocation buffer composition diff --git a/doc/modules/ROOT/pages/7.examples/7c.buffer-composition.adoc b/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc similarity index 95% rename from doc/modules/ROOT/pages/7.examples/7c.buffer-composition.adoc rename to doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc index 9b9e992f6..88cb07da3 100644 --- a/doc/modules/ROOT/pages/7.examples/7c.buffer-composition.adoc +++ b/doc/modules/ROOT/pages/8.examples/8c.buffer-composition.adoc @@ -10,7 +10,7 @@ Composing buffer sequences without allocation for scatter/gather I/O. == Prerequisites -* Completed xref:7.examples/7b.producer-consumer.adoc[Producer-Consumer] +* Completed xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] * Understanding of buffer types from xref:../5.buffers/5b.types.adoc[Buffer Types] == Source Code @@ -223,9 +223,8 @@ Prepared 2 buffers with 128 bytes total capacity == Exercises 1. Create a function that takes any `ConstBufferSequence` and prints its contents -2. Measure the difference between copying data into a single buffer vs. using `cat()` -3. Implement a simple message framing protocol using buffer composition +2. Implement a simple message framing protocol using buffer composition == Next Steps -* xref:7.examples/7d.mock-stream-testing.adoc[Mock Stream Testing] — Unit testing with mock streams +* xref:8.examples/8d.mock-stream-testing.adoc[Mock Stream Testing] — Unit testing with mock streams diff --git a/doc/modules/ROOT/pages/7.examples/7d.mock-stream-testing.adoc b/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc similarity index 98% rename from doc/modules/ROOT/pages/7.examples/7d.mock-stream-testing.adoc rename to doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc index f73288d48..4d7fc39c0 100644 --- a/doc/modules/ROOT/pages/7.examples/7d.mock-stream-testing.adoc +++ b/doc/modules/ROOT/pages/8.examples/8d.mock-stream-testing.adoc @@ -10,7 +10,7 @@ Unit testing protocol code with mock streams and error injection. == Prerequisites -* Completed xref:7.examples/7c.buffer-composition.adoc[Buffer Composition] +* Completed xref:8.examples/8c.buffer-composition.adoc[Buffer Composition] * Understanding of streams from xref:../6.streams/6b.streams.adoc[Streams] == Source Code @@ -253,4 +253,4 @@ All tests passed! == Next Steps -* xref:7.examples/7e.type-erased-echo.adoc[Type-Erased Echo] — Compilation firewall pattern +* xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] — Compilation firewall pattern diff --git a/doc/modules/ROOT/pages/7.examples/7e.type-erased-echo.adoc b/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc similarity index 96% rename from doc/modules/ROOT/pages/7.examples/7e.type-erased-echo.adoc rename to doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc index 0bac9b119..61f01f2a0 100644 --- a/doc/modules/ROOT/pages/7.examples/7e.type-erased-echo.adoc +++ b/doc/modules/ROOT/pages/8.examples/8e.type-erased-echo.adoc @@ -10,7 +10,7 @@ Echo server demonstrating the compilation firewall pattern. == Prerequisites -* Completed xref:7.examples/7d.mock-stream-testing.adoc[Mock Stream Testing] +* Completed xref:8.examples/8d.mock-stream-testing.adoc[Mock Stream Testing] * Understanding of type erasure from xref:../6.streams/6f.isolation.adoc[Physical Isolation] == Source Code @@ -184,4 +184,4 @@ Echo output: Hello, World! == Next Steps -* xref:7.examples/7f.timeout-cancellation.adoc[Timeout with Cancellation] — Stop tokens for timeout +* xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] — Stop tokens for timeout diff --git a/doc/modules/ROOT/pages/7.examples/7f.timeout-cancellation.adoc b/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc similarity index 97% rename from doc/modules/ROOT/pages/7.examples/7f.timeout-cancellation.adoc rename to doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc index 835bfdd1d..4bd6d9a3d 100644 --- a/doc/modules/ROOT/pages/7.examples/7f.timeout-cancellation.adoc +++ b/doc/modules/ROOT/pages/8.examples/8f.timeout-cancellation.adoc @@ -10,7 +10,7 @@ Using stop tokens to implement operation timeouts. == Prerequisites -* Completed xref:7.examples/7e.type-erased-echo.adoc[Type-Erased Echo] +* Completed xref:8.examples/8e.type-erased-echo.adoc[Type-Erased Echo] * Understanding of stop tokens from xref:../4.coroutines/4e.cancellation.adoc[Cancellation] == Source Code @@ -241,4 +241,4 @@ Cancelled (returned nullopt) == Next Steps -* xref:7.examples/7g.parallel-fetch.adoc[Parallel Fetch] — Concurrent operations with when_all +* xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] — Concurrent operations with when_all diff --git a/doc/modules/ROOT/pages/7.examples/7g.parallel-fetch.adoc b/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc similarity index 98% rename from doc/modules/ROOT/pages/7.examples/7g.parallel-fetch.adoc rename to doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc index 78e610a76..9efdc0492 100644 --- a/doc/modules/ROOT/pages/7.examples/7g.parallel-fetch.adoc +++ b/doc/modules/ROOT/pages/8.examples/8g.parallel-fetch.adoc @@ -10,7 +10,7 @@ Running multiple operations concurrently with `when_all`. == Prerequisites -* Completed xref:7.examples/7f.timeout-cancellation.adoc[Timeout with Cancellation] +* Completed xref:8.examples/8f.timeout-cancellation.adoc[Timeout with Cancellation] * Understanding of `when_all` from xref:../4.coroutines/4f.composition.adoc[Composition] == Source Code @@ -258,4 +258,4 @@ Caught error: B failed! == Next Steps -* xref:7.examples/7h.custom-dynamic-buffer.adoc[Custom Dynamic Buffer] — Implementing your own buffer +* xref:8.examples/8h.custom-dynamic-buffer.adoc[Custom Dynamic Buffer] — Implementing your own buffer diff --git a/doc/modules/ROOT/pages/7.examples/7h.custom-dynamic-buffer.adoc b/doc/modules/ROOT/pages/8.examples/8h.custom-dynamic-buffer.adoc similarity index 98% rename from doc/modules/ROOT/pages/7.examples/7h.custom-dynamic-buffer.adoc rename to doc/modules/ROOT/pages/8.examples/8h.custom-dynamic-buffer.adoc index 0a77591d1..fd628da6a 100644 --- a/doc/modules/ROOT/pages/7.examples/7h.custom-dynamic-buffer.adoc +++ b/doc/modules/ROOT/pages/8.examples/8h.custom-dynamic-buffer.adoc @@ -10,7 +10,7 @@ Implementing the DynamicBuffer concept for a custom allocation strategy. == Prerequisites -* Completed xref:7.examples/7g.parallel-fetch.adoc[Parallel Fetch] +* Completed xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] * Understanding of dynamic buffers from xref:../5.buffers/5f.dynamic.adoc[Dynamic Buffers] == Source Code @@ -296,4 +296,4 @@ Buffer statistics: == Next Steps -* xref:7.examples/7i.echo-server-corosio.adoc[Echo Server with Corosio] — Real networking +* xref:8.examples/8i.echo-server-corosio.adoc[Echo Server with Corosio] — Real networking diff --git a/doc/modules/ROOT/pages/7.examples/7i.echo-server-corosio.adoc b/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc similarity index 96% rename from doc/modules/ROOT/pages/7.examples/7i.echo-server-corosio.adoc rename to doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc index 67dbb7dd3..e4c073d27 100644 --- a/doc/modules/ROOT/pages/7.examples/7i.echo-server-corosio.adoc +++ b/doc/modules/ROOT/pages/8.examples/8i.echo-server-corosio.adoc @@ -10,7 +10,7 @@ A complete echo server using Corosio for real network I/O. == Prerequisites -* Completed xref:7.examples/7h.custom-dynamic-buffer.adoc[Custom Dynamic Buffer] +* Completed xref:8.examples/8h.custom-dynamic-buffer.adoc[Custom Dynamic Buffer] * Corosio library installed * Understanding of TCP networking basics @@ -188,4 +188,4 @@ Connection from 127.0.0.1:54321 == Next Steps -* xref:7.examples/7j.stream-pipeline.adoc[Stream Pipeline] -- Data transformation chains +* xref:8.examples/8j.stream-pipeline.adoc[Stream Pipeline] -- Data transformation chains diff --git a/doc/modules/ROOT/pages/7.examples/7j.stream-pipeline.adoc b/doc/modules/ROOT/pages/8.examples/8j.stream-pipeline.adoc similarity index 99% rename from doc/modules/ROOT/pages/7.examples/7j.stream-pipeline.adoc rename to doc/modules/ROOT/pages/8.examples/8j.stream-pipeline.adoc index 60010211e..6256f307e 100644 --- a/doc/modules/ROOT/pages/7.examples/7j.stream-pipeline.adoc +++ b/doc/modules/ROOT/pages/8.examples/8j.stream-pipeline.adoc @@ -10,7 +10,7 @@ Data transformation through a pipeline of sources and sinks. == Prerequisites -* Completed xref:7.examples/7i.echo-server-corosio.adoc[Echo Server with Corosio] +* 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 @@ -435,7 +435,7 @@ Output (52 bytes): == Next Steps -* xref:7.examples/7k.strand-serialization.adoc[Strand Serialization] -- Lock-free shared state with strands +* xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] -- Lock-free shared state with strands == Summary diff --git a/doc/modules/ROOT/pages/7.examples/7k.strand-serialization.adoc b/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc similarity index 96% rename from doc/modules/ROOT/pages/7.examples/7k.strand-serialization.adoc rename to doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc index 8509d7403..d7c66fc85 100644 --- a/doc/modules/ROOT/pages/7.examples/7k.strand-serialization.adoc +++ b/doc/modules/ROOT/pages/8.examples/8k.strand-serialization.adoc @@ -10,7 +10,7 @@ Protecting shared state with a strand instead of a mutex. == Prerequisites -* Completed xref:7.examples/7b.producer-consumer.adoc[Producer-Consumer] (introduces `strand`) +* Completed xref:8.examples/8b.producer-consumer.adoc[Producer-Consumer] (introduces `strand`) * Understanding of `when_all` from xref:../4.coroutines/4f.composition.adoc[Composition] == Source Code @@ -143,4 +143,4 @@ Final counter: 10000 (expected 10000) == Next Steps -* xref:7.examples/7l.async-mutex.adoc[Async Mutex] -- FIFO coroutine locking +* xref:8.examples/8l.async-mutex.adoc[Async Mutex] -- FIFO coroutine locking diff --git a/doc/modules/ROOT/pages/7.examples/7l.async-mutex.adoc b/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc similarity index 97% rename from doc/modules/ROOT/pages/7.examples/7l.async-mutex.adoc rename to doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc index c0eb10a7a..dfcac7d90 100644 --- a/doc/modules/ROOT/pages/7.examples/7l.async-mutex.adoc +++ b/doc/modules/ROOT/pages/8.examples/8l.async-mutex.adoc @@ -11,7 +11,7 @@ Fair FIFO coroutine locking with `async_mutex`. == Prerequisites -* Completed xref:7.examples/7k.strand-serialization.adoc[Strand Serialization] +* Completed xref:8.examples/8k.strand-serialization.adoc[Strand Serialization] == Source Code @@ -165,4 +165,4 @@ Acquisition order: W0 -> W1 -> W2 -> W3 -> W4 -> W5 == Next Steps -* xref:7.examples/7m.parallel-tasks.adoc[Parallel Tasks] -- Distributing work across a thread pool +* xref:8.examples/8m.parallel-tasks.adoc[Parallel Tasks] -- Distributing work across a thread pool diff --git a/doc/modules/ROOT/pages/7.examples/7m.parallel-tasks.adoc b/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc similarity index 97% rename from doc/modules/ROOT/pages/7.examples/7m.parallel-tasks.adoc rename to doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc index a35250346..facdd194e 100644 --- a/doc/modules/ROOT/pages/7.examples/7m.parallel-tasks.adoc +++ b/doc/modules/ROOT/pages/8.examples/8m.parallel-tasks.adoc @@ -10,7 +10,7 @@ Distributing CPU-bound work across a thread pool and collecting results. == Prerequisites -* Completed xref:7.examples/7g.parallel-fetch.adoc[Parallel Fetch] (introduces `when_all`) +* Completed xref:8.examples/8g.parallel-fetch.adoc[Parallel Fetch] (introduces `when_all`) == Source Code @@ -159,4 +159,4 @@ Total: 49995000 (expected 49995000) == Next Steps -* xref:7.examples/7n.custom-executor.adoc[Custom Executor] -- Building your own execution context +* xref:8.examples/8n.custom-executor.adoc[Custom Executor] -- Building your own execution context diff --git a/doc/modules/ROOT/pages/7.examples/7n.custom-executor.adoc b/doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc similarity index 100% rename from doc/modules/ROOT/pages/7.examples/7n.custom-executor.adoc rename to doc/modules/ROOT/pages/8.examples/8n.custom-executor.adoc diff --git a/doc/modules/ROOT/pages/8.design/8.intro.adoc b/doc/modules/ROOT/pages/9.design/9.intro.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8.intro.adoc rename to doc/modules/ROOT/pages/9.design/9.intro.adoc diff --git a/doc/modules/ROOT/pages/8.design/8a.CapyLayering.adoc b/doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8a.CapyLayering.adoc rename to doc/modules/ROOT/pages/9.design/9a.CapyLayering.adoc diff --git a/doc/modules/ROOT/pages/8.design/8b.Separation.adoc b/doc/modules/ROOT/pages/9.design/9b.Separation.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8b.Separation.adoc rename to doc/modules/ROOT/pages/9.design/9b.Separation.adoc diff --git a/doc/modules/ROOT/pages/8.design/8c.ReadStream.adoc b/doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8c.ReadStream.adoc rename to doc/modules/ROOT/pages/9.design/9c.ReadStream.adoc diff --git a/doc/modules/ROOT/pages/8.design/8d.ReadSource.adoc b/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc similarity index 99% rename from doc/modules/ROOT/pages/8.design/8d.ReadSource.adoc rename to doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc index 10db090b5..0e0a61b73 100644 --- a/doc/modules/ROOT/pages/8.design/8d.ReadSource.adoc +++ b/doc/modules/ROOT/pages/9.design/9d.ReadSource.adoc @@ -352,7 +352,7 @@ Examples of types that satisfy `ReadSource`: == Errors May Accompany Data -The `read_some` contract (inherited from `ReadStream`) permits `n > 0` when `ec` is set, including on EOF. The implementation reports exactly what happened: the bytes that arrived and the condition that stopped the transfer. See xref:8.design/8c.ReadStream.adoc#_design_foundations_why_errors_may_accompany_data[ReadStream: Why Errors May Accompany Data] for the full rationale. The key points: +The `read_some` contract (inherited from `ReadStream`) permits `n > 0` when `ec` is set, including on EOF. The implementation reports exactly what happened: the bytes that arrived and the condition that stopped the transfer. See xref:9.design/9c.ReadStream.adoc#_design_foundations_why_errors_may_accompany_data[ReadStream: Why Errors May Accompany Data] for the full rationale. The key points: - The `(error_code, size_t)` return type can carry both values simultaneously, transcending the POSIX limitation of reporting only one per call. - Layered streams (TLS, compression) may encounter an error after a partial transfer. Allowing `(ec, n)` with `n > 0` avoids forcing deferred-error bookkeeping or data loss. diff --git a/doc/modules/ROOT/pages/8.design/8e.BufferSource.adoc b/doc/modules/ROOT/pages/9.design/9e.BufferSource.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8e.BufferSource.adoc rename to doc/modules/ROOT/pages/9.design/9e.BufferSource.adoc diff --git a/doc/modules/ROOT/pages/8.design/8f.WriteStream.adoc b/doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8f.WriteStream.adoc rename to doc/modules/ROOT/pages/9.design/9f.WriteStream.adoc diff --git a/doc/modules/ROOT/pages/8.design/8g.WriteSink.adoc b/doc/modules/ROOT/pages/9.design/9g.WriteSink.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8g.WriteSink.adoc rename to doc/modules/ROOT/pages/9.design/9g.WriteSink.adoc diff --git a/doc/modules/ROOT/pages/8.design/8h.BufferSink.adoc b/doc/modules/ROOT/pages/9.design/9h.BufferSink.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8h.BufferSink.adoc rename to doc/modules/ROOT/pages/9.design/9h.BufferSink.adoc diff --git a/doc/modules/ROOT/pages/8.design/8i.TypeEraseAwaitable.adoc b/doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8i.TypeEraseAwaitable.adoc rename to doc/modules/ROOT/pages/9.design/9i.TypeEraseAwaitable.adoc diff --git a/doc/modules/ROOT/pages/8.design/8j.any_buffer_sink.adoc b/doc/modules/ROOT/pages/9.design/9j.any_buffer_sink.adoc similarity index 99% rename from doc/modules/ROOT/pages/8.design/8j.any_buffer_sink.adoc rename to doc/modules/ROOT/pages/9.design/9j.any_buffer_sink.adoc index fe31e5bc4..48e756573 100644 --- a/doc/modules/ROOT/pages/8.design/8j.any_buffer_sink.adoc +++ b/doc/modules/ROOT/pages/9.design/9j.any_buffer_sink.adoc @@ -153,7 +153,7 @@ The serializer never allocates a scratch buffer for the formatted output. The by == Awaitable Caching -`any_buffer_sink` uses the split vtable pattern described in xref:8.design/8h.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. +`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. diff --git a/doc/modules/ROOT/pages/8.design/8k.Executor.adoc b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc similarity index 99% rename from doc/modules/ROOT/pages/8.design/8k.Executor.adoc rename to doc/modules/ROOT/pages/9.design/9k.Executor.adoc index c931c5748..bafb069b6 100644 --- a/doc/modules/ROOT/pages/8.design/8k.Executor.adoc +++ b/doc/modules/ROOT/pages/9.design/9k.Executor.adoc @@ -43,7 +43,7 @@ If the executor determines it is safe (e.g., the current thread is already assoc Queues the continuation for later execution without ever executing it inline. Never blocks. The continuation is linked into the executor's internal queue via its intrusive `next` pointer -- no per-post heap allocation. -Both operations accept `continuation&` rather than `std::coroutine_handle<>`. A `continuation` wraps a coroutine handle with an intrusive list pointer, enabling zero-allocation queuing. See xref:../continuation-rationale.adoc[Continuation Rationale] for the design of this type. +Both operations accept `continuation&` rather than `std::coroutine_handle<>`. A `continuation` wraps a coroutine handle with an intrusive list pointer, enabling zero-allocation queuing. The remaining operations support context access, lifecycle management, and identity: @@ -372,7 +372,7 @@ start(op); // -- too late For coroutines, this ordering is fatal. Coroutine frame allocation happens _before_ the coroutine body executes. The compiler calls `operator new` first, then constructs the promise, then begins execution. Any mechanism that provides the allocator _after_ the coroutine call -- receiver queries, `await_transform`, explicit method calls -- arrives after the frame is already allocated with the wrong (or default) allocator. -Capy's model flows context _forward_ from launcher to task. The `run_async(ex, alloc)(my_task())` two-phase invocation sets the thread-local allocator _before_ the task expression is evaluated, so `operator new` reads it in time. This is described in detail in xref:8.design/8l.RunApi.adoc[Run API]. +Capy's model flows context _forward_ from launcher to task. The `run_async(ex, alloc)(my_task())` two-phase invocation sets the thread-local allocator _before_ the task expression is evaluated, so `operator new` reads it in time. This is described in detail in xref:9.design/9l.RunApi.adoc[Run API]. The same forward-flowing model applies to executors. The launcher binds the executor before the task runs. The task's promise stores the executor and propagates it to nested awaitables via `await_transform`. Context flows from caller to callee at every level, never backward. diff --git a/doc/modules/ROOT/pages/8.design/8l.RunApi.adoc b/doc/modules/ROOT/pages/9.design/9l.RunApi.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8l.RunApi.adoc rename to doc/modules/ROOT/pages/9.design/9l.RunApi.adoc diff --git a/doc/modules/ROOT/pages/8.design/8m.WhyNotCobalt.adoc b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc similarity index 99% rename from doc/modules/ROOT/pages/8.design/8m.WhyNotCobalt.adoc rename to doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc index a0b1635da..40273abcc 100644 --- a/doc/modules/ROOT/pages/8.design/8m.WhyNotCobalt.adoc +++ b/doc/modules/ROOT/pages/9.design/9m.WhyNotCobalt.adoc @@ -134,7 +134,7 @@ Templates can achieve this by type-erasing every customization point. The cost m == Stream Concepts -Capy defines seven coroutine-only stream concepts. Cobalt inherits Asio's `AsyncReadStream` and `AsyncWriteStream`, which are hybrid concepts supporting callbacks, futures, and coroutines. Cobalt's `cobalt::io` wrappers simplify the API and Cobalt defines stream abstractions (`write_stream`, `read_stream`, `stream`) as abstract base classes, a distinct approach from Capy's concept-based hierarchy. Cobalt's wrappers still include full Asio headers. See xref:8.design/8n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a detailed comparison of the two approaches. +Capy defines seven coroutine-only stream concepts. Cobalt inherits Asio's `AsyncReadStream` and `AsyncWriteStream`, which are hybrid concepts supporting callbacks, futures, and coroutines. Cobalt's `cobalt::io` wrappers simplify the API and Cobalt defines stream abstractions (`write_stream`, `read_stream`, `stream`) as abstract base classes, a distinct approach from Capy's concept-based hierarchy. Cobalt's wrappers still include full Asio headers. See xref:9.design/9n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a detailed comparison of the two approaches. Capy's concepts form a refinement hierarchy that emerged naturally from use-case-first design: @@ -192,7 +192,7 @@ Traditional approaches to type erasure in Asio focus on the lowest-level element Capy type-erases the stream itself. This is possible because coroutines provide structural type erasure — the continuation is always a handle, not a template parameter. When the library is coroutines-only, one virtual call per I/O operation is the total cost. The completion handler, executor, and allocator do not need individual erasure because they are not part of the stream's operation signature. -Cobalt defines stream abstractions (`write_stream`, `read_stream`, `stream`) as abstract base classes in `cobalt/io/stream.hpp`, taking a different approach from Capy's concept + type-erased wrapper model. See xref:8.design/8n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a side-by-side analysis. +Cobalt defines stream abstractions (`write_stream`, `read_stream`, `stream`) as abstract base classes in `cobalt/io/stream.hpp`, taking a different approach from Capy's concept + type-erased wrapper model. See xref:9.design/9n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a side-by-side analysis. The wrappers compose. `any_buffer_source` also satisfies `ReadSource` — natively if the wrapped type supports both, synthesized otherwise. `any_buffer_sink` also satisfies `WriteSink`. You pick the abstraction level you need. @@ -245,7 +245,7 @@ This is how the Dimovian Ideal is mechanically achieved. == Mock Streams and Testability -When algorithms operate on type-erased interfaces, testing becomes deterministic. Capy provides mock implementations for every stream concept. Cobalt defines stream abstractions as abstract base classes but does not provide mock implementations for testing. See xref:8.design/8n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a comparison of the two stream designs. +When algorithms operate on type-erased interfaces, testing becomes deterministic. Capy provides mock implementations for every stream concept. Cobalt defines stream abstractions as abstract base classes but does not provide mock implementations for testing. See xref:9.design/9n.WhyNotCobaltConcepts.adoc[Write Stream Design] for a comparison of the two stream designs. Capy's mock types: diff --git a/doc/modules/ROOT/pages/8.design/8n.WhyNotCobaltConcepts.adoc b/doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8n.WhyNotCobaltConcepts.adoc rename to doc/modules/ROOT/pages/9.design/9n.WhyNotCobaltConcepts.adoc diff --git a/doc/modules/ROOT/pages/8.design/8o.WhyNotTMC.adoc b/doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc similarity index 100% rename from doc/modules/ROOT/pages/8.design/8o.WhyNotTMC.adoc rename to doc/modules/ROOT/pages/9.design/9o.WhyNotTMC.adoc diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 9307f3a2d..a0153eef3 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -114,8 +114,8 @@ The `task<>` return type (equivalent to `task`) creates a lazy coroutine t == Next Steps * xref:quick-start.adoc[Quick Start] — Set up your first Capy project -* xref:cpp20-coroutines/foundations.adoc[{cpp}20 Coroutines Tutorial] — Learn coroutines from the ground up -* xref:concurrency/foundations.adoc[Concurrency Tutorial] — Understand threads, mutexes, and synchronization -* xref:coroutines/tasks.adoc[Coroutines in Capy] — Deep dive into `task` and the IoAwaitable protocol -* xref:buffers/overview.adoc[Buffer Sequences] — Master the concept-driven buffer model -* xref:streams/overview.adoc[Stream Concepts] — Understand the six stream concepts +* xref:2.cpp20-coroutines/2a.foundations.adoc[{cpp}20 Coroutines Tutorial] — Learn coroutines from the ground up +* xref:3.concurrency/3a.foundations.adoc[Concurrency Tutorial] — Understand threads, mutexes, and synchronization +* xref:4.coroutines/4a.tasks.adoc[Coroutines in Capy] — Deep dive into `task` and the IoAwaitable protocol +* xref:5.buffers/5a.overview.adoc[Buffer Sequences] — Master the concept-driven buffer model +* xref:6.streams/6a.overview.adoc[Stream Concepts] — Understand the six stream concepts diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index f8ddf18a0..d04ef68f5 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -115,6 +115,6 @@ capy::run_async(executor)(might_fail(), Now that you have a working program: -* xref:coroutines/tasks.adoc[Tasks] — Learn how lazy tasks work -* xref:coroutines/launching.adoc[Launching Tasks] — Understand `run_async` in detail -* xref:coroutines/affinity.adoc[Executor Affinity] — Control where coroutines execute +* xref:4.coroutines/4a.tasks.adoc[Tasks] — Learn how lazy tasks work +* xref:4.coroutines/4b.launching.adoc[Launching Tasks] — Understand `run_async` in detail +* xref:4.coroutines/4c.executors.adoc[Executors and Execution Contexts] — Control where coroutines execute