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