TLA 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 <coroutine>
18 : #include <boost/capy/ex/io_env.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 : 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
74 HIT 236 : consume_match_() noexcept
75 : {
76 236 : if(data_.empty() || expect_.empty())
77 228 : return {};
78 8 : std::size_t const n = (std::min)(data_.size(), expect_.size());
79 8 : if(std::string_view(data_.data(), n) !=
80 16 : std::string_view(expect_.data(), n))
81 4 : return error::test_failure;
82 4 : data_.erase(0, n);
83 4 : expect_.erase(0, n);
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 : */
95 412 : explicit write_sink(
96 : fuse f = {},
97 : std::size_t max_write_size = std::size_t(-1)) noexcept
98 412 : : f_(std::move(f))
99 412 : , max_write_size_(max_write_size)
100 : {
101 412 : }
102 :
103 : /// Return the written data as a string view.
104 : std::string_view
105 100 : data() const noexcept
106 : {
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
121 16 : expect(std::string_view sv)
122 : {
123 16 : expect_.assign(sv);
124 16 : return consume_match_();
125 : }
126 :
127 : /// Return the number of bytes written.
128 : std::size_t
129 6 : size() const noexcept
130 : {
131 6 : return data_.size();
132 : }
133 :
134 : /// Return whether write_eof has been called.
135 : bool
136 64 : eof_called() const noexcept
137 : {
138 64 : return eof_called_;
139 : }
140 :
141 : /// Clear all data and reset state.
142 : void
143 4 : clear() noexcept
144 : {
145 4 : data_.clear();
146 4 : expect_.clear();
147 4 : eof_called_ = false;
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 : @see fuse
161 : */
162 : template<ConstBufferSequence CB>
163 : auto
164 76 : write_some(CB buffers)
165 : {
166 : struct awaitable
167 : {
168 : write_sink* self_;
169 : CB buffers_;
170 :
171 76 : bool await_ready() const noexcept { return true; }
172 :
173 MIS 0 : void await_suspend(
174 : std::coroutine_handle<>,
175 : io_env const*) const noexcept
176 : {
177 0 : }
178 :
179 : io_result<std::size_t>
180 HIT 76 : await_resume()
181 : {
182 76 : if(buffer_empty(buffers_))
183 2 : return {{}, 0};
184 :
185 74 : auto ec = self_->f_.maybe_fail();
186 53 : if(ec)
187 21 : return {ec, 0};
188 :
189 32 : std::size_t n = buffer_size(buffers_);
190 32 : n = (std::min)(n, self_->max_write_size_);
191 :
192 32 : std::size_t const old_size = self_->data_.size();
193 32 : self_->data_.resize(old_size + n);
194 32 : buffer_copy(make_buffer(
195 32 : self_->data_.data() + old_size, n), buffers_, n);
196 :
197 32 : ec = self_->consume_match_();
198 32 : if(ec)
199 : {
200 MIS 0 : self_->data_.resize(old_size);
201 0 : return {ec, 0};
202 : }
203 :
204 HIT 32 : return {{}, n};
205 : }
206 : };
207 76 : return awaitable{this, buffers};
208 : }
209 :
210 : /** Asynchronously write data to the sink.
211 :
212 : Transfers all bytes from the provided const buffer sequence
213 : to the internal buffer. Unlike @ref write_some, this ignores
214 : `max_write_size` and writes all available data, matching the
215 : @ref WriteSink semantic contract.
216 :
217 : @par Exception Safety
218 : Injected I/O conditions are reported via the `error_code`
219 : component of the result. Throws `std::system_error` only when
220 : the attached @ref fuse is in exception mode and reaches its
221 : failure point; no-throw otherwise.
222 :
223 : @param buffers The const buffer sequence containing data to write.
224 :
225 : @return An awaitable that await-returns `(error_code,std::size_t)`.
226 :
227 : @throws std::system_error When the attached @ref fuse is in
228 : exception mode and reaches its failure point.
229 :
230 : @see fuse
231 : */
232 : template<ConstBufferSequence CB>
233 : auto
234 302 : write(CB buffers)
235 : {
236 : struct awaitable
237 : {
238 : write_sink* self_;
239 : CB buffers_;
240 :
241 302 : bool await_ready() const noexcept { return true; }
242 :
243 MIS 0 : void await_suspend(
244 : std::coroutine_handle<>,
245 : io_env const*) const noexcept
246 : {
247 0 : }
248 :
249 : io_result<std::size_t>
250 HIT 302 : await_resume()
251 : {
252 302 : auto ec = self_->f_.maybe_fail();
253 241 : if(ec)
254 61 : return {ec, 0};
255 :
256 180 : std::size_t n = buffer_size(buffers_);
257 180 : if(n == 0)
258 2 : return {{}, 0};
259 :
260 178 : std::size_t const old_size = self_->data_.size();
261 178 : self_->data_.resize(old_size + n);
262 178 : buffer_copy(make_buffer(
263 178 : self_->data_.data() + old_size, n), buffers_);
264 :
265 178 : ec = self_->consume_match_();
266 178 : if(ec)
267 2 : return {ec, n};
268 :
269 176 : return {{}, n};
270 : }
271 : };
272 302 : return awaitable{this, buffers};
273 : }
274 :
275 : /** Atomically write data and signal end-of-stream.
276 :
277 : Transfers all bytes from the provided const buffer sequence to
278 : the internal buffer and signals end-of-stream. Before the write,
279 : the attached @ref fuse is consulted to possibly inject an error
280 : for testing fault scenarios.
281 :
282 : @par Effects
283 : On success, appends the written bytes to the internal buffer
284 : and marks the sink as finalized.
285 : If an error is injected by the fuse, the internal buffer remains
286 : unchanged.
287 :
288 : @par Exception Safety
289 : Injected I/O conditions are reported via the `error_code`
290 : component of the result. Throws `std::system_error` only when
291 : the attached @ref fuse is in exception mode and reaches its
292 : failure point; no-throw otherwise.
293 :
294 : @param buffers The const buffer sequence containing data to write.
295 :
296 : @return An awaitable that await-returns `(error_code,std::size_t)`.
297 :
298 : @throws std::system_error When the attached @ref fuse is in
299 : exception mode and reaches its failure point.
300 :
301 : @see fuse
302 : */
303 : template<ConstBufferSequence CB>
304 : auto
305 34 : write_eof(CB buffers)
306 : {
307 : struct awaitable
308 : {
309 : write_sink* self_;
310 : CB buffers_;
311 :
312 34 : bool await_ready() const noexcept { return true; }
313 :
314 MIS 0 : void await_suspend(
315 : std::coroutine_handle<>,
316 : io_env const*) const noexcept
317 : {
318 0 : }
319 :
320 : io_result<std::size_t>
321 HIT 34 : await_resume()
322 : {
323 34 : auto ec = self_->f_.maybe_fail();
324 23 : if(ec)
325 11 : return {ec, 0};
326 :
327 12 : std::size_t n = buffer_size(buffers_);
328 12 : if(n > 0)
329 : {
330 10 : std::size_t const old_size = self_->data_.size();
331 10 : self_->data_.resize(old_size + n);
332 10 : buffer_copy(make_buffer(
333 10 : self_->data_.data() + old_size, n), buffers_);
334 :
335 10 : ec = self_->consume_match_();
336 10 : if(ec)
337 MIS 0 : return {ec, n};
338 : }
339 :
340 HIT 12 : self_->eof_called_ = true;
341 :
342 12 : return {{}, n};
343 : }
344 : };
345 34 : return awaitable{this, buffers};
346 : }
347 :
348 : /** Signal end-of-stream.
349 :
350 : Marks the sink as finalized, indicating no more data will be
351 : written. Before signaling, the attached @ref fuse is consulted
352 : to possibly inject an error for testing fault scenarios.
353 :
354 : @par Effects
355 : On success, marks the sink as finalized.
356 : If an error is injected by the fuse, the state remains unchanged.
357 :
358 : @par Exception Safety
359 : Injected I/O conditions are reported via the `error_code`
360 : component of the result. Throws `std::system_error` only when
361 : the attached @ref fuse is in exception mode and reaches its
362 : failure point; no-throw otherwise.
363 :
364 : @return An awaitable that await-returns `(error_code)`.
365 :
366 : @throws std::system_error When the attached @ref fuse is in
367 : exception mode and reaches its failure point.
368 :
369 : @see fuse
370 : */
371 : auto
372 82 : write_eof()
373 : {
374 : struct awaitable
375 : {
376 : write_sink* self_;
377 :
378 82 : bool await_ready() const noexcept { return true; }
379 :
380 : // This method is required to satisfy Capy's IoAwaitable concept,
381 : // but is never called because await_ready() returns true.
382 : // See the comment on write(CB buffers) for a detailed explanation.
383 MIS 0 : void await_suspend(
384 : std::coroutine_handle<>,
385 : io_env const*) const noexcept
386 : {
387 0 : }
388 :
389 : io_result<>
390 HIT 82 : await_resume()
391 : {
392 82 : auto ec = self_->f_.maybe_fail();
393 60 : if(ec)
394 22 : return {ec};
395 :
396 38 : self_->eof_called_ = true;
397 38 : return {};
398 : }
399 : };
400 82 : return awaitable{this};
401 : }
402 : };
403 :
404 : } // test
405 : } // capy
406 : } // boost
407 :
408 : #endif
|