1//
2// Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot 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
8#include <boost/mysql/client_errc.hpp>
9#include <boost/mysql/error_code.hpp>
10
11#include <boost/mysql/impl/internal/sansio/algo_runner.hpp>
12#include <boost/mysql/impl/internal/sansio/connection_state_data.hpp>
13#include <boost/mysql/impl/internal/sansio/message_reader.hpp>
14#include <boost/mysql/impl/internal/sansio/next_action.hpp>
15#include <boost/mysql/impl/internal/sansio/sansio_algorithm.hpp>
16
17#include <boost/asio/coroutine.hpp>
18#include <boost/core/span.hpp>
19#include <boost/test/unit_test.hpp>
20
21#include <cstddef>
22#include <cstdint>
23#include <cstring>
24
25#include "test_common/assert_buffer_equals.hpp"
26#include "test_unit/create_frame.hpp"
27#include "test_unit/mock_message.hpp"
28#include "test_unit/printing.hpp"
29
30using namespace boost::mysql::detail;
31using namespace boost::mysql::test;
32using boost::span;
33using boost::asio::coroutine;
34using boost::mysql::client_errc;
35using boost::mysql::error_code;
36using u8vec = std::vector<std::uint8_t>;
37
38BOOST_AUTO_TEST_SUITE(test_algo_runner)
39
40void transfer(span<std::uint8_t> buff, span<const std::uint8_t> bytes)
41{
42 assert(buff.size() >= bytes.size());
43 std::memcpy(dest: buff.data(), src: bytes.data(), n: bytes.size());
44}
45
46void transfer(span<std::uint8_t> buff, const std::vector<std::uint8_t>& bytes)
47{
48 transfer(buff, bytes: boost::span<const std::uint8_t>(bytes));
49}
50
51const u8vec msg1{0x01, 0x02, 0x03};
52const u8vec msg2(50, 0x04);
53
54BOOST_AUTO_TEST_CASE(read_cached)
55{
56 struct mock_algo : sansio_algorithm
57 {
58 coroutine coro;
59 std::uint8_t seqnum{};
60
61 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
62
63 next_action resume(error_code ec)
64 {
65 BOOST_ASIO_CORO_REENTER(coro)
66 {
67 BOOST_TEST(ec == error_code());
68 BOOST_ASIO_CORO_YIELD return read(seqnum);
69 BOOST_TEST(ec == error_code());
70 BOOST_TEST(seqnum == 1u);
71 BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st_->reader.message(), msg1);
72 BOOST_ASIO_CORO_YIELD return read(seqnum);
73 BOOST_TEST(ec == error_code());
74 BOOST_TEST(seqnum == 2u);
75 BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st_->reader.message(), msg2);
76 }
77 return next_action();
78 }
79 };
80
81 connection_state_data st(512);
82 mock_algo algo(st);
83 algo_runner runner(algo);
84
85 // Initial run yields a read request. We don't have cached data, so run_op returns it
86 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
87 BOOST_TEST(act.type() == next_action::type_t::read);
88 BOOST_TEST(act.read_args().buffer.data() == st.reader.buffer().data());
89 BOOST_TEST(act.read_args().buffer.size() == st.reader.buffer().size());
90 BOOST_TEST(!act.read_args().use_ssl);
91
92 // Acknowledge the read request
93 auto bytes = concat_copy(lhs: create_frame(seqnum: 0, body: msg1), rhs: create_frame(seqnum: 1, body: msg2));
94 transfer(buff: act.read_args().buffer, bytes);
95 act = runner.resume(ec: error_code(), bytes_transferred: bytes.size());
96
97 // The second read request is acknowledged directly, since it has cached data
98 BOOST_TEST(act.success());
99}
100
101BOOST_AUTO_TEST_CASE(read_short_and_buffer_resizing)
102{
103 struct mock_algo : sansio_algorithm
104 {
105 coroutine coro;
106 std::uint8_t seqnum{};
107
108 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
109
110 next_action resume(error_code ec)
111 {
112 BOOST_ASIO_CORO_REENTER(coro)
113 {
114 BOOST_TEST(ec == error_code());
115 BOOST_ASIO_CORO_YIELD return read(seqnum);
116 BOOST_TEST(ec == error_code());
117 BOOST_TEST(seqnum == 1u);
118 BOOST_MYSQL_ASSERT_BUFFER_EQUALS(st_->reader.message(), msg2);
119 }
120 return next_action();
121 }
122 };
123
124 connection_state_data st(0);
125 mock_algo algo(st);
126 algo_runner runner(algo);
127
128 // Initial run yields a read request and resizes the buffer aprorpiately
129 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
130 BOOST_TEST(act.type() == next_action::type_t::read);
131 BOOST_TEST(act.read_args().buffer.data() == st.reader.buffer().data());
132 BOOST_TEST(act.read_args().buffer.size() == st.reader.buffer().size());
133 BOOST_TEST(!act.read_args().use_ssl);
134
135 // Acknowledge the read request. There is space for the header, at least
136 auto bytes = create_frame(seqnum: 0, body: msg2);
137 transfer(buff: act.read_args().buffer, bytes: span<const std::uint8_t>(bytes.data(), 4));
138 act = runner.resume(ec: error_code(), bytes_transferred: 4);
139
140 // The read request wasn't completely satisified, so more bytes are asked for
141 BOOST_TEST(act.type() == next_action::type_t::read);
142
143 // Read part of the body
144 transfer(buff: act.read_args().buffer, bytes: span<const std::uint8_t>(bytes.data() + 4, 10));
145 act = runner.resume(ec: error_code(), bytes_transferred: 10);
146 BOOST_TEST(act.type() == next_action::type_t::read);
147
148 // Complete
149 transfer(buff: act.read_args().buffer, bytes: span<const std::uint8_t>(bytes.data() + 14, bytes.size() - 14));
150 act = runner.resume(ec: error_code(), bytes_transferred: bytes.size() - 14);
151 BOOST_TEST(act.success());
152}
153
154BOOST_AUTO_TEST_CASE(read_parsing_error)
155{
156 struct mock_algo : sansio_algorithm
157 {
158 coroutine coro;
159 std::uint8_t seqnum{42u};
160
161 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
162
163 next_action resume(error_code ec)
164 {
165 BOOST_ASIO_CORO_REENTER(coro)
166 {
167 BOOST_TEST(ec == error_code());
168 BOOST_ASIO_CORO_YIELD return read(seqnum);
169 BOOST_TEST(ec == error_code(client_errc::sequence_number_mismatch));
170 }
171 return next_action();
172 }
173 };
174
175 connection_state_data st(512);
176 mock_algo algo(st);
177 algo_runner runner(algo);
178
179 // Initial run yields a read request. We don't have cached data, so run_op returns it
180 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
181 BOOST_TEST(act.type() == next_action::type_t::read);
182
183 // Acknowledge the read request. This causes a seqnum mismatch that is transmitted to the op
184 auto bytes = create_frame(seqnum: 0, body: msg1);
185 transfer(buff: act.read_args().buffer, bytes);
186 act = runner.resume(ec: error_code(), bytes_transferred: bytes.size());
187
188 // Op done
189 BOOST_TEST(act.success());
190}
191
192BOOST_AUTO_TEST_CASE(read_io_error)
193{
194 struct mock_algo : sansio_algorithm
195 {
196 coroutine coro;
197 std::uint8_t seqnum{};
198
199 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
200
201 next_action resume(error_code ec)
202 {
203 BOOST_ASIO_CORO_REENTER(coro)
204 {
205 BOOST_TEST(ec == error_code());
206 BOOST_ASIO_CORO_YIELD return read(seqnum);
207 BOOST_TEST(ec == error_code(client_errc::wrong_num_params));
208 }
209 return next_action();
210 }
211 };
212
213 connection_state_data st(512);
214 mock_algo algo(st);
215 algo_runner runner(algo);
216
217 // Initial run yields a read request. We don't have cached data, so run_op returns it
218 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
219 BOOST_TEST(act.type() == next_action::type_t::read);
220
221 // Read request fails with an error
222 act = runner.resume(ec: client_errc::wrong_num_params, bytes_transferred: 0);
223
224 // Op done
225 BOOST_TEST(act.success());
226}
227
228BOOST_AUTO_TEST_CASE(read_ssl_active)
229{
230 struct mock_algo : sansio_algorithm
231 {
232 std::uint8_t seqnum{};
233
234 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
235
236 next_action resume(error_code ec)
237 {
238 BOOST_TEST(ec == error_code());
239 return read(seqnum);
240 }
241 };
242
243 connection_state_data st(512);
244 mock_algo algo(st);
245 algo_runner runner(algo);
246 st.ssl = ssl_state::active;
247
248 // Yielding a read with ssl active sets the use_ssl flag
249 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
250 BOOST_TEST(act.type() == next_action::type_t::read);
251 BOOST_TEST(act.read_args().buffer.data() == st.reader.buffer().data());
252 BOOST_TEST(act.read_args().buffer.size() == st.reader.buffer().size());
253 BOOST_TEST(act.read_args().use_ssl);
254}
255
256BOOST_AUTO_TEST_CASE(write_short)
257{
258 struct mock_algo : sansio_algorithm
259 {
260 coroutine coro;
261 std::uint8_t seqnum{};
262
263 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
264
265 next_action resume(error_code ec)
266 {
267 BOOST_ASIO_CORO_REENTER(coro)
268 {
269 BOOST_TEST(ec == error_code());
270 BOOST_ASIO_CORO_YIELD return write(msg: mock_message{.data: msg1}, seqnum);
271 BOOST_TEST(ec == error_code());
272 BOOST_TEST(seqnum == 1u);
273 }
274 return next_action();
275 }
276 };
277
278 connection_state_data st(0);
279 mock_algo algo(st);
280 algo_runner runner(algo);
281
282 // Initial run yields a write request
283 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
284 BOOST_TEST(act.type() == next_action::type_t::write);
285 BOOST_MYSQL_ASSERT_BUFFER_EQUALS(act.write_args().buffer, create_frame(0, msg1));
286 BOOST_TEST(!act.write_args().use_ssl);
287
288 // Acknowledge part of the write. This will ask for more bytes to be written
289 act = runner.resume(ec: error_code(), bytes_transferred: 4);
290 BOOST_TEST(act.type() == next_action::type_t::write);
291 BOOST_MYSQL_ASSERT_BUFFER_EQUALS(act.write_args().buffer, msg1);
292
293 // Complete
294 act = runner.resume(ec: error_code(), bytes_transferred: 3);
295 BOOST_TEST(act.success());
296}
297
298BOOST_AUTO_TEST_CASE(write_io_error)
299{
300 struct mock_algo : sansio_algorithm
301 {
302 coroutine coro;
303 std::uint8_t seqnum{};
304
305 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
306
307 next_action resume(error_code ec)
308 {
309 BOOST_ASIO_CORO_REENTER(coro)
310 {
311 BOOST_TEST(ec == error_code());
312 BOOST_ASIO_CORO_YIELD return write(msg: mock_message{.data: msg1}, seqnum);
313 BOOST_TEST(ec == error_code(client_errc::wrong_num_params));
314 }
315 return next_action();
316 }
317 };
318
319 connection_state_data st(0);
320 mock_algo algo(st);
321 algo_runner runner(algo);
322
323 // Initial run yields a write request. Fail it
324 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
325 BOOST_TEST(act.type() == next_action::type_t::write);
326 act = runner.resume(ec: client_errc::wrong_num_params, bytes_transferred: 0);
327
328 // Done
329 BOOST_TEST(act.success());
330}
331
332BOOST_AUTO_TEST_CASE(write_ssl_active)
333{
334 struct mock_algo : sansio_algorithm
335 {
336 std::uint8_t seqnum{};
337
338 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
339
340 next_action resume(error_code ec)
341 {
342 BOOST_TEST(ec == error_code());
343 return write(msg: mock_message{.data: msg1}, seqnum);
344 }
345 };
346
347 connection_state_data st(0);
348 mock_algo algo(st);
349 algo_runner runner(algo);
350 st.ssl = ssl_state::active;
351
352 // Yielding a write request when ssl_active() returns an action with the flag set
353 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
354 BOOST_TEST(act.type() == next_action::type_t::write);
355 BOOST_MYSQL_ASSERT_BUFFER_EQUALS(act.write_args().buffer, create_frame(0, msg1));
356 BOOST_TEST(act.write_args().use_ssl);
357}
358
359BOOST_AUTO_TEST_CASE(ssl_handshake)
360{
361 struct mock_algo : sansio_algorithm
362 {
363 boost::asio::coroutine coro;
364
365 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
366
367 next_action resume(error_code ec)
368 {
369 BOOST_ASIO_CORO_REENTER(coro)
370 {
371 BOOST_TEST(ec == error_code());
372 BOOST_ASIO_CORO_YIELD return next_action::ssl_handshake();
373 BOOST_TEST(ec == error_code(client_errc::wrong_num_params));
374 }
375 return next_action();
376 }
377 };
378
379 connection_state_data st(0);
380 mock_algo algo(st);
381 algo_runner runner(algo);
382
383 // Initial run yields a SSL handshake request. These are always returned
384 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
385 BOOST_TEST(act.type() == next_action::type_t::ssl_handshake);
386
387 // Fail the op
388 act = runner.resume(ec: client_errc::wrong_num_params, bytes_transferred: 0);
389
390 // Done
391 BOOST_TEST(act.success());
392}
393
394BOOST_AUTO_TEST_CASE(ssl_shutdown)
395{
396 struct mock_algo : sansio_algorithm
397 {
398 coroutine coro;
399
400 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
401
402 next_action resume(error_code ec)
403 {
404 BOOST_ASIO_CORO_REENTER(coro)
405 {
406 BOOST_TEST(ec == error_code());
407 BOOST_ASIO_CORO_YIELD return next_action::ssl_shutdown();
408 BOOST_TEST(ec == error_code(client_errc::wrong_num_params));
409 }
410 return next_action();
411 }
412 };
413
414 connection_state_data st(0);
415 mock_algo algo(st);
416 algo_runner runner(algo);
417
418 // Initial run yields a SSL handshake request. These are always returned
419 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
420 BOOST_TEST(act.type() == next_action::type_t::ssl_shutdown);
421
422 // Fail the op
423 act = runner.resume(ec: client_errc::wrong_num_params, bytes_transferred: 0);
424
425 // Done
426 BOOST_TEST(act.success());
427}
428
429BOOST_AUTO_TEST_CASE(connect)
430{
431 struct mock_algo : sansio_algorithm
432 {
433 boost::asio::coroutine coro;
434
435 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
436
437 next_action resume(error_code ec)
438 {
439 BOOST_ASIO_CORO_REENTER(coro)
440 {
441 BOOST_TEST(ec == error_code());
442 BOOST_ASIO_CORO_YIELD return next_action::connect();
443 BOOST_TEST(ec == error_code(client_errc::wrong_num_params));
444 }
445 return next_action();
446 }
447 };
448
449 connection_state_data st(0);
450 mock_algo algo(st);
451 algo_runner runner(algo);
452
453 // Initial run yields a connect request. These are always returned
454 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
455 BOOST_TEST(act.type() == next_action::type_t::connect);
456
457 // Fail the op
458 act = runner.resume(ec: client_errc::wrong_num_params, bytes_transferred: 0);
459
460 // Done
461 BOOST_TEST(act.success());
462}
463
464BOOST_AUTO_TEST_CASE(close)
465{
466 struct mock_algo : sansio_algorithm
467 {
468 boost::asio::coroutine coro;
469
470 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
471
472 next_action resume(error_code ec)
473 {
474 BOOST_ASIO_CORO_REENTER(coro)
475 {
476 BOOST_TEST(ec == error_code());
477 BOOST_ASIO_CORO_YIELD return next_action::close();
478 BOOST_TEST(ec == error_code(client_errc::wrong_num_params));
479 }
480 return next_action();
481 }
482 };
483
484 connection_state_data st(0);
485 mock_algo algo(st);
486 algo_runner runner(algo);
487
488 // Initial run yields a close request. These are always returned
489 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
490 BOOST_TEST(act.type() == next_action::type_t::close);
491
492 // Fail the op
493 act = runner.resume(ec: client_errc::wrong_num_params, bytes_transferred: 0);
494
495 // Done
496 BOOST_TEST(act.success());
497}
498
499BOOST_AUTO_TEST_CASE(immediate_completion)
500{
501 struct mock_algo : sansio_algorithm
502 {
503 boost::asio::coroutine coro;
504
505 mock_algo(connection_state_data& st) : sansio_algorithm(st) {}
506
507 next_action resume(error_code ec)
508 {
509 BOOST_ASIO_CORO_REENTER(coro)
510 {
511 BOOST_TEST(ec == error_code());
512 BOOST_ASIO_CORO_YIELD return next_action();
513 BOOST_TEST(false); // Should never be called again after next_action() is returned
514 }
515 return next_action();
516 }
517 };
518
519 connection_state_data st(0);
520 mock_algo algo(st);
521 algo_runner runner(algo);
522
523 // Initial run yields completion
524 auto act = runner.resume(ec: error_code(), bytes_transferred: 0);
525 BOOST_TEST(act.success());
526}
527
528BOOST_AUTO_TEST_SUITE_END()
529

source code of boost/libs/mysql/test/unit/test/sansio/algo_runner.cpp