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_READ_STREAM_HPP
11 : #define BOOST_CAPY_TEST_READ_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/cond.hpp>
18 : #include <coroutine>
19 : #include <boost/capy/ex/io_env.hpp>
20 : #include <boost/capy/io_result.hpp>
21 : #include <boost/capy/test/fuse.hpp>
22 :
23 : #include <string>
24 : #include <string_view>
25 :
26 : namespace boost {
27 : namespace capy {
28 : namespace test {
29 :
30 : /** A mock stream for testing read operations.
31 :
32 : Use this to verify code that performs reads without needing
33 : real I/O. Call @ref provide to supply data, then @ref read_some
34 : to consume it. The associated @ref fuse enables error injection
35 : at controlled points. An optional `max_read_size` constructor
36 : parameter limits bytes per read to simulate chunked delivery.
37 :
38 : This class satisfies the @ref ReadStream concept.
39 :
40 : @par Thread Safety
41 : Not thread-safe.
42 :
43 : @par Example
44 : @code
45 : fuse f;
46 : read_stream rs( f );
47 : rs.provide( "Hello, " );
48 : rs.provide( "World!" );
49 :
50 : auto r = f.armed( [&]( fuse& ) -> task<void> {
51 : char buf[32];
52 : auto [ec, n] = co_await rs.read_some(
53 : mutable_buffer( buf, sizeof( buf ) ) );
54 : if( ec )
55 : co_return;
56 : // buf contains "Hello, World!"
57 : } );
58 : @endcode
59 :
60 : @see fuse, ReadStream
61 : */
62 : class read_stream
63 : {
64 : fuse f_;
65 : std::string data_;
66 : std::size_t pos_ = 0;
67 : std::size_t max_read_size_;
68 :
69 : public:
70 : /** Construct a read stream.
71 :
72 : @param f The fuse used to inject errors during reads.
73 :
74 : @param max_read_size Maximum bytes returned per read.
75 : Use to simulate chunked network delivery.
76 : */
77 1328 : explicit read_stream(
78 : fuse f = {},
79 : std::size_t max_read_size = std::size_t(-1)) noexcept
80 1328 : : f_(std::move(f))
81 1328 : , max_read_size_(max_read_size)
82 : {
83 1328 : }
84 :
85 : /** Append data to be returned by subsequent reads.
86 :
87 : Multiple calls accumulate data that @ref read_some returns.
88 :
89 : @param sv The data to append.
90 : */
91 : void
92 1300 : provide(std::string_view sv)
93 : {
94 1300 : data_.append(sv);
95 1300 : }
96 :
97 : /// Clear all data and reset the read position.
98 : void
99 : clear() noexcept
100 : {
101 : data_.clear();
102 : pos_ = 0;
103 : }
104 :
105 : /// Return the number of bytes available for reading.
106 : std::size_t
107 4 : available() const noexcept
108 : {
109 4 : return data_.size() - pos_;
110 : }
111 :
112 : /** Asynchronously read data from the stream.
113 :
114 : Transfers up to `buffer_size( buffers )` bytes from the internal
115 : buffer to the provided mutable buffer sequence. If no data remains,
116 : returns `error::eof`. Before every read, the attached @ref fuse is
117 : consulted to possibly inject an error for testing fault scenarios.
118 : The returned `std::size_t` is the number of bytes transferred.
119 :
120 : @par Effects
121 : On success, advances the internal read position by the number of
122 : bytes copied. If an error is injected by the fuse, the read position
123 : remains unchanged.
124 :
125 : @par Exception Safety
126 : No-throw guarantee.
127 :
128 : @param buffers The mutable buffer sequence to receive data.
129 :
130 : @return An awaitable yielding `(error_code,std::size_t)`.
131 :
132 : @see fuse
133 : */
134 : template<MutableBufferSequence MB>
135 : auto
136 1589 : read_some(MB buffers)
137 : {
138 : struct awaitable
139 : {
140 : read_stream* self_;
141 : MB buffers_;
142 :
143 1589 : bool await_ready() const noexcept { return true; }
144 :
145 : // This method is required to satisfy Capy's IoAwaitable concept,
146 : // but is never called because await_ready() returns true.
147 : //
148 : // Capy uses a two-layer awaitable system: the promise's
149 : // await_transform wraps awaitables in a transform_awaiter whose
150 : // standard await_suspend(coroutine_handle) calls this custom
151 : // 2-argument overload, passing the io_env from the coroutine's
152 : // context. For synchronous test awaitables like this one, the
153 : // coroutine never suspends, so this is not invoked. The signature
154 : // exists to allow the same awaitable type to work with both
155 : // synchronous (test) and asynchronous (real I/O) code.
156 0 : void await_suspend(
157 : std::coroutine_handle<>,
158 : io_env const*) const noexcept
159 : {
160 0 : }
161 :
162 : io_result<std::size_t>
163 1589 : await_resume()
164 : {
165 : // Empty buffer is a no-op regardless of
166 : // stream state or fuse.
167 1589 : if(buffer_empty(buffers_))
168 3 : return {{}, 0};
169 :
170 1586 : auto ec = self_->f_.maybe_fail();
171 1386 : if(ec)
172 200 : return {ec, 0};
173 :
174 1186 : if(self_->pos_ >= self_->data_.size())
175 85 : return {error::eof, 0};
176 :
177 1101 : std::size_t avail = self_->data_.size() - self_->pos_;
178 1101 : if(avail > self_->max_read_size_)
179 236 : avail = self_->max_read_size_;
180 1101 : auto src = make_buffer(self_->data_.data() + self_->pos_, avail);
181 1101 : std::size_t const n = buffer_copy(buffers_, src);
182 1101 : self_->pos_ += n;
183 1101 : return {{}, n};
184 : }
185 : };
186 1589 : return awaitable{this, buffers};
187 : }
188 : };
189 :
190 : } // test
191 : } // capy
192 : } // boost
193 :
194 : #endif
|