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, with optional chunking to exercise partial-transfer paths.
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(). 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
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). 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.
#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);
}