TLA Line data Source 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/http
9 : //
10 :
11 : #ifndef BOOST_HTTP_TEST_WRITE_SINK_HPP
12 : #define BOOST_HTTP_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 http {
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 capy::test::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 : capy::test::fuse f;
49 : write_sink ws( f );
50 :
51 : auto r = f.armed( [&]( capy::test::fuse& ) -> task<void> {
52 : auto [ec, n] = co_await ws.write(
53 : capy::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 capy::test::fuse, WriteSink
64 : */
65 : class write_sink
66 : {
67 : capy::test::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 HIT 1056 : consume_match_() noexcept
75 : {
76 1056 : if(data_.empty() || expect_.empty())
77 1056 : return {};
78 MIS 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 capy::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 capy::test::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 HIT 402 : explicit write_sink(
96 : capy::test::fuse f = {},
97 : std::size_t max_write_size = std::size_t(-1)) noexcept
98 402 : : f_(std::move(f))
99 402 : , max_write_size_(max_write_size)
100 : {
101 402 : }
102 :
103 : /// Return the written data as a string view.
104 : std::string_view
105 54 : data() const noexcept
106 : {
107 54 : 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 42 : eof_called() const noexcept
137 : {
138 42 : 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 some data to the sink.
151 :
152 : Transfers up to `capy::buffer_size( buffers )` bytes from the provided
153 : const buffer sequence to the internal buffer. Before every write,
154 : the attached @ref capy::test::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 `capy::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 capy::test::fuse
167 : */
168 : template<capy::ConstBufferSequence CB>
169 : auto
170 38 : write_some(CB buffers)
171 : {
172 : struct awaitable
173 : {
174 : write_sink* self_;
175 : CB buffers_;
176 : bool canceled_ = false;
177 :
178 38 : bool await_ready() const noexcept { return false; }
179 :
180 : // The operation completes synchronously, but await_suspend is
181 : // the only place capy::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 capy::io_env,
185 : // IoAwaitable.
186 : bool
187 38 : await_suspend(
188 : std::coroutine_handle<>,
189 : capy::io_env const* env) noexcept
190 : {
191 38 : canceled_ = env->stop_token.stop_requested();
192 38 : return false;
193 : }
194 :
195 : capy::io_result<std::size_t>
196 38 : await_resume()
197 : {
198 38 : if(capy::buffer_empty(buffers_))
199 MIS 0 : return {std::error_code(), 0};
200 :
201 HIT 38 : if(canceled_)
202 MIS 0 : return {capy::error::canceled, 0};
203 :
204 HIT 38 : auto ec = self_->f_.maybe_fail();
205 28 : if(ec)
206 10 : return {ec, 0};
207 :
208 18 : std::size_t n = capy::buffer_size(buffers_);
209 18 : n = (std::min)(n, self_->max_write_size_);
210 :
211 18 : std::size_t const old_size = self_->data_.size();
212 18 : self_->data_.resize(old_size + n);
213 18 : capy::buffer_copy(capy::make_buffer(
214 18 : self_->data_.data() + old_size, n), buffers_, n);
215 :
216 18 : ec = self_->consume_match_();
217 18 : if(ec)
218 : {
219 MIS 0 : self_->data_.resize(old_size);
220 0 : return {ec, 0};
221 : }
222 :
223 HIT 18 : return {std::error_code(), n};
224 : }
225 : };
226 38 : 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 capy::test::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 `capy::error::canceled` and transfers no
249 : data.
250 :
251 : @throws std::system_error When the attached @ref capy::test::fuse is in
252 : exception mode and reaches its failure point.
253 :
254 : @see capy::test::fuse
255 : */
256 : template<capy::ConstBufferSequence CB>
257 : auto
258 1150 : write(CB buffers)
259 : {
260 : struct awaitable
261 : {
262 : write_sink* self_;
263 : CB buffers_;
264 : bool canceled_ = false;
265 :
266 1150 : 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
271 1150 : await_suspend(
272 : std::coroutine_handle<>,
273 : capy::io_env const* env) noexcept
274 : {
275 1150 : canceled_ = env->stop_token.stop_requested();
276 1150 : return false;
277 : }
278 :
279 : capy::io_result<std::size_t>
280 1150 : await_resume()
281 : {
282 1150 : if(canceled_)
283 MIS 0 : return {capy::error::canceled, 0};
284 :
285 HIT 1150 : auto ec = self_->f_.maybe_fail();
286 1091 : if(ec)
287 59 : return {ec, 0};
288 :
289 1032 : std::size_t n = capy::buffer_size(buffers_);
290 1032 : if(n == 0)
291 MIS 0 : return {std::error_code(), 0};
292 :
293 HIT 1032 : std::size_t const old_size = self_->data_.size();
294 1032 : self_->data_.resize(old_size + n);
295 1032 : capy::buffer_copy(capy::make_buffer(
296 1032 : self_->data_.data() + old_size, n), buffers_);
297 :
298 1032 : ec = self_->consume_match_();
299 1032 : if(ec)
300 MIS 0 : return {ec, n};
301 :
302 HIT 1032 : return {std::error_code(), n};
303 : }
304 : };
305 1150 : 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 capy::test::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 capy::test::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 capy::test::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 `capy::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 capy::test::fuse is in
337 : exception mode and reaches its failure point.
338 :
339 : @see capy::test::fuse
340 : */
341 : template<capy::ConstBufferSequence CB>
342 : auto
343 16 : write_eof(CB buffers)
344 : {
345 : struct awaitable
346 : {
347 : write_sink* self_;
348 : CB buffers_;
349 : bool canceled_ = false;
350 :
351 16 : 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
356 16 : await_suspend(
357 : std::coroutine_handle<>,
358 : capy::io_env const* env) noexcept
359 : {
360 16 : canceled_ = env->stop_token.stop_requested();
361 16 : return false;
362 : }
363 :
364 : capy::io_result<std::size_t>
365 16 : await_resume()
366 : {
367 16 : if(canceled_)
368 MIS 0 : return {capy::error::canceled, 0};
369 :
370 HIT 16 : auto ec = self_->f_.maybe_fail();
371 11 : if(ec)
372 5 : return {ec, 0};
373 :
374 6 : std::size_t n = capy::buffer_size(buffers_);
375 6 : if(n > 0)
376 : {
377 6 : std::size_t const old_size = self_->data_.size();
378 6 : self_->data_.resize(old_size + n);
379 6 : capy::buffer_copy(capy::make_buffer(
380 6 : self_->data_.data() + old_size, n), buffers_);
381 :
382 6 : ec = self_->consume_match_();
383 6 : if(ec)
384 MIS 0 : return {ec, n};
385 : }
386 :
387 HIT 6 : self_->eof_called_ = true;
388 :
389 6 : return {std::error_code(), n};
390 : }
391 : };
392 16 : 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 capy::test::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 capy::test::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 capy::test::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 `capy::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 capy::test::fuse is in
419 : exception mode and reaches its failure point.
420 :
421 : @see capy::test::fuse
422 : */
423 : auto
424 68 : write_eof()
425 : {
426 : struct awaitable
427 : {
428 : write_sink* self_;
429 : bool canceled_ = false;
430 :
431 68 : 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
436 68 : await_suspend(
437 : std::coroutine_handle<>,
438 : capy::io_env const* env) noexcept
439 : {
440 68 : canceled_ = env->stop_token.stop_requested();
441 68 : return false;
442 : }
443 :
444 : capy::io_result<>
445 68 : await_resume()
446 : {
447 68 : if(canceled_)
448 MIS 0 : return {capy::error::canceled};
449 :
450 HIT 68 : auto ec = self_->f_.maybe_fail();
451 50 : if(ec)
452 18 : return {ec};
453 :
454 32 : self_->eof_called_ = true;
455 32 : return {};
456 : }
457 : };
458 68 : return awaitable{this};
459 : }
460 : };
461 :
462 : } // test
463 : } // capy
464 : } // boost
465 :
466 : #endif
|