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 : /** Captures bytes passed to `write_some`, retrievable afterward through `data`.
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 : @par !example example
48 :
49 :
50 : @see fuse, WriteStream
51 : */
52 : class write_stream
53 : {
54 : fuse f_;
55 : std::string data_;
56 : std::string expect_;
57 : std::size_t max_write_size_;
58 :
59 : std::error_code
60 HIT 364 : consume_match_() noexcept
61 : {
62 364 : if(data_.empty() || expect_.empty())
63 347 : return {};
64 17 : std::size_t const n = (std::min)(data_.size(), expect_.size());
65 17 : if(std::string_view(data_.data(), n) !=
66 34 : std::string_view(expect_.data(), n))
67 4 : return error::test_failure;
68 13 : data_.erase(0, n);
69 13 : expect_.erase(0, n);
70 13 : return {};
71 : }
72 :
73 : public:
74 : /** Construct a write stream.
75 :
76 : @param f The fuse used to inject errors during writes.
77 :
78 : @param max_write_size Maximum bytes transferred per write.
79 : Use to simulate chunked network delivery.
80 : */
81 417 : explicit write_stream(
82 : fuse f = {},
83 : std::size_t max_write_size = std::size_t(-1)) noexcept
84 417 : : f_(std::move(f))
85 417 : , max_write_size_(max_write_size)
86 : {
87 417 : }
88 :
89 : /** Return the written data as a string view.
90 :
91 : @return A view of bytes written but not yet matched by @ref expect.
92 : */
93 : std::string_view
94 314 : data() const noexcept
95 : {
96 314 : return data_;
97 : }
98 :
99 : /** Set the expected data for subsequent writes.
100 :
101 : Stores the expected data and immediately tries to match
102 : against any data already written. Matched data is consumed
103 : from both buffers.
104 :
105 : @param sv The expected data.
106 :
107 : @return An error if existing data does not match.
108 : */
109 : std::error_code
110 31 : expect(std::string_view sv)
111 : {
112 31 : expect_.assign(sv);
113 31 : return consume_match_();
114 : }
115 :
116 : /** Return the number of bytes written.
117 :
118 : @return The number of bytes written but not yet matched by @ref expect.
119 : */
120 : std::size_t
121 5 : size() const noexcept
122 : {
123 5 : return data_.size();
124 : }
125 :
126 : /** Asynchronously write data to the stream.
127 :
128 : Transfers up to `buffer_size( buffers )` bytes from the provided
129 : const buffer sequence to the internal buffer. Before every write,
130 : the attached @ref fuse is consulted to possibly inject an error
131 : for testing fault scenarios. The returned `std::size_t` is the
132 : number of bytes transferred.
133 :
134 : @par Effects
135 : On success, appends the written bytes to the internal buffer.
136 : If an error is injected by the fuse, the internal buffer remains
137 : unchanged.
138 :
139 : @par Exception Safety
140 : Injected I/O conditions are reported via the `error_code`
141 : component of the result. Throws `std::system_error` only when
142 : the attached @ref fuse is in exception mode and reaches its
143 : failure point; no-throw otherwise.
144 :
145 : @par Cancellation
146 : If the environment's stop token is requested, the write
147 : completes immediately with `error::canceled` and transfers no
148 : data. An empty buffer sequence is a no-op that completes
149 : successfully regardless of the stop token.
150 :
151 : @param buffers The const buffer sequence containing data to write.
152 :
153 : @return An awaitable that await-returns `(error_code,std::size_t)`.
154 :
155 : @throws std::system_error When the attached @ref fuse is in
156 : exception mode and reaches its failure point.
157 :
158 : @see fuse
159 : */
160 : template<ConstBufferSequence CB>
161 : auto
162 531 : write_some(CB buffers)
163 : {
164 : struct awaitable
165 : {
166 : write_stream* self_;
167 : CB buffers_;
168 : bool canceled_ = false;
169 :
170 531 : bool await_ready() const noexcept { return false; }
171 :
172 : // The operation completes synchronously, but await_suspend is
173 : // the only place io_env is delivered (the promise's
174 : // transform_awaiter forwards it here). Returning false means
175 : // the coroutine does not actually suspend; it resumes
176 : // immediately, having observed the stop token. See io_env,
177 : // IoAwaitable.
178 : bool
179 531 : await_suspend(
180 : std::coroutine_handle<>,
181 : io_env const* env) noexcept
182 : {
183 531 : canceled_ = env->stop_token.stop_requested();
184 531 : return false;
185 : }
186 :
187 : [[nodiscard]] io_result<std::size_t>
188 531 : await_resume()
189 : {
190 531 : if(buffer_empty(buffers_))
191 3 : return {std::error_code(), 0};
192 :
193 528 : if(canceled_)
194 1 : return {error::canceled, 0};
195 :
196 527 : auto ec = self_->f_.maybe_fail();
197 430 : if(ec)
198 97 : return {ec, 0};
199 :
200 333 : std::size_t n = buffer_size(buffers_);
201 333 : n = (std::min)(n, self_->max_write_size_);
202 :
203 333 : std::size_t const old_size = self_->data_.size();
204 333 : self_->data_.resize(old_size + n);
205 333 : buffer_copy(make_buffer(
206 333 : self_->data_.data() + old_size, n), buffers_, n);
207 :
208 333 : ec = self_->consume_match_();
209 333 : if(ec)
210 : {
211 2 : self_->data_.resize(old_size);
212 2 : return {ec, 0};
213 : }
214 :
215 331 : return {std::error_code(), n};
216 : }
217 : };
218 531 : return awaitable{this, buffers};
219 : }
220 : };
221 :
222 : } // test
223 : } // capy
224 : } // boost
225 :
226 : #endif
|