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_WRITE_SINK_HPP
11 : #define BOOST_CAPY_TEST_WRITE_SINK_HPP
12 :
13 : #include <boost/capy/detail/config.hpp>
14 : #include <boost/capy/buffers.hpp>
15 : #include <boost/capy/buffers/buffer_copy.hpp>
16 : #include <boost/capy/buffers/make_buffer.hpp>
17 : #include <boost/capy/coro.hpp>
18 : #include <boost/capy/ex/executor_ref.hpp>
19 : #include <boost/capy/io_result.hpp>
20 : #include <boost/capy/error.hpp>
21 : #include <boost/capy/test/fuse.hpp>
22 :
23 : #include <algorithm>
24 : #include <stop_token>
25 : #include <string>
26 : #include <string_view>
27 :
28 : namespace boost {
29 : namespace capy {
30 : namespace test {
31 :
32 : /** A mock sink for testing write operations.
33 :
34 : Use this to verify code that performs complete writes without needing
35 : real I/O. Call @ref write to write data, then @ref data to retrieve
36 : what was written. The associated @ref fuse enables error injection
37 : at controlled points.
38 :
39 : Unlike @ref write_stream which provides partial writes via `write_some`,
40 : this class satisfies the @ref WriteSink concept by providing complete
41 : writes and EOF signaling.
42 :
43 : @par Thread Safety
44 : Not thread-safe.
45 :
46 : @par Example
47 : @code
48 : fuse f;
49 : write_sink ws( f );
50 :
51 : auto r = f.armed( [&]( fuse& ) -> task<void> {
52 : auto [ec, n] = co_await ws.write(
53 : const_buffer( "Hello", 5 ) );
54 : if( ec )
55 : co_return;
56 : auto [ec2] = co_await ws.write_eof();
57 : if( ec2 )
58 : co_return;
59 : // ws.data() returns "Hello"
60 : } );
61 : @endcode
62 :
63 : @see fuse, WriteSink
64 : */
65 : class write_sink
66 : {
67 : fuse* f_;
68 : std::string data_;
69 : std::string expect_;
70 : std::size_t max_write_size_;
71 : bool eof_called_ = false;
72 :
73 : std::error_code
74 172 : consume_match_() noexcept
75 : {
76 172 : if(data_.empty() || expect_.empty())
77 172 : return {};
78 0 : std::size_t const n = (std::min)(data_.size(), expect_.size());
79 0 : if(std::string_view(data_.data(), n) !=
80 0 : std::string_view(expect_.data(), n))
81 0 : return error::test_failure;
82 0 : data_.erase(0, n);
83 0 : expect_.erase(0, n);
84 0 : return {};
85 : }
86 :
87 : public:
88 : /** Construct a write sink.
89 :
90 : @param f The fuse used to inject errors during writes.
91 :
92 : @param max_write_size Maximum bytes transferred per write.
93 : Use to simulate chunked delivery.
94 : */
95 236 : explicit write_sink(
96 : fuse& f,
97 : std::size_t max_write_size = std::size_t(-1)) noexcept
98 236 : : f_(&f)
99 236 : , max_write_size_(max_write_size)
100 : {
101 236 : }
102 :
103 : /// Return the written data as a string view.
104 : std::string_view
105 38 : data() const noexcept
106 : {
107 38 : return data_;
108 : }
109 :
110 : /** Set the expected data for subsequent writes.
111 :
112 : Stores the expected data and immediately tries to match
113 : against any data already written. Matched data is consumed
114 : from both buffers.
115 :
116 : @param sv The expected data.
117 :
118 : @return An error if existing data does not match.
119 : */
120 : std::error_code
121 : expect(std::string_view sv)
122 : {
123 : expect_.assign(sv);
124 : return consume_match_();
125 : }
126 :
127 : /// Return the number of bytes written.
128 : std::size_t
129 2 : size() const noexcept
130 : {
131 2 : return data_.size();
132 : }
133 :
134 : /// Return whether write_eof has been called.
135 : bool
136 36 : eof_called() const noexcept
137 : {
138 36 : return eof_called_;
139 : }
140 :
141 : /// Clear all data and reset state.
142 : void
143 : clear() noexcept
144 : {
145 : data_.clear();
146 : expect_.clear();
147 : eof_called_ = false;
148 : }
149 :
150 : /** Asynchronously write data to the sink.
151 :
152 : Transfers all bytes from the provided const buffer sequence to
153 : the internal buffer. Before every write, the attached @ref fuse
154 : is consulted to possibly inject an error for testing fault
155 : scenarios. The returned `std::size_t` is the number of bytes
156 : transferred.
157 :
158 : @par Effects
159 : On success, appends the written bytes to the internal buffer.
160 : If an error is injected by the fuse, the internal buffer remains
161 : unchanged.
162 :
163 : @par Exception Safety
164 : No-throw guarantee.
165 :
166 : @param buffers The const buffer sequence containing data to write.
167 :
168 : @return An awaitable yielding `(error_code,std::size_t)`.
169 :
170 : @see fuse
171 : */
172 : template<ConstBufferSequence CB>
173 : auto
174 128 : write(CB buffers)
175 : {
176 : struct awaitable
177 : {
178 : write_sink* self_;
179 : CB buffers_;
180 :
181 128 : bool await_ready() const noexcept { return true; }
182 :
183 : // This method is required to satisfy Capy's IoAwaitable concept,
184 : // but is never called because await_ready() returns true.
185 : //
186 : // Capy uses a two-layer awaitable system: the promise's
187 : // await_transform wraps awaitables in a transform_awaiter whose
188 : // standard await_suspend(coroutine_handle) calls this custom
189 : // 3-argument overload, passing the executor and stop_token from
190 : // the coroutine's context. For synchronous test awaitables like
191 : // this one, the coroutine never suspends, so this is not invoked.
192 : // The signature exists to allow the same awaitable type to work
193 : // with both synchronous (test) and asynchronous (real I/O) code.
194 0 : void await_suspend(
195 : coro,
196 : executor_ref,
197 : std::stop_token) const noexcept
198 : {
199 0 : }
200 :
201 : io_result<std::size_t>
202 128 : await_resume()
203 : {
204 128 : auto ec = self_->f_->maybe_fail();
205 115 : if(ec)
206 13 : return {ec, 0};
207 :
208 102 : std::size_t n = buffer_size(buffers_);
209 102 : n = (std::min)(n, self_->max_write_size_);
210 102 : if(n == 0)
211 0 : return {{}, 0};
212 :
213 102 : std::size_t const old_size = self_->data_.size();
214 102 : self_->data_.resize(old_size + n);
215 102 : buffer_copy(make_buffer(
216 102 : self_->data_.data() + old_size, n), buffers_, n);
217 :
218 102 : ec = self_->consume_match_();
219 102 : if(ec)
220 0 : return {ec, n};
221 :
222 102 : return {{}, n};
223 : }
224 : };
225 128 : return awaitable{this, buffers};
226 : }
227 :
228 : /** Asynchronously write data to the sink with optional EOF.
229 :
230 : Transfers all bytes from the provided const buffer sequence to
231 : the internal buffer, optionally signaling end-of-stream. Before
232 : every write, the attached @ref fuse is consulted to possibly
233 : inject an error for testing fault scenarios. The returned
234 : `std::size_t` is the number of bytes transferred.
235 :
236 : @par Effects
237 : On success, appends the written bytes to the internal buffer.
238 : If `eof` is `true`, marks the sink as finalized.
239 : If an error is injected by the fuse, the internal buffer remains
240 : unchanged.
241 :
242 : @par Exception Safety
243 : No-throw guarantee.
244 :
245 : @param buffers The const buffer sequence containing data to write.
246 : @param eof If true, signals end-of-stream after writing.
247 :
248 : @return An awaitable yielding `(error_code,std::size_t)`.
249 :
250 : @see fuse
251 : */
252 : template<ConstBufferSequence CB>
253 : auto
254 126 : write(CB buffers, bool eof)
255 : {
256 : struct awaitable
257 : {
258 : write_sink* self_;
259 : CB buffers_;
260 : bool eof_;
261 :
262 126 : bool await_ready() const noexcept { return true; }
263 :
264 : // This method is required to satisfy Capy's IoAwaitable concept,
265 : // but is never called because await_ready() returns true.
266 : // See the comment on write(CB buffers) for a detailed explanation.
267 0 : void await_suspend(
268 : coro,
269 : executor_ref,
270 : std::stop_token) const noexcept
271 : {
272 0 : }
273 :
274 : io_result<std::size_t>
275 126 : await_resume()
276 : {
277 126 : auto ec = self_->f_->maybe_fail();
278 98 : if(ec)
279 28 : return {ec, 0};
280 :
281 70 : std::size_t n = buffer_size(buffers_);
282 70 : n = (std::min)(n, self_->max_write_size_);
283 70 : if(n > 0)
284 : {
285 70 : std::size_t const old_size = self_->data_.size();
286 70 : self_->data_.resize(old_size + n);
287 70 : buffer_copy(make_buffer(
288 70 : self_->data_.data() + old_size, n), buffers_, n);
289 :
290 70 : ec = self_->consume_match_();
291 70 : if(ec)
292 0 : return {ec, n};
293 : }
294 :
295 70 : if(eof_)
296 0 : self_->eof_called_ = true;
297 :
298 70 : return {{}, n};
299 : }
300 : };
301 126 : return awaitable{this, buffers, eof};
302 : }
303 :
304 : /** Signal end-of-stream.
305 :
306 : Marks the sink as finalized, indicating no more data will be
307 : written. Before signaling, the attached @ref fuse is consulted
308 : to possibly inject an error for testing fault scenarios.
309 :
310 : @par Effects
311 : On success, marks the sink as finalized.
312 : If an error is injected by the fuse, the state remains unchanged.
313 :
314 : @par Exception Safety
315 : No-throw guarantee.
316 :
317 : @return An awaitable yielding `(error_code)`.
318 :
319 : @see fuse
320 : */
321 : auto
322 68 : write_eof()
323 : {
324 : struct awaitable
325 : {
326 : write_sink* self_;
327 :
328 68 : bool await_ready() const noexcept { return true; }
329 :
330 : // This method is required to satisfy Capy's IoAwaitable concept,
331 : // but is never called because await_ready() returns true.
332 : // See the comment on write(CB buffers) for a detailed explanation.
333 0 : void await_suspend(
334 : coro,
335 : executor_ref,
336 : std::stop_token) const noexcept
337 : {
338 0 : }
339 :
340 : io_result<>
341 68 : await_resume()
342 : {
343 68 : auto ec = self_->f_->maybe_fail();
344 50 : if(ec)
345 18 : return {ec};
346 :
347 32 : self_->eof_called_ = true;
348 32 : return {};
349 : }
350 : };
351 68 : return awaitable{this};
352 : }
353 : };
354 :
355 : } // test
356 : } // capy
357 : } // boost
358 :
359 : #endif
|