Mock Buffer Sources and Sinks

Concept-conforming test doubles for the buffer concepts in Buffer Sources and Sinks. These mocks let you test code that consumes via a BufferSource or produces via a BufferSink without wiring up a real dynamic buffer.

buffer_source

buffer_source implements the BufferSource concept. Test code stages bytes via provide(), and the system under test pulls them through the pull()/consume() interface that BufferSource requires. Pulled buffers point directly into the source’s internal storage, so no copy occurs. The attached fuse injects errors at every pull() call, exercising the caller’s error-handling paths. Because fuse copies share state (see Shared State Across Copies), constructing buffer_source bs(f) by value still ties bs to the same fail-point machinery as f.

#include <boost/capy/test/buffer_source.hpp>
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/test/buffer_to_string.hpp>
#include <boost/capy/buffers.hpp>
#include <boost/capy/task.hpp>

using namespace boost::capy;
using namespace boost::capy::test;

void test_buffer_source()
{
    fuse f;
    buffer_source bs(f);
    bs.provide("Hello, ");
    bs.provide("World!");

    auto r = f.armed([&](fuse&) -> task<void> {
        const_buffer arr[16];
        auto [ec, bufs] = co_await bs.pull(arr);
        if(ec)
            co_return;
        BOOST_TEST(buffer_to_string(bufs) == "Hello, World!");
        bs.consume(buffer_size(bufs));
    });
    BOOST_TEST(r.success);
}

Staging Data

Call provide() one or more times before the system under test runs. Each call appends bytes to the internal buffer; the next pull() returns a span covering all accumulated unconsumed data, up to max_pull_size if a limit was set.

buffer_source bs(f);
bs.provide("part one ");
bs.provide("part two");  // total: "part one part two"

Consume Loop

pull() returns the same data on repeated calls until consume() advances the read position. A typical consumer loops until pull() returns cond::eof, consuming the returned bytes each time:

const_buffer arr[16];
for(;;)
{
    auto [ec, bufs] = co_await bs.pull(arr);
    if(ec == cond::eof)
        break;
    if(ec)
        co_return;  // fuse injected error, or real failure
    // process bufs ...
    bs.consume(buffer_size(bufs));
}

Chunked Delivery

The second constructor parameter caps the bytes returned per pull(), simulating a source that delivers data in small pieces:

buffer_source bs(f, 5);   // at most 5 bytes per pull
bs.provide("hello world");
// first pull returns "hello"; second returns " worl"; etc.
Member Description

explicit buffer_source(fuse f = {}, std::size_t max_pull_size = std::size_t(-1))

Construct with an optional shared fuse and an optional per-pull byte ceiling. When omitted, the fuse is inert and each pull returns all remaining data. Set max_pull_size to simulate chunked delivery.

provide(std::string_view sv)

Append bytes to the internal buffer for subsequent pulls. Multiple calls accumulate data.

pull(std::span<const_buffer> dest)

Fills dest with buffer descriptors pointing into internal storage. Await-returns (error_code, std::span<const_buffer>). Returns cond::eof when no data remains. Consults the fuse before every call. Repeated calls without consume() return the same data.

consume(std::size_t n)

Advance the read position by n bytes. The next pull() returns data starting after the consumed bytes.

available() → std::size_t

Return the number of bytes not yet consumed.

clear()

Clear all data and reset the read position.

buffer_sink

buffer_sink implements the BufferSink concept. The system under test follows the callee-owns-buffers pattern: it calls prepare() to get writable buffer space from the sink, writes directly into those buffers, then calls commit() or commit_eof() to finalize the bytes. The test then inspects what was captured via data() and checks whether the end-of-stream was signaled via eof_called(). The attached fuse injects errors at every async step. Because fuse copies share state (see Shared State Across Copies), constructing buffer_sink bs(f) by value still ties bs to the same fail-point machinery as f.

#include <boost/capy/test/buffer_sink.hpp>
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/task.hpp>

#include <cstring>

using namespace boost::capy;
using namespace boost::capy::test;

void test_buffer_sink()
{
    fuse f;
    auto r = f.armed([&](fuse&) -> task<void> {
        buffer_sink bs(f);

        mutable_buffer arr[16];
        auto bufs = bs.prepare(arr);

        std::memcpy(bufs[0].data(), "Hello", 5);

        auto [ec] = co_await bs.commit(5);
        if(ec)
            co_return;

        auto [ec2] = co_await bs.commit_eof(0);
        if(ec2)
            co_return;

        BOOST_TEST(bs.data() == "Hello");
        BOOST_TEST(bs.eof_called());
    });
    BOOST_TEST(r.success);
}

Reading What Was Written

After the coroutine completes, data() returns a string_view of all committed bytes. size() gives the byte count. eof_called() returns true if commit_eof() succeeded during the run.

BOOST_TEST(bs.data() == "expected output");
BOOST_TEST(bs.size() == 15u);
BOOST_TEST(bs.eof_called());

Call these accessors inside the f.armed() lambda after the system under test completes successfully. They are the primary mechanism for asserting what the system under test produced.

The prepare/commit Protocol

prepare() is synchronous. It fills the provided span with one writable buffer descriptor pointing into the sink’s internal storage. The caller writes data into those buffers, then calls commit(n) to finalize n bytes, or commit_eof(n) to finalize n bytes and signal end-of-stream in a single step. Passing n = 0 to commit_eof signals EOF without writing additional bytes.

Limited Buffer Space

The second constructor parameter caps the bytes available per prepare(), simulating a sink with constrained internal space:

buffer_sink bs(f, 8);   // prepare returns at most 8 bytes at a time
Member Description

explicit buffer_sink(fuse f = {}, std::size_t max_prepare_size = 4096)

Construct with an optional shared fuse and an optional per-prepare byte ceiling. When omitted, the fuse is inert and prepare() exposes 4096 bytes of buffer space. Set max_prepare_size to simulate limited buffer space.

prepare(std::span<mutable_buffer> dest)

Synchronously fills dest with writable buffer descriptors into internal storage. Returns the filled span (one buffer in this implementation, or empty if dest is empty). Does not consult the fuse.

commit(std::size_t n)

Finalize n bytes written to the most recent prepare() buffers. Await-returns (error_code). Consults the fuse before committing.

commit_eof(std::size_t n)

Finalize n bytes and signal end-of-stream. Await-returns (error_code). Sets eof_called() to true on success. Consults the fuse before committing. Pass n = 0 to signal EOF without additional data.

data() → std::string_view

Return all bytes committed so far.

size() → std::size_t

Return the number of bytes committed.

eof_called() → bool

Return true if commit_eof() has succeeded.

clear()

Clear all committed data and reset eof_called to false.

Putting It Together

The following snippet tests a copy algorithm that pulls from a BufferSource and writes into a BufferSink. The fuse.armed() loop exercises every error site in both the pull and commit paths.

#include <boost/capy/buffers.hpp>
#include <boost/capy/concept/buffer_sink.hpp>
#include <boost/capy/concept/buffer_source.hpp>
#include <boost/capy/cond.hpp>
#include <boost/capy/task.hpp>
#include <boost/capy/test/buffer_sink.hpp>
#include <boost/capy/test/buffer_source.hpp>
#include <boost/capy/test/fuse.hpp>

#include <cstring>

using namespace boost::capy;
using namespace boost::capy::test;

// Function under test: copy all bytes from source into sink
template<BufferSource Source, BufferSink Sink>
task<std::error_code>
copy_all(Source& source, Sink& sink)
{
    const_buffer src_arr[16];
    mutable_buffer dst_arr[16];

    for(;;)
    {
        auto [ec1, src_bufs] = co_await source.pull(src_arr);
        if(ec1 == cond::eof)
        {
            auto [eof_ec] = co_await sink.commit_eof(0);
            co_return eof_ec;
        }
        if(ec1)
            co_return ec1;

        auto dst_bufs = sink.prepare(dst_arr);
        std::size_t n = buffer_copy(dst_bufs, src_bufs);

        auto [ec2] = co_await sink.commit(n);
        if(ec2)
            co_return ec2;

        source.consume(n);
    }
}

void test_copy_all()
{
    fuse f;
    auto r = f.armed([&](fuse&) -> task<void> {
        buffer_source src(f);
        buffer_sink   dst(f);
        src.provide("ping");

        auto ec = co_await copy_all(src, dst);
        if(ec)
            co_return;  // fuse injected an error; exit gracefully
        BOOST_TEST(dst.data() == "ping");
        BOOST_TEST(dst.eof_called());
    });
    BOOST_TEST(r.success);
}

Reference

Header Contents

<boost/capy/test/buffer_source.hpp>

Mock BufferSource for callee-owns-buffers pull tests.

<boost/capy/test/buffer_sink.hpp>

Mock BufferSink for callee-owns-buffers write tests.

Continue to Buffer Inspection.