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 <boost/capy/coro.hpp>
16  
#include <boost/capy/coro.hpp>
17  
#include <boost/capy/ex/executor_ref.hpp>
17  
#include <boost/capy/ex/executor_ref.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 <stop_token>
23  
#include <stop_token>
24  
#include <string>
24  
#include <string>
25  
#include <string_view>
25  
#include <string_view>
26  

26  

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

30  

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

32  

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

37  

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

40  

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

43  

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

48  

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

54  

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

57  

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

61  

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

66  

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

77  

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

80  

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

82  

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

94  

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

101  

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

108  

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

115  

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

124  

125  
    /** Prepare writable buffers.
125  
    /** Prepare writable buffers.
126  

126  

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

130  

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

132  

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

140  

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

145  

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

147  

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

151  

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

153  

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

155  

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

165  

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

167  

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

185  

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

192  

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

196  

197  
                return {};
197  
                return {};
198  
            }
198  
            }
199  
        };
199  
        };
200  
        return awaitable{this, n};
200  
        return awaitable{this, n};
201  
    }
201  
    }
202  

202  

203 -
    /** Commit bytes written with optional end-of-stream.
203 +
    /** Commit final bytes and signal end-of-stream.
204  

204  

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

209  

208 -
        @param eof If true, signals end-of-stream after committing.
 
209  
        @param n The number of bytes to commit.
210  
        @param n The number of bytes to commit.
210  

211  

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

213  

213  
        @see fuse
214  
        @see fuse
214  
    */
215  
    */
215  
    auto
216  
    auto
216 -
    commit(std::size_t n, bool eof)
217 +
    commit_eof(std::size_t n)
217  
    {
218  
    {
218  
        struct awaitable
219  
        struct awaitable
219  
        {
220  
        {
220  
            buffer_sink* self_;
221  
            buffer_sink* self_;
221 -
            bool eof_;
 
222  
            std::size_t n_;
222  
            std::size_t n_;
223  

223  

224  
            bool await_ready() const noexcept { return true; }
224  
            bool await_ready() const noexcept { return true; }
225  

225  

226  
            // This method is required to satisfy Capy's IoAwaitable concept,
226  
            // This method is required to satisfy Capy's IoAwaitable concept,
227  
            // but is never called because await_ready() returns true.
227  
            // but is never called because await_ready() returns true.
228  
            // See the comment on commit(std::size_t) for a detailed explanation.
228  
            // See the comment on commit(std::size_t) for a detailed explanation.
229  
            void await_suspend(
229  
            void await_suspend(
230  
                coro,
230  
                coro,
231  
                executor_ref,
231  
                executor_ref,
232  
                std::stop_token) const noexcept
232  
                std::stop_token) const noexcept
233  
            {
233  
            {
234  
            }
234  
            }
235  

235  

236  
            io_result<>
236  
            io_result<>
237  
            await_resume()
237  
            await_resume()
238  
            {
238  
            {
239 -
                auto ec = self_->f_->maybe_fail();
239 +
                auto ec = self_->f_.maybe_fail();
240  
                if(ec)
240  
                if(ec)
241  
                    return {ec};
241  
                    return {ec};
242  

242  

243  
                std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
243  
                std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
244  
                self_->data_.append(self_->prepare_buf_.data(), to_commit);
244  
                self_->data_.append(self_->prepare_buf_.data(), to_commit);
245  
                self_->prepare_size_ = 0;
245  
                self_->prepare_size_ = 0;
246 -
                if(eof_)
 
247 -
                    self_->eof_called_ = true;
 
248 -

 
249 -
                return {};
 
250 -
            }
 
251 -
        };
 
252 -
        return awaitable{this, n, eof};
 
253 -
    }
 
254 -

 
255 -
    /** Signal end-of-stream.
 
256 -

 
257 -
        Marks the sink as finalized, indicating no more data will be
 
258 -
        written. Before signaling, the attached @ref fuse is consulted
 
259 -
        to possibly inject an error for testing fault scenarios.
 
260 -

 
261 -
        @return An awaitable yielding `(error_code)`.
 
262 -

 
263 -
        @see fuse
 
264 -
    */
 
265 -
    auto
 
266 -
    commit_eof()
 
267 -
    {
 
268 -
        struct awaitable
 
269 -
        {
 
270 -
            buffer_sink* self_;
 
271 -

 
272 -
            bool await_ready() const noexcept { return true; }
 
273 -

 
274 -
            // This method is required to satisfy Capy's IoAwaitable concept,
 
275 -
            // but is never called because await_ready() returns true.
 
276 -
            // See the comment on commit(std::size_t) for a detailed explanation.
 
277 -
            void await_suspend(
 
278 -
                coro,
 
279 -
                executor_ref,
 
280 -
                std::stop_token) const noexcept
 
281 -
            {
 
282 -
            }
 
283 -

 
284 -
            io_result<>
 
285 -
            await_resume()
 
286 -
            {
 
287 -
                auto ec = self_->f_->maybe_fail();
 
288 -
                if(ec)
 
289 -
                    return {ec};
 
290 -

 
291  

246  

292  
                self_->eof_called_ = true;
247  
                self_->eof_called_ = true;
293  
                return {};
248  
                return {};
294  
            }
249  
            }
295  
        };
250  
        };
296 -
        return awaitable{this};
251 +
        return awaitable{this, n};
297  
    }
252  
    }
298  
};
253  
};
299  

254  

300  
} // test
255  
} // test
301  
} // capy
256  
} // capy
302  
} // boost
257  
} // boost
303  

258  

304  
#endif
259  
#endif