LCOV - code coverage report
Current view: top level - corosio/test - mocket.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 85.6 % 167 143 24
Test Date: 2026-02-18 17:16:27 Functions: 95.1 % 81 77 4

           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
        

Generated by: LCOV version 2.3