0.00% Lines (0/0) 0.00% Functions (0/0)
TLA Baseline Branch
Line Hits Code Line Hits Code
1 - //  
2 - // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)  
3 - // Copyright (c) 2026 Michael Vandeberg  
4 - //  
5 - // Distributed under the Boost Software License, Version 1.0. (See accompanying  
6 - // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)  
7 - //  
8 - // Official repository: https://github.com/cppalliance/capy  
9 - //  
10 -  
11 - #ifndef BOOST_CAPY_TEST_WRITE_SINK_HPP  
12 - #define BOOST_CAPY_TEST_WRITE_SINK_HPP  
13 -  
14 - #include <boost/capy/detail/config.hpp>  
15 - #include <boost/capy/buffers.hpp>  
16 - #include <boost/capy/buffers/buffer_copy.hpp>  
17 - #include <boost/capy/buffers/make_buffer.hpp>  
18 - #include <coroutine>  
19 - #include <boost/capy/ex/io_env.hpp>  
20 - #include <boost/capy/io_result.hpp>  
21 - #include <boost/capy/error.hpp>  
22 - #include <boost/capy/test/fuse.hpp>  
23 -  
24 - #include <algorithm>  
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 - This class satisfies the @ref WriteSink concept by providing partial  
40 - writes via `write_some` (satisfying @ref WriteStream), complete  
41 - writes via `write`, and EOF signaling via `write_eof`.  
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  
DCB 74 - 236 consume_match_() noexcept  
75 - {  
DCB 76 - 236 if(data_.empty() || expect_.empty())  
DCB 77 - 228 return {};  
DCB 78 - 8 std::size_t const n = (std::min)(data_.size(), expect_.size());  
DCB 79 - 8 if(std::string_view(data_.data(), n) !=  
DCB 80 - 16 std::string_view(expect_.data(), n))  
DCB 81 - 4 return error::test_failure;  
DCB 82 - 4 data_.erase(0, n);  
DCB 83 - 4 expect_.erase(0, n);  
DCB 84 - 4 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 - */  
DCB 95 - 416 explicit write_sink(  
96 - fuse f = {},  
97 - std::size_t max_write_size = std::size_t(-1)) noexcept  
DCB 98 - 416 : f_(std::move(f))  
DCB 99 - 416 , max_write_size_(max_write_size)  
100 - {  
DCB 101 - 416 }  
102 -  
103 - /// Return the written data as a string view.  
104 - std::string_view  
DCB 105 - 100 data() const noexcept  
106 - {  
DCB 107 - 100 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  
DCB 121 - 16 expect(std::string_view sv)  
122 - {  
DCB 123 - 16 expect_.assign(sv);  
DCB 124 - 16 return consume_match_();  
125 - }  
126 -  
127 - /// Return the number of bytes written.  
128 - std::size_t  
DCB 129 - 9 size() const noexcept  
130 - {  
DCB 131 - 9 return data_.size();  
132 - }  
133 -  
134 - /// Return whether write_eof has been called.  
135 - bool  
DCB 136 - 66 eof_called() const noexcept  
137 - {  
DCB 138 - 66 return eof_called_;  
139 - }  
140 -  
141 - /// Clear all data and reset state.  
142 - void  
DCB 143 - 4 clear() noexcept  
144 - {  
DCB 145 - 4 data_.clear();  
DCB 146 - 4 expect_.clear();  
DCB 147 - 4 eof_called_ = false;  
DCB 148 - 4 }  
149 -  
150 - /** Asynchronously write some data to the sink.  
151 -  
152 - Transfers up to `buffer_size( buffers )` bytes from the provided  
153 - const buffer sequence to the internal buffer. Before every write,  
154 - the attached @ref fuse is consulted to possibly inject an error.  
155 -  
156 - @param buffers The const buffer sequence containing data to write.  
157 -  
158 - @return An awaitable that await-returns `(error_code,std::size_t)`.  
159 -  
160 - @par Cancellation  
161 - If the environment's stop token has been requested, the write  
162 - completes immediately with `error::canceled` and transfers no  
163 - data. An empty buffer sequence is a no-op that completes  
164 - successfully regardless of the stop token.  
165 -  
166 - @see fuse  
167 - */  
168 - template<ConstBufferSequence CB>  
169 - auto  
DCB 170 - 77 write_some(CB buffers)  
171 - {  
172 - struct awaitable  
173 - {  
174 - write_sink* self_;  
175 - CB buffers_;  
176 - bool canceled_ = false;  
177 -  
DCB 178 - 77 bool await_ready() const noexcept { return false; }  
179 -  
180 - // The operation completes synchronously, but await_suspend is  
181 - // the only place io_env is delivered (the promise's  
182 - // transform_awaiter forwards it here). Returning false means  
183 - // the coroutine does not actually suspend; it resumes  
184 - // immediately, having observed the stop token. See io_env,  
185 - // IoAwaitable.  
186 - bool  
DCB 187 - 77 await_suspend(  
188 - std::coroutine_handle<>,  
189 - io_env const* env) noexcept  
190 - {  
DCB 191 - 77 canceled_ = env->stop_token.stop_requested();  
DCB 192 - 77 return false;  
193 - }  
194 -  
195 - io_result<std::size_t>  
DCB 196 - 77 await_resume()  
197 - {  
DCB 198 - 77 if(buffer_empty(buffers_))  
DCB 199 - 2 return {{}, 0};  
200 -  
DCB 201 - 75 if(canceled_)  
DCB 202 - 1 return {error::canceled, 0};  
203 -  
DCB 204 - 74 auto ec = self_->f_.maybe_fail();  
DCB 205 - 53 if(ec)  
DCB 206 - 21 return {ec, 0};  
207 -  
DCB 208 - 32 std::size_t n = buffer_size(buffers_);  
DCB 209 - 32 n = (std::min)(n, self_->max_write_size_);  
210 -  
DCB 211 - 32 std::size_t const old_size = self_->data_.size();  
DCB 212 - 32 self_->data_.resize(old_size + n);  
DCB 213 - 32 buffer_copy(make_buffer(  
DCB 214 - 32 self_->data_.data() + old_size, n), buffers_, n);  
215 -  
DCB 216 - 32 ec = self_->consume_match_();  
DCB 217 - 32 if(ec)  
218 - {  
DUB 219 - self_->data_.resize(old_size);  
DUB 220 - return {ec, 0};  
221 - }  
222 -  
DCB 223 - 32 return {{}, n};  
224 - }  
225 - };  
DCB 226 - 77 return awaitable{this, buffers};  
227 - }  
228 -  
229 - /** Asynchronously write data to the sink.  
230 -  
231 - Transfers all bytes from the provided const buffer sequence  
232 - to the internal buffer. Unlike @ref write_some, this ignores  
233 - `max_write_size` and writes all available data, matching the  
234 - @ref WriteSink semantic contract.  
235 -  
236 - @par Exception Safety  
237 - Injected I/O conditions are reported via the `error_code`  
238 - component of the result. Throws `std::system_error` only when  
239 - the attached @ref fuse is in exception mode and reaches its  
240 - failure point; no-throw otherwise.  
241 -  
242 - @param buffers The const buffer sequence containing data to write.  
243 -  
244 - @return An awaitable that await-returns `(error_code,std::size_t)`.  
245 -  
246 - @par Cancellation  
247 - If the environment's stop token has been requested, the write  
248 - completes immediately with `error::canceled` and transfers no  
249 - data.  
250 -  
251 - @throws std::system_error When the attached @ref fuse is in  
252 - exception mode and reaches its failure point.  
253 -  
254 - @see fuse  
255 - */  
256 - template<ConstBufferSequence CB>  
257 - auto  
DCB 258 - 303 write(CB buffers)  
259 - {  
260 - struct awaitable  
261 - {  
262 - write_sink* self_;  
263 - CB buffers_;  
264 - bool canceled_ = false;  
265 -  
DCB 266 - 303 bool await_ready() const noexcept { return false; }  
267 -  
268 - // Reads the stop token without suspending; see the comment  
269 - // on write_some() for details.  
270 - bool  
DCB 271 - 303 await_suspend(  
272 - std::coroutine_handle<>,  
273 - io_env const* env) noexcept  
274 - {  
DCB 275 - 303 canceled_ = env->stop_token.stop_requested();  
DCB 276 - 303 return false;  
277 - }  
278 -  
279 - io_result<std::size_t>  
DCB 280 - 303 await_resume()  
281 - {  
DCB 282 - 303 if(canceled_)  
DCB 283 - 1 return {error::canceled, 0};  
284 -  
DCB 285 - 302 auto ec = self_->f_.maybe_fail();  
DCB 286 - 241 if(ec)  
DCB 287 - 61 return {ec, 0};  
288 -  
DCB 289 - 180 std::size_t n = buffer_size(buffers_);  
DCB 290 - 180 if(n == 0)  
DCB 291 - 2 return {{}, 0};  
292 -  
DCB 293 - 178 std::size_t const old_size = self_->data_.size();  
DCB 294 - 178 self_->data_.resize(old_size + n);  
DCB 295 - 178 buffer_copy(make_buffer(  
DCB 296 - 178 self_->data_.data() + old_size, n), buffers_);  
297 -  
DCB 298 - 178 ec = self_->consume_match_();  
DCB 299 - 178 if(ec)  
DCB 300 - 2 return {ec, n};  
301 -  
DCB 302 - 176 return {{}, n};  
303 - }  
304 - };  
DCB 305 - 303 return awaitable{this, buffers};  
306 - }  
307 -  
308 - /** Atomically write data and signal end-of-stream.  
309 -  
310 - Transfers all bytes from the provided const buffer sequence to  
311 - the internal buffer and signals end-of-stream. Before the write,  
312 - the attached @ref fuse is consulted to possibly inject an error  
313 - for testing fault scenarios.  
314 -  
315 - @par Effects  
316 - On success, appends the written bytes to the internal buffer  
317 - and marks the sink as finalized.  
318 - If an error is injected by the fuse, the internal buffer remains  
319 - unchanged.  
320 -  
321 - @par Exception Safety  
322 - Injected I/O conditions are reported via the `error_code`  
323 - component of the result. Throws `std::system_error` only when  
324 - the attached @ref fuse is in exception mode and reaches its  
325 - failure point; no-throw otherwise.  
326 -  
327 - @par Cancellation  
328 - If the environment's stop token has been requested, the operation  
329 - completes immediately with `error::canceled`, transfers no data,  
330 - and does not signal end-of-stream.  
331 -  
332 - @param buffers The const buffer sequence containing data to write.  
333 -  
334 - @return An awaitable that await-returns `(error_code,std::size_t)`.  
335 -  
336 - @throws std::system_error When the attached @ref fuse is in  
337 - exception mode and reaches its failure point.  
338 -  
339 - @see fuse  
340 - */  
341 - template<ConstBufferSequence CB>  
342 - auto  
DCB 343 - 35 write_eof(CB buffers)  
344 - {  
345 - struct awaitable  
346 - {  
347 - write_sink* self_;  
348 - CB buffers_;  
349 - bool canceled_ = false;  
350 -  
DCB 351 - 35 bool await_ready() const noexcept { return false; }  
352 -  
353 - // Reads the stop token without suspending; see the comment  
354 - // on write_some() for details.  
355 - bool  
DCB 356 - 35 await_suspend(  
357 - std::coroutine_handle<>,  
358 - io_env const* env) noexcept  
359 - {  
DCB 360 - 35 canceled_ = env->stop_token.stop_requested();  
DCB 361 - 35 return false;  
362 - }  
363 -  
364 - io_result<std::size_t>  
DCB 365 - 35 await_resume()  
366 - {  
DCB 367 - 35 if(canceled_)  
DCB 368 - 1 return {error::canceled, 0};  
369 -  
DCB 370 - 34 auto ec = self_->f_.maybe_fail();  
DCB 371 - 23 if(ec)  
DCB 372 - 11 return {ec, 0};  
373 -  
DCB 374 - 12 std::size_t n = buffer_size(buffers_);  
DCB 375 - 12 if(n > 0)  
376 - {  
DCB 377 - 10 std::size_t const old_size = self_->data_.size();  
DCB 378 - 10 self_->data_.resize(old_size + n);  
DCB 379 - 10 buffer_copy(make_buffer(  
DCB 380 - 10 self_->data_.data() + old_size, n), buffers_);  
381 -  
DCB 382 - 10 ec = self_->consume_match_();  
DCB 383 - 10 if(ec)  
DUB 384 - return {ec, n};  
385 - }  
386 -  
DCB 387 - 12 self_->eof_called_ = true;  
388 -  
DCB 389 - 12 return {{}, n};  
390 - }  
391 - };  
DCB 392 - 35 return awaitable{this, buffers};  
393 - }  
394 -  
395 - /** Signal end-of-stream.  
396 -  
397 - Marks the sink as finalized, indicating no more data will be  
398 - written. Before signaling, the attached @ref fuse is consulted  
399 - to possibly inject an error for testing fault scenarios.  
400 -  
401 - @par Effects  
402 - On success, marks the sink as finalized.  
403 - If an error is injected by the fuse, the state remains unchanged.  
404 -  
405 - @par Exception Safety  
406 - Injected I/O conditions are reported via the `error_code`  
407 - component of the result. Throws `std::system_error` only when  
408 - the attached @ref fuse is in exception mode and reaches its  
409 - failure point; no-throw otherwise.  
410 -  
411 - @par Cancellation  
412 - If the environment's stop token has been requested, the operation  
413 - completes immediately with `error::canceled` and does not signal  
414 - end-of-stream.  
415 -  
416 - @return An awaitable that await-returns `(error_code)`.  
417 -  
418 - @throws std::system_error When the attached @ref fuse is in  
419 - exception mode and reaches its failure point.  
420 -  
421 - @see fuse  
422 - */  
423 - auto  
DCB 424 - 83 write_eof()  
425 - {  
426 - struct awaitable  
427 - {  
428 - write_sink* self_;  
429 - bool canceled_ = false;  
430 -  
DCB 431 - 83 bool await_ready() const noexcept { return false; }  
432 -  
433 - // Reads the stop token without suspending; see the comment  
434 - // on write_some() for details.  
435 - bool  
DCB 436 - 83 await_suspend(  
437 - std::coroutine_handle<>,  
438 - io_env const* env) noexcept  
439 - {  
DCB 440 - 83 canceled_ = env->stop_token.stop_requested();  
DCB 441 - 83 return false;  
442 - }  
443 -  
444 - io_result<>  
DCB 445 - 83 await_resume()  
446 - {  
DCB 447 - 83 if(canceled_)  
DCB 448 - 1 return {error::canceled};  
449 -  
DCB 450 - 82 auto ec = self_->f_.maybe_fail();  
DCB 451 - 60 if(ec)  
DCB 452 - 22 return {ec};  
453 -  
DCB 454 - 38 self_->eof_called_ = true;  
DCB 455 - 38 return {};  
456 - }  
457 - };  
DCB 458 - 83 return awaitable{this};  
459 - }  
460 - };  
461 -  
462 - } // test  
463 - } // capy  
464 - } // boost  
465 -  
466 - #endif