Mock Streams
Concept-conforming test doubles for the partial-I/O concepts in Streams. Use them to drive protocol code without real network I/O.
read_stream
test::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 test::fuse injects errors at
every read call, exercising the caller’s error-handling paths. Because
fuse copies share state (see
Shared State Across Copies),
constructing read_stream rs(f) by value still ties rs to the same
fail-point machinery as f.
#include <boost/capy/test/read_stream.hpp>
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/buffers/make_buffer.hpp>
#include <boost/capy/task.hpp>
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<void> {
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.
// 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<void> {
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 is consumed, read_some returns
cond::eof with a byte count of zero. The stream does not
suspend; the result is available immediately.
fuse f;
read_stream rs(f);
rs.provide("hi");
auto r = f.inert([&](fuse&) -> task<void> {
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);
See test::read_stream for the complete member list: the
constructor’s fuse and max_read_size parameters, provide,
read_some, available, and clear.
write_stream
test::write_stream implements the WriteStream concept. The system under
test calls write_some() and the test inspects what was written via
data(). The fuse is a separate concern used only for error injection.
Because fuse copies
share state (see
Shared State Across Copies),
constructing write_stream ws(f) by value still ties ws to the same
fail-point machinery as f.
#include <boost/capy/test/write_stream.hpp>
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/buffers/make_buffer.hpp>
#include <boost/capy/task.hpp>
using namespace boost::capy;
using namespace boost::capy::test;
void test_write_stream()
{
fuse f;
auto r = f.armed([&](fuse&) -> task<void> {
write_stream ws(f);
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.
fuse f;
write_stream ws(f, 4); // accept at most 4 bytes per call
auto r = f.inert([&](fuse&) -> task<void> {
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.
fuse f;
write_stream ws(f);
ws.expect("Hello World");
auto r = f.inert([&](fuse&) -> task<void> {
// Writing matching data succeeds
auto [ec, n] = co_await ws.write_some(
const_buffer("Hello World", 11));
BOOST_TEST(!ec);
});
BOOST_TEST(r.success);
See test::write_stream for the complete member list: the
constructor’s fuse and max_write_size parameters, write_some,
data, size, and expect.
stream
test::stream is a connected bidirectional test double. Create a pair with
test::make_stream_pair(f) to test client/server code without real
sockets.
Both stream ends satisfy ReadStream and WriteStream.
#include <boost/capy/test/stream.hpp>
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/buffers/make_buffer.hpp>
#include <boost/capy/task.hpp>
using namespace boost::capy;
using namespace boost::capy::test;
void test_stream_pair()
{
fuse f;
auto r = f.armed([&](fuse&) -> task<void> {
auto [a, b] = make_stream_pair(f);
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, and any suspended reader on the other end is resumed with
cond::eof. 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.
See test::stream and test::make_stream_pair for the
complete member list: pair construction, read_some, write_some,
close, set_max_read_size, provide, expect, and data.
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.
#include <boost/capy/buffers/make_buffer.hpp>
#include <boost/capy/concept/read_stream.hpp>
#include <boost/capy/task.hpp>
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/test/read_stream.hpp>
using namespace boost::capy;
using namespace boost::capy::test;
// Function under test: read until '\n' or EOF
template<ReadStream S>
task<std::pair<std::error_code, std::string>>
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<void> {
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);
}