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