diff --git a/CMakeLists.txt b/CMakeLists.txt index 35df41fc..967f1ccd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ if (BOOST_REDIS_MAIN_PROJECT) test json endian + compat ) foreach(dep IN LISTS deps) diff --git a/include/boost/redis/adapter/detail/adapters.hpp b/include/boost/redis/adapter/detail/adapters.hpp index 7830f0ed..8419e220 100644 --- a/include/boost/redis/adapter/detail/adapters.hpp +++ b/include/boost/redis/adapter/detail/adapters.hpp @@ -9,10 +9,10 @@ #include #include +#include #include #include #include -#include #include #include @@ -216,7 +216,12 @@ public: : tree_(c) { } - void on_init() { } + void on_init() + { + if (tree_->has_value()) { + tree_->value().notify_init(); + } + } void on_done() { BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer"); @@ -255,11 +260,8 @@ public: : tree_(c) { } - void on_init() { } - void on_done() - { - tree_->notify_done(); - } + void on_init() { tree_->notify_init(); } + void on_done() { tree_->notify_done(); } template void on_node(resp3::basic_node const& nd, system::error_code&) diff --git a/include/boost/redis/impl/flat_tree.ipp b/include/boost/redis/impl/flat_tree.ipp index 73aeabe3..7ee1aac4 100644 --- a/include/boost/redis/impl/flat_tree.ipp +++ b/include/boost/redis/impl/flat_tree.ipp @@ -104,6 +104,27 @@ inline void grow(flat_buffer& buff, std::size_t new_capacity, view_tree& nodes) ++buff.reallocs; } +// Erases the first num_bytes bytes from the buffer by moving +// the remaining bytes forward. Rebases the strings in nodes as required. +inline void erase_first(flat_buffer& buff, std::size_t num_bytes, view_tree& nodes) +{ + BOOST_ASSERT(num_bytes <= buff.size); + if (num_bytes > 0u) { + // If we have any data to move, we should always have a buffer + BOOST_ASSERT(buff.data.get() != nullptr); + + // Record the old base + const char* old_base = buff.data.get() + num_bytes; + + // Move all that we're gonna keep to the start of the buffer + auto bytes_left = buff.size - num_bytes; + std::memmove(buff.data.get(), old_base, bytes_left); + + // Rebase strings + rebase_strings(nodes, old_base, buff.data.get()); + } +} + // Appends a string to the buffer. // Might rebase the string in nodes, but doesn't append any new node. inline std::string_view append(flat_buffer& buff, std::string_view value, view_tree& nodes) @@ -129,6 +150,8 @@ flat_tree::flat_tree(flat_tree const& other) : data_{detail::copy_construct(other.data_)} , view_tree_{other.view_tree_} , total_msgs_{other.total_msgs_} +, node_tmp_offset_{other.node_tmp_offset_} +, data_tmp_offset_{other.data_tmp_offset_} { detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get()); } @@ -145,6 +168,8 @@ flat_tree& flat_tree::operator=(const flat_tree& other) // Copy the other fields total_msgs_ = other.total_msgs_; + node_tmp_offset_ = other.node_tmp_offset_; + data_tmp_offset_ = other.data_tmp_offset_; } return *this; @@ -161,8 +186,15 @@ void flat_tree::reserve(std::size_t bytes, std::size_t nodes) void flat_tree::clear() noexcept { - data_.size = 0u; - view_tree_.clear(); + // Discard everything except for the tmp area + view_tree_.erase(view_tree_.begin(), view_tree_.begin() + node_tmp_offset_); + node_tmp_offset_ = 0u; + + // Do the same for the data area + detail::erase_first(data_, data_tmp_offset_, view_tree_); + data_tmp_offset_ = 0u; + + // We now have no messages total_msgs_ = 0u; } @@ -180,10 +212,31 @@ void flat_tree::push(node_view const& nd) }); } +void flat_tree::notify_init() +{ + // Discard any data in the tmp area, as it belongs to an operation that never finished + BOOST_ASSERT(node_tmp_offset_ <= view_tree_.size()); + BOOST_ASSERT(data_tmp_offset_ <= data_.size); + view_tree_.resize(node_tmp_offset_); + data_.size = data_tmp_offset_; +} + +void flat_tree::notify_done() +{ + ++total_msgs_; + node_tmp_offset_ = view_tree_.size(); + data_tmp_offset_ = data_.size; +} + bool operator==(flat_tree const& a, flat_tree const& b) { // data is already taken into account by comparing the nodes. - return a.view_tree_ == b.view_tree_ && a.total_msgs_ == b.total_msgs_; + // Only committed nodes should be taken into account. + auto a_nodes = a.get_view(); + auto b_nodes = b.get_view(); + return a_nodes.size() == b_nodes.size() && + std::equal(a_nodes.begin(), a_nodes.end(), b_nodes.begin()) && + a.total_msgs_ == b.total_msgs_; } } // namespace boost::redis::resp3 diff --git a/include/boost/redis/resp3/flat_tree.hpp b/include/boost/redis/resp3/flat_tree.hpp index b0bff101..4c6158c5 100644 --- a/include/boost/redis/resp3/flat_tree.hpp +++ b/include/boost/redis/resp3/flat_tree.hpp @@ -12,6 +12,8 @@ #include #include +#include + #include #include @@ -164,7 +166,16 @@ public: * * @returns The number of bytes in use in the data buffer. */ - auto data_size() const noexcept -> std::size_t { return data_.size; } + auto data_size() const noexcept -> std::size_t { return data_tmp_offset_; } + + /** @brief Returns the capacity of the node container. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns The capacity of the object, in number of nodes. + */ + auto capacity() const noexcept -> std::size_t { return view_tree_.capacity(); } /** @brief Returns the capacity of the data buffer, in bytes. * @@ -187,7 +198,7 @@ public: * * @returns The nodes in the tree. */ - auto get_view() const noexcept -> view_tree const& { return view_tree_; } + span get_view() const noexcept { return {view_tree_.data(), node_tmp_offset_}; } /** @brief Returns the number of memory reallocations that took place in the data buffer. * @@ -215,7 +226,8 @@ public: private: template friend class adapter::detail::general_aggregate; - void notify_done() { ++total_msgs_; } + void notify_init(); + void notify_done(); // Push a new node to the response void push(node_view const& node); @@ -223,6 +235,13 @@ private: detail::flat_buffer data_; view_tree view_tree_; std::size_t total_msgs_ = 0u; + + // flat_tree supports a "temporary working area" for incrementally reading messages. + // Nodes in the tmp area are not part of the object representation until they + // are committed with notify_done(). + // These offsets delimit this area. + std::size_t node_tmp_offset_ = 0u; + std::size_t data_tmp_offset_ = 0u; }; /** diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ea1e4b16..40a1ea05 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -53,6 +53,7 @@ make_test(test_multiplexer) make_test(test_parse_sentinel_response) make_test(test_update_sentinel_list) make_test(test_flat_tree) +make_test(test_generic_flat_response) make_test(test_read_buffer) # Tests that require a real Redis server diff --git a/test/Jamfile b/test/Jamfile index 3bf9b443..3aa72460 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -70,6 +70,7 @@ local tests = test_parse_sentinel_response test_update_sentinel_list test_flat_tree + test_generic_flat_response test_read_buffer ; diff --git a/test/test_conn_exec.cpp b/test/test_conn_exec.cpp index 28a23497..22216f9b 100644 --- a/test/test_conn_exec.cpp +++ b/test/test_conn_exec.cpp @@ -6,6 +6,7 @@ #include #include +#include #include @@ -180,4 +181,31 @@ BOOST_AUTO_TEST_CASE(exec_any_adapter) BOOST_TEST(std::get<0>(res).value() == "PONG"); } +BOOST_AUTO_TEST_CASE(exec_generic_flat_response) +{ + // Executing with a generic_flat_response works + request req; + req.push("PING", "PONG"); + boost::redis::generic_flat_response resp; + + net::io_context ioc; + + auto conn = std::make_shared(ioc); + + bool finished = false; + + conn->async_exec(req, resp, [&](error_code ec, std::size_t) { + BOOST_TEST(ec == error_code()); + conn->cancel(); + finished = true; + }); + + run(conn); + ioc.run_for(test_timeout); + BOOST_TEST_REQUIRE(finished); + + BOOST_TEST(resp.has_value()); + BOOST_TEST(resp->get_view().front().value == "PONG"); +} + } // namespace diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 095af2c7..87b7f804 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,7 @@ using boost::redis::resp3::type; using boost::redis::resp3::detail::deserialize; using boost::redis::resp3::node; using boost::redis::resp3::node_view; +using boost::redis::resp3::parser; using boost::redis::resp3::to_string; using boost::redis::response; using boost::system::error_code; @@ -49,6 +51,20 @@ void add_nodes( std::cerr << "Called from " << loc << std::endl; } +bool parse_checked( + flat_tree& to, + parser& p, + std::string_view data, + boost::source_location loc = BOOST_CURRENT_LOCATION) +{ + error_code ec; + auto adapter = adapt2(to); + bool done = boost::redis::resp3::parse(p, data, adapter, ec); + if (!BOOST_TEST_EQ(ec, error_code{})) + std::cerr << "Called from " << loc << std::endl; + return done; +} + void check_nodes( const flat_tree& tree, boost::span expected, @@ -202,6 +218,116 @@ void test_add_nodes_big_node() BOOST_TEST_EQ(t.get_total_msgs(), 1u); } +// Flat trees have a temporary area (tmp) where nodes are stored while +// messages are being parsed. Nodes in the tmp area are not part of the representation +// until they are committed when the message has been fully parsed +void test_add_nodes_tmp() +{ + flat_tree t; + parser p; + + // Add part of a message, but not all of it. + // These nodes are stored but are not part of the user-facing representation + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // Finish the message. Nodes will now show up + BOOST_TEST(parse_checked(t, p, "*2\r\n+hello\r\n+world\r\n")); + std::vector expected_nodes{ + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello"}, + {type::simple_string, 1u, 1u, "world"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 10u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + // We can repeat this cycle again + p.reset(); + BOOST_TEST_NOT(parse_checked(t, p, ">2\r\n+good\r\n")); + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 10u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + BOOST_TEST(parse_checked(t, p, ">2\r\n+good\r\n+bye\r\n")); + expected_nodes.push_back({type::push, 2u, 0u, ""}); + expected_nodes.push_back({type::simple_string, 1u, 1u, "good"}); + expected_nodes.push_back({type::simple_string, 1u, 1u, "bye"}); + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 17u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_total_msgs(), 2u); +} + +// If there was an unfinished message when another message is started, +// the former is discarded +void test_add_nodes_existing_tmp() +{ + flat_tree t; + parser p; + + // Add part of a message + BOOST_TEST_NOT(parse_checked(t, p, ">3\r\n+some message\r\n")); + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // This message is abandoned, and another one is started + p.reset(); + BOOST_TEST_NOT(parse_checked(t, p, "%66\r\n+abandoned\r\n")); + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // This happens again, but this time a complete message is added + add_nodes(t, "*2\r\n+hello\r\n+world\r\n"); + std::vector expected_nodes{ + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello"}, + {type::simple_string, 1u, 1u, "world"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 10u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// The same works even if there is existing committed data +void test_add_nodes_existing_data_and_tmp() +{ + flat_tree t; + parser p; + + // Add a full message + add_nodes(t, "*2\r\n+hello\r\n+world\r\n"); + std::vector expected_nodes{ + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello"}, + {type::simple_string, 1u, 1u, "world"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 10u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + // Add part of a message + p.reset(); + BOOST_TEST_NOT(parse_checked(t, p, "%66\r\n+abandoned\r\n")); + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 10u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + // This message is abandoned, and replaced by a full one + add_nodes(t, "+complete message\r\n"); + expected_nodes.push_back({type::simple_string, 1u, 0u, "complete message"}); + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 26u); + BOOST_TEST_EQ(t.get_total_msgs(), 2u); +} + // --- Reserving space --- // The usual case, calling it before using it void test_reserve() @@ -210,7 +336,7 @@ void test_reserve() t.reserve(1024u, 5u); check_nodes(t, {}); - BOOST_TEST_EQ(t.get_view().capacity(), 5u); + BOOST_TEST_GE(t.capacity(), 5u); BOOST_TEST_EQ(t.data_size(), 0u); BOOST_TEST_EQ(t.data_capacity(), 1024); BOOST_TEST_EQ(t.get_reallocs(), 1u); @@ -285,6 +411,32 @@ void test_reserve_with_data() BOOST_TEST_EQ(t.get_total_msgs(), 1u); } +// Reserve also handles the tmp area +void test_reserve_with_tmp() +{ + flat_tree t; + parser p; + + // Add a partial message, and then reserve + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + t.reserve(1000u, 10u); + + // Finish the current message so nodes in the tmp area show up + BOOST_TEST(parse_checked(t, p, "*2\r\n+hello\r\n+world\r\n")); + + // Check + std::vector expected_nodes{ + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello"}, + {type::simple_string, 1u, 1u, "world"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 10u); + BOOST_TEST_EQ(t.data_capacity(), 1024u); + BOOST_TEST_EQ(t.get_reallocs(), 2u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + // --- Clear --- void test_clear() { @@ -349,6 +501,93 @@ void test_clear_reuse() BOOST_TEST_EQ(t.get_total_msgs(), 1u); } +// Clear doesn't remove the tmp area +void test_clear_tmp() +{ + flat_tree t; + parser p; + + // Add a full message and part of another + add_nodes(t, ">2\r\n+orange\r\n+apple\r\n"); + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + std::vector expected_nodes{ + {type::push, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "orange"}, + {type::simple_string, 1u, 1u, "apple" }, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + // Clearing removes the user-facing representation + t.clear(); + check_nodes(t, {}); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // The nodes in the tmp area are still alive. Adding the remaining yields the full message + BOOST_TEST(parse_checked(t, p, "*2\r\n+hello\r\n+world\r\n")); + expected_nodes = { + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello"}, + {type::simple_string, 1u, 1u, "world"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// Clearing having only tmp area is safe +void test_clear_only_tmp() +{ + flat_tree t; + parser p; + + // Add part of a message + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + check_nodes(t, {}); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // Clearing here does nothing + t.clear(); + check_nodes(t, {}); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // The nodes in the tmp area are still alive. Adding the remaining yields the full message + BOOST_TEST(parse_checked(t, p, "*2\r\n+hello\r\n+world\r\n")); + std::vector expected_nodes = { + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello"}, + {type::simple_string, 1u, 1u, "world"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// Clearing having tmp nodes but no data is also safe +void test_clear_only_tmp_nodes() +{ + flat_tree t; + parser p; + + // Add part of a message + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n")); + check_nodes(t, {}); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // Clearing here does nothing + t.clear(); + check_nodes(t, {}); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // The nodes in the tmp area are still alive. Adding the remaining yields the full message + BOOST_TEST(parse_checked(t, p, "*2\r\n+hello\r\n+world\r\n")); + std::vector expected_nodes = { + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello"}, + {type::simple_string, 1u, 1u, "world"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + // --- Default ctor --- void test_default_constructor() { @@ -437,6 +676,37 @@ void test_copy_ctor_adjust_capacity() BOOST_TEST_EQ(t2.get_total_msgs(), 1u); } +// Copying an object also copies its tmp area +void test_copy_ctor_tmp() +{ + // Setup + flat_tree t; + parser p; + add_nodes(t, "+message\r\n"); + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "message"}, + }; + + // Copy. The copy has the tmp nodes but they're hidden in its tmp area + flat_tree t2{t}; + check_nodes(t2, expected_nodes); + BOOST_TEST_EQ(t2.data_size(), 7u); + BOOST_TEST_EQ(t2.get_total_msgs(), 1u); + + // Finishing the message in the copy works + BOOST_TEST(parse_checked(t2, p, "*2\r\n+hello\r\n+world\r\n")); + expected_nodes = { + {type::simple_string, 1u, 0u, "message"}, + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello" }, + {type::simple_string, 1u, 1u, "world" }, + }; + check_nodes(t2, expected_nodes); + BOOST_TEST_EQ(t2.data_size(), 17u); + BOOST_TEST_EQ(t2.get_total_msgs(), 2u); +} + // --- Move ctor --- void test_move_ctor() { @@ -486,6 +756,37 @@ void test_move_ctor_with_capacity() BOOST_TEST_EQ(t2.get_total_msgs(), 0u); } +// Moving an object also moves its tmp area +void test_move_ctor_tmp() +{ + // Setup + flat_tree t; + parser p; + add_nodes(t, "+message\r\n"); + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+hello\r\n")); + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "message"}, + }; + + // Move. The new object has the same tmp area + flat_tree t2{std::move(t)}; + check_nodes(t2, expected_nodes); + BOOST_TEST_EQ(t2.data_size(), 7u); + BOOST_TEST_EQ(t2.get_total_msgs(), 1u); + + // Finishing the message in the copy works + BOOST_TEST(parse_checked(t2, p, "*2\r\n+hello\r\n+world\r\n")); + expected_nodes = { + {type::simple_string, 1u, 0u, "message"}, + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello" }, + {type::simple_string, 1u, 1u, "world" }, + }; + check_nodes(t2, expected_nodes); + BOOST_TEST_EQ(t2.data_size(), 17u); + BOOST_TEST_EQ(t2.get_total_msgs(), 2u); +} + // --- Copy assignment --- void test_copy_assign() { @@ -645,6 +946,40 @@ void test_copy_assign_self() BOOST_TEST_EQ(t.get_total_msgs(), 1u); } +// Copy assignment also assigns the tmp area +void test_copy_assign_tmp() +{ + parser p; + + flat_tree t; + add_nodes(t, "+some_data\r\n"); + + flat_tree t2; + add_nodes(t2, "+message\r\n"); + BOOST_TEST_NOT(parse_checked(t2, p, "*2\r\n+hello\r\n")); + + // Assigning also copies where the tmp area starts + t = t2; + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "message"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 7u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + // The tmp area was also copied + BOOST_TEST(parse_checked(t, p, "*2\r\n+hello\r\n+world\r\n")); + expected_nodes = { + {type::simple_string, 1u, 0u, "message"}, + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello" }, + {type::simple_string, 1u, 1u, "world" }, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 17u); + BOOST_TEST_EQ(t.get_total_msgs(), 2u); +} + // --- Move assignment --- void test_move_assign() { @@ -721,6 +1056,40 @@ void test_move_assign_both_empty() BOOST_TEST_EQ(t.get_total_msgs(), 0u); } +// Move assignment also propagates the tmp area +void test_move_assign_tmp() +{ + parser p; + + flat_tree t; + add_nodes(t, "+some_data\r\n"); + + flat_tree t2; + add_nodes(t2, "+message\r\n"); + BOOST_TEST_NOT(parse_checked(t2, p, "*2\r\n+hello\r\n")); + + // When moving, the tmp area is moved, too + t = std::move(t2); + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "message"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 7u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + // Finish the message + BOOST_TEST(parse_checked(t, p, "*2\r\n+hello\r\n+world\r\n")); + expected_nodes = { + {type::simple_string, 1u, 0u, "message"}, + {type::array, 2u, 0u, "" }, + {type::simple_string, 1u, 1u, "hello" }, + {type::simple_string, 1u, 1u, "world" }, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 17u); + BOOST_TEST_EQ(t.get_total_msgs(), 2u); +} + // --- Comparison --- void test_comparison_different() { @@ -826,6 +1195,67 @@ void test_comparison_self() BOOST_TEST_NOT(tempty != tempty); } +// The tmp area is not taken into account when comparing +void test_comparison_tmp() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + flat_tree t2; + add_nodes(t2, "+hello\r\n"); + parser p; + BOOST_TEST_NOT(parse_checked(t2, p, "*2\r\n+more data\r\n")); + + BOOST_TEST(t == t2); + BOOST_TEST_NOT(t != t2); +} + +void test_comparison_tmp_different() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + flat_tree t2; + add_nodes(t2, "+world\r\n"); + parser p; + BOOST_TEST_NOT(parse_checked(t2, p, "*2\r\n+more data\r\n")); + + BOOST_TEST_NOT(t == t2); + BOOST_TEST(t != t2); +} + +// Comparing object with only tmp area doesn't cause trouble +void test_comparison_only_tmp() +{ + flat_tree t; + parser p; + BOOST_TEST_NOT(parse_checked(t, p, "*2\r\n+more data\r\n")); + + flat_tree t2; + parser p2; + BOOST_TEST_NOT(parse_checked(t2, p2, "*2\r\n+random\r\n")); + + BOOST_TEST(t == t2); + BOOST_TEST_NOT(t != t2); +} + +// --- Capacity --- +// Delegates to the underlying vector function +void test_capacity() +{ + flat_tree t; + BOOST_TEST_EQ(t.capacity(), 0u); + + // Inserting a node increases capacity. + // It is not specified how capacity grows, though. + add_nodes(t, "+hello\r\n"); + BOOST_TEST_GE(t.capacity(), 1u); + + // Reserve also affects capacity + t.reserve(1000u, 8u); + BOOST_TEST_GE(t.capacity(), 8u); +} + } // namespace int main() @@ -834,15 +1264,22 @@ int main() test_add_nodes_copies(); test_add_nodes_capacity_limit(); test_add_nodes_big_node(); + test_add_nodes_tmp(); + test_add_nodes_existing_tmp(); + test_add_nodes_existing_data_and_tmp(); test_reserve(); test_reserve_not_power_of_2(); test_reserve_below_current_capacity(); test_reserve_with_data(); + test_reserve_with_tmp(); test_clear(); test_clear_empty(); test_clear_reuse(); + test_clear_tmp(); + test_clear_only_tmp(); + test_clear_only_tmp_nodes(); test_default_constructor(); @@ -850,15 +1287,12 @@ int main() test_copy_ctor_empty(); test_copy_ctor_empty_with_capacity(); test_copy_ctor_adjust_capacity(); + test_copy_ctor_tmp(); test_move_ctor(); test_move_ctor_empty(); test_move_ctor_with_capacity(); - - test_move_assign(); - test_move_assign_target_empty(); - test_move_assign_source_empty(); - test_move_assign_both_empty(); + test_move_ctor_tmp(); test_copy_assign(); test_copy_assign_target_empty(); @@ -868,6 +1302,13 @@ int main() test_copy_assign_source_with_extra_capacity(); test_copy_assign_both_empty(); test_copy_assign_self(); + test_copy_assign_tmp(); + + test_move_assign(); + test_move_assign_target_empty(); + test_move_assign_source_empty(); + test_move_assign_both_empty(); + test_move_assign_tmp(); test_comparison_different(); test_comparison_different_node_types(); @@ -876,6 +1317,11 @@ int main() test_comparison_equal_capacity(); test_comparison_empty(); test_comparison_self(); + test_comparison_tmp(); + test_comparison_tmp_different(); + test_comparison_only_tmp(); + + test_capacity(); return boost::report_errors(); } diff --git a/test/test_generic_flat_response.cpp b/test/test_generic_flat_response.cpp new file mode 100644 index 00000000..ff9e2b86 --- /dev/null +++ b/test/test_generic_flat_response.cpp @@ -0,0 +1,119 @@ +/* Copyright (c) 2018-2026 Marcelo Zimbres Silva (mzimbres@gmail.com), + * Ruben Perez Hidalgo (rubenperez038@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include "print_node.hpp" + +using namespace boost::redis; +using boost::system::error_code; +using boost::redis::resp3::detail::deserialize; +using resp3::node_view; +using resp3::type; +using adapter::adapt2; + +// Regular nodes are just stored +void test_success() +{ + generic_flat_response resp; + + error_code ec; + deserialize("+hello\r\n", adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + BOOST_TEST(resp.has_value()); + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "hello"}, + }; + BOOST_TEST_ALL_EQ( + resp->get_view().begin(), + resp->get_view().end(), + expected_nodes.begin(), + expected_nodes.end()); +} + +// If an error of any kind appears, we set the overall result to error +void test_simple_error() +{ + generic_flat_response resp; + + error_code ec; + deserialize("-Error\r\n", adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + BOOST_TEST(resp.has_error()); + auto const error = resp.error(); + + BOOST_TEST_EQ(error.data_type, resp3::type::simple_error); + BOOST_TEST_EQ(error.diagnostic, "Error"); +} + +void test_blob_error() +{ + generic_flat_response resp; + + error_code ec; + deserialize("!5\r\nError\r\n", adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + BOOST_TEST(resp.has_error()); + auto const error = resp.error(); + + BOOST_TEST_EQ(error.data_type, resp3::type::blob_error); + BOOST_TEST_EQ(error.diagnostic, "Error"); +} + +// Mixing success and error nodes is safe. Only the last error is stored +void test_mix_success_error() +{ + generic_flat_response resp; + error_code ec; + + // Success message + deserialize("+message\r\n", adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + // An error + deserialize("-Error\r\n", adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + // Another success message + deserialize("+other data\r\n", adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + // Another error + deserialize("-Different err\r\n", adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + // Final success message + deserialize("*1\r\n+last message\r\n", adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + // Check + BOOST_TEST(resp.has_error()); + auto const error = resp.error(); + + BOOST_TEST_EQ(error.data_type, resp3::type::simple_error); + BOOST_TEST_EQ(error.diagnostic, "Different err"); +} + +int main() +{ + test_success(); + test_simple_error(); + test_blob_error(); + test_mix_success_error(); + + return boost::report_errors(); +} diff --git a/test/test_low_level_sync_sans_io.cpp b/test/test_low_level_sync_sans_io.cpp index 69c3c221..9628dfdb 100644 --- a/test/test_low_level_sync_sans_io.cpp +++ b/test/test_low_level_sync_sans_io.cpp @@ -23,7 +23,6 @@ using boost::redis::request; using boost::redis::adapter::adapt2; using boost::redis::adapter::result; using boost::redis::resp3::tree; -using boost::redis::resp3::flat_tree; using boost::redis::generic_flat_response; using boost::redis::ignore_t; using boost::redis::resp3::detail::deserialize; @@ -253,39 +252,3 @@ BOOST_AUTO_TEST_CASE(check_counter_adapter) BOOST_CHECK_EQUAL(node, 7); BOOST_CHECK_EQUAL(done, 1); } - -BOOST_AUTO_TEST_CASE(generic_flat_response_simple_error) -{ - generic_flat_response resp; - - char const* wire = "-Error\r\n"; - - error_code ec; - deserialize(wire, adapt2(resp), ec); - BOOST_CHECK_EQUAL(ec, error_code{}); - - BOOST_TEST(!resp.has_value()); - BOOST_TEST(resp.has_error()); - auto const error = resp.error(); - - BOOST_CHECK_EQUAL(error.data_type, boost::redis::resp3::type::simple_error); - BOOST_CHECK_EQUAL(error.diagnostic, std::string{"Error"}); -} - -BOOST_AUTO_TEST_CASE(generic_flat_response_blob_error) -{ - generic_flat_response resp; - - char const* wire = "!5\r\nError\r\n"; - - error_code ec; - deserialize(wire, adapt2(resp), ec); - BOOST_CHECK_EQUAL(ec, error_code{}); - - BOOST_TEST(!resp.has_value()); - BOOST_TEST(resp.has_error()); - auto const error = resp.error(); - - BOOST_CHECK_EQUAL(error.data_type, boost::redis::resp3::type::blob_error); - BOOST_CHECK_EQUAL(error.diagnostic, std::string{"Error"}); -}