diff --git a/include/boost/redis/connection.hpp b/include/boost/redis/connection.hpp index cf626a06..63fceda1 100644 --- a/include/boost/redis/connection.hpp +++ b/include/boost/redis/connection.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -217,74 +218,49 @@ struct reader_op { static constexpr std::size_t buffer_growth_hint = 4096; Conn* conn_; - std::pair res_{std::make_pair(std::nullopt, 0)}; - asio::coroutine coro{}; + detail::reader_fsm fsm_; + +public: + reader_op(Conn& conn) noexcept + : conn_{&conn} + , fsm_{conn.mpx_} + { } template void operator()(Self& self, system::error_code ec = {}, std::size_t n = 0) { - BOOST_ASIO_CORO_REENTER(coro) for (;;) - { - // Appends some data to the buffer if necessary. - BOOST_ASIO_CORO_YIELD - async_append_some( - conn_->stream_, - dyn_buffer_type{conn_->mpx_.get_read_buffer(), conn_->cfg_.max_read_size}, - conn_->mpx_.get_parser().get_suggested_buffer_growth(buffer_growth_hint), - std::move(self)); + using dyn_buffer_type = asio::dynamic_string_buffer< + char, + std::char_traits, + std::allocator>; - conn_->logger_.on_read(ec, n); + for (;;) { + auto act = fsm_.resume(n, ec, self.get_cancellation_state().cancelled()); - // The connection is not viable after an error. - if (ec) { - conn_->logger_.trace("reader_op (1)", ec); - conn_->cancel(operation::run); - self.complete(ec); - return; - } + conn_->logger_.on_fsm_resume(act); - // The connection might have been canceled while this op was - // suspended or after queueing so we have to check. - if (!conn_->is_open()) { - conn_->logger_.trace("reader_op (2): connection is closed."); - self.complete(ec); - return; - } - - while (!conn_->mpx_.get_read_buffer().empty()) { - res_ = conn_->mpx_.consume_next(ec); - - if (ec) { - conn_->logger_.trace("reader_op (3)", ec); - conn_->cancel(operation::run); - self.complete(ec); + switch (act.type_) { + case reader_fsm::action::type::setup_cancellation: + self.reset_cancellation_state(asio::enable_terminal_cancellation()); + continue; + case reader_fsm::action::type::needs_more: + case reader_fsm::action::type::append_some: + async_append_some( + conn_->stream_, + dyn_buffer_type{conn_->mpx_.get_read_buffer(), conn_->cfg_.max_read_size}, + conn_->mpx_.get_parser().get_suggested_buffer_growth(buffer_growth_hint), + std::move(self)); return; - } - - if (!res_.first.has_value()) { - // More data is needed. - break; - } - - if (res_.first.value()) { - if (!conn_->receive_channel_.try_send(ec, res_.second)) { - BOOST_ASIO_CORO_YIELD - conn_->receive_channel_.async_send(ec, res_.second, std::move(self)); - } - - if (ec) { - conn_->logger_.trace("reader_op (4)", ec); - conn_->cancel(operation::run); - self.complete(ec); + case reader_fsm::action::type::notify_push_receiver: + if (conn_->receive_channel_.try_send(ec, act.push_size_)) { + continue; + } else { + conn_->receive_channel_.async_send(ec, act.push_size_, std::move(self)); return; } - - if (!conn_->is_open()) { - conn_->logger_.trace("reader_op (5): connection is closed."); - self.complete(asio::error::operation_aborted); - return; - } - } + return; + case reader_fsm::action::type::cancel_run: conn_->cancel(operation::run); continue; + case reader_fsm::action::type::done: self.complete(act.ec_); return; } } } @@ -844,7 +820,7 @@ private: auto reader(CompletionToken&& token) { return asio::async_compose( - detail::reader_op{this}, + detail::reader_op{*this}, std::forward(token), writer_timer_); } diff --git a/include/boost/redis/detail/connection_logger.hpp b/include/boost/redis/detail/connection_logger.hpp index 51fd24f6..34d6a4b7 100644 --- a/include/boost/redis/detail/connection_logger.hpp +++ b/include/boost/redis/detail/connection_logger.hpp @@ -7,6 +7,7 @@ #ifndef BOOST_REDIS_CONNECTION_LOGGER_HPP #define BOOST_REDIS_CONNECTION_LOGGER_HPP +#include #include #include @@ -37,7 +38,7 @@ public: void on_connect(system::error_code const& ec, std::string_view unix_socket_ep); void on_ssl_handshake(system::error_code const& ec); void on_write(system::error_code const& ec, std::size_t n); - void on_read(system::error_code const& ec, std::size_t n); + void on_fsm_resume(reader_fsm::action const& action); void on_hello(system::error_code const& ec, generic_response const& resp); void log(logger::level lvl, std::string_view msg); void log(logger::level lvl, std::string_view op, system::error_code const& ec); diff --git a/include/boost/redis/detail/coroutine.hpp b/include/boost/redis/detail/coroutine.hpp index f8fe22d5..62ee508b 100644 --- a/include/boost/redis/detail/coroutine.hpp +++ b/include/boost/redis/detail/coroutine.hpp @@ -25,7 +25,7 @@ #define BOOST_REDIS_YIELD(resume_point_var, resume_point_id, ...) \ { \ resume_point_var = resume_point_id; \ - return __VA_ARGS__; \ + return {__VA_ARGS__}; \ case resume_point_id: \ { \ } \ diff --git a/include/boost/redis/detail/reader_fsm.hpp b/include/boost/redis/detail/reader_fsm.hpp new file mode 100644 index 00000000..575ee97f --- /dev/null +++ b/include/boost/redis/detail/reader_fsm.hpp @@ -0,0 +1,54 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#ifndef BOOST_REDIS_READER_FSM_HPP +#define BOOST_REDIS_READER_FSM_HPP + +#include + +#include +#include + +#include + +namespace boost::redis::detail { + +class reader_fsm { +public: + struct action { + enum class type + { + setup_cancellation, + append_some, + needs_more, + notify_push_receiver, + cancel_run, + done, + }; + + type type_ = type::setup_cancellation; + std::size_t push_size_ = 0; + system::error_code ec_ = {}; + }; + + explicit reader_fsm(multiplexer& mpx) noexcept; + + action resume( + std::size_t bytes_read, + system::error_code ec, + asio::cancellation_type_t /*cancel_state*/); + +private: + int resume_point_{0}; + action action_after_resume_; + action::type next_read_type_ = action::type::append_some; + multiplexer* mpx_ = nullptr; + std::pair res_{std::make_pair(std::nullopt, 0)}; +}; + +} // namespace boost::redis::detail + +#endif // BOOST_REDIS_READER_FSM_HPP diff --git a/include/boost/redis/impl/connection_logger.ipp b/include/boost/redis/impl/connection_logger.ipp index 8610bc73..54f8b33f 100644 --- a/include/boost/redis/impl/connection_logger.ipp +++ b/include/boost/redis/impl/connection_logger.ipp @@ -1,4 +1,4 @@ -/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com) +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) * * Distributed under the Boost Software License, Version 1.0. (See * accompanying file LICENSE.txt) @@ -14,6 +14,38 @@ namespace boost::redis::detail { +#define BOOST_REDIS_READER_SWITCH_CASE(elem) \ + case reader_fsm::action::type::elem: return "reader_fsm::action::type::" #elem + +#define BOOST_REDIS_EXEC_SWITCH_CASE(elem) \ + case exec_action_type::elem: return "exec_action_type::" #elem + +auto to_string(reader_fsm::action::type t) noexcept -> char const* +{ + switch (t) { + BOOST_REDIS_READER_SWITCH_CASE(setup_cancellation); + BOOST_REDIS_READER_SWITCH_CASE(append_some); + BOOST_REDIS_READER_SWITCH_CASE(needs_more); + BOOST_REDIS_READER_SWITCH_CASE(notify_push_receiver); + BOOST_REDIS_READER_SWITCH_CASE(cancel_run); + BOOST_REDIS_READER_SWITCH_CASE(done); + default: return "action::type::"; + } +} + +auto to_string(exec_action_type t) noexcept -> char const* +{ + switch (t) { + BOOST_REDIS_EXEC_SWITCH_CASE(setup_cancellation); + BOOST_REDIS_EXEC_SWITCH_CASE(immediate); + BOOST_REDIS_EXEC_SWITCH_CASE(done); + BOOST_REDIS_EXEC_SWITCH_CASE(notify_writer); + BOOST_REDIS_EXEC_SWITCH_CASE(wait_for_response); + BOOST_REDIS_EXEC_SWITCH_CASE(cancel_run); + default: return "exec_action_type::"; + } +} + inline void format_tcp_endpoint(const asio::ip::tcp::endpoint& ep, std::string& to) { // This formatting is inspired by Asio's endpoint operator<< @@ -125,20 +157,21 @@ void connection_logger::on_write(system::error_code const& ec, std::size_t n) logger_.fn(logger::level::info, msg_); } -void connection_logger::on_read(system::error_code const& ec, std::size_t n) +void connection_logger::on_fsm_resume(reader_fsm::action const& action) { - if (logger_.lvl < logger::level::info) + if (logger_.lvl < logger::level::debug) return; - msg_ = "reader_op: "; - if (ec) { - format_error_code(ec, msg_); - } else { - msg_ += std::to_string(n); - msg_ += " bytes read."; - } + std::string msg; + msg += "("; + msg += to_string(action.type_); + msg += ", "; + msg += std::to_string(action.push_size_); + msg += ", "; + msg += action.ec_.message(); + msg += ")"; - logger_.fn(logger::level::info, msg_); + logger_.fn(logger::level::debug, msg); } void connection_logger::on_hello(system::error_code const& ec, generic_response const& resp) @@ -180,4 +213,4 @@ void connection_logger::log(logger::level lvl, std::string_view op, system::erro logger_.fn(lvl, msg_); } -} // namespace boost::redis::detail \ No newline at end of file +} // namespace boost::redis::detail diff --git a/include/boost/redis/impl/exec_fsm.ipp b/include/boost/redis/impl/exec_fsm.ipp index 658d330c..25675e02 100644 --- a/include/boost/redis/impl/exec_fsm.ipp +++ b/include/boost/redis/impl/exec_fsm.ipp @@ -25,11 +25,7 @@ inline bool is_cancellation(asio::cancellation_type_t type) asio::cancellation_type_t::terminal)); } -} // namespace boost::redis::detail - -boost::redis::detail::exec_action boost::redis::detail::exec_fsm::resume( - bool connection_is_open, - asio::cancellation_type_t cancel_state) +exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t cancel_state) { switch (resume_point_) { BOOST_REDIS_CORO_INITIAL @@ -91,4 +87,6 @@ boost::redis::detail::exec_action boost::redis::detail::exec_fsm::resume( return exec_action{system::error_code()}; } -#endif \ No newline at end of file +} // namespace boost::redis::detail + +#endif diff --git a/include/boost/redis/impl/reader_fsm.ipp b/include/boost/redis/impl/reader_fsm.ipp new file mode 100644 index 00000000..b248f315 --- /dev/null +++ b/include/boost/redis/impl/reader_fsm.ipp @@ -0,0 +1,74 @@ +/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include +#include + +namespace boost::redis::detail { + +reader_fsm::reader_fsm(multiplexer& mpx) noexcept +: mpx_{&mpx} +{ } + +reader_fsm::action reader_fsm::resume( + std::size_t bytes_read, + system::error_code ec, + asio::cancellation_type_t /*cancel_state*/) +{ + switch (resume_point_) { + BOOST_REDIS_CORO_INITIAL + BOOST_REDIS_YIELD(resume_point_, 1, action::type::setup_cancellation) + + for (;;) { + BOOST_REDIS_YIELD(resume_point_, 2, next_read_type_) + if (ec) { + // TODO: If an error occurred but data was read (i.e. + // bytes_read != 0) we should try to process that data and + // deliver it to the user before calling cancel_run. + action_after_resume_ = {action::type::done, bytes_read, ec}; + BOOST_REDIS_YIELD(resume_point_, 3, action::type::cancel_run) + return action_after_resume_; + } + + next_read_type_ = action::type::append_some; + while (!mpx_->get_read_buffer().empty()) { + res_ = mpx_->consume_next(ec); + if (ec) { + action_after_resume_ = {action::type::done, res_.second, ec}; + BOOST_REDIS_YIELD(resume_point_, 4, action::type::cancel_run) + return action_after_resume_; + } + + if (!res_.first.has_value()) { + next_read_type_ = action::type::needs_more; + break; + } + + if (res_.first.value()) { + BOOST_REDIS_YIELD(resume_point_, 6, action::type::notify_push_receiver, res_.second) + if (ec) { + action_after_resume_ = {action::type::done, 0u, ec}; + BOOST_REDIS_YIELD(resume_point_, 7, action::type::cancel_run) + return action_after_resume_; + } + } else { + // TODO: Here we should notify the exec operation that + // it can be completed. This will improve log clarity + // and will make this code symmetrical in how it + // handles pushes and other messages. The new action + // type can be named notify_exec. To do that we need to + // refactor the multiplexer. + } + } + } + } + + BOOST_ASSERT(false); + return {action::type::done, 0, system::error_code()}; +} + +} // namespace boost::redis::detail diff --git a/include/boost/redis/src.hpp b/include/boost/redis/src.hpp index 687cb3ca..f3a2ac4b 100644 --- a/include/boost/redis/src.hpp +++ b/include/boost/redis/src.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ae31f0a2..ef7bd4a9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -40,6 +40,7 @@ make_test(test_any_adapter) make_test(test_exec_fsm) make_test(test_log_to_file) make_test(test_conn_logging) +make_test(test_reader_fsm) # Tests that require a real Redis server make_test(test_conn_quit) diff --git a/test/Jamfile b/test/Jamfile index 311d1ed5..f5425573 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -56,6 +56,7 @@ local tests = test_exec_fsm test_log_to_file test_conn_logging + test_reader_fsm ; # Build and run the tests diff --git a/test/test_conn_push.cpp b/test/test_conn_push.cpp index e4eae755..8533fc0b 100644 --- a/test/test_conn_push.cpp +++ b/test/test_conn_push.cpp @@ -331,4 +331,4 @@ BOOST_AUTO_TEST_CASE(many_subscribers) BOOST_TEST(finished); } -} // namespace \ No newline at end of file +} // namespace diff --git a/test/test_exec_fsm.cpp b/test/test_exec_fsm.cpp index f0b034ed..32b9a72b 100644 --- a/test/test_exec_fsm.cpp +++ b/test/test_exec_fsm.cpp @@ -33,16 +33,12 @@ using boost::asio::cancellation_type_t; // Operators namespace boost::redis::detail { +extern auto to_string(exec_action_type t) noexcept -> char const*; + std::ostream& operator<<(std::ostream& os, exec_action_type type) { - switch (type) { - case exec_action_type::immediate: return os << "exec_action_type::immediate"; - case exec_action_type::done: return os << "exec_action_type::done"; - case exec_action_type::notify_writer: return os << "exec_action_type::notify_writer"; - case exec_action_type::wait_for_response: return os << "exec_action_type::wait_for_response"; - case exec_action_type::cancel_run: return os << "exec_action_type::cancel_run"; - default: return os << ""; - } + os << to_string(type); + return os; } bool operator==(exec_action lhs, exec_action rhs) noexcept diff --git a/test/test_reader_fsm.cpp b/test/test_reader_fsm.cpp new file mode 100644 index 00000000..4aa0c430 --- /dev/null +++ b/test/test_reader_fsm.cpp @@ -0,0 +1,233 @@ +// +// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include + +#include +#include +#include +#include + +namespace net = boost::asio; +namespace redis = boost::redis; +using boost::system::error_code; +using net::cancellation_type_t; +using redis::detail::reader_fsm; +using redis::detail::multiplexer; +using redis::generic_response; +using action = redis::detail::reader_fsm::action; + +namespace boost::redis::detail { + +extern auto to_string(reader_fsm::action::type t) noexcept -> char const*; + +std::ostream& operator<<(std::ostream& os, reader_fsm::action::type t) +{ + os << to_string(t); + return os; +} +} // namespace boost::redis::detail + +// Operators +namespace { + +void test_push() +{ + multiplexer mpx; + generic_response resp; + mpx.set_receive_response(resp); + reader_fsm fsm{mpx}; + error_code ec; + action act; + + // Initiate + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::setup_cancellation); + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::append_some); + + // The fsm is asking for data. + mpx.get_read_buffer().append(">1\r\n+msg1\r\n"); + mpx.get_read_buffer().append(">1\r\n+msg2 \r\n"); + mpx.get_read_buffer().append(">1\r\n+msg3 \r\n"); + auto const bytes_read = mpx.get_read_buffer().size(); + + // Deliver the 1st push + act = fsm.resume(bytes_read, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver); + BOOST_TEST_EQ(act.push_size_, 11u); + BOOST_TEST_EQ(act.ec_, error_code()); + + // Deliver the 2st push + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver); + BOOST_TEST_EQ(act.push_size_, 12u); + BOOST_TEST_EQ(act.ec_, error_code()); + + // Deliver the 3rd push + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver); + BOOST_TEST_EQ(act.push_size_, 13u); + BOOST_TEST_EQ(act.ec_, error_code()); + + // All pushes were delivered so the fsm should demand more data + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::append_some); + BOOST_TEST_EQ(act.ec_, error_code()); +} + +void test_read_needs_more() +{ + multiplexer mpx; + generic_response resp; + mpx.set_receive_response(resp); + reader_fsm fsm{mpx}; + error_code ec; + action act; + + // Initiate + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::setup_cancellation); + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::append_some); + + // Split the incoming message in three random parts and deliver + // them to the reader individually. + std::string const msg[] = {">3\r", "\n+msg1\r\n+ms", "g2\r\n+msg3\r\n"}; + + // Passes the first part to the fsm. + mpx.get_read_buffer().append(msg[0]); + act = fsm.resume(msg[0].size(), ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::needs_more); + BOOST_TEST_EQ(act.ec_, error_code()); + + // Passes the second part to the fsm. + mpx.get_read_buffer().append(msg[1]); + act = fsm.resume(msg[1].size(), ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::needs_more); + BOOST_TEST_EQ(act.ec_, error_code()); + + // Passes the third and last part to the fsm, next it should ask us + // to deliver the message. + mpx.get_read_buffer().append(msg[2]); + act = fsm.resume(msg[2].size(), ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver); + BOOST_TEST_EQ(act.push_size_, msg[0].size() + msg[1].size() + msg[2].size()); + BOOST_TEST_EQ(act.ec_, error_code()); + + // All pushes were delivered so the fsm should demand more data + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::append_some); + BOOST_TEST_EQ(act.ec_, error_code()); +} + +void test_read_error() +{ + multiplexer mpx; + generic_response resp; + mpx.set_receive_response(resp); + reader_fsm fsm{mpx}; + error_code ec; + action act; + + // Initiate + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::setup_cancellation); + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::append_some); + + // The fsm is asking for data. + mpx.get_read_buffer().append(">1\r\n+msg1\r\n"); + auto const bytes_read = mpx.get_read_buffer().size(); + + // Deliver the data + act = fsm.resume(bytes_read, {net::error::operation_aborted}, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::cancel_run); + BOOST_TEST_EQ(act.ec_, error_code()); + + // Finish + act = fsm.resume(bytes_read, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::done); + BOOST_TEST_EQ(act.ec_, error_code{net::error::operation_aborted}); +} + +void test_parse_error() +{ + multiplexer mpx; + generic_response resp; + mpx.set_receive_response(resp); + reader_fsm fsm{mpx}; + error_code ec; + action act; + + // Initiate + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::setup_cancellation); + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::append_some); + + // The fsm is asking for data. + mpx.get_read_buffer().append(">a\r\n"); + auto const bytes_read = mpx.get_read_buffer().size(); + + // Deliver the data + act = fsm.resume(bytes_read, {}, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::cancel_run); + BOOST_TEST_EQ(act.ec_, error_code()); + + // Finish + act = fsm.resume(bytes_read, {}, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::done); + BOOST_TEST_EQ(act.ec_, error_code{redis::error::not_a_number}); +} + +void test_push_deliver_error() +{ + multiplexer mpx; + generic_response resp; + mpx.set_receive_response(resp); + reader_fsm fsm{mpx}; + error_code ec; + action act; + + // Initiate + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::setup_cancellation); + act = fsm.resume(0, ec, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::append_some); + + // The fsm is asking for data. + mpx.get_read_buffer().append(">1\r\n+msg1\r\n"); + auto const bytes_read = mpx.get_read_buffer().size(); + + // Deliver the data + act = fsm.resume(bytes_read, {}, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::notify_push_receiver); + BOOST_TEST_EQ(act.ec_, error_code()); + + // Resumes from notifying a push with an error. + act = fsm.resume(bytes_read, net::error::operation_aborted, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::cancel_run); + + // Finish + act = fsm.resume(0, {}, cancellation_type_t::none); + BOOST_TEST_EQ(act.type_, action::type::done); + BOOST_TEST_EQ(act.ec_, error_code{net::error::operation_aborted}); +} + +} // namespace + +int main() +{ + test_push_deliver_error(); + test_read_needs_more(); + test_push(); + test_read_error(); + test_parse_error(); + + return boost::report_errors(); +}