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