Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : //
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)
6 : //
7 : // Official repository: https://github.com/cppalliance/capy
8 : //
9 :
10 : #ifndef BOOST_CAPY_TEST_BUFFER_SINK_HPP
11 : #define BOOST_CAPY_TEST_BUFFER_SINK_HPP
12 :
13 : #include <boost/capy/detail/config.hpp>
14 : #include <boost/capy/buffers.hpp>
15 : #include <boost/capy/buffers/make_buffer.hpp>
16 : #include <coroutine>
17 : #include <boost/capy/ex/io_env.hpp>
18 : #include <boost/capy/io_result.hpp>
19 : #include <boost/capy/test/fuse.hpp>
20 :
21 : #include <algorithm>
22 : #include <span>
23 : #include <string>
24 : #include <string_view>
25 :
26 : namespace boost {
27 : namespace capy {
28 : namespace test {
29 :
30 : /** A mock buffer sink for testing callee-owns-buffers write operations.
31 :
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
34 : buffers, write into them, then call @ref commit to finalize. The
35 : associated @ref fuse enables error injection at controlled points.
36 :
37 : This class satisfies the @ref BufferSink concept by providing
38 : internal storage that callers write into directly.
39 :
40 : @par Thread Safety
41 : Not thread-safe.
42 :
43 : @par Example
44 : @code
45 : fuse f;
46 : buffer_sink bs( f );
47 :
48 : auto r = f.armed( [&]( fuse& ) -> task<void> {
49 : mutable_buffer arr[16];
50 : std::size_t count = bs.prepare( arr, 16 );
51 : if( count == 0 )
52 : co_return;
53 :
54 : // Write data into arr[0]
55 : std::memcpy( arr[0].data(), "Hello", 5 );
56 :
57 : auto [ec] = co_await bs.commit( 5 );
58 : if( ec )
59 : co_return;
60 :
61 : auto [ec2] = co_await bs.commit_eof();
62 : // bs.data() returns "Hello"
63 : } );
64 : @endcode
65 :
66 : @see fuse, BufferSink
67 : */
68 : class buffer_sink
69 : {
70 : fuse f_;
71 : std::string data_;
72 : std::string prepare_buf_;
73 : std::size_t prepare_size_ = 0;
74 : std::size_t max_prepare_size_;
75 : bool eof_called_ = false;
76 :
77 : public:
78 : /** Construct a buffer sink.
79 :
80 : @param f The fuse used to inject errors during commits.
81 :
82 : @param max_prepare_size Maximum bytes available per prepare.
83 : Use to simulate limited buffer space.
84 : */
85 464 : explicit buffer_sink(
86 : fuse f = {},
87 : std::size_t max_prepare_size = 4096) noexcept
88 464 : : f_(std::move(f))
89 464 : , max_prepare_size_(max_prepare_size)
90 : {
91 464 : prepare_buf_.resize(max_prepare_size_);
92 464 : }
93 :
94 : /// Return the written data as a string view.
95 : std::string_view
96 64 : data() const noexcept
97 : {
98 64 : return data_;
99 : }
100 :
101 : /// Return the number of bytes written.
102 : std::size_t
103 4 : size() const noexcept
104 : {
105 4 : return data_.size();
106 : }
107 :
108 : /// Return whether commit_eof has been called.
109 : bool
110 62 : eof_called() const noexcept
111 : {
112 62 : return eof_called_;
113 : }
114 :
115 : /// Clear all data and reset state.
116 : void
117 : clear() noexcept
118 : {
119 : data_.clear();
120 : prepare_size_ = 0;
121 : eof_called_ = false;
122 : }
123 :
124 : /** Prepare writable buffers.
125 :
126 : Fills the provided span with mutable buffer descriptors pointing
127 : to internal storage. The caller writes data into these buffers,
128 : then calls @ref commit to finalize.
129 :
130 : @param dest Span of mutable_buffer to fill.
131 :
132 : @return A span of filled buffers (empty or 1 buffer in this implementation).
133 : */
134 : std::span<mutable_buffer>
135 708 : prepare(std::span<mutable_buffer> dest)
136 : {
137 708 : if(dest.empty())
138 0 : return {};
139 :
140 708 : prepare_size_ = max_prepare_size_;
141 708 : dest[0] = make_buffer(prepare_buf_.data(), prepare_size_);
142 708 : return dest.first(1);
143 : }
144 :
145 : /** Commit bytes written to the prepared buffers.
146 :
147 : Transfers `n` bytes from the prepared buffer to the internal
148 : data buffer. Before committing, the attached @ref fuse is
149 : consulted to possibly inject an error for testing fault scenarios.
150 :
151 : @param n The number of bytes to commit.
152 :
153 : @return An awaitable yielding `(error_code)`.
154 :
155 : @see fuse
156 : */
157 : auto
158 476 : commit(std::size_t n)
159 : {
160 : struct awaitable
161 : {
162 : buffer_sink* self_;
163 : std::size_t n_;
164 :
165 476 : bool await_ready() const noexcept { return true; }
166 :
167 : // This method is required to satisfy Capy's IoAwaitable concept,
168 : // but is never called because await_ready() returns true.
169 : //
170 : // Capy uses a two-layer awaitable system: the promise's
171 : // await_transform wraps awaitables in a transform_awaiter whose
172 : // standard await_suspend(coroutine_handle) calls this custom
173 : // 2-argument overload, passing the io_env from the coroutine's
174 : // context. For synchronous test awaitables like this one, the
175 : // coroutine never suspends, so this is not invoked. The signature
176 : // exists to allow the same awaitable type to work with both
177 : // synchronous (test) and asynchronous (real I/O) code.
178 0 : void await_suspend(
179 : std::coroutine_handle<>,
180 : io_env const*) const noexcept
181 : {
182 0 : }
183 :
184 : io_result<>
185 476 : await_resume()
186 : {
187 476 : auto ec = self_->f_.maybe_fail();
188 416 : if(ec)
189 60 : return {ec};
190 :
191 356 : std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
192 356 : self_->data_.append(self_->prepare_buf_.data(), to_commit);
193 356 : self_->prepare_size_ = 0;
194 :
195 356 : return {};
196 : }
197 : };
198 476 : return awaitable{this, n};
199 : }
200 :
201 : /** Commit final bytes and signal end-of-stream.
202 :
203 : Transfers `n` bytes from the prepared buffer to the internal
204 : data buffer and marks the sink as finalized. Before committing,
205 : the attached @ref fuse is consulted to possibly inject an error
206 : for testing fault scenarios.
207 :
208 : @param n The number of bytes to commit.
209 :
210 : @return An awaitable yielding `(error_code)`.
211 :
212 : @see fuse
213 : */
214 : auto
215 162 : commit_eof(std::size_t n)
216 : {
217 : struct awaitable
218 : {
219 : buffer_sink* self_;
220 : std::size_t n_;
221 :
222 162 : bool await_ready() const noexcept { return true; }
223 :
224 : // This method is required to satisfy Capy's IoAwaitable concept,
225 : // but is never called because await_ready() returns true.
226 : // See the comment on commit(std::size_t) for a detailed explanation.
227 0 : void await_suspend(
228 : std::coroutine_handle<>,
229 : io_env const*) const noexcept
230 : {
231 0 : }
232 :
233 : io_result<>
234 162 : await_resume()
235 : {
236 162 : auto ec = self_->f_.maybe_fail();
237 118 : if(ec)
238 44 : return {ec};
239 :
240 74 : std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
241 74 : self_->data_.append(self_->prepare_buf_.data(), to_commit);
242 74 : self_->prepare_size_ = 0;
243 :
244 74 : self_->eof_called_ = true;
245 74 : return {};
246 : }
247 : };
248 162 : return awaitable{this, n};
249 : }
250 : };
251 :
252 : } // test
253 : } // capy
254 : } // boost
255 :
256 : #endif
|