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 0 : void await_suspend(
169 : coro,
170 : executor_ref,
171 : std::stop_token) const noexcept
172 : {
173 0 : }
174 :
175 : io_result<>
176 468 : await_resume()
177 : {
178 468 : auto ec = self_->f_->maybe_fail();
179 428 : if(ec)
180 40 : return {ec};
181 :
182 388 : std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
183 388 : self_->data_.append(self_->prepare_buf_.data(), to_commit);
184 388 : self_->prepare_size_ = 0;
185 :
186 388 : return {};
187 : }
188 : };
189 468 : 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 48 : 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 48 : 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 48 : await_resume()
224 : {
225 48 : auto ec = self_->f_->maybe_fail();
226 37 : if(ec)
227 11 : return {ec};
228 :
229 26 : std::size_t to_commit = (std::min)(n_, self_->prepare_size_);
230 26 : self_->data_.append(self_->prepare_buf_.data(), to_commit);
231 26 : self_->prepare_size_ = 0;
232 :
233 26 : if(eof_)
234 4 : self_->eof_called_ = true;
235 :
236 26 : return {};
237 : }
238 : };
239 48 : 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
|