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_STREAM_HPP
11 : #define BOOST_CAPY_TEST_WRITE_STREAM_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 <string>
25 : #include <string_view>
26 :
27 : namespace boost {
28 : namespace capy {
29 : namespace test {
30 :
31 : /** A mock stream for testing write operations.
32 :
33 : Use this to verify code that performs writes without needing
34 : real I/O. Call @ref write_some to write data, then @ref data
35 : to retrieve what was written. The associated @ref fuse enables
36 : error injection at controlled points. An optional
37 : `max_write_size` constructor parameter limits bytes per write
38 : to simulate chunked delivery.
39 :
40 : This class satisfies the @ref WriteStream concept.
41 :
42 : @par Thread Safety
43 : Not thread-safe.
44 :
45 : @par Example
46 : @code
47 : fuse f;
48 : write_stream ws( f );
49 :
50 : auto r = f.armed( [&]( fuse& ) -> task<void> {
51 : auto [ec, n] = co_await ws.write_some(
52 : const_buffer( "Hello", 5 ) );
53 : if( ec )
54 : co_return;
55 : // ws.data() returns "Hello"
56 : } );
57 : @endcode
58 :
59 : @see fuse, WriteStream
60 : */
61 : class write_stream
62 : {
63 : fuse f_;
64 : std::string data_;
65 : std::string expect_;
66 : std::size_t max_write_size_;
67 :
68 : std::error_code
69 879 : consume_match_() noexcept
70 : {
71 879 : if(data_.empty() || expect_.empty())
72 879 : return {};
73 0 : std::size_t const n = (std::min)(data_.size(), expect_.size());
74 0 : if(std::string_view(data_.data(), n) !=
75 0 : std::string_view(expect_.data(), n))
76 0 : return error::test_failure;
77 0 : data_.erase(0, n);
78 0 : expect_.erase(0, n);
79 0 : return {};
80 : }
81 :
82 : public:
83 : /** Construct a write stream.
84 :
85 : @param f The fuse used to inject errors during writes.
86 :
87 : @param max_write_size Maximum bytes transferred per write.
88 : Use to simulate chunked network delivery.
89 : */
90 1097 : explicit write_stream(
91 : fuse f = {},
92 : std::size_t max_write_size = std::size_t(-1)) noexcept
93 1097 : : f_(std::move(f))
94 1097 : , max_write_size_(max_write_size)
95 : {
96 1097 : }
97 :
98 : /// Return the written data as a string view.
99 : std::string_view
100 922 : data() const noexcept
101 : {
102 922 : return data_;
103 : }
104 :
105 : /** Set the expected data for subsequent writes.
106 :
107 : Stores the expected data and immediately tries to match
108 : against any data already written. Matched data is consumed
109 : from both buffers.
110 :
111 : @param sv The expected data.
112 :
113 : @return An error if existing data does not match.
114 : */
115 : std::error_code
116 : expect(std::string_view sv)
117 : {
118 : expect_.assign(sv);
119 : return consume_match_();
120 : }
121 :
122 : /// Return the number of bytes written.
123 : std::size_t
124 2 : size() const noexcept
125 : {
126 2 : return data_.size();
127 : }
128 :
129 : /** Asynchronously write data to the stream.
130 :
131 : Transfers up to `buffer_size( buffers )` bytes from the provided
132 : const buffer sequence to the internal buffer. Before every write,
133 : the attached @ref fuse is consulted to possibly inject an error
134 : for testing fault scenarios. The returned `std::size_t` is the
135 : number of bytes transferred.
136 :
137 : @par Effects
138 : On success, appends the written bytes to the internal buffer.
139 : If an error is injected by the fuse, the internal buffer remains
140 : unchanged.
141 :
142 : @par Exception Safety
143 : No-throw guarantee.
144 :
145 : @param buffers The const buffer sequence containing data to write.
146 :
147 : @return An awaitable yielding `(error_code,std::size_t)`.
148 :
149 : @see fuse
150 : */
151 : template<ConstBufferSequence CB>
152 : auto
153 1079 : write_some(CB buffers)
154 : {
155 : struct awaitable
156 : {
157 : write_stream* self_;
158 : CB buffers_;
159 :
160 1079 : bool await_ready() const noexcept { return true; }
161 :
162 : // This method is required to satisfy Capy's IoAwaitable concept,
163 : // but is never called because await_ready() returns true.
164 : //
165 : // Capy uses a two-layer awaitable system: the promise's
166 : // await_transform wraps awaitables in a transform_awaiter whose
167 : // standard await_suspend(coroutine_handle) calls this custom
168 : // 2-argument overload, passing the io_env from the coroutine's
169 : // context. For synchronous test awaitables like this one, the
170 : // coroutine never suspends, so this is not invoked. The signature
171 : // exists to allow the same awaitable type to work with both
172 : // synchronous (test) and asynchronous (real I/O) code.
173 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 1079 : await_resume()
181 : {
182 1079 : if(buffer_empty(buffers_))
183 0 : return {{}, 0};
184 :
185 1079 : auto ec = self_->f_.maybe_fail();
186 979 : if(ec)
187 100 : return {ec, 0};
188 :
189 879 : std::size_t n = buffer_size(buffers_);
190 879 : n = (std::min)(n, self_->max_write_size_);
191 :
192 879 : std::size_t const old_size = self_->data_.size();
193 879 : self_->data_.resize(old_size + n);
194 879 : buffer_copy(make_buffer(
195 879 : self_->data_.data() + old_size, n), buffers_, n);
196 :
197 879 : ec = self_->consume_match_();
198 879 : if(ec)
199 : {
200 0 : self_->data_.resize(old_size);
201 0 : return {ec, 0};
202 : }
203 :
204 879 : return {{}, n};
205 : }
206 : };
207 1079 : return awaitable{this, buffers};
208 : }
209 : };
210 :
211 : } // test
212 : } // capy
213 : } // boost
214 :
215 : #endif
|