From 1c1a47091fdcc64a99082f3785e65321e87a5d5a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Mon, 5 Jan 2026 22:06:15 +0100 Subject: [PATCH 01/20] Initial impl --- .../boost/redis/adapter/detail/adapters.hpp | 16 ++--- include/boost/redis/impl/flat_tree.ipp | 59 ++++++++++++++++++- include/boost/redis/resp3/flat_tree.hpp | 16 ++++- 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/include/boost/redis/adapter/detail/adapters.hpp b/include/boost/redis/adapter/detail/adapters.hpp index 7830f0ed3..8419e2200 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 @@ class general_aggregate { : 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 @@ class general_aggregate { : 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 73aeabe30..51ae9b955 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 b0bff1014..d523eaf34 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,7 @@ class flat_tree { * * @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 data buffer, in bytes. * @@ -187,7 +189,7 @@ class flat_tree { * * @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 +217,8 @@ class flat_tree { 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 +226,13 @@ class flat_tree { 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; }; /** From 1986666ba6bdd3af6eaacbf53209dbbc149844e3 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 12:21:36 +0100 Subject: [PATCH 02/20] add capacity --- include/boost/redis/resp3/flat_tree.hpp | 9 +++++++++ test/test_flat_tree.cpp | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/include/boost/redis/resp3/flat_tree.hpp b/include/boost/redis/resp3/flat_tree.hpp index d523eaf34..4c6158c53 100644 --- a/include/boost/redis/resp3/flat_tree.hpp +++ b/include/boost/redis/resp3/flat_tree.hpp @@ -168,6 +168,15 @@ class flat_tree { */ 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. * * Note that the actual capacity of the data buffer may be bigger diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 095af2c7f..332856f49 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -36,6 +36,8 @@ using boost::redis::resp3::to_string; using boost::redis::response; using boost::system::error_code; +// TODO: test capacity + namespace { void add_nodes( @@ -210,7 +212,7 @@ void test_reserve() t.reserve(1024u, 5u); check_nodes(t, {}); - BOOST_TEST_EQ(t.get_view().capacity(), 5u); + BOOST_TEST_EQ(t.capacity(), 5u); BOOST_TEST_EQ(t.data_size(), 0u); BOOST_TEST_EQ(t.data_capacity(), 1024); BOOST_TEST_EQ(t.get_reallocs(), 1u); From 4655690bbe695785461b7c94957bc4d1018e8fd2 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 12:22:37 +0100 Subject: [PATCH 03/20] bugfix --- include/boost/redis/impl/flat_tree.ipp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/boost/redis/impl/flat_tree.ipp b/include/boost/redis/impl/flat_tree.ipp index 51ae9b955..7ee1aac4f 100644 --- a/include/boost/redis/impl/flat_tree.ipp +++ b/include/boost/redis/impl/flat_tree.ipp @@ -215,8 +215,8 @@ 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); + 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_; } From d70b978093133da4b2574ff1ae342510ba46a785 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 12:43:38 +0100 Subject: [PATCH 04/20] Test add partial messages --- test/test_flat_tree.cpp | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 332856f49..0787663ff 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; @@ -51,6 +53,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, @@ -204,6 +220,52 @@ 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_partial_msg() +{ + 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); +} + // --- Reserving space --- // The usual case, calling it before using it void test_reserve() @@ -836,6 +898,7 @@ int main() test_add_nodes_copies(); test_add_nodes_capacity_limit(); test_add_nodes_big_node(); + test_add_nodes_partial_msg(); test_reserve(); test_reserve_not_power_of_2(); From 6a0993176be256dab7768f3575f2de5c98cd2c43 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 12:47:56 +0100 Subject: [PATCH 05/20] add nodes with existing tmp area --- test/test_flat_tree.cpp | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 0787663ff..f240db5ea 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -266,6 +266,41 @@ void test_add_nodes_partial_msg() 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_partial_msg() +{ + 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.data_capacity(), 512u); + 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.data_capacity(), 512u); + 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.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + // --- Reserving space --- // The usual case, calling it before using it void test_reserve() @@ -899,6 +934,7 @@ int main() test_add_nodes_capacity_limit(); test_add_nodes_big_node(); test_add_nodes_partial_msg(); + test_add_nodes_existing_partial_msg(); test_reserve(); test_reserve_not_power_of_2(); From 81cdc4c72a7ef35e49b6f26a0aa46594eb3c7e5a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 12:52:32 +0100 Subject: [PATCH 06/20] existing data and tmp --- test/test_flat_tree.cpp | 44 ++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index f240db5ea..03571cbd4 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -223,7 +223,7 @@ void test_add_nodes_big_node() // 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_partial_msg() +void test_add_nodes_tmp() { flat_tree t; parser p; @@ -268,7 +268,7 @@ void test_add_nodes_partial_msg() // If there was an unfinished message when another message is started, // the former is discarded -void test_add_nodes_existing_partial_msg() +void test_add_nodes_existing_tmp() { flat_tree t; parser p; @@ -277,7 +277,6 @@ void test_add_nodes_existing_partial_msg() 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.data_capacity(), 512u); BOOST_TEST_EQ(t.get_total_msgs(), 0u); // This message is abandoned, and another one is started @@ -285,7 +284,6 @@ void test_add_nodes_existing_partial_msg() 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.data_capacity(), 512u); BOOST_TEST_EQ(t.get_total_msgs(), 0u); // This happens again, but this time a complete message is added @@ -297,10 +295,41 @@ void test_add_nodes_existing_partial_msg() }; 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); } +// 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() @@ -933,8 +962,9 @@ int main() test_add_nodes_copies(); test_add_nodes_capacity_limit(); test_add_nodes_big_node(); - test_add_nodes_partial_msg(); - test_add_nodes_existing_partial_msg(); + test_add_nodes_tmp(); + test_add_nodes_existing_tmp(); + test_add_nodes_existing_data_and_tmp(); test_reserve(); test_reserve_not_power_of_2(); From be7f7607ffed64ad031715544ce09ef3fed04ee8 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 12:55:58 +0100 Subject: [PATCH 07/20] reserve with tmp --- test/test_flat_tree.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 03571cbd4..5762f39ce 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -413,6 +413,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() { @@ -970,6 +996,7 @@ int main() test_reserve_not_power_of_2(); test_reserve_below_current_capacity(); test_reserve_with_data(); + test_reserve_with_tmp(); test_clear(); test_clear_empty(); From 4d57d62f87a8fc89b6fcce9c50fe78aba68e2753 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:05:35 +0100 Subject: [PATCH 08/20] clear tests --- test/test_flat_tree.cpp | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 5762f39ce..dca875f87 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -503,6 +503,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() { @@ -1001,6 +1088,9 @@ int main() test_clear(); test_clear_empty(); test_clear_reuse(); + test_clear_tmp(); + test_clear_only_tmp(); + test_clear_only_tmp_nodes(); test_default_constructor(); From e324069db15104779f3f1742e330d8cfb55d8908 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:10:05 +0100 Subject: [PATCH 09/20] copy tmp --- test/test_flat_tree.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index dca875f87..1da471712 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -678,6 +678,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() { @@ -1098,6 +1129,7 @@ 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(); From e8ffee0500ee5912f53d712e6d1f06ce9022d066 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:11:38 +0100 Subject: [PATCH 10/20] move tmp --- test/test_flat_tree.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 1da471712..e1d22ff1b 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -758,6 +758,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() { @@ -1134,6 +1165,7 @@ int main() test_move_ctor(); test_move_ctor_empty(); test_move_ctor_with_capacity(); + test_move_ctor_tmp(); test_move_assign(); test_move_assign_target_empty(); From 171fb0bc6e947ad8e8f6d609861087e4b433e863 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:20:27 +0100 Subject: [PATCH 11/20] copy assign tmp --- test/test_flat_tree.cpp | 45 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index e1d22ff1b..126d0f578 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -948,6 +948,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() { @@ -1167,11 +1201,6 @@ int main() test_move_ctor_with_capacity(); test_move_ctor_tmp(); - test_move_assign(); - test_move_assign_target_empty(); - test_move_assign_source_empty(); - test_move_assign_both_empty(); - test_copy_assign(); test_copy_assign_target_empty(); test_copy_assign_target_not_enough_capacity(); @@ -1180,6 +1209,12 @@ 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_comparison_different(); test_comparison_different_node_types(); From b2b9240c386a6f919aa3e18fb945a869165f9fff Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:24:46 +0100 Subject: [PATCH 12/20] move assign tmp --- test/test_flat_tree.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 126d0f578..b9c92f92c 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -1058,6 +1058,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() { @@ -1215,6 +1249,7 @@ int main() 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(); From 2073e4b6165f8ad355d064a14eff095451d56f92 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:32:12 +0100 Subject: [PATCH 13/20] comparison --- test/test_flat_tree.cpp | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index b9c92f92c..de2aab395 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -1197,6 +1197,50 @@ 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); +} + } // namespace int main() @@ -1258,6 +1302,9 @@ int main() test_comparison_equal_capacity(); test_comparison_empty(); test_comparison_self(); + test_comparison_tmp(); + test_comparison_tmp_different(); + test_comparison_only_tmp(); return boost::report_errors(); } From dc906afeb5d2634df3187fe92674e304b774c92a Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:36:12 +0100 Subject: [PATCH 14/20] capacity test --- test/test_flat_tree.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index de2aab395..89a78af39 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -38,8 +38,6 @@ using boost::redis::resp3::to_string; using boost::redis::response; using boost::system::error_code; -// TODO: test capacity - namespace { void add_nodes( @@ -1241,6 +1239,23 @@ void test_comparison_only_tmp() 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() @@ -1306,5 +1321,7 @@ int main() test_comparison_tmp_different(); test_comparison_only_tmp(); + test_capacity(); + return boost::report_errors(); } From 1ad2075700760096b9cb44693054ac374cb033df Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:37:43 +0100 Subject: [PATCH 15/20] fix implementation defined assertion --- test/test_flat_tree.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp index 89a78af39..87b7f804a 100644 --- a/test/test_flat_tree.cpp +++ b/test/test_flat_tree.cpp @@ -336,7 +336,7 @@ void test_reserve() t.reserve(1024u, 5u); check_nodes(t, {}); - BOOST_TEST_EQ(t.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); From a3acb259056a76fc9a7a74bd3f81c1129f994152 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:45:50 +0100 Subject: [PATCH 16/20] split generic_flat_response tests --- test/CMakeLists.txt | 1 + test/Jamfile | 1 + test/test_generic_flat_response.cpp | 62 ++++++++++++++++++++++++++++ test/test_low_level_sync_sans_io.cpp | 37 ----------------- 4 files changed, 64 insertions(+), 37 deletions(-) create mode 100644 test/test_generic_flat_response.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ea1e4b165..40a1ea05d 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 3bf9b443c..3aa72460f 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_generic_flat_response.cpp b/test/test_generic_flat_response.cpp new file mode 100644 index 000000000..fc58c6887 --- /dev/null +++ b/test/test_generic_flat_response.cpp @@ -0,0 +1,62 @@ +/* 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 + +using namespace boost::redis; +using boost::system::error_code; +using boost::redis::resp3::detail::deserialize; +using adapter::adapt2; + +void test_simple_error() +{ + generic_flat_response resp; + + char const* wire = "-Error\r\n"; + + error_code ec; + deserialize(wire, adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + BOOST_TEST(!resp.has_value()); + BOOST_TEST(resp.has_error()); + auto const error = resp.error(); + + BOOST_TEST_EQ(error.data_type, boost::redis::resp3::type::simple_error); + BOOST_TEST_EQ(error.diagnostic, std::string{"Error"}); +} + +void test_blob_error() +{ + generic_flat_response resp; + + char const* wire = "!5\r\nError\r\n"; + + error_code ec; + deserialize(wire, adapt2(resp), ec); + BOOST_TEST_EQ(ec, error_code{}); + + BOOST_TEST(!resp.has_value()); + BOOST_TEST(resp.has_error()); + auto const error = resp.error(); + + BOOST_TEST_EQ(error.data_type, boost::redis::resp3::type::blob_error); + BOOST_TEST_EQ(error.diagnostic, std::string{"Error"}); +} + +int main() +{ + test_simple_error(); + test_blob_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 69c3c221f..9628dfdb5 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"}); -} From 2ce87010d1478cdc69949b04d7945fb61d1bb379 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:47:03 +0100 Subject: [PATCH 17/20] refactor --- test/test_generic_flat_response.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/test_generic_flat_response.cpp b/test/test_generic_flat_response.cpp index fc58c6887..6e6ae5f31 100644 --- a/test/test_generic_flat_response.cpp +++ b/test/test_generic_flat_response.cpp @@ -21,36 +21,30 @@ void test_simple_error() { generic_flat_response resp; - char const* wire = "-Error\r\n"; - error_code ec; - deserialize(wire, adapt2(resp), ec); + deserialize("-Error\r\n", adapt2(resp), ec); BOOST_TEST_EQ(ec, error_code{}); - BOOST_TEST(!resp.has_value()); BOOST_TEST(resp.has_error()); auto const error = resp.error(); - BOOST_TEST_EQ(error.data_type, boost::redis::resp3::type::simple_error); - BOOST_TEST_EQ(error.diagnostic, std::string{"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; - char const* wire = "!5\r\nError\r\n"; - error_code ec; - deserialize(wire, adapt2(resp), ec); + deserialize("!5\r\nError\r\n", adapt2(resp), ec); BOOST_TEST_EQ(ec, error_code{}); - BOOST_TEST(!resp.has_value()); BOOST_TEST(resp.has_error()); auto const error = resp.error(); - BOOST_TEST_EQ(error.data_type, boost::redis::resp3::type::blob_error); - BOOST_TEST_EQ(error.diagnostic, std::string{"Error"}); + BOOST_TEST_EQ(error.data_type, resp3::type::blob_error); + BOOST_TEST_EQ(error.diagnostic, "Error"); } int main() From 25a0e5c194730d4129ad99d5f2710090bfb2b6e3 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 13:56:51 +0100 Subject: [PATCH 18/20] More tests --- test/test_generic_flat_response.cpp | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/test_generic_flat_response.cpp b/test/test_generic_flat_response.cpp index 6e6ae5f31..ff9e2b863 100644 --- a/test/test_generic_flat_response.cpp +++ b/test/test_generic_flat_response.cpp @@ -6,17 +6,44 @@ */ #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; @@ -47,10 +74,46 @@ void test_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(); } From 42ac717963be03488b9144e44700461c22b40cee Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 14:00:58 +0100 Subject: [PATCH 19/20] Integration spotcheck --- test/test_conn_exec.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/test_conn_exec.cpp b/test/test_conn_exec.cpp index 28a23497b..22216f9b8 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 From 3aaa1467d57bf2a0c0db3a90e7ff570b5d91844e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 6 Jan 2026 15:36:05 +0100 Subject: [PATCH 20/20] Add compat to transitive dependencies --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 35df41fcd..967f1ccd7 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)