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