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