TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : // Copyright (c) 2026 Steve Gerbino
4 : //
5 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 : //
8 : // Official repository: https://github.com/cppalliance/corosio
9 : //
10 :
11 : #ifndef BOOST_COROSIO_TEST_MOCKET_HPP
12 : #define BOOST_COROSIO_TEST_MOCKET_HPP
13 :
14 : #include <boost/corosio/detail/except.hpp>
15 : #include <boost/corosio/io_context.hpp>
16 : #include <boost/corosio/socket_option.hpp>
17 : #include <boost/corosio/tcp_acceptor.hpp>
18 : #include <boost/corosio/tcp_socket.hpp>
19 : #include <boost/capy/buffers/buffer_copy.hpp>
20 : #include <boost/capy/buffers/make_buffer.hpp>
21 : #include <boost/capy/error.hpp>
22 : #include <boost/capy/ex/run_async.hpp>
23 : #include <boost/capy/io_result.hpp>
24 : #include <boost/capy/task.hpp>
25 : #include <boost/capy/test/fuse.hpp>
26 :
27 : #include <cstddef>
28 : #include <cstdio>
29 : #include <cstring>
30 : #include <stdexcept>
31 : #include <string>
32 : #include <system_error>
33 : #include <utility>
34 :
35 : namespace boost::corosio::test {
36 :
37 : /** A mock socket for testing I/O operations.
38 :
39 : This class provides a testable socket-like interface where data
40 : can be staged for reading and expected data can be validated on
41 : writes. A mocket is paired with a regular socket using
42 : @ref make_mocket_pair, allowing bidirectional communication testing.
43 :
44 : When reading, data comes from the `provide()` buffer first.
45 : When writing, data is validated against the `expect()` buffer.
46 : Once buffers are exhausted, I/O passes through to the underlying
47 : socket connection.
48 :
49 : Satisfies the `capy::Stream` concept.
50 :
51 : @tparam Socket The underlying socket type (default `tcp_socket`).
52 :
53 : @par Thread Safety
54 : Not thread-safe. All operations must occur on a single thread.
55 : All coroutines using the mocket must be suspended when calling
56 : `expect()` or `provide()`.
57 :
58 : @see make_mocket_pair
59 : */
60 : template<class Socket = tcp_socket>
61 : class basic_mocket
62 : {
63 : Socket sock_;
64 : std::string provide_;
65 : std::string expect_;
66 : capy::test::fuse fuse_;
67 : std::size_t max_read_size_;
68 : std::size_t max_write_size_;
69 :
70 : template<class MutableBufferSequence>
71 : std::size_t consume_provide(MutableBufferSequence const& buffers) noexcept;
72 :
73 : template<class ConstBufferSequence>
74 : bool validate_expect(
75 : ConstBufferSequence const& buffers, std::size_t& bytes_written);
76 :
77 : public:
78 : template<class MutableBufferSequence>
79 : class read_some_awaitable;
80 :
81 : template<class ConstBufferSequence>
82 : class write_some_awaitable;
83 :
84 : /** Destructor.
85 : */
86 HIT 12 : ~basic_mocket() = default;
87 :
88 : /** Construct a mocket.
89 :
90 : @param ctx The execution context for the socket.
91 : @param f The fuse for error injection testing.
92 : @param max_read_size Maximum bytes per read operation.
93 : @param max_write_size Maximum bytes per write operation.
94 : */
95 6 : basic_mocket(
96 : capy::execution_context& ctx,
97 : capy::test::fuse f = {},
98 : std::size_t max_read_size = std::size_t(-1),
99 : std::size_t max_write_size = std::size_t(-1))
100 6 : : sock_(ctx)
101 6 : , fuse_(std::move(f))
102 6 : , max_read_size_(max_read_size)
103 6 : , max_write_size_(max_write_size)
104 : {
105 6 : if (max_read_size == 0)
106 MIS 0 : detail::throw_logic_error("mocket: max_read_size cannot be 0");
107 HIT 6 : if (max_write_size == 0)
108 MIS 0 : detail::throw_logic_error("mocket: max_write_size cannot be 0");
109 HIT 6 : }
110 :
111 : /** Move constructor.
112 : */
113 6 : basic_mocket(basic_mocket&& other) noexcept
114 6 : : sock_(std::move(other.sock_))
115 6 : , provide_(std::move(other.provide_))
116 6 : , expect_(std::move(other.expect_))
117 6 : , fuse_(std::move(other.fuse_))
118 6 : , max_read_size_(other.max_read_size_)
119 6 : , max_write_size_(other.max_write_size_)
120 : {
121 6 : }
122 :
123 : /** Move assignment.
124 : */
125 : basic_mocket& operator=(basic_mocket&& other) noexcept
126 : {
127 : if (this != &other)
128 : {
129 : sock_ = std::move(other.sock_);
130 : provide_ = std::move(other.provide_);
131 : expect_ = std::move(other.expect_);
132 : fuse_ = other.fuse_;
133 : max_read_size_ = other.max_read_size_;
134 : max_write_size_ = other.max_write_size_;
135 : }
136 : return *this;
137 : }
138 :
139 : basic_mocket(basic_mocket const&) = delete;
140 : basic_mocket& operator=(basic_mocket const&) = delete;
141 :
142 : /** Return the execution context.
143 :
144 : @return Reference to the execution context that owns this mocket.
145 : */
146 : capy::execution_context& context() const noexcept
147 : {
148 : return sock_.context();
149 : }
150 :
151 : /** Return the underlying socket.
152 :
153 : @return Reference to the underlying socket.
154 : */
155 6 : Socket& socket() noexcept
156 : {
157 6 : return sock_;
158 : }
159 :
160 : /** Stage data for reads.
161 :
162 : Appends the given string to this mocket's provide buffer.
163 : When `read_some` is called, it will receive this data first
164 : before reading from the underlying socket.
165 :
166 : @param s The data to provide.
167 :
168 : @pre All coroutines using this mocket must be suspended.
169 : */
170 4 : void provide(std::string const& s)
171 : {
172 4 : provide_.append(s);
173 4 : }
174 :
175 : /** Set expected data for writes.
176 :
177 : Appends the given string to this mocket's expect buffer.
178 : When the caller writes to this mocket, the written data
179 : must match the expected data. On mismatch, `fuse::fail()`
180 : is called.
181 :
182 : @param s The expected data.
183 :
184 : @pre All coroutines using this mocket must be suspended.
185 : */
186 4 : void expect(std::string const& s)
187 : {
188 4 : expect_.append(s);
189 4 : }
190 :
191 : /** Close the mocket and verify test expectations.
192 :
193 : Closes the underlying socket and verifies that both the
194 : `expect()` and `provide()` buffers are empty. If either
195 : buffer contains unconsumed data, returns `test_failure`
196 : and calls `fuse::fail()`.
197 :
198 : @return An error code indicating success or failure.
199 : Returns `error::test_failure` if buffers are not empty.
200 : */
201 6 : std::error_code close()
202 : {
203 6 : if (!sock_.is_open())
204 MIS 0 : return {};
205 :
206 HIT 6 : if (!expect_.empty())
207 : {
208 1 : fuse_.fail();
209 1 : sock_.close();
210 1 : return capy::error::test_failure;
211 : }
212 5 : if (!provide_.empty())
213 : {
214 1 : fuse_.fail();
215 1 : sock_.close();
216 1 : return capy::error::test_failure;
217 : }
218 :
219 4 : sock_.close();
220 4 : return {};
221 : }
222 :
223 : /** Cancel pending I/O operations.
224 :
225 : Cancels any pending asynchronous operations on the underlying
226 : socket. Outstanding operations complete with `cond::canceled`.
227 : */
228 : void cancel()
229 : {
230 : sock_.cancel();
231 : }
232 :
233 : /** Check if the mocket is open.
234 :
235 : @return `true` if the mocket is open.
236 : */
237 3 : bool is_open() const noexcept
238 : {
239 3 : return sock_.is_open();
240 : }
241 :
242 : /** Initiate an asynchronous read operation.
243 :
244 : Reads available data into the provided buffer sequence. If the
245 : provide buffer has data, it is consumed first. Otherwise, the
246 : operation delegates to the underlying socket.
247 :
248 : @param buffers The buffer sequence to read data into.
249 :
250 : @return An awaitable yielding `(error_code, std::size_t)`.
251 : */
252 : template<class MutableBufferSequence>
253 4 : auto read_some(MutableBufferSequence const& buffers)
254 : {
255 4 : return read_some_awaitable<MutableBufferSequence>(*this, buffers);
256 : }
257 :
258 : /** Initiate an asynchronous write operation.
259 :
260 : Writes data from the provided buffer sequence. If the expect
261 : buffer has data, it is validated. Otherwise, the operation
262 : delegates to the underlying socket.
263 :
264 : @param buffers The buffer sequence containing data to write.
265 :
266 : @return An awaitable yielding `(error_code, std::size_t)`.
267 : */
268 : template<class ConstBufferSequence>
269 4 : auto write_some(ConstBufferSequence const& buffers)
270 : {
271 4 : return write_some_awaitable<ConstBufferSequence>(*this, buffers);
272 : }
273 : };
274 :
275 : /// Default mocket type using `tcp_socket`.
276 : using mocket = basic_mocket<>;
277 :
278 : template<class Socket>
279 : template<class MutableBufferSequence>
280 : std::size_t
281 3 : basic_mocket<Socket>::consume_provide(
282 : MutableBufferSequence const& buffers) noexcept
283 : {
284 : auto n =
285 3 : capy::buffer_copy(buffers, capy::make_buffer(provide_), max_read_size_);
286 3 : provide_.erase(0, n);
287 3 : return n;
288 : }
289 :
290 : template<class Socket>
291 : template<class ConstBufferSequence>
292 : bool
293 3 : basic_mocket<Socket>::validate_expect(
294 : ConstBufferSequence const& buffers, std::size_t& bytes_written)
295 : {
296 3 : if (expect_.empty())
297 MIS 0 : return true;
298 :
299 : // Build the write data up to max_write_size_
300 HIT 3 : std::string written;
301 3 : auto total = capy::buffer_size(buffers);
302 3 : if (total > max_write_size_)
303 MIS 0 : total = max_write_size_;
304 HIT 3 : written.resize(total);
305 3 : capy::buffer_copy(capy::make_buffer(written), buffers, max_write_size_);
306 :
307 : // Check if written data matches expect prefix
308 3 : auto const match_size = (std::min)(written.size(), expect_.size());
309 3 : if (std::memcmp(written.data(), expect_.data(), match_size) != 0)
310 : {
311 MIS 0 : fuse_.fail();
312 0 : bytes_written = 0;
313 0 : return false;
314 : }
315 :
316 : // Consume matched portion
317 HIT 3 : expect_.erase(0, match_size);
318 3 : bytes_written = written.size();
319 3 : return true;
320 3 : }
321 :
322 : template<class Socket>
323 : template<class MutableBufferSequence>
324 : class basic_mocket<Socket>::read_some_awaitable
325 : {
326 : using sock_awaitable = decltype(std::declval<Socket&>().read_some(
327 : std::declval<MutableBufferSequence>()));
328 :
329 : basic_mocket* m_;
330 : MutableBufferSequence buffers_;
331 : std::size_t n_ = 0;
332 : union
333 : {
334 : char dummy_;
335 : sock_awaitable underlying_;
336 : };
337 : bool sync_ = true;
338 :
339 : public:
340 4 : read_some_awaitable(basic_mocket& m, MutableBufferSequence buffers) noexcept
341 4 : : m_(&m)
342 4 : , buffers_(std::move(buffers))
343 : {
344 4 : }
345 :
346 8 : ~read_some_awaitable()
347 : {
348 8 : if (!sync_)
349 1 : underlying_.~sock_awaitable();
350 8 : }
351 :
352 4 : read_some_awaitable(read_some_awaitable&& other) noexcept
353 4 : : m_(other.m_)
354 4 : , buffers_(std::move(other.buffers_))
355 4 : , n_(other.n_)
356 4 : , sync_(other.sync_)
357 : {
358 4 : if (!sync_)
359 : {
360 MIS 0 : new (&underlying_) sock_awaitable(std::move(other.underlying_));
361 0 : other.underlying_.~sock_awaitable();
362 0 : other.sync_ = true;
363 : }
364 HIT 4 : }
365 :
366 : read_some_awaitable(read_some_awaitable const&) = delete;
367 : read_some_awaitable& operator=(read_some_awaitable const&) = delete;
368 : read_some_awaitable& operator=(read_some_awaitable&&) = delete;
369 :
370 4 : bool await_ready()
371 : {
372 4 : if (!m_->provide_.empty())
373 : {
374 3 : n_ = m_->consume_provide(buffers_);
375 3 : return true;
376 : }
377 1 : new (&underlying_) sock_awaitable(m_->sock_.read_some(buffers_));
378 1 : sync_ = false;
379 1 : return underlying_.await_ready();
380 : }
381 :
382 : template<class... Args>
383 1 : auto await_suspend(Args&&... args)
384 : {
385 1 : return underlying_.await_suspend(std::forward<Args>(args)...);
386 : }
387 :
388 4 : capy::io_result<std::size_t> await_resume()
389 : {
390 4 : if (sync_)
391 3 : return {{}, n_};
392 1 : return underlying_.await_resume();
393 : }
394 : };
395 :
396 : template<class Socket>
397 : template<class ConstBufferSequence>
398 : class basic_mocket<Socket>::write_some_awaitable
399 : {
400 : using sock_awaitable = decltype(std::declval<Socket&>().write_some(
401 : std::declval<ConstBufferSequence>()));
402 :
403 : basic_mocket* m_;
404 : ConstBufferSequence buffers_;
405 : std::size_t n_ = 0;
406 : std::error_code ec_;
407 : union
408 : {
409 : char dummy_;
410 : sock_awaitable underlying_;
411 : };
412 : bool sync_ = true;
413 :
414 : public:
415 4 : write_some_awaitable(basic_mocket& m, ConstBufferSequence buffers) noexcept
416 4 : : m_(&m)
417 4 : , buffers_(std::move(buffers))
418 : {
419 4 : }
420 :
421 8 : ~write_some_awaitable()
422 : {
423 8 : if (!sync_)
424 1 : underlying_.~sock_awaitable();
425 8 : }
426 :
427 4 : write_some_awaitable(write_some_awaitable&& other) noexcept
428 4 : : m_(other.m_)
429 4 : , buffers_(std::move(other.buffers_))
430 4 : , n_(other.n_)
431 4 : , ec_(other.ec_)
432 4 : , sync_(other.sync_)
433 : {
434 4 : if (!sync_)
435 : {
436 MIS 0 : new (&underlying_) sock_awaitable(std::move(other.underlying_));
437 0 : other.underlying_.~sock_awaitable();
438 0 : other.sync_ = true;
439 : }
440 HIT 4 : }
441 :
442 : write_some_awaitable(write_some_awaitable const&) = delete;
443 : write_some_awaitable& operator=(write_some_awaitable const&) = delete;
444 : write_some_awaitable& operator=(write_some_awaitable&&) = delete;
445 :
446 4 : bool await_ready()
447 : {
448 4 : if (!m_->expect_.empty())
449 : {
450 3 : if (!m_->validate_expect(buffers_, n_))
451 : {
452 MIS 0 : ec_ = capy::error::test_failure;
453 0 : n_ = 0;
454 : }
455 HIT 3 : return true;
456 : }
457 1 : new (&underlying_) sock_awaitable(m_->sock_.write_some(buffers_));
458 1 : sync_ = false;
459 1 : return underlying_.await_ready();
460 : }
461 :
462 : template<class... Args>
463 1 : auto await_suspend(Args&&... args)
464 : {
465 1 : return underlying_.await_suspend(std::forward<Args>(args)...);
466 : }
467 :
468 4 : capy::io_result<std::size_t> await_resume()
469 : {
470 4 : if (sync_)
471 3 : return {ec_, n_};
472 1 : return underlying_.await_resume();
473 : }
474 : };
475 :
476 : /** Create a mocket paired with a socket.
477 :
478 : Creates a mocket and a socket connected via loopback.
479 : Data written to one can be read from the other.
480 :
481 : The mocket has fuse checks enabled via `maybe_fail()` and
482 : supports provide/expect buffers for test instrumentation.
483 : The socket is the "peer" end with no test instrumentation.
484 :
485 : Optional max_read_size and max_write_size parameters limit the
486 : number of bytes transferred per I/O operation on the mocket,
487 : simulating chunked network delivery for testing purposes.
488 :
489 : @tparam Socket The socket type (default `tcp_socket`).
490 : @tparam Acceptor The acceptor type (default `tcp_acceptor`).
491 :
492 : @param ctx The I/O context for the sockets.
493 : @param f The fuse for error injection testing.
494 : @param max_read_size Maximum bytes per read operation (default unlimited).
495 : @param max_write_size Maximum bytes per write operation (default unlimited).
496 :
497 : @return A pair of (mocket, socket).
498 :
499 : @note Mockets are not thread-safe and must be used in a
500 : single-threaded, deterministic context.
501 : */
502 : template<class Socket = tcp_socket, class Acceptor = tcp_acceptor>
503 : std::pair<basic_mocket<Socket>, Socket>
504 6 : make_mocket_pair(
505 : io_context& ctx,
506 : capy::test::fuse f = {},
507 : std::size_t max_read_size = std::size_t(-1),
508 : std::size_t max_write_size = std::size_t(-1))
509 : {
510 6 : auto ex = ctx.get_executor();
511 :
512 6 : basic_mocket<Socket> m(ctx, std::move(f), max_read_size, max_write_size);
513 :
514 6 : Socket peer(ctx);
515 :
516 6 : std::error_code accept_ec;
517 6 : std::error_code connect_ec;
518 6 : bool accept_done = false;
519 6 : bool connect_done = false;
520 :
521 6 : Acceptor acc(ctx);
522 6 : acc.open();
523 6 : acc.set_option(socket_option::reuse_address(true));
524 6 : if (auto bind_ec = acc.bind(endpoint(ipv4_address::loopback(), 0)))
525 MIS 0 : throw std::runtime_error(
526 : "mocket bind failed: " + bind_ec.message());
527 HIT 6 : if (auto listen_ec = acc.listen())
528 MIS 0 : throw std::runtime_error(
529 : "mocket listen failed: " + listen_ec.message());
530 HIT 6 : auto port = acc.local_endpoint().port();
531 :
532 6 : peer.open();
533 :
534 6 : Socket accepted_socket(ctx);
535 :
536 6 : capy::run_async(ex)(
537 12 : [](Acceptor& a, Socket& s, std::error_code& ec_out,
538 : bool& done_out) -> capy::task<> {
539 : auto [ec] = co_await a.accept(s);
540 : ec_out = ec;
541 : done_out = true;
542 : }(acc, accepted_socket, accept_ec, accept_done));
543 :
544 6 : capy::run_async(ex)(
545 12 : [](Socket& s, endpoint ep, std::error_code& ec_out,
546 : bool& done_out) -> capy::task<> {
547 : auto [ec] = co_await s.connect(ep);
548 : ec_out = ec;
549 : done_out = true;
550 : }(peer, endpoint(ipv4_address::loopback(), port), connect_ec,
551 : connect_done));
552 :
553 6 : ctx.run();
554 6 : ctx.restart();
555 :
556 6 : if (!accept_done || accept_ec)
557 : {
558 MIS 0 : std::fprintf(
559 : stderr, "make_mocket_pair: accept failed (done=%d, ec=%s)\n",
560 : accept_done, accept_ec.message().c_str());
561 0 : acc.close();
562 0 : throw std::runtime_error("mocket accept failed");
563 : }
564 :
565 HIT 6 : if (!connect_done || connect_ec)
566 : {
567 MIS 0 : std::fprintf(
568 : stderr, "make_mocket_pair: connect failed (done=%d, ec=%s)\n",
569 : connect_done, connect_ec.message().c_str());
570 0 : acc.close();
571 0 : accepted_socket.close();
572 0 : throw std::runtime_error("mocket connect failed");
573 : }
574 :
575 HIT 6 : m.socket() = std::move(accepted_socket);
576 :
577 6 : acc.close();
578 :
579 12 : return {std::move(m), std::move(peer)};
580 6 : }
581 :
582 : } // namespace boost::corosio::test
583 :
584 : #endif
|