LCOV - code coverage report
Current view: top level - /jenkins/workspace/boost-root/boost/corosio/test - mocket.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 86.0 % 93 80
Test Date: 2026-02-04 19:19:32 Functions: 100.0 % 17 17

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

Generated by: LCOV version 2.3