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.
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.
#include <boost/capy/task.hpp>
#include <boost/capy/test/run_blocking.hpp>
using namespace boost::capy;
using namespace boost::capy::test;
task<int> 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:
// 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.
| Overload | Behavior |
|---|---|
|
Discard result. Rethrows captured exceptions. |
|
Invoke |
|
Invoke |
|
Drive with an external stop token; discard result. |
|
Drive with an external stop token; invoke |
|
Drive with an external stop token; invoke |
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.
#include <boost/capy/test/fuse.hpp>
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:
fuse f;
// Smoke test: happy path
auto r1 = f.inert([&](fuse&) -> task<void> {
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<void> {
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:
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.
// 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<void>) and drives it to completion via run_blocking internally.
You do not need to call run_blocking yourself:
fuse f;
auto r = f.armed([&](fuse&) -> task<void> {
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.
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:
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);
| Member | Description |
|---|---|
|
Construct with the default error code ( |
|
Construct with a custom error code delivered by |
|
Run |
|
Run |
|
Alias for |
|
Return the injected error code at the active failure point, or |
|
Signal an explicit test failure and stop execution. Records the call
site in |
|
Signal a test failure with an associated exception. Stored in
|
|
|
|
Source location of the last |
|
Exception pointer captured from a |
|
Returns |
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
#include <boost/capy/ex/run_async.hpp>
#include <boost/capy/ex/thread_pool.hpp>
#include <boost/capy/task.hpp>
#include <boost/capy/test/thread_name.hpp>
using namespace boost::capy;
thread_pool pool(4);
run_async(pool.get_executor())([]() -> task<void> {
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.
| Function | Description |
|---|---|
|
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:
#include <boost/capy/task.hpp>
#include <boost/capy/test/fuse.hpp>
using namespace boost::capy;
using namespace boost::capy::test;
task<int> add(int a, int b)
{
co_return a + b;
}
void test_add()
{
fuse f;
auto r = f.armed([&](fuse&) -> task<void> {
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<state>. 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
| Header | Contents |
|---|---|
|
Synchronous coroutine driver. |
|
Systematic error injection. |
|
Thread naming for diagnostics. |
Continue to Mock Streams.