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