TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/corosio
8 : //
9 :
10 : #ifndef BOOST_COROSIO_TEST_MOCKET_HPP
11 : #define BOOST_COROSIO_TEST_MOCKET_HPP
12 :
13 : #include <boost/corosio/detail/config.hpp>
14 : #include <boost/corosio/tcp_socket.hpp>
15 : #include <boost/capy/buffers/buffer_copy.hpp>
16 : #include <boost/capy/buffers/make_buffer.hpp>
17 : #include <boost/capy/error.hpp>
18 : #include <boost/capy/io_result.hpp>
19 : #include <boost/capy/test/fuse.hpp>
20 : #include <system_error>
21 :
22 : #include <cstddef>
23 : #include <new>
24 : #include <string>
25 : #include <utility>
26 :
27 : namespace boost::capy {
28 : class execution_context;
29 : } // namespace boost::capy
30 :
31 : namespace boost::corosio::test {
32 :
33 : /** A mock socket for testing I/O operations.
34 :
35 : This class provides a testable socket-like interface where data
36 : can be staged for reading and expected data can be validated on
37 : writes. A mocket is paired with a regular tcp_socket using
38 : @ref make_mocket_pair, allowing bidirectional communication testing.
39 :
40 : When reading, data comes from the `provide()` buffer first.
41 : When writing, data is validated against the `expect()` buffer.
42 : Once buffers are exhausted, I/O passes through to the underlying
43 : socket connection.
44 :
45 : Satisfies the `capy::Stream` concept.
46 :
47 : @par Thread Safety
48 : Not thread-safe. All operations must occur on a single thread.
49 : All coroutines using the mocket must be suspended when calling
50 : `expect()` or `provide()`.
51 :
52 : @see make_mocket_pair
53 : */
54 : class BOOST_COROSIO_DECL mocket
55 : {
56 : tcp_socket sock_;
57 : std::string provide_;
58 : std::string expect_;
59 : capy::test::fuse fuse_;
60 : std::size_t max_read_size_;
61 : std::size_t max_write_size_;
62 :
63 : template<class MutableBufferSequence>
64 : std::size_t consume_provide(MutableBufferSequence const& buffers) noexcept;
65 :
66 : template<class ConstBufferSequence>
67 : bool validate_expect(
68 : ConstBufferSequence const& buffers, std::size_t& bytes_written);
69 :
70 : public:
71 : template<class MutableBufferSequence>
72 : class read_some_awaitable;
73 :
74 : template<class ConstBufferSequence>
75 : class write_some_awaitable;
76 :
77 : /** Destructor.
78 : */
79 : ~mocket();
80 :
81 : /** Construct a mocket.
82 :
83 : @param ctx The execution context for the socket.
84 : @param f The fuse for error injection testing.
85 : @param max_read_size Maximum bytes per read operation.
86 : @param max_write_size Maximum bytes per write operation.
87 : */
88 : mocket(
89 : capy::execution_context& ctx,
90 : capy::test::fuse f = {},
91 : std::size_t max_read_size = std::size_t(-1),
92 : std::size_t max_write_size = std::size_t(-1));
93 :
94 : /** Move constructor.
95 : */
96 : mocket(mocket&& other) noexcept;
97 :
98 : /** Move assignment.
99 : */
100 : mocket& operator=(mocket&& other) noexcept;
101 :
102 : mocket(mocket const&) = delete;
103 : mocket& operator=(mocket const&) = delete;
104 :
105 : /** Return the execution context.
106 :
107 : @return Reference to the execution context that owns this mocket.
108 : */
109 : capy::execution_context& context() const noexcept
110 : {
111 : return sock_.context();
112 : }
113 :
114 : /** Return the underlying socket.
115 :
116 : @return Reference to the underlying tcp_socket.
117 : */
118 HIT 4 : tcp_socket& socket() noexcept
119 : {
120 4 : return sock_;
121 : }
122 :
123 : /** Stage data for reads.
124 :
125 : Appends the given string to this mocket's provide buffer.
126 : When `read_some` is called, it will receive this data first
127 : before reading from the underlying socket.
128 :
129 : @param s The data to provide.
130 :
131 : @pre All coroutines using this mocket must be suspended.
132 : */
133 : void provide(std::string const& s);
134 :
135 : /** Set expected data for writes.
136 :
137 : Appends the given string to this mocket's expect buffer.
138 : When the caller writes to this mocket, the written data
139 : must match the expected data. On mismatch, `fuse::fail()`
140 : is called.
141 :
142 : @param s The expected data.
143 :
144 : @pre All coroutines using this mocket must be suspended.
145 : */
146 : void expect(std::string const& s);
147 :
148 : /** Close the mocket and verify test expectations.
149 :
150 : Closes the underlying socket and verifies that both the
151 : `expect()` and `provide()` buffers are empty. If either
152 : buffer contains unconsumed data, returns `test_failure`
153 : and calls `fuse::fail()`.
154 :
155 : @return An error code indicating success or failure.
156 : Returns `error::test_failure` if buffers are not empty.
157 : */
158 : std::error_code close();
159 :
160 : /** Cancel pending I/O operations.
161 :
162 : Cancels any pending asynchronous operations on the underlying
163 : socket. Outstanding operations complete with `cond::canceled`.
164 : */
165 : void cancel();
166 :
167 : /** Check if the mocket is open.
168 :
169 : @return `true` if the mocket is open.
170 : */
171 : bool is_open() const noexcept;
172 :
173 : /** Initiate an asynchronous read operation.
174 :
175 : Reads available data into the provided buffer sequence. If the
176 : provide buffer has data, it is consumed first. Otherwise, the
177 : operation delegates to the underlying socket.
178 :
179 : @param buffers The buffer sequence to read data into.
180 :
181 : @return An awaitable yielding `(error_code, std::size_t)`.
182 : */
183 : template<class MutableBufferSequence>
184 2 : auto read_some(MutableBufferSequence const& buffers)
185 : {
186 2 : return read_some_awaitable<MutableBufferSequence>(*this, buffers);
187 : }
188 :
189 : /** Initiate an asynchronous write operation.
190 :
191 : Writes data from the provided buffer sequence. If the expect
192 : buffer has data, it is validated. Otherwise, the operation
193 : delegates to the underlying socket.
194 :
195 : @param buffers The buffer sequence containing data to write.
196 :
197 : @return An awaitable yielding `(error_code, std::size_t)`.
198 : */
199 : template<class ConstBufferSequence>
200 2 : auto write_some(ConstBufferSequence const& buffers)
201 : {
202 2 : return write_some_awaitable<ConstBufferSequence>(*this, buffers);
203 : }
204 : };
205 :
206 :
207 : template<class MutableBufferSequence>
208 : std::size_t
209 1 : mocket::consume_provide(MutableBufferSequence const& buffers) noexcept
210 : {
211 : auto n =
212 1 : capy::buffer_copy(buffers, capy::make_buffer(provide_), max_read_size_);
213 1 : provide_.erase(0, n);
214 1 : return n;
215 : }
216 :
217 : template<class ConstBufferSequence>
218 : bool
219 1 : mocket::validate_expect(
220 : ConstBufferSequence const& buffers, std::size_t& bytes_written)
221 : {
222 1 : if (expect_.empty())
223 MIS 0 : return true;
224 :
225 : // Build the write data up to max_write_size_
226 HIT 1 : std::string written;
227 1 : auto total = capy::buffer_size(buffers);
228 1 : if (total > max_write_size_)
229 MIS 0 : total = max_write_size_;
230 HIT 1 : written.resize(total);
231 1 : capy::buffer_copy(capy::make_buffer(written), buffers, max_write_size_);
232 :
233 : // Check if written data matches expect prefix
234 1 : auto const match_size = (std::min)(written.size(), expect_.size());
235 1 : if (std::memcmp(written.data(), expect_.data(), match_size) != 0)
236 : {
237 MIS 0 : fuse_.fail();
238 0 : bytes_written = 0;
239 0 : return false;
240 : }
241 :
242 : // Consume matched portion
243 HIT 1 : expect_.erase(0, match_size);
244 1 : bytes_written = written.size();
245 1 : return true;
246 1 : }
247 :
248 :
249 : template<class MutableBufferSequence>
250 : class mocket::read_some_awaitable
251 : {
252 : using sock_awaitable = decltype(std::declval<tcp_socket&>().read_some(
253 : std::declval<MutableBufferSequence>()));
254 :
255 : mocket* m_;
256 : MutableBufferSequence buffers_;
257 : std::size_t n_ = 0;
258 : union
259 : {
260 : char dummy_;
261 : sock_awaitable underlying_;
262 : };
263 : bool sync_ = true;
264 :
265 : public:
266 2 : read_some_awaitable(mocket& m, MutableBufferSequence buffers) noexcept
267 2 : : m_(&m)
268 2 : , buffers_(std::move(buffers))
269 : {
270 2 : }
271 :
272 4 : ~read_some_awaitable()
273 : {
274 4 : if (!sync_)
275 1 : underlying_.~sock_awaitable();
276 4 : }
277 :
278 2 : read_some_awaitable(read_some_awaitable&& other) noexcept
279 2 : : m_(other.m_)
280 2 : , buffers_(std::move(other.buffers_))
281 2 : , n_(other.n_)
282 2 : , sync_(other.sync_)
283 : {
284 2 : if (!sync_)
285 : {
286 MIS 0 : new (&underlying_) sock_awaitable(std::move(other.underlying_));
287 0 : other.underlying_.~sock_awaitable();
288 0 : other.sync_ = true;
289 : }
290 HIT 2 : }
291 :
292 : read_some_awaitable(read_some_awaitable const&) = delete;
293 : read_some_awaitable& operator=(read_some_awaitable const&) = delete;
294 : read_some_awaitable& operator=(read_some_awaitable&&) = delete;
295 :
296 2 : bool await_ready()
297 : {
298 2 : if (!m_->provide_.empty())
299 : {
300 1 : n_ = m_->consume_provide(buffers_);
301 1 : return true;
302 : }
303 1 : new (&underlying_) sock_awaitable(m_->sock_.read_some(buffers_));
304 1 : sync_ = false;
305 1 : return underlying_.await_ready();
306 : }
307 :
308 : template<class... Args>
309 1 : auto await_suspend(Args&&... args)
310 : {
311 1 : return underlying_.await_suspend(std::forward<Args>(args)...);
312 : }
313 :
314 2 : capy::io_result<std::size_t> await_resume()
315 : {
316 2 : if (sync_)
317 1 : return {{}, n_};
318 1 : return underlying_.await_resume();
319 : }
320 : };
321 :
322 :
323 : template<class ConstBufferSequence>
324 : class mocket::write_some_awaitable
325 : {
326 : using sock_awaitable = decltype(std::declval<tcp_socket&>().write_some(
327 : std::declval<ConstBufferSequence>()));
328 :
329 : mocket* m_;
330 : ConstBufferSequence buffers_;
331 : std::size_t n_ = 0;
332 : std::error_code ec_;
333 : union
334 : {
335 : char dummy_;
336 : sock_awaitable underlying_;
337 : };
338 : bool sync_ = true;
339 :
340 : public:
341 2 : write_some_awaitable(mocket& m, ConstBufferSequence buffers) noexcept
342 2 : : m_(&m)
343 2 : , buffers_(std::move(buffers))
344 : {
345 2 : }
346 :
347 4 : ~write_some_awaitable()
348 : {
349 4 : if (!sync_)
350 1 : underlying_.~sock_awaitable();
351 4 : }
352 :
353 2 : write_some_awaitable(write_some_awaitable&& other) noexcept
354 2 : : m_(other.m_)
355 2 : , buffers_(std::move(other.buffers_))
356 2 : , n_(other.n_)
357 2 : , ec_(other.ec_)
358 2 : , sync_(other.sync_)
359 : {
360 2 : if (!sync_)
361 : {
362 MIS 0 : new (&underlying_) sock_awaitable(std::move(other.underlying_));
363 0 : other.underlying_.~sock_awaitable();
364 0 : other.sync_ = true;
365 : }
366 HIT 2 : }
367 :
368 : write_some_awaitable(write_some_awaitable const&) = delete;
369 : write_some_awaitable& operator=(write_some_awaitable const&) = delete;
370 : write_some_awaitable& operator=(write_some_awaitable&&) = delete;
371 :
372 2 : bool await_ready()
373 : {
374 2 : if (!m_->expect_.empty())
375 : {
376 1 : if (!m_->validate_expect(buffers_, n_))
377 : {
378 MIS 0 : ec_ = capy::error::test_failure;
379 0 : n_ = 0;
380 : }
381 HIT 1 : return true;
382 : }
383 1 : new (&underlying_) sock_awaitable(m_->sock_.write_some(buffers_));
384 1 : sync_ = false;
385 1 : return underlying_.await_ready();
386 : }
387 :
388 : template<class... Args>
389 1 : auto await_suspend(Args&&... args)
390 : {
391 1 : return underlying_.await_suspend(std::forward<Args>(args)...);
392 : }
393 :
394 2 : capy::io_result<std::size_t> await_resume()
395 : {
396 2 : if (sync_)
397 1 : return {ec_, n_};
398 1 : return underlying_.await_resume();
399 : }
400 : };
401 :
402 :
403 : /** Create a mocket paired with a socket.
404 :
405 : Creates a mocket and a tcp_socket connected via loopback.
406 : Data written to one can be read from the other.
407 :
408 : The mocket has fuse checks enabled via `maybe_fail()` and
409 : supports provide/expect buffers for test instrumentation.
410 : The tcp_socket is the "peer" end with no test instrumentation.
411 :
412 : Optional max_read_size and max_write_size parameters limit the
413 : number of bytes transferred per I/O operation on the mocket,
414 : simulating chunked network delivery for testing purposes.
415 :
416 : @param ctx The execution context for the sockets.
417 : @param f The fuse for error injection testing.
418 : @param max_read_size Maximum bytes per read operation (default unlimited).
419 : @param max_write_size Maximum bytes per write operation (default unlimited).
420 :
421 : @return A pair of (mocket, tcp_socket).
422 :
423 : @note Mockets are not thread-safe and must be used in a
424 : single-threaded, deterministic context.
425 : */
426 : BOOST_COROSIO_DECL
427 : std::pair<mocket, tcp_socket> make_mocket_pair(
428 : capy::execution_context& ctx,
429 : capy::test::fuse f = {},
430 : std::size_t max_read_size = std::size_t(-1),
431 : std::size_t max_write_size = std::size_t(-1));
432 :
433 : } // namespace boost::corosio::test
434 :
435 : #endif
|