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_BUFFER_SOURCE_HPP
11 : #define BOOST_CAPY_TEST_BUFFER_SOURCE_HPP
12 :
13 : #include <boost/capy/detail/config.hpp>
14 : #include <boost/capy/buffers.hpp>
15 : #include <boost/capy/buffers/make_buffer.hpp>
16 : #include <coroutine>
17 : #include <boost/capy/error.hpp>
18 : #include <boost/capy/ex/io_env.hpp>
19 : #include <boost/capy/io_result.hpp>
20 : #include <boost/capy/test/fuse.hpp>
21 :
22 : #include <algorithm>
23 : #include <string>
24 : #include <string_view>
25 :
26 : namespace boost {
27 : namespace capy {
28 : namespace test {
29 :
30 : /** A mock buffer source for testing push operations.
31 :
32 : Use this to verify code that transfers data from a buffer source to
33 : a sink without needing real I/O. Call @ref provide to supply data,
34 : then @ref pull to retrieve buffer descriptors. The associated
35 : @ref fuse enables error injection at controlled points.
36 :
37 : This class satisfies the @ref BufferSource concept by providing
38 : a pull interface that fills an array of buffer descriptors and
39 : a consume interface to indicate bytes used.
40 :
41 : @par Thread Safety
42 : Not thread-safe.
43 :
44 : @par Example
45 : @code
46 : fuse f;
47 : buffer_source bs( f );
48 : bs.provide( "Hello, " );
49 : bs.provide( "World!" );
50 :
51 : auto r = f.armed( [&]( fuse& ) -> task<void> {
52 : const_buffer arr[16];
53 : auto [ec, bufs] = co_await bs.pull( arr );
54 : if( ec )
55 : co_return;
56 : // bufs contains buffer descriptors
57 : std::size_t n = buffer_size( bufs );
58 : bs.consume( n );
59 : } );
60 : @endcode
61 :
62 : @see fuse, BufferSource
63 : */
64 : class buffer_source
65 : {
66 : fuse f_;
67 : std::string data_;
68 : std::size_t pos_ = 0;
69 : std::size_t max_pull_size_;
70 :
71 : public:
72 : /** Construct a buffer source.
73 :
74 : @param f The fuse used to inject errors during pulls.
75 :
76 : @param max_pull_size Maximum bytes returned per pull.
77 : Use to simulate chunked delivery.
78 : */
79 302 : explicit buffer_source(
80 : fuse f = {},
81 : std::size_t max_pull_size = std::size_t(-1)) noexcept
82 302 : : f_(std::move(f))
83 302 : , max_pull_size_(max_pull_size)
84 : {
85 302 : }
86 :
87 : /** Append data to be returned by subsequent pulls.
88 :
89 : Multiple calls accumulate data that @ref pull returns.
90 :
91 : @param sv The data to append.
92 : */
93 : void
94 316 : provide(std::string_view sv)
95 : {
96 316 : data_.append(sv);
97 316 : }
98 :
99 : /// Clear all data and reset the read position.
100 : void
101 : clear() noexcept
102 : {
103 : data_.clear();
104 : pos_ = 0;
105 : }
106 :
107 : /// Return the number of bytes available for pulling.
108 : std::size_t
109 : available() const noexcept
110 : {
111 : return data_.size() - pos_;
112 : }
113 :
114 : /** Consume bytes from the source.
115 :
116 : Advances the internal read position by the specified number
117 : of bytes. The next call to @ref pull returns data starting
118 : after the consumed bytes.
119 :
120 : @param n The number of bytes to consume. Must not exceed the
121 : total size of buffers returned by the previous @ref pull.
122 : */
123 : void
124 271 : consume(std::size_t n) noexcept
125 : {
126 271 : pos_ += n;
127 271 : }
128 :
129 : /** Pull buffer data from the source.
130 :
131 : Fills the provided span with buffer descriptors pointing to
132 : internal data starting from the current unconsumed position.
133 : Returns a span of filled buffers. When no data remains,
134 : returns an empty span to signal completion.
135 :
136 : Calling pull multiple times without intervening @ref consume
137 : returns the same data. Use consume to advance past processed
138 : bytes.
139 :
140 : @param dest Span of const_buffer to fill.
141 :
142 : @return An awaitable yielding `(error_code,std::span<const_buffer>)`.
143 :
144 : @see consume, fuse
145 : */
146 : auto
147 552 : pull(std::span<const_buffer> dest)
148 : {
149 : struct awaitable
150 : {
151 : buffer_source* self_;
152 : std::span<const_buffer> dest_;
153 :
154 552 : bool await_ready() const noexcept { return true; }
155 :
156 : // This method is required to satisfy Capy's IoAwaitable concept,
157 : // but is never called because await_ready() returns true.
158 : //
159 : // Capy uses a two-layer awaitable system: the promise's
160 : // await_transform wraps awaitables in a transform_awaiter whose
161 : // standard await_suspend(coroutine_handle) calls this custom
162 : // 2-argument overload, passing the io_env from the coroutine's
163 : // context. For synchronous test awaitables like this one, the
164 : // coroutine never suspends, so this is not invoked. The signature
165 : // exists to allow the same awaitable type to work with both
166 : // synchronous (test) and asynchronous (real I/O) code.
167 0 : void await_suspend(
168 : std::coroutine_handle<>,
169 : io_env const*) const noexcept
170 : {
171 0 : }
172 :
173 : io_result<std::span<const_buffer>>
174 552 : await_resume()
175 : {
176 552 : auto ec = self_->f_.maybe_fail();
177 467 : if(ec)
178 85 : return {ec, {}};
179 :
180 382 : if(self_->pos_ >= self_->data_.size())
181 66 : return {error::eof, {}};
182 :
183 316 : std::size_t avail = self_->data_.size() - self_->pos_;
184 316 : std::size_t to_return = (std::min)(avail, self_->max_pull_size_);
185 :
186 316 : if(dest_.empty())
187 0 : return {{}, {}};
188 :
189 : // Fill a single buffer descriptor
190 316 : dest_[0] = make_buffer(
191 316 : self_->data_.data() + self_->pos_,
192 : to_return);
193 :
194 316 : return {{}, dest_.first(1)};
195 : }
196 : };
197 552 : return awaitable{this, dest};
198 : }
199 : };
200 :
201 : } // test
202 : } // capy
203 : } // boost
204 :
205 : #endif
|