Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot 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/corosio
8 : //
9 :
10 : #ifndef BOOST_COROSIO_TEST_MOCKET_HPP
11 : #define BOOST_COROSIO_TEST_MOCKET_HPP
12 :
13 : #include <boost/corosio/detail/config.hpp>
14 : #include <boost/corosio/tcp_socket.hpp>
15 : #include <boost/capy/buffers/buffer_copy.hpp>
16 : #include <boost/capy/buffers/make_buffer.hpp>
17 : #include <boost/capy/error.hpp>
18 : #include <boost/capy/io_result.hpp>
19 : #include <boost/capy/test/fuse.hpp>
20 : #include <system_error>
21 :
22 : #include <cstddef>
23 : #include <new>
24 : #include <string>
25 : #include <utility>
26 :
27 : namespace boost::capy {
28 : class execution_context;
29 : } // namespace boost::capy
30 :
31 : namespace boost::corosio::test {
32 :
33 : /** A mock socket for testing I/O operations.
34 :
35 : This class provides a testable socket-like interface where data
36 : can be staged for reading and expected data can be validated on
37 : writes. A mocket is paired with a regular tcp_socket using
38 : @ref make_mocket_pair, allowing bidirectional communication testing.
39 :
40 : When reading, data comes from the `provide()` buffer first.
41 : When writing, data is validated against the `expect()` buffer.
42 : Once buffers are exhausted, I/O passes through to the underlying
43 : socket connection.
44 :
45 : Satisfies the `capy::Stream` concept.
46 :
47 : @par Thread Safety
48 : Not thread-safe. All operations must occur on a single thread.
49 : All coroutines using the mocket must be suspended when calling
50 : `expect()` or `provide()`.
51 :
52 : @see make_mocket_pair
53 : */
54 : class BOOST_COROSIO_DECL mocket
55 : {
56 : tcp_socket sock_;
57 : std::string provide_;
58 : std::string expect_;
59 : capy::test::fuse* fuse_;
60 : std::size_t max_read_size_;
61 : std::size_t max_write_size_;
62 :
63 : template<class MutableBufferSequence>
64 : std::size_t
65 : consume_provide(MutableBufferSequence const& buffers) noexcept;
66 :
67 : template<class ConstBufferSequence>
68 : bool
69 : validate_expect(
70 : ConstBufferSequence const& buffers,
71 : std::size_t& bytes_written);
72 :
73 : public:
74 : template<class MutableBufferSequence>
75 : class read_some_awaitable;
76 :
77 : template<class ConstBufferSequence>
78 : class write_some_awaitable;
79 :
80 : /** Destructor.
81 : */
82 : ~mocket();
83 :
84 : /** Construct a mocket.
85 :
86 : @param ctx The execution context for the socket.
87 : @param f The fuse for error injection testing.
88 : @param max_read_size Maximum bytes per read operation.
89 : @param max_write_size Maximum bytes per write operation.
90 : */
91 : mocket(
92 : capy::execution_context& ctx,
93 : capy::test::fuse& f,
94 : std::size_t max_read_size = std::size_t(-1),
95 : std::size_t max_write_size = std::size_t(-1));
96 :
97 : /** Move constructor.
98 : */
99 : mocket(mocket&& other) noexcept;
100 :
101 : /** Move assignment.
102 : */
103 : mocket& operator=(mocket&& other) noexcept;
104 :
105 : mocket(mocket const&) = delete;
106 : mocket& operator=(mocket const&) = delete;
107 :
108 : /** Return the execution context.
109 :
110 : @return Reference to the execution context that owns this mocket.
111 : */
112 : capy::execution_context&
113 : context() const noexcept
114 : {
115 : return sock_.context();
116 : }
117 :
118 : /** Return the underlying socket.
119 :
120 : @return Reference to the underlying tcp_socket.
121 : */
122 : tcp_socket&
123 4 : socket() noexcept
124 : {
125 4 : return sock_;
126 : }
127 :
128 : /** Stage data for reads.
129 :
130 : Appends the given string to this mocket's provide buffer.
131 : When `read_some` is called, it will receive this data first
132 : before reading from the underlying socket.
133 :
134 : @param s The data to provide.
135 :
136 : @pre All coroutines using this mocket must be suspended.
137 : */
138 : void provide(std::string s);
139 :
140 : /** Set expected data for writes.
141 :
142 : Appends the given string to this mocket's expect buffer.
143 : When the caller writes to this mocket, the written data
144 : must match the expected data. On mismatch, `fuse::fail()`
145 : is called.
146 :
147 : @param s The expected data.
148 :
149 : @pre All coroutines using this mocket must be suspended.
150 : */
151 : void expect(std::string s);
152 :
153 : /** Close the mocket and verify test expectations.
154 :
155 : Closes the underlying socket and verifies that both the
156 : `expect()` and `provide()` buffers are empty. If either
157 : buffer contains unconsumed data, returns `test_failure`
158 : and calls `fuse::fail()`.
159 :
160 : @return An error code indicating success or failure.
161 : Returns `error::test_failure` if buffers are not empty.
162 : */
163 : std::error_code close();
164 :
165 : /** Cancel pending I/O operations.
166 :
167 : Cancels any pending asynchronous operations on the underlying
168 : socket. Outstanding operations complete with `cond::canceled`.
169 : */
170 : void cancel();
171 :
172 : /** Check if the mocket is open.
173 :
174 : @return `true` if the mocket is open.
175 : */
176 : bool is_open() const noexcept;
177 :
178 : /** Initiate an asynchronous read operation.
179 :
180 : Reads available data into the provided buffer sequence. If the
181 : provide buffer has data, it is consumed first. Otherwise, the
182 : operation delegates to the underlying socket.
183 :
184 : @param buffers The buffer sequence to read data into.
185 :
186 : @return An awaitable yielding `(error_code, std::size_t)`.
187 : */
188 : template<class MutableBufferSequence>
189 2 : auto read_some(MutableBufferSequence const& buffers)
190 : {
191 2 : return read_some_awaitable<MutableBufferSequence>(*this, buffers);
192 : }
193 :
194 : /** Initiate an asynchronous write operation.
195 :
196 : Writes data from the provided buffer sequence. If the expect
197 : buffer has data, it is validated. Otherwise, the operation
198 : delegates to the underlying socket.
199 :
200 : @param buffers The buffer sequence containing data to write.
201 :
202 : @return An awaitable yielding `(error_code, std::size_t)`.
203 : */
204 : template<class ConstBufferSequence>
205 2 : auto write_some(ConstBufferSequence const& buffers)
206 : {
207 2 : return write_some_awaitable<ConstBufferSequence>(*this, buffers);
208 : }
209 : };
210 :
211 : //------------------------------------------------------------------------------
212 :
213 : template<class MutableBufferSequence>
214 : std::size_t
215 1 : mocket::
216 : consume_provide(MutableBufferSequence const& buffers) noexcept
217 : {
218 1 : auto n = capy::buffer_copy(buffers, capy::make_buffer(provide_), max_read_size_);
219 1 : provide_.erase(0, n);
220 1 : return n;
221 : }
222 :
223 : template<class ConstBufferSequence>
224 : bool
225 1 : mocket::
226 : validate_expect(
227 : ConstBufferSequence const& buffers,
228 : std::size_t& bytes_written)
229 : {
230 1 : if (expect_.empty())
231 0 : return true;
232 :
233 : // Build the write data up to max_write_size_
234 1 : std::string written;
235 1 : auto total = capy::buffer_size(buffers);
236 1 : if (total > max_write_size_)
237 0 : total = max_write_size_;
238 1 : written.resize(total);
239 1 : capy::buffer_copy(capy::make_buffer(written), buffers, max_write_size_);
240 :
241 : // Check if written data matches expect prefix
242 1 : auto const match_size = (std::min)(written.size(), expect_.size());
243 1 : if (std::memcmp(written.data(), expect_.data(), match_size) != 0)
244 : {
245 0 : fuse_->fail();
246 0 : bytes_written = 0;
247 0 : return false;
248 : }
249 :
250 : // Consume matched portion
251 1 : expect_.erase(0, match_size);
252 1 : bytes_written = written.size();
253 1 : return true;
254 1 : }
255 :
256 : //------------------------------------------------------------------------------
257 :
258 : template<class MutableBufferSequence>
259 : class mocket::read_some_awaitable
260 : {
261 : using sock_awaitable =
262 : decltype(std::declval<tcp_socket&>().read_some(
263 : std::declval<MutableBufferSequence>()));
264 :
265 : mocket* m_;
266 : MutableBufferSequence buffers_;
267 : std::size_t n_ = 0;
268 : union {
269 : char dummy_;
270 : sock_awaitable underlying_;
271 : };
272 : bool sync_ = true;
273 :
274 : public:
275 2 : read_some_awaitable(
276 : mocket& m,
277 : MutableBufferSequence buffers) noexcept
278 2 : : m_(&m)
279 2 : , buffers_(std::move(buffers))
280 : {
281 2 : }
282 :
283 4 : ~read_some_awaitable()
284 : {
285 4 : if (!sync_)
286 1 : underlying_.~sock_awaitable();
287 4 : }
288 :
289 2 : read_some_awaitable(read_some_awaitable&& other) noexcept
290 2 : : m_(other.m_)
291 2 : , buffers_(std::move(other.buffers_))
292 2 : , n_(other.n_)
293 2 : , sync_(other.sync_)
294 : {
295 2 : if (!sync_)
296 : {
297 0 : new (&underlying_) sock_awaitable(std::move(other.underlying_));
298 0 : other.underlying_.~sock_awaitable();
299 0 : other.sync_ = true;
300 : }
301 2 : }
302 :
303 : read_some_awaitable(read_some_awaitable const&) = delete;
304 : read_some_awaitable& operator=(read_some_awaitable const&) = delete;
305 : read_some_awaitable& operator=(read_some_awaitable&&) = delete;
306 :
307 2 : bool await_ready()
308 : {
309 2 : if (!m_->provide_.empty())
310 : {
311 1 : n_ = m_->consume_provide(buffers_);
312 1 : return true;
313 : }
314 1 : new (&underlying_) sock_awaitable(m_->sock_.read_some(buffers_));
315 1 : sync_ = false;
316 1 : return underlying_.await_ready();
317 : }
318 :
319 : template<class... Args>
320 1 : auto await_suspend(Args&&... args)
321 : {
322 1 : return underlying_.await_suspend(std::forward<Args>(args)...);
323 : }
324 :
325 2 : capy::io_result<std::size_t> await_resume()
326 : {
327 2 : if (sync_)
328 1 : return {{}, n_};
329 1 : return underlying_.await_resume();
330 : }
331 : };
332 :
333 : //------------------------------------------------------------------------------
334 :
335 : template<class ConstBufferSequence>
336 : class mocket::write_some_awaitable
337 : {
338 : using sock_awaitable =
339 : decltype(std::declval<tcp_socket&>().write_some(
340 : std::declval<ConstBufferSequence>()));
341 :
342 : mocket* m_;
343 : ConstBufferSequence buffers_;
344 : std::size_t n_ = 0;
345 : std::error_code ec_;
346 : union {
347 : char dummy_;
348 : sock_awaitable underlying_;
349 : };
350 : bool sync_ = true;
351 :
352 : public:
353 2 : write_some_awaitable(
354 : mocket& m,
355 : ConstBufferSequence buffers) noexcept
356 2 : : m_(&m)
357 2 : , buffers_(std::move(buffers))
358 : {
359 2 : }
360 :
361 4 : ~write_some_awaitable()
362 : {
363 4 : if (!sync_)
364 1 : underlying_.~sock_awaitable();
365 4 : }
366 :
367 2 : write_some_awaitable(write_some_awaitable&& other) noexcept
368 2 : : m_(other.m_)
369 2 : , buffers_(std::move(other.buffers_))
370 2 : , n_(other.n_)
371 2 : , ec_(other.ec_)
372 2 : , sync_(other.sync_)
373 : {
374 2 : if (!sync_)
375 : {
376 0 : new (&underlying_) sock_awaitable(std::move(other.underlying_));
377 0 : other.underlying_.~sock_awaitable();
378 0 : other.sync_ = true;
379 : }
380 2 : }
381 :
382 : write_some_awaitable(write_some_awaitable const&) = delete;
383 : write_some_awaitable& operator=(write_some_awaitable const&) = delete;
384 : write_some_awaitable& operator=(write_some_awaitable&&) = delete;
385 :
386 2 : bool await_ready()
387 : {
388 2 : if (!m_->expect_.empty())
389 : {
390 1 : if (!m_->validate_expect(buffers_, n_))
391 : {
392 0 : ec_ = capy::error::test_failure;
393 0 : n_ = 0;
394 : }
395 1 : return true;
396 : }
397 1 : new (&underlying_) sock_awaitable(m_->sock_.write_some(buffers_));
398 1 : sync_ = false;
399 1 : return underlying_.await_ready();
400 : }
401 :
402 : template<class... Args>
403 1 : auto await_suspend(Args&&... args)
404 : {
405 1 : return underlying_.await_suspend(std::forward<Args>(args)...);
406 : }
407 :
408 2 : capy::io_result<std::size_t> await_resume()
409 : {
410 2 : if (sync_)
411 1 : return {ec_, n_};
412 1 : return underlying_.await_resume();
413 : }
414 : };
415 :
416 : //------------------------------------------------------------------------------
417 :
418 : /** Create a mocket paired with a socket.
419 :
420 : Creates a mocket and a tcp_socket connected via loopback.
421 : Data written to one can be read from the other.
422 :
423 : The mocket has fuse checks enabled via `maybe_fail()` and
424 : supports provide/expect buffers for test instrumentation.
425 : The tcp_socket is the "peer" end with no test instrumentation.
426 :
427 : Optional max_read_size and max_write_size parameters limit the
428 : number of bytes transferred per I/O operation on the mocket,
429 : simulating chunked network delivery for testing purposes.
430 :
431 : @param ctx The execution context for the sockets.
432 : @param f The fuse for error injection testing.
433 : @param max_read_size Maximum bytes per read operation (default unlimited).
434 : @param max_write_size Maximum bytes per write operation (default unlimited).
435 :
436 : @return A pair of (mocket, tcp_socket).
437 :
438 : @note Mockets are not thread-safe and must be used in a
439 : single-threaded, deterministic context.
440 : */
441 : BOOST_COROSIO_DECL
442 : std::pair<mocket, tcp_socket>
443 : make_mocket_pair(
444 : capy::execution_context& ctx,
445 : capy::test::fuse& f,
446 : std::size_t max_read_size = std::size_t(-1),
447 : std::size_t max_write_size = std::size_t(-1));
448 :
449 : } // namespace boost::corosio::test
450 :
451 : #endif
|