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/capy
9 : //
10 :
11 : #ifndef BOOST_CAPY_TEST_WRITE_STREAM_HPP
12 : #define BOOST_CAPY_TEST_WRITE_STREAM_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 stream for testing write operations.
33 :
34 : Use this to verify code that performs writes without needing
35 : real I/O. Call @ref write_some to write data, then @ref data
36 : to retrieve what was written. The associated @ref fuse enables
37 : error injection at controlled points. An optional
38 : `max_write_size` constructor parameter limits bytes per write
39 : to simulate chunked delivery.
40 :
41 : This class satisfies the @ref WriteStream concept.
42 :
43 : @par Thread Safety
44 : Not thread-safe.
45 :
46 : @par Example
47 : @code
48 : fuse f;
49 : write_stream ws( f );
50 :
51 : auto r = f.armed( [&]( fuse& ) -> task<void> {
52 : auto [ec, n] = co_await ws.write_some(
53 : const_buffer( "Hello", 5 ) );
54 : if( ec )
55 : co_return;
56 : // ws.data() returns "Hello"
57 : } );
58 : @endcode
59 :
60 : @see fuse, WriteStream
61 : */
62 : class write_stream
63 : {
64 : fuse f_;
65 : std::string data_;
66 : std::string expect_;
67 : std::size_t max_write_size_;
68 :
69 : std::error_code
70 HIT 465 : consume_match_() noexcept
71 : {
72 465 : if(data_.empty() || expect_.empty())
73 449 : return {};
74 16 : std::size_t const n = (std::min)(data_.size(), expect_.size());
75 16 : if(std::string_view(data_.data(), n) !=
76 32 : std::string_view(expect_.data(), n))
77 4 : return error::test_failure;
78 12 : data_.erase(0, n);
79 12 : expect_.erase(0, n);
80 12 : return {};
81 : }
82 :
83 : public:
84 : /** Construct a write stream.
85 :
86 : @param f The fuse used to inject errors during writes.
87 :
88 : @param max_write_size Maximum bytes transferred per write.
89 : Use to simulate chunked network delivery.
90 : */
91 503 : explicit write_stream(
92 : fuse f = {},
93 : std::size_t max_write_size = std::size_t(-1)) noexcept
94 503 : : f_(std::move(f))
95 503 : , max_write_size_(max_write_size)
96 : {
97 503 : }
98 :
99 : /// Return the written data as a string view.
100 : std::string_view
101 317 : data() const noexcept
102 : {
103 317 : return data_;
104 : }
105 :
106 : /** Set the expected data for subsequent writes.
107 :
108 : Stores the expected data and immediately tries to match
109 : against any data already written. Matched data is consumed
110 : from both buffers.
111 :
112 : @param sv The expected data.
113 :
114 : @return An error if existing data does not match.
115 : */
116 : std::error_code
117 30 : expect(std::string_view sv)
118 : {
119 30 : expect_.assign(sv);
120 30 : return consume_match_();
121 : }
122 :
123 : /// Return the number of bytes written.
124 : std::size_t
125 7 : size() const noexcept
126 : {
127 7 : return data_.size();
128 : }
129 :
130 : /** Asynchronously write data to the stream.
131 :
132 : Transfers up to `buffer_size( buffers )` bytes from the provided
133 : const buffer sequence to the internal buffer. Before every write,
134 : the attached @ref fuse is consulted to possibly inject an error
135 : for testing fault scenarios. The returned `std::size_t` is the
136 : number of bytes transferred.
137 :
138 : @par Effects
139 : On success, appends the written bytes to the internal buffer.
140 : If an error is injected by the fuse, the internal buffer remains
141 : unchanged.
142 :
143 : @par Exception Safety
144 : Injected I/O conditions are reported via the `error_code`
145 : component of the result. Throws `std::system_error` only when
146 : the attached @ref fuse is in exception mode and reaches its
147 : failure point; no-throw otherwise.
148 :
149 : @par Cancellation
150 : If the environment's stop token has been requested, the write
151 : completes immediately with `error::canceled` and transfers no
152 : data. An empty buffer sequence is a no-op that completes
153 : successfully regardless of the stop token.
154 :
155 : @param buffers The const buffer sequence containing data to write.
156 :
157 : @return An awaitable that await-returns `(error_code,std::size_t)`.
158 :
159 : @throws std::system_error When the attached @ref fuse is in
160 : exception mode and reaches its failure point.
161 :
162 : @see fuse
163 : */
164 : template<ConstBufferSequence CB>
165 : auto
166 656 : write_some(CB buffers)
167 : {
168 : struct awaitable
169 : {
170 : write_stream* self_;
171 : CB buffers_;
172 : bool canceled_ = false;
173 :
174 656 : bool await_ready() const noexcept { return false; }
175 :
176 : // The operation completes synchronously, but await_suspend is
177 : // the only place io_env is delivered (the promise's
178 : // transform_awaiter forwards it here). Returning false means
179 : // the coroutine does not actually suspend; it resumes
180 : // immediately, having observed the stop token. See io_env,
181 : // IoAwaitable.
182 : bool
183 656 : await_suspend(
184 : std::coroutine_handle<>,
185 : io_env const* env) noexcept
186 : {
187 656 : canceled_ = env->stop_token.stop_requested();
188 656 : return false;
189 : }
190 :
191 : io_result<std::size_t>
192 656 : await_resume()
193 : {
194 656 : if(buffer_empty(buffers_))
195 2 : return {{}, 0};
196 :
197 654 : if(canceled_)
198 1 : return {error::canceled, 0};
199 :
200 653 : auto ec = self_->f_.maybe_fail();
201 544 : if(ec)
202 109 : return {ec, 0};
203 :
204 435 : std::size_t n = buffer_size(buffers_);
205 435 : n = (std::min)(n, self_->max_write_size_);
206 :
207 435 : std::size_t const old_size = self_->data_.size();
208 435 : self_->data_.resize(old_size + n);
209 435 : buffer_copy(make_buffer(
210 435 : self_->data_.data() + old_size, n), buffers_, n);
211 :
212 435 : ec = self_->consume_match_();
213 435 : if(ec)
214 : {
215 2 : self_->data_.resize(old_size);
216 2 : return {ec, 0};
217 : }
218 :
219 433 : return {{}, n};
220 : }
221 : };
222 656 : return awaitable{this, buffers};
223 : }
224 : };
225 :
226 : } // test
227 : } // capy
228 : } // boost
229 :
230 : #endif
|