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