1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/capy
7  
// Official repository: https://github.com/cppalliance/capy
8  
//
8  
//
9  

9  

10  
#ifndef BOOST_CAPY_TEST_BUFFER_SINK_HPP
10  
#ifndef BOOST_CAPY_TEST_BUFFER_SINK_HPP
11  
#define BOOST_CAPY_TEST_BUFFER_SINK_HPP
11  
#define BOOST_CAPY_TEST_BUFFER_SINK_HPP
12  

12  

13  
#include <boost/capy/detail/config.hpp>
13  
#include <boost/capy/detail/config.hpp>
14  
#include <boost/capy/buffers.hpp>
14  
#include <boost/capy/buffers.hpp>
15  
#include <boost/capy/buffers/make_buffer.hpp>
15  
#include <boost/capy/buffers/make_buffer.hpp>
16  
#include <coroutine>
16  
#include <coroutine>
17  
#include <boost/capy/ex/io_env.hpp>
17  
#include <boost/capy/ex/io_env.hpp>
18  
#include <boost/capy/io_result.hpp>
18  
#include <boost/capy/io_result.hpp>
19  
#include <boost/capy/test/fuse.hpp>
19  
#include <boost/capy/test/fuse.hpp>
20  

20  

21  
#include <algorithm>
21  
#include <algorithm>
22  
#include <span>
22  
#include <span>
23  
#include <string>
23  
#include <string>
24  
#include <string_view>
24  
#include <string_view>
25  

25  

26  
namespace boost {
26  
namespace boost {
27  
namespace capy {
27  
namespace capy {
28  
namespace test {
28  
namespace test {
29  

29  

30  
/** A mock buffer sink for testing callee-owns-buffers write operations.
30  
/** A mock buffer sink for testing callee-owns-buffers write operations.
31  

31  

32  
    Use this to verify code that writes data using the callee-owns-buffers
32  
    Use this to verify code that writes data using the callee-owns-buffers
33  
    pattern without needing real I/O. Call @ref prepare to get writable
33  
    pattern without needing real I/O. Call @ref prepare to get writable
34  
    buffers, write into them, then call @ref commit to finalize. The
34  
    buffers, write into them, then call @ref commit to finalize. The
35  
    associated @ref fuse enables error injection at controlled points.
35  
    associated @ref fuse enables error injection at controlled points.
36  

36  

37  
    This class satisfies the @ref BufferSink concept by providing
37  
    This class satisfies the @ref BufferSink concept by providing
38  
    internal storage that callers write into directly.
38  
    internal storage that callers write into directly.
39  

39  

40  
    @par Thread Safety
40  
    @par Thread Safety
41  
    Not thread-safe.
41  
    Not thread-safe.
42  

42  

43  
    @par Example
43  
    @par Example
44  
    @code
44  
    @code
45  
    fuse f;
45  
    fuse f;
46  
    buffer_sink bs( f );
46  
    buffer_sink bs( f );
47  

47  

48  
    auto r = f.armed( [&]( fuse& ) -> task<void> {
48  
    auto r = f.armed( [&]( fuse& ) -> task<void> {
49  
        mutable_buffer arr[16];
49  
        mutable_buffer arr[16];
50  
        std::size_t count = bs.prepare( arr, 16 );
50  
        std::size_t count = bs.prepare( arr, 16 );
51  
        if( count == 0 )
51  
        if( count == 0 )
52  
            co_return;
52  
            co_return;
53  

53  

54  
        // Write data into arr[0]
54  
        // Write data into arr[0]
55  
        std::memcpy( arr[0].data(), "Hello", 5 );
55  
        std::memcpy( arr[0].data(), "Hello", 5 );
56  

56  

57  
        auto [ec] = co_await bs.commit( 5 );
57  
        auto [ec] = co_await bs.commit( 5 );
58  
        if( ec )
58  
        if( ec )
59  
            co_return;
59  
            co_return;
60  

60  

61  
        auto [ec2] = co_await bs.commit_eof();
61  
        auto [ec2] = co_await bs.commit_eof();
62  
        // bs.data() returns "Hello"
62  
        // bs.data() returns "Hello"
63  
    } );
63  
    } );
64  
    @endcode
64  
    @endcode
65  

65  

66  
    @see fuse, BufferSink
66  
    @see fuse, BufferSink
67  
*/
67  
*/
68  
class buffer_sink
68  
class buffer_sink
69  
{
69  
{
70  
    fuse f_;
70  
    fuse f_;
71  
    std::string data_;
71  
    std::string data_;
72  
    std::string prepare_buf_;
72  
    std::string prepare_buf_;
73  
    std::size_t prepare_size_ = 0;
73  
    std::size_t prepare_size_ = 0;
74  
    std::size_t max_prepare_size_;
74  
    std::size_t max_prepare_size_;
75  
    bool eof_called_ = false;
75  
    bool eof_called_ = false;
76  

76  

77  
public:
77  
public:
78  
    /** Construct a buffer sink.
78  
    /** Construct a buffer sink.
79  

79  

80  
        @param f The fuse used to inject errors during commits.
80  
        @param f The fuse used to inject errors during commits.
81  

81  

82  
        @param max_prepare_size Maximum bytes available per prepare.
82  
        @param max_prepare_size Maximum bytes available per prepare.
83  
        Use to simulate limited buffer space.
83  
        Use to simulate limited buffer space.
84  
    */
84  
    */
85  
    explicit buffer_sink(
85  
    explicit buffer_sink(
86  
        fuse f = {},
86  
        fuse f = {},
87  
        std::size_t max_prepare_size = 4096) noexcept
87  
        std::size_t max_prepare_size = 4096) noexcept
88  
        : f_(std::move(f))
88  
        : f_(std::move(f))
89  
        , max_prepare_size_(max_prepare_size)
89  
        , max_prepare_size_(max_prepare_size)
90  
    {
90  
    {
91  
        prepare_buf_.resize(max_prepare_size_);
91  
        prepare_buf_.resize(max_prepare_size_);
92  
    }
92  
    }
93  

93  

94  
    /// Return the written data as a string view.
94  
    /// Return the written data as a string view.
95  
    std::string_view
95  
    std::string_view
96  
    data() const noexcept
96  
    data() const noexcept
97  
    {
97  
    {
98  
        return data_;
98  
        return data_;
99  
    }
99  
    }
100  

100  

101  
    /// Return the number of bytes written.
101  
    /// Return the number of bytes written.
102  
    std::size_t
102  
    std::size_t
103  
    size() const noexcept
103  
    size() const noexcept
104  
    {
104  
    {
105  
        return data_.size();
105  
        return data_.size();
106  
    }
106  
    }
107  

107  

108  
    /// Return whether commit_eof has been called.
108  
    /// Return whether commit_eof has been called.
109  
    bool
109  
    bool
110  
    eof_called() const noexcept
110  
    eof_called() const noexcept
111  
    {
111  
    {
112  
        return eof_called_;
112  
        return eof_called_;
113  
    }
113  
    }
114  

114  

115  
    /// Clear all data and reset state.
115  
    /// Clear all data and reset state.
116  
    void
116  
    void
117  
    clear() noexcept
117  
    clear() noexcept
118  
    {
118  
    {
119  
        data_.clear();
119  
        data_.clear();
120  
        prepare_size_ = 0;
120  
        prepare_size_ = 0;
121  
        eof_called_ = false;
121  
        eof_called_ = false;
122  
    }
122  
    }
123  

123  

124  
    /** Prepare writable buffers.
124  
    /** Prepare writable buffers.
125  

125  

126  
        Fills the provided span with mutable buffer descriptors pointing
126  
        Fills the provided span with mutable buffer descriptors pointing
127  
        to internal storage. The caller writes data into these buffers,
127  
        to internal storage. The caller writes data into these buffers,
128  
        then calls @ref commit to finalize.
128  
        then calls @ref commit to finalize.
129  

129  

130  
        @param dest Span of mutable_buffer to fill.
130  
        @param dest Span of mutable_buffer to fill.
131  

131  

132  
        @return A span of filled buffers (empty or 1 buffer in this implementation).
132  
        @return A span of filled buffers (empty or 1 buffer in this implementation).
133  
    */
133  
    */
134  
    std::span<mutable_buffer>
134  
    std::span<mutable_buffer>
135  
    prepare(std::span<mutable_buffer> dest)
135  
    prepare(std::span<mutable_buffer> dest)
136  
    {
136  
    {
137  
        if(dest.empty())
137  
        if(dest.empty())
138  
            return {};
138  
            return {};
139  

139  

140  
        prepare_size_ = max_prepare_size_;
140  
        prepare_size_ = max_prepare_size_;
141  
        dest[0] = make_buffer(prepare_buf_.data(), prepare_size_);
141  
        dest[0] = make_buffer(prepare_buf_.data(), prepare_size_);
142  
        return dest.first(1);
142  
        return dest.first(1);
143  
    }
143  
    }
144  

144  

145  
    /** Commit bytes written to the prepared buffers.
145  
    /** Commit bytes written to the prepared buffers.
146  

146  

147  
        Transfers `n` bytes from the prepared buffer to the internal
147  
        Transfers `n` bytes from the prepared buffer to the internal
148  
        data buffer. Before committing, the attached @ref fuse is
148  
        data buffer. Before committing, the attached @ref fuse is
149  
        consulted to possibly inject an error for testing fault scenarios.
149  
        consulted to possibly inject an error for testing fault scenarios.
150  

150  

151  
        @param n The number of bytes to commit.
151  
        @param n The number of bytes to commit.
152  

152  

153  
        @return An awaitable yielding `(error_code)`.
153  
        @return An awaitable yielding `(error_code)`.
154  

154  

155  
        @see fuse
155  
        @see fuse
156  
    */
156  
    */
157  
    auto
157  
    auto
158  
    commit(std::size_t n)
158  
    commit(std::size_t n)
159  
    {
159  
    {
160  
        struct awaitable
160  
        struct awaitable
161  
        {
161  
        {
162  
            buffer_sink* self_;
162  
            buffer_sink* self_;
163  
            std::size_t n_;
163  
            std::size_t n_;
164  

164  

165  
            bool await_ready() const noexcept { return true; }
165  
            bool await_ready() const noexcept { return true; }
166  

166  

167  
            // This method is required to satisfy Capy's IoAwaitable concept,
167  
            // This method is required to satisfy Capy's IoAwaitable concept,
168  
            // but is never called because await_ready() returns true.
168  
            // but is never called because await_ready() returns true.
169  
            //
169  
            //
170  
            // Capy uses a two-layer awaitable system: the promise's
170  
            // Capy uses a two-layer awaitable system: the promise's
171  
            // await_transform wraps awaitables in a transform_awaiter whose
171  
            // await_transform wraps awaitables in a transform_awaiter whose
172  
            // standard await_suspend(coroutine_handle) calls this custom
172  
            // standard await_suspend(coroutine_handle) calls this custom
173  
            // 2-argument overload, passing the io_env from the coroutine's
173  
            // 2-argument overload, passing the io_env from the coroutine's
174  
            // context. For synchronous test awaitables like this one, the
174  
            // context. For synchronous test awaitables like this one, the
175  
            // coroutine never suspends, so this is not invoked. The signature
175  
            // coroutine never suspends, so this is not invoked. The signature
176  
            // exists to allow the same awaitable type to work with both
176  
            // exists to allow the same awaitable type to work with both
177  
            // synchronous (test) and asynchronous (real I/O) code.
177  
            // synchronous (test) and asynchronous (real I/O) code.
178  
            void await_suspend(
178  
            void await_suspend(
179  
                std::coroutine_handle<>,
179  
                std::coroutine_handle<>,
180  
                io_env const*) const noexcept
180  
                io_env const*) const noexcept
181  
            {
181  
            {
182  
            }
182  
            }
183  

183  

184  
            io_result<>
184  
            io_result<>
185  
            await_resume()
185  
            await_resume()
186  
            {
186  
            {
187  
                auto ec = self_->f_.maybe_fail();
187  
                auto ec = self_->f_.maybe_fail();
188  
                if(ec)
188  
                if(ec)
189  
                    return {ec};
189  
                    return {ec};
190  

190  

191  
                std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
191  
                std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
192  
                self_->data_.append(self_->prepare_buf_.data(), to_commit);
192  
                self_->data_.append(self_->prepare_buf_.data(), to_commit);
193  
                self_->prepare_size_ = 0;
193  
                self_->prepare_size_ = 0;
194  

194  

195  
                return {};
195  
                return {};
196  
            }
196  
            }
197  
        };
197  
        };
198  
        return awaitable{this, n};
198  
        return awaitable{this, n};
199  
    }
199  
    }
200  

200  

201  
    /** Commit final bytes and signal end-of-stream.
201  
    /** Commit final bytes and signal end-of-stream.
202  

202  

203  
        Transfers `n` bytes from the prepared buffer to the internal
203  
        Transfers `n` bytes from the prepared buffer to the internal
204  
        data buffer and marks the sink as finalized. Before committing,
204  
        data buffer and marks the sink as finalized. Before committing,
205  
        the attached @ref fuse is consulted to possibly inject an error
205  
        the attached @ref fuse is consulted to possibly inject an error
206  
        for testing fault scenarios.
206  
        for testing fault scenarios.
207  

207  

208  
        @param n The number of bytes to commit.
208  
        @param n The number of bytes to commit.
209  

209  

210  
        @return An awaitable yielding `(error_code)`.
210  
        @return An awaitable yielding `(error_code)`.
211  

211  

212  
        @see fuse
212  
        @see fuse
213  
    */
213  
    */
214  
    auto
214  
    auto
215  
    commit_eof(std::size_t n)
215  
    commit_eof(std::size_t n)
216  
    {
216  
    {
217  
        struct awaitable
217  
        struct awaitable
218  
        {
218  
        {
219  
            buffer_sink* self_;
219  
            buffer_sink* self_;
220  
            std::size_t n_;
220  
            std::size_t n_;
221  

221  

222  
            bool await_ready() const noexcept { return true; }
222  
            bool await_ready() const noexcept { return true; }
223  

223  

224  
            // This method is required to satisfy Capy's IoAwaitable concept,
224  
            // This method is required to satisfy Capy's IoAwaitable concept,
225  
            // but is never called because await_ready() returns true.
225  
            // but is never called because await_ready() returns true.
226  
            // See the comment on commit(std::size_t) for a detailed explanation.
226  
            // See the comment on commit(std::size_t) for a detailed explanation.
227  
            void await_suspend(
227  
            void await_suspend(
228  
                std::coroutine_handle<>,
228  
                std::coroutine_handle<>,
229  
                io_env const*) const noexcept
229  
                io_env const*) const noexcept
230  
            {
230  
            {
231  
            }
231  
            }
232  

232  

233  
            io_result<>
233  
            io_result<>
234  
            await_resume()
234  
            await_resume()
235  
            {
235  
            {
236  
                auto ec = self_->f_.maybe_fail();
236  
                auto ec = self_->f_.maybe_fail();
237  
                if(ec)
237  
                if(ec)
238  
                    return {ec};
238  
                    return {ec};
239  

239  

240  
                std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
240  
                std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
241  
                self_->data_.append(self_->prepare_buf_.data(), to_commit);
241  
                self_->data_.append(self_->prepare_buf_.data(), to_commit);
242  
                self_->prepare_size_ = 0;
242  
                self_->prepare_size_ = 0;
243  

243  

244  
                self_->eof_called_ = true;
244  
                self_->eof_called_ = true;
245  
                return {};
245  
                return {};
246  
            }
246  
            }
247  
        };
247  
        };
248  
        return awaitable{this, n};
248  
        return awaitable{this, n};
249  
    }
249  
    }
250  
};
250  
};
251  

251  

252  
} // test
252  
} // test
253  
} // capy
253  
} // capy
254  
} // boost
254  
} // boost
255  

255  

256  
#endif
256  
#endif