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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions doc/modules/ROOT/pages/7.testing/7a.drivers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,36 @@ context such as a thread pool.
`on_error(ep)` on failure.
|===

=== Exercising Cancellation

The stop token supplied to `run_blocking` propagates through the execution
environment to every `capy::test` awaitable. When the token is requested, the
next awaited test operation resolves to `error::canceled` instead of
performing its work, so code under test can be driven down its cancellation
paths without real I/O.

[source,cpp]
----
std::stop_source src;
src.request_stop();

run_blocking(src.get_token())([&]() -> task<>
{
read_stream rs;
rs.provide("ignored");

char buf[32];
auto [ec, n] = co_await rs.read_some(make_buffer(buf));
assert(ec == cond::canceled); // honored the stop token
}());
----

The mock sources, sinks, and buffer adapters complete synchronously, so they
check the token up front: an outstanding stop request yields `error::canceled`
on the next operation. A connected `stream` whose `read_some` is *blocked*
waiting for its peer is resumed with `error::canceled` when the token fires; a
read that can satisfy from already-buffered data is unaffected.

== fuse

`fuse` tests every error-handling path in a coroutine by injecting failures
Expand Down
62 changes: 40 additions & 22 deletions include/boost/capy/test/buffer_sink.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand Down Expand Up @@ -152,6 +153,10 @@ class buffer_sink

@return An awaitable that await-returns `(error_code)`.

@par Cancellation
If the environment's stop token has been requested, the commit
completes immediately with `error::canceled` and commits no data.

@see fuse
*/
auto
Expand All @@ -161,29 +166,31 @@ class buffer_sink
{
buffer_sink* self_;
std::size_t n_;

bool await_ready() const noexcept { return true; }

// This method is required to satisfy Capy's IoAwaitable concept,
// but is never called because await_ready() returns true.
//
// Capy uses a two-layer awaitable system: the promise's
// await_transform wraps awaitables in a transform_awaiter whose
// standard await_suspend(coroutine_handle) calls this custom
// 2-argument overload, passing the io_env from the coroutine's
// context. For synchronous test awaitables like this one, the
// coroutine never suspends, so this is not invoked. The signature
// exists to allow the same awaitable type to work with both
// synchronous (test) and asynchronous (real I/O) code.
void await_suspend(
bool canceled_ = false;

bool await_ready() const noexcept { return false; }

// The operation completes synchronously, but await_suspend is
// the only place io_env is delivered (the promise's
// transform_awaiter forwards it here). Returning false means
// the coroutine does not actually suspend; it resumes
// immediately, having observed the stop token. See io_env,
// IoAwaitable.
bool
await_suspend(
std::coroutine_handle<>,
io_env const*) const noexcept
io_env const* env) noexcept
{
canceled_ = env->stop_token.stop_requested();
return false;
}

io_result<>
await_resume()
{
if(canceled_)
return {error::canceled};

auto ec = self_->f_.maybe_fail();
if(ec)
return {ec};
Expand All @@ -209,6 +216,11 @@ class buffer_sink

@return An awaitable that await-returns `(error_code)`.

@par Cancellation
If the environment's stop token has been requested, the operation
completes immediately with `error::canceled`, commits no data, and
does not signal end-of-stream.

@see fuse
*/
auto
Expand All @@ -218,21 +230,27 @@ class buffer_sink
{
buffer_sink* self_;
std::size_t n_;
bool canceled_ = false;

bool await_ready() const noexcept { return true; }
bool await_ready() const noexcept { return false; }

// This method is required to satisfy Capy's IoAwaitable concept,
// but is never called because await_ready() returns true.
// See the comment on commit(std::size_t) for a detailed explanation.
void await_suspend(
// Reads the stop token without suspending; see the comment
// on commit() for details.
bool
await_suspend(
std::coroutine_handle<>,
io_env const*) const noexcept
io_env const* env) noexcept
{
canceled_ = env->stop_token.stop_requested();
return false;
}

io_result<>
await_resume()
{
if(canceled_)
return {error::canceled};

auto ec = self_->f_.maybe_fail();
if(ec)
return {ec};
Expand Down
39 changes: 23 additions & 16 deletions include/boost/capy/test/buffer_source.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand Down Expand Up @@ -142,6 +143,10 @@ class buffer_source

@return An awaitable that await-returns `(error_code,std::span<const_buffer>)`.

@par Cancellation
If the environment's stop token has been requested, the pull
completes immediately with `error::canceled` and an empty span.

@see consume, fuse
*/
auto
Expand All @@ -151,29 +156,31 @@ class buffer_source
{
buffer_source* self_;
std::span<const_buffer> dest_;

bool await_ready() const noexcept { return true; }

// This method is required to satisfy Capy's IoAwaitable concept,
// but is never called because await_ready() returns true.
//
// Capy uses a two-layer awaitable system: the promise's
// await_transform wraps awaitables in a transform_awaiter whose
// standard await_suspend(coroutine_handle) calls this custom
// 2-argument overload, passing the io_env from the coroutine's
// context. For synchronous test awaitables like this one, the
// coroutine never suspends, so this is not invoked. The signature
// exists to allow the same awaitable type to work with both
// synchronous (test) and asynchronous (real I/O) code.
void await_suspend(
bool canceled_ = false;

bool await_ready() const noexcept { return false; }

// The operation completes synchronously, but await_suspend is
// the only place io_env is delivered (the promise's
// transform_awaiter forwards it here). Returning false means
// the coroutine does not actually suspend; it resumes
// immediately, having observed the stop token. See io_env,
// IoAwaitable.
bool
await_suspend(
std::coroutine_handle<>,
io_env const*) const noexcept
io_env const* env) noexcept
{
canceled_ = env->stop_token.stop_requested();
return false;
}

io_result<std::span<const_buffer>>
await_resume()
{
if(canceled_)
return {error::canceled, {}};

auto ec = self_->f_.maybe_fail();
if(ec)
return {ec, {}};
Expand Down
51 changes: 43 additions & 8 deletions include/boost/capy/test/read_source.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand Down Expand Up @@ -123,6 +124,12 @@ class read_source

@return An awaitable that await-returns `(error_code,std::size_t)`.

@par Cancellation
If the environment's stop token has been requested, the read
completes immediately with `error::canceled` and transfers no
data. An empty buffer sequence is a no-op that completes
successfully regardless of the stop token.

@see fuse
*/
template<MutableBufferSequence MB>
Expand All @@ -133,13 +140,23 @@ class read_source
{
read_source* self_;
MB buffers_;

bool await_ready() const noexcept { return true; }

void await_suspend(
bool canceled_ = false;

bool await_ready() const noexcept { return false; }

// The operation completes synchronously, but await_suspend is
// the only place io_env is delivered (the promise's
// transform_awaiter forwards it here). Returning false means
// the coroutine does not actually suspend; it resumes
// immediately, having observed the stop token. See io_env,
// IoAwaitable.
bool
await_suspend(
std::coroutine_handle<>,
io_env const*) const noexcept
io_env const* env) noexcept
{
canceled_ = env->stop_token.stop_requested();
return false;
}

io_result<std::size_t>
Expand All @@ -148,6 +165,9 @@ class read_source
if(buffer_empty(buffers_))
return {{}, 0};

if(canceled_)
return {error::canceled, 0};

auto ec = self_->f_.maybe_fail();
if(ec)
return {ec, 0};
Expand Down Expand Up @@ -183,6 +203,12 @@ class read_source

@return An awaitable that await-returns `(error_code,std::size_t)`.

@par Cancellation
If the environment's stop token has been requested, the read
completes immediately with `error::canceled` and transfers no
data. An empty buffer sequence is a no-op that completes
successfully regardless of the stop token.

@see fuse
*/
template<MutableBufferSequence MB>
Expand All @@ -193,13 +219,19 @@ class read_source
{
read_source* self_;
MB buffers_;
bool canceled_ = false;

bool await_ready() const noexcept { return true; }
bool await_ready() const noexcept { return false; }

void await_suspend(
// Reads the stop token without suspending; see the comment
// on read_some() for details.
bool
await_suspend(
std::coroutine_handle<>,
io_env const*) const noexcept
io_env const* env) noexcept
{
canceled_ = env->stop_token.stop_requested();
return false;
}

io_result<std::size_t>
Expand All @@ -208,6 +240,9 @@ class read_source
if(buffer_empty(buffers_))
return {{}, 0};

if(canceled_)
return {error::canceled, 0};

auto ec = self_->f_.maybe_fail();
if(ec)
return {ec, 0};
Expand Down
44 changes: 27 additions & 17 deletions include/boost/capy/test/read_stream.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand Down Expand Up @@ -125,6 +126,13 @@ class read_stream
@par Exception Safety
No-throw guarantee.

@par Cancellation
If the environment's stop token has been requested, the read
completes immediately with `error::canceled` and transfers no
data. This lets code under test exercise its cancellation paths.
An empty buffer sequence is a no-op that completes successfully
regardless of the stop token.

@param buffers The mutable buffer sequence to receive data.

@return An awaitable that await-returns `(error_code,std::size_t)`.
Expand All @@ -139,34 +147,36 @@ class read_stream
{
read_stream* self_;
MB buffers_;

bool await_ready() const noexcept { return true; }

// This method is required to satisfy Capy's IoAwaitable concept,
// but is never called because await_ready() returns true.
//
// Capy uses a two-layer awaitable system: the promise's
// await_transform wraps awaitables in a transform_awaiter whose
// standard await_suspend(coroutine_handle) calls this custom
// 2-argument overload, passing the io_env from the coroutine's
// context. For synchronous test awaitables like this one, the
// coroutine never suspends, so this is not invoked. The signature
// exists to allow the same awaitable type to work with both
// synchronous (test) and asynchronous (real I/O) code.
void await_suspend(
bool canceled_ = false;

bool await_ready() const noexcept { return false; }

// The operation completes synchronously, but await_suspend
// is the only place io_env is delivered (the promise's
// transform_awaiter forwards it here). Returning false means
// the coroutine does not actually suspend — it resumes
// immediately — so the read still completes synchronously
// while having observed the stop token. See io_env, IoAwaitable.
bool
await_suspend(
std::coroutine_handle<>,
io_env const*) const noexcept
io_env const* env) noexcept
{
canceled_ = env->stop_token.stop_requested();
return false;
}

io_result<std::size_t>
await_resume()
{
// Empty buffer is a no-op regardless of
// stream state or fuse.
// stream state, stop token, or fuse.
if(buffer_empty(buffers_))
return {{}, 0};

if(canceled_)
return {error::canceled, 0};

auto ec = self_->f_.maybe_fail();
if(ec)
return {ec, 0};
Expand Down
Loading
Loading