LCOV - code coverage report
Current view: top level - include/boost/corosio/test - mocket.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 86.0 % 93 80 13
Test Date: 2026-02-16 16:21:08 Functions: 100.0 % 17 17

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

Generated by: LCOV version 2.3