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