1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
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)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/capy
7  
// Official repository: https://github.com/cppalliance/capy
8  
//
8  
//
9  

9  

10  
#ifndef BOOST_CAPY_TEST_WRITE_SINK_HPP
10  
#ifndef BOOST_CAPY_TEST_WRITE_SINK_HPP
11  
#define BOOST_CAPY_TEST_WRITE_SINK_HPP
11  
#define BOOST_CAPY_TEST_WRITE_SINK_HPP
12  

12  

13  
#include <boost/capy/detail/config.hpp>
13  
#include <boost/capy/detail/config.hpp>
14  
#include <boost/capy/buffers.hpp>
14  
#include <boost/capy/buffers.hpp>
15  
#include <boost/capy/buffers/buffer_copy.hpp>
15  
#include <boost/capy/buffers/buffer_copy.hpp>
16  
#include <boost/capy/buffers/make_buffer.hpp>
16  
#include <boost/capy/buffers/make_buffer.hpp>
17  
#include <boost/capy/coro.hpp>
17  
#include <boost/capy/coro.hpp>
18  
#include <boost/capy/ex/executor_ref.hpp>
18  
#include <boost/capy/ex/executor_ref.hpp>
19  
#include <boost/capy/io_result.hpp>
19  
#include <boost/capy/io_result.hpp>
20  
#include <boost/capy/error.hpp>
20  
#include <boost/capy/error.hpp>
21  
#include <boost/capy/test/fuse.hpp>
21  
#include <boost/capy/test/fuse.hpp>
22  

22  

23  
#include <algorithm>
23  
#include <algorithm>
24  
#include <stop_token>
24  
#include <stop_token>
25  
#include <string>
25  
#include <string>
26  
#include <string_view>
26  
#include <string_view>
27  

27  

28  
namespace boost {
28  
namespace boost {
29  
namespace capy {
29  
namespace capy {
30  
namespace test {
30  
namespace test {
31  

31  

32  
/** A mock sink for testing write operations.
32  
/** A mock sink for testing write operations.
33  

33  

34  
    Use this to verify code that performs complete writes without needing
34  
    Use this to verify code that performs complete writes without needing
35  
    real I/O. Call @ref write to write data, then @ref data to retrieve
35  
    real I/O. Call @ref write to write data, then @ref data to retrieve
36  
    what was written. The associated @ref fuse enables error injection
36  
    what was written. The associated @ref fuse enables error injection
37  
    at controlled points.
37  
    at controlled points.
38  

38  

39  
    Unlike @ref write_stream which provides partial writes via `write_some`,
39  
    Unlike @ref write_stream which provides partial writes via `write_some`,
40  
    this class satisfies the @ref WriteSink concept by providing complete
40  
    this class satisfies the @ref WriteSink concept by providing complete
41  
    writes and EOF signaling.
41  
    writes and EOF signaling.
42  

42  

43  
    @par Thread Safety
43  
    @par Thread Safety
44  
    Not thread-safe.
44  
    Not thread-safe.
45  

45  

46  
    @par Example
46  
    @par Example
47  
    @code
47  
    @code
48  
    fuse f;
48  
    fuse f;
49  
    write_sink ws( f );
49  
    write_sink ws( f );
50  

50  

51  
    auto r = f.armed( [&]( fuse& ) -> task<void> {
51  
    auto r = f.armed( [&]( fuse& ) -> task<void> {
52  
        auto [ec, n] = co_await ws.write(
52  
        auto [ec, n] = co_await ws.write(
53  
            const_buffer( "Hello", 5 ) );
53  
            const_buffer( "Hello", 5 ) );
54  
        if( ec )
54  
        if( ec )
55  
            co_return;
55  
            co_return;
56  
        auto [ec2] = co_await ws.write_eof();
56  
        auto [ec2] = co_await ws.write_eof();
57  
        if( ec2 )
57  
        if( ec2 )
58  
            co_return;
58  
            co_return;
59  
        // ws.data() returns "Hello"
59  
        // ws.data() returns "Hello"
60  
    } );
60  
    } );
61  
    @endcode
61  
    @endcode
62  

62  

63  
    @see fuse, WriteSink
63  
    @see fuse, WriteSink
64  
*/
64  
*/
65  
class write_sink
65  
class write_sink
66  
{
66  
{
67  
    fuse* f_;
67  
    fuse* f_;
68  
    std::string data_;
68  
    std::string data_;
69  
    std::string expect_;
69  
    std::string expect_;
70  
    std::size_t max_write_size_;
70  
    std::size_t max_write_size_;
71  
    bool eof_called_ = false;
71  
    bool eof_called_ = false;
72  

72  

73  
    std::error_code
73  
    std::error_code
74  
    consume_match_() noexcept
74  
    consume_match_() noexcept
75  
    {
75  
    {
76  
        if(data_.empty() || expect_.empty())
76  
        if(data_.empty() || expect_.empty())
77  
            return {};
77  
            return {};
78  
        std::size_t const n = (std::min)(data_.size(), expect_.size());
78  
        std::size_t const n = (std::min)(data_.size(), expect_.size());
79  
        if(std::string_view(data_.data(), n) !=
79  
        if(std::string_view(data_.data(), n) !=
80  
            std::string_view(expect_.data(), n))
80  
            std::string_view(expect_.data(), n))
81  
            return error::test_failure;
81  
            return error::test_failure;
82  
        data_.erase(0, n);
82  
        data_.erase(0, n);
83  
        expect_.erase(0, n);
83  
        expect_.erase(0, n);
84  
        return {};
84  
        return {};
85  
    }
85  
    }
86  

86  

87  
public:
87  
public:
88  
    /** Construct a write sink.
88  
    /** Construct a write sink.
89  

89  

90  
        @param f The fuse used to inject errors during writes.
90  
        @param f The fuse used to inject errors during writes.
91  

91  

92  
        @param max_write_size Maximum bytes transferred per write.
92  
        @param max_write_size Maximum bytes transferred per write.
93  
        Use to simulate chunked delivery.
93  
        Use to simulate chunked delivery.
94  
    */
94  
    */
95  
    explicit write_sink(
95  
    explicit write_sink(
96  
        fuse& f,
96  
        fuse& f,
97  
        std::size_t max_write_size = std::size_t(-1)) noexcept
97  
        std::size_t max_write_size = std::size_t(-1)) noexcept
98  
        : f_(&f)
98  
        : f_(&f)
99  
        , max_write_size_(max_write_size)
99  
        , max_write_size_(max_write_size)
100  
    {
100  
    {
101  
    }
101  
    }
102  

102  

103  
    /// Return the written data as a string view.
103  
    /// Return the written data as a string view.
104  
    std::string_view
104  
    std::string_view
105  
    data() const noexcept
105  
    data() const noexcept
106  
    {
106  
    {
107  
        return data_;
107  
        return data_;
108  
    }
108  
    }
109  

109  

110  
    /** Set the expected data for subsequent writes.
110  
    /** Set the expected data for subsequent writes.
111  

111  

112  
        Stores the expected data and immediately tries to match
112  
        Stores the expected data and immediately tries to match
113  
        against any data already written. Matched data is consumed
113  
        against any data already written. Matched data is consumed
114  
        from both buffers.
114  
        from both buffers.
115  

115  

116  
        @param sv The expected data.
116  
        @param sv The expected data.
117  

117  

118  
        @return An error if existing data does not match.
118  
        @return An error if existing data does not match.
119  
    */
119  
    */
120  
    std::error_code
120  
    std::error_code
121  
    expect(std::string_view sv)
121  
    expect(std::string_view sv)
122  
    {
122  
    {
123  
        expect_.assign(sv);
123  
        expect_.assign(sv);
124  
        return consume_match_();
124  
        return consume_match_();
125  
    }
125  
    }
126  

126  

127  
    /// Return the number of bytes written.
127  
    /// Return the number of bytes written.
128  
    std::size_t
128  
    std::size_t
129  
    size() const noexcept
129  
    size() const noexcept
130  
    {
130  
    {
131  
        return data_.size();
131  
        return data_.size();
132  
    }
132  
    }
133  

133  

134  
    /// Return whether write_eof has been called.
134  
    /// Return whether write_eof has been called.
135  
    bool
135  
    bool
136  
    eof_called() const noexcept
136  
    eof_called() const noexcept
137  
    {
137  
    {
138  
        return eof_called_;
138  
        return eof_called_;
139  
    }
139  
    }
140  

140  

141  
    /// Clear all data and reset state.
141  
    /// Clear all data and reset state.
142  
    void
142  
    void
143  
    clear() noexcept
143  
    clear() noexcept
144  
    {
144  
    {
145  
        data_.clear();
145  
        data_.clear();
146  
        expect_.clear();
146  
        expect_.clear();
147  
        eof_called_ = false;
147  
        eof_called_ = false;
148  
    }
148  
    }
149  

149  

150  
    /** Asynchronously write data to the sink.
150  
    /** Asynchronously write data to the sink.
151  

151  

152  
        Transfers all bytes from the provided const buffer sequence to
152  
        Transfers all bytes from the provided const buffer sequence to
153  
        the internal buffer. Before every write, the attached @ref fuse
153  
        the internal buffer. Before every write, the attached @ref fuse
154  
        is consulted to possibly inject an error for testing fault
154  
        is consulted to possibly inject an error for testing fault
155  
        scenarios. The returned `std::size_t` is the number of bytes
155  
        scenarios. The returned `std::size_t` is the number of bytes
156  
        transferred.
156  
        transferred.
157  

157  

158  
        @par Effects
158  
        @par Effects
159  
        On success, appends the written bytes to the internal buffer.
159  
        On success, appends the written bytes to the internal buffer.
160  
        If an error is injected by the fuse, the internal buffer remains
160  
        If an error is injected by the fuse, the internal buffer remains
161  
        unchanged.
161  
        unchanged.
162  

162  

163  
        @par Exception Safety
163  
        @par Exception Safety
164  
        No-throw guarantee.
164  
        No-throw guarantee.
165  

165  

166  
        @param buffers The const buffer sequence containing data to write.
166  
        @param buffers The const buffer sequence containing data to write.
167  

167  

168  
        @return An awaitable yielding `(error_code,std::size_t)`.
168  
        @return An awaitable yielding `(error_code,std::size_t)`.
169  

169  

170  
        @see fuse
170  
        @see fuse
171  
    */
171  
    */
172  
    template<ConstBufferSequence CB>
172  
    template<ConstBufferSequence CB>
173  
    auto
173  
    auto
174  
    write(CB buffers)
174  
    write(CB buffers)
175  
    {
175  
    {
176  
        struct awaitable
176  
        struct awaitable
177  
        {
177  
        {
178  
            write_sink* self_;
178  
            write_sink* self_;
179  
            CB buffers_;
179  
            CB buffers_;
180  

180  

181  
            bool await_ready() const noexcept { return true; }
181  
            bool await_ready() const noexcept { return true; }
182  

182  

 
183 +
            // This method is required to satisfy Capy's IoAwaitable concept,
 
184 +
            // but is never called because await_ready() returns true.
 
185 +
            //
 
186 +
            // Capy uses a two-layer awaitable system: the promise's
 
187 +
            // await_transform wraps awaitables in a transform_awaiter whose
 
188 +
            // standard await_suspend(coroutine_handle) calls this custom
 
189 +
            // 3-argument overload, passing the executor and stop_token from
 
190 +
            // the coroutine's context. For synchronous test awaitables like
 
191 +
            // this one, the coroutine never suspends, so this is not invoked.
 
192 +
            // The signature exists to allow the same awaitable type to work
 
193 +
            // with both synchronous (test) and asynchronous (real I/O) code.
183  
            void await_suspend(
194  
            void await_suspend(
184  
                coro,
195  
                coro,
185  
                executor_ref,
196  
                executor_ref,
186  
                std::stop_token) const noexcept
197  
                std::stop_token) const noexcept
187  
            {
198  
            {
188  
            }
199  
            }
189  

200  

190  
            io_result<std::size_t>
201  
            io_result<std::size_t>
191  
            await_resume()
202  
            await_resume()
192  
            {
203  
            {
193  
                auto ec = self_->f_->maybe_fail();
204  
                auto ec = self_->f_->maybe_fail();
194  
                if(ec)
205  
                if(ec)
195  
                    return {ec, 0};
206  
                    return {ec, 0};
196  

207  

197  
                std::size_t n = buffer_size(buffers_);
208  
                std::size_t n = buffer_size(buffers_);
198  
                n = (std::min)(n, self_->max_write_size_);
209  
                n = (std::min)(n, self_->max_write_size_);
199  
                if(n == 0)
210  
                if(n == 0)
200  
                    return {{}, 0};
211  
                    return {{}, 0};
201  

212  

202  
                std::size_t const old_size = self_->data_.size();
213  
                std::size_t const old_size = self_->data_.size();
203  
                self_->data_.resize(old_size + n);
214  
                self_->data_.resize(old_size + n);
204  
                buffer_copy(make_buffer(
215  
                buffer_copy(make_buffer(
205  
                    self_->data_.data() + old_size, n), buffers_, n);
216  
                    self_->data_.data() + old_size, n), buffers_, n);
206  

217  

207  
                ec = self_->consume_match_();
218  
                ec = self_->consume_match_();
208  
                if(ec)
219  
                if(ec)
209  
                    return {ec, n};
220  
                    return {ec, n};
210  

221  

211  
                return {{}, n};
222  
                return {{}, n};
212  
            }
223  
            }
213  
        };
224  
        };
214  
        return awaitable{this, buffers};
225  
        return awaitable{this, buffers};
215  
    }
226  
    }
216  

227  

217  
    /** Asynchronously write data to the sink with optional EOF.
228  
    /** Asynchronously write data to the sink with optional EOF.
218  

229  

219  
        Transfers all bytes from the provided const buffer sequence to
230  
        Transfers all bytes from the provided const buffer sequence to
220  
        the internal buffer, optionally signaling end-of-stream. Before
231  
        the internal buffer, optionally signaling end-of-stream. Before
221  
        every write, the attached @ref fuse is consulted to possibly
232  
        every write, the attached @ref fuse is consulted to possibly
222  
        inject an error for testing fault scenarios. The returned
233  
        inject an error for testing fault scenarios. The returned
223  
        `std::size_t` is the number of bytes transferred.
234  
        `std::size_t` is the number of bytes transferred.
224  

235  

225  
        @par Effects
236  
        @par Effects
226  
        On success, appends the written bytes to the internal buffer.
237  
        On success, appends the written bytes to the internal buffer.
227  
        If `eof` is `true`, marks the sink as finalized.
238  
        If `eof` is `true`, marks the sink as finalized.
228  
        If an error is injected by the fuse, the internal buffer remains
239  
        If an error is injected by the fuse, the internal buffer remains
229  
        unchanged.
240  
        unchanged.
230  

241  

231  
        @par Exception Safety
242  
        @par Exception Safety
232  
        No-throw guarantee.
243  
        No-throw guarantee.
233  

244  

234  
        @param buffers The const buffer sequence containing data to write.
245  
        @param buffers The const buffer sequence containing data to write.
235  
        @param eof If true, signals end-of-stream after writing.
246  
        @param eof If true, signals end-of-stream after writing.
236  

247  

237  
        @return An awaitable yielding `(error_code,std::size_t)`.
248  
        @return An awaitable yielding `(error_code,std::size_t)`.
238  

249  

239  
        @see fuse
250  
        @see fuse
240  
    */
251  
    */
241  
    template<ConstBufferSequence CB>
252  
    template<ConstBufferSequence CB>
242  
    auto
253  
    auto
243  
    write(CB buffers, bool eof)
254  
    write(CB buffers, bool eof)
244  
    {
255  
    {
245  
        struct awaitable
256  
        struct awaitable
246  
        {
257  
        {
247  
            write_sink* self_;
258  
            write_sink* self_;
248  
            CB buffers_;
259  
            CB buffers_;
249  
            bool eof_;
260  
            bool eof_;
250  

261  

251  
            bool await_ready() const noexcept { return true; }
262  
            bool await_ready() const noexcept { return true; }
252  

263  

 
264 +
            // This method is required to satisfy Capy's IoAwaitable concept,
 
265 +
            // but is never called because await_ready() returns true.
 
266 +
            // See the comment on write(CB buffers) for a detailed explanation.
253  
            void await_suspend(
267  
            void await_suspend(
254  
                coro,
268  
                coro,
255  
                executor_ref,
269  
                executor_ref,
256  
                std::stop_token) const noexcept
270  
                std::stop_token) const noexcept
257  
            {
271  
            {
258  
            }
272  
            }
259  

273  

260  
            io_result<std::size_t>
274  
            io_result<std::size_t>
261  
            await_resume()
275  
            await_resume()
262  
            {
276  
            {
263  
                auto ec = self_->f_->maybe_fail();
277  
                auto ec = self_->f_->maybe_fail();
264  
                if(ec)
278  
                if(ec)
265  
                    return {ec, 0};
279  
                    return {ec, 0};
266  

280  

267  
                std::size_t n = buffer_size(buffers_);
281  
                std::size_t n = buffer_size(buffers_);
268  
                n = (std::min)(n, self_->max_write_size_);
282  
                n = (std::min)(n, self_->max_write_size_);
269  
                if(n > 0)
283  
                if(n > 0)
270  
                {
284  
                {
271  
                    std::size_t const old_size = self_->data_.size();
285  
                    std::size_t const old_size = self_->data_.size();
272  
                    self_->data_.resize(old_size + n);
286  
                    self_->data_.resize(old_size + n);
273  
                    buffer_copy(make_buffer(
287  
                    buffer_copy(make_buffer(
274  
                        self_->data_.data() + old_size, n), buffers_, n);
288  
                        self_->data_.data() + old_size, n), buffers_, n);
275  

289  

276  
                    ec = self_->consume_match_();
290  
                    ec = self_->consume_match_();
277  
                    if(ec)
291  
                    if(ec)
278  
                        return {ec, n};
292  
                        return {ec, n};
279  
                }
293  
                }
280  

294  

281  
                if(eof_)
295  
                if(eof_)
282  
                    self_->eof_called_ = true;
296  
                    self_->eof_called_ = true;
283  

297  

284  
                return {{}, n};
298  
                return {{}, n};
285  
            }
299  
            }
286  
        };
300  
        };
287  
        return awaitable{this, buffers, eof};
301  
        return awaitable{this, buffers, eof};
288  
    }
302  
    }
289  

303  

290  
    /** Signal end-of-stream.
304  
    /** Signal end-of-stream.
291  

305  

292  
        Marks the sink as finalized, indicating no more data will be
306  
        Marks the sink as finalized, indicating no more data will be
293  
        written. Before signaling, the attached @ref fuse is consulted
307  
        written. Before signaling, the attached @ref fuse is consulted
294  
        to possibly inject an error for testing fault scenarios.
308  
        to possibly inject an error for testing fault scenarios.
295  

309  

296  
        @par Effects
310  
        @par Effects
297  
        On success, marks the sink as finalized.
311  
        On success, marks the sink as finalized.
298  
        If an error is injected by the fuse, the state remains unchanged.
312  
        If an error is injected by the fuse, the state remains unchanged.
299  

313  

300  
        @par Exception Safety
314  
        @par Exception Safety
301  
        No-throw guarantee.
315  
        No-throw guarantee.
302  

316  

303  
        @return An awaitable yielding `(error_code)`.
317  
        @return An awaitable yielding `(error_code)`.
304  

318  

305  
        @see fuse
319  
        @see fuse
306  
    */
320  
    */
307  
    auto
321  
    auto
308  
    write_eof()
322  
    write_eof()
309  
    {
323  
    {
310  
        struct awaitable
324  
        struct awaitable
311  
        {
325  
        {
312  
            write_sink* self_;
326  
            write_sink* self_;
313  

327  

314  
            bool await_ready() const noexcept { return true; }
328  
            bool await_ready() const noexcept { return true; }
315  

329  

 
330 +
            // This method is required to satisfy Capy's IoAwaitable concept,
 
331 +
            // but is never called because await_ready() returns true.
 
332 +
            // See the comment on write(CB buffers) for a detailed explanation.
316  
            void await_suspend(
333  
            void await_suspend(
317  
                coro,
334  
                coro,
318  
                executor_ref,
335  
                executor_ref,
319  
                std::stop_token) const noexcept
336  
                std::stop_token) const noexcept
320  
            {
337  
            {
321  
            }
338  
            }
322  

339  

323  
            io_result<>
340  
            io_result<>
324  
            await_resume()
341  
            await_resume()
325  
            {
342  
            {
326  
                auto ec = self_->f_->maybe_fail();
343  
                auto ec = self_->f_->maybe_fail();
327  
                if(ec)
344  
                if(ec)
328  
                    return {ec};
345  
                    return {ec};
329  

346  

330  
                self_->eof_called_ = true;
347  
                self_->eof_called_ = true;
331  
                return {};
348  
                return {};
332  
            }
349  
            }
333  
        };
350  
        };
334  
        return awaitable{this};
351  
        return awaitable{this};
335  
    }
352  
    }
336  
};
353  
};
337  

354  

338  
} // test
355  
} // test
339  
} // capy
356  
} // capy
340  
} // boost
357  
} // boost
341  

358  

342  
#endif
359  
#endif