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 <stop_token>
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 404 : explicit buffer_sink(
86 : fuse& f,
87 : std::size_t max_prepare_size = 4096) noexcept
88 404 : : f_(&f)
89 404 : , max_prepare_size_(max_prepare_size)
90 : {
91 404 : prepare_buf_.resize(max_prepare_size_);
92 404 : }
93 :
94 : /// Return the written data as a string view.
95 : std::string_view
96 48 : data() const noexcept
97 : {
98 48 : 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 50 : eof_called() const noexcept
111 : {
112 50 : 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 array 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 arr Pointer to array of mutable_buffer to fill.
131 : @param max_count Maximum number of buffers to fill.
132 :
133 : @return The number of buffers filled (0 or 1 in this implementation).
134 : */
135 : std::size_t
136 800 : prepare(mutable_buffer* arr, std::size_t max_count)
137 : {
138 800 : if(max_count == 0)
139 0 : return 0;
140 :
141 800 : prepare_size_ = max_prepare_size_;
142 800 : arr[0] = make_buffer(prepare_buf_.data(), prepare_size_);
143 800 : return 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 506 : commit(std::size_t n)
160 : {
161 : struct awaitable
162 : {
163 : buffer_sink* self_;
164 : std::size_t n_;
165 :
166 506 : bool await_ready() const noexcept { return true; }
167 :
168 0 : void await_suspend(
169 : coro,
170 : executor_ref,
171 : std::stop_token) const noexcept
172 : {
173 0 : }
174 :
175 : io_result<>
176 506 : await_resume()
177 : {
178 506 : auto ec = self_->f_->maybe_fail();
179 458 : if(ec)
180 48 : return {ec};
181 :
182 410 : std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
183 410 : self_->data_.append(self_->prepare_buf_.data(), to_commit);
184 410 : self_->prepare_size_ = 0;
185 :
186 410 : return {};
187 : }
188 : };
189 506 : return awaitable{this, n};
190 : }
191 :
192 : /** Commit bytes written with optional end-of-stream.
193 :
194 : Transfers `n` bytes from the prepared buffer to the internal
195 : data buffer. If `eof` is true, marks the sink as finalized.
196 :
197 : @param n The number of bytes to commit.
198 : @param eof If true, signals end-of-stream after committing.
199 :
200 : @return An awaitable yielding `(error_code)`.
201 :
202 : @see fuse
203 : */
204 : auto
205 10 : commit(std::size_t n, bool eof)
206 : {
207 : struct awaitable
208 : {
209 : buffer_sink* self_;
210 : std::size_t n_;
211 : bool eof_;
212 :
213 10 : bool await_ready() const noexcept { return true; }
214 :
215 0 : void await_suspend(
216 : coro,
217 : executor_ref,
218 : std::stop_token) const noexcept
219 : {
220 0 : }
221 :
222 : io_result<>
223 10 : await_resume()
224 : {
225 10 : auto ec = self_->f_->maybe_fail();
226 7 : if(ec)
227 3 : return {ec};
228 :
229 4 : std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
230 4 : self_->data_.append(self_->prepare_buf_.data(), to_commit);
231 4 : self_->prepare_size_ = 0;
232 :
233 4 : if(eof_)
234 4 : self_->eof_called_ = true;
235 :
236 4 : return {};
237 : }
238 : };
239 10 : return awaitable{this, n, eof};
240 : }
241 :
242 : /** Signal end-of-stream.
243 :
244 : Marks the sink as finalized, indicating no more data will be
245 : written. Before signaling, the attached @ref fuse is consulted
246 : to possibly inject an error for testing fault scenarios.
247 :
248 : @return An awaitable yielding `(error_code)`.
249 :
250 : @see fuse
251 : */
252 : auto
253 110 : commit_eof()
254 : {
255 : struct awaitable
256 : {
257 : buffer_sink* self_;
258 :
259 110 : bool await_ready() const noexcept { return true; }
260 :
261 0 : void await_suspend(
262 : coro,
263 : executor_ref,
264 : std::stop_token) const noexcept
265 : {
266 0 : }
267 :
268 : io_result<>
269 110 : await_resume()
270 : {
271 110 : auto ec = self_->f_->maybe_fail();
272 82 : if(ec)
273 28 : return {ec};
274 :
275 54 : self_->eof_called_ = true;
276 54 : return {};
277 : }
278 : };
279 110 : return awaitable{this};
280 : }
281 : };
282 :
283 : } // test
284 : } // capy
285 : } // boost
286 :
287 : #endif
|