Testing Facilities
Capy provides a complete testing toolkit for I/O code—mock streams, systematic error injection, and buffer utilities. Test your code without real network I/O.
Code examples assume using namespace boost::capy; and
using namespace boost::capy::test; are in effect.
|
Why Test Utilities?
Testing I/O code is hard:
-
Real network operations are slow and non-deterministic
-
Errors are difficult to trigger reliably
-
Buffer boundary conditions require careful testing
Capy’s test utilities solve these problems:
-
Mock streams eliminate real I/O
-
fuseinjects errors at every possible point -
bufgrindtests all buffer split points
run_blocking
Execute coroutines synchronously for unit tests:
#include <boost/capy/test/run_blocking.hpp>
task<int> compute()
{
co_return 42;
}
void test_compute()
{
int result = 0;
run_blocking([&](int v) { result = v; })(compute());
BOOST_TEST(result == 42);
}
How It Works
run_blocking uses an inline_executor that executes work immediately
on the calling thread. The calling thread blocks until the coroutine
completes.
Overloads
// Discard result
run_blocking()(my_task());
// Capture result
run_blocking([&](auto v) { result = v; })(my_task());
// Separate success/error handlers
run_blocking(
[&](auto v) { result = v; },
[](std::exception_ptr ep) { std::rethrow_exception(ep); }
)(my_task());
// With executor
run_blocking(my_executor)(my_task());
// With stop token
run_blocking(stop_token)(my_task());
Reference: <boost/capy/test/run_blocking.hpp>
fuse
Systematic error injection that tests every failure path:
#include <boost/capy/test/fuse.hpp>
fuse f;
auto r = f.armed([](fuse& f) {
auto ec = f.maybe_fail();
if (ec.failed())
return; // Exit on injected error
ec = f.maybe_fail();
if (ec.failed())
return;
// Success path
});
BOOST_TEST(r.success);
How It Works
armed() runs your test function repeatedly, failing at successive
maybe_fail() points:
-
Round 0: Fail at first
maybe_fail() -
Round 1: Fail at second
maybe_fail() -
Round N: Continue until no failure triggered (success)
Then it repeats the entire process in exception mode, where maybe_fail()
throws instead of returning an error code.
Error Handling Pattern
Tests must handle injected errors by returning early:
// CORRECT: early return on injected error
auto [ec, n] = co_await rs.read_some(buf);
if (ec.failed())
co_return; // fuse injected error, exit gracefully
// WRONG: asserting success unconditionally
auto [ec, n] = co_await rs.read_some(buf);
BOOST_TEST(!ec.failed()); // FAILS when fuse injects error!
Coroutine Support
fuse works seamlessly with coroutines:
fuse f;
read_stream rs(f);
rs.provide("test data");
auto r = f.armed([&](fuse&) -> task<> {
char buf[32];
auto [ec, n] = co_await rs.read_some(make_buffer(buf));
if (ec.failed())
co_return;
// Process data...
});
BOOST_TEST(r.success);
inert Mode
Run a test once without error injection:
auto r = f.inert([](fuse& f) {
auto ec = f.maybe_fail(); // Always succeeds
// ...
if (some_condition)
f.fail(); // Explicit failure
});
Dependency Injection
fuse is a no-op when used outside armed()/inert():
class MyService
{
fuse& f_;
public:
explicit MyService(fuse& f) : f_(f) {}
system::error_code do_work()
{
auto ec = f_.maybe_fail(); // No-op in production
if (ec.failed())
return ec;
// ... actual work ...
return {};
}
};
// Production: fuse is inactive
fuse f;
MyService svc(f);
svc.do_work(); // maybe_fail() returns {} always
// Test: fuse is active
auto r = f.armed([&](fuse&) {
svc.do_work(); // maybe_fail() injects failures
});
bufgrind
Test all buffer sequence split points:
#include <boost/capy/test/bufgrind.hpp>
std::string data = "hello world";
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();
// b1 = first N bytes
// b2 = remaining bytes
// concatenating b1 + b2 equals original
BOOST_TEST(buffer_to_string(b1, b2) == data);
}
});
Iteration Pattern
For an 11-byte buffer, bufgrind yields:
pos=0: b1="" b2="hello world"
pos=1: b1="h" b2="ello world"
pos=2: b1="he" b2="llo world"
...
pos=11: b1="hello world" b2=""
buffer_to_string
Convert buffer sequences to strings for verification:
#include <boost/capy/test/buffer_to_string.hpp>
// Single buffer
const_buffer cb("hello", 5);
std::string s = buffer_to_string(cb); // "hello"
// Multiple buffers (concatenation)
const_buffer b1("hello", 5);
const_buffer b2(" world", 6);
std::string s = buffer_to_string(b1, b2); // "hello world"
read_stream
Mock ReadStream for testing read operations:
#include <boost/capy/test/read_stream.hpp>
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(make_buffer(buf));
if (ec.failed())
co_return;
// buf contains up to 13 bytes
});
API
| Method | Description |
|---|---|
|
Append data for subsequent reads |
|
Partial read, returns |
|
Bytes remaining to read |
|
Reset all data and position |
write_stream
Mock WriteStream for testing write operations:
#include <boost/capy/test/write_stream.hpp>
fuse f;
write_stream ws(f);
auto r = f.armed([&](fuse&) -> task<> {
auto [ec, n] = co_await ws.write_some(make_buffer("Hello"));
if (ec.failed())
co_return;
});
BOOST_TEST(ws.data() == "Hello");
API
| Method | Description |
|---|---|
|
Partial write, returns |
|
Retrieve written data as string view |
|
Number of bytes written |
|
Set expected data, fails on mismatch |
read_source
Mock ReadSource for complete read operations:
#include <boost/capy/test/read_source.hpp>
fuse f;
read_source rs(f);
rs.provide("Hello, World!");
auto r = f.armed([&](fuse&) -> task<> {
char buf[32];
auto [ec, n] = co_await rs.read(make_buffer(buf));
if (ec.failed())
co_return;
// buf filled completely before returning
});
Unlike read_stream::read_some(), read_source::read() fills the
entire buffer before returning (looping internally if needed).
write_sink
Mock WriteSink for complete write operations with EOF:
#include <boost/capy/test/write_sink.hpp>
fuse f;
write_sink ws(f);
auto r = f.armed([&](fuse&) -> task<> {
auto [ec, n] = co_await ws.write(make_buffer("Hello"));
if (ec.failed())
co_return;
auto [ec2] = co_await ws.write_eof();
if (ec2.failed())
co_return;
});
BOOST_TEST(ws.data() == "Hello");
BOOST_TEST(ws.eof_called());
API
| Method | Description |
|---|---|
|
Complete write, returns |
|
Write with optional EOF signal |
|
Signal end-of-stream, returns |
|
Retrieve written data as string view |
|
Check if EOF was signaled |
|
Set expected data, fails on mismatch |
|
Reset all data and state |
Reference: <boost/capy/test/write_sink.hpp>
Complete Example
Testing a protocol parser with full coverage:
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/test/read_stream.hpp>
#include <boost/capy/test/bufgrind.hpp>
#include <boost/capy/test/buffer_to_string.hpp>
void test_parser()
{
std::string input = "GET / HTTP/1.1\r\n\r\n";
auto cb = make_buffer(input);
// Test all buffer split points
bufgrind bg(cb);
while (bg) {
fuse f;
read_stream rs(f);
// Provide data in two chunks
auto [b1, b2] = run_blocking()(
[&]() -> task<bufgrind<const_buffer>::split_type> {
co_return co_await bg.next();
}());
rs.provide(buffer_to_string(b1));
rs.provide(buffer_to_string(b2));
auto r = f.armed([&](fuse&) -> task<> {
http_request req;
auto [ec] = co_await parse_request(rs, req);
if (ec.failed())
co_return;
BOOST_TEST(req.method == "GET");
BOOST_TEST(req.target == "/");
});
BOOST_TEST(r.success);
}
}
This test:
-
Uses
bufgrindto test every split point in the input -
Uses
fuse.armed()to inject errors at everymaybe_fail()call -
Uses
read_streamto provide mock data -
Verifies all code paths handle errors correctly
Summary
| Component | Purpose |
|---|---|
|
Execute coroutines synchronously for tests |
|
Systematic error injection at all failure points |
|
Iterate all buffer sequence split points |
|
Convert buffers to strings for verification |
|
Mock |
|
Mock |
|
Mock |
|
Mock |