2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-19 04:42:09 +00:00

Makes flat_tree aware of incremental parsing to avoid race conditions with pushes (#378)

Adds the concept of a "temporary working area" to flat_tree. Nodes in this area belong to a partially parsed message, and are hidden from the user. Now flat_tree can be used as the receive response without explicitly handling partial messages.
Changes flat_tree::get_view return type from const vector& to span.
Adds flat_tree::capacity.
Splits generic_flat_response tests to a separate file and adds extra cases.

close #369
This commit is contained in:
Anarthal (Rubén Pérez)
2026-01-07 09:55:23 +01:00
committed by GitHub
parent 7750a6b126
commit 3b07119e54
10 changed files with 689 additions and 56 deletions

View File

@@ -77,6 +77,7 @@ if (BOOST_REDIS_MAIN_PROJECT)
test
json
endian
compat
)
foreach(dep IN LISTS deps)

View File

@@ -9,10 +9,10 @@
#include <boost/redis/adapter/result.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/response.hpp>
#include <boost/assert.hpp>
@@ -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 <class String>
void on_node(resp3::basic_node<String> const& nd, system::error_code&)

View File

@@ -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

View File

@@ -12,6 +12,8 @@
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/tree.hpp>
#include <boost/core/span.hpp>
#include <cstddef>
#include <memory>
@@ -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<const node_view> 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 <class> 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;
};
/**

View File

@@ -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

View File

@@ -70,6 +70,7 @@ local tests =
test_parse_sentinel_response
test_update_sentinel_list
test_flat_tree
test_generic_flat_response
test_read_buffer
;

View File

@@ -6,6 +6,7 @@
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/connection.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/detached.hpp>
@@ -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<connection>(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

View File

@@ -7,6 +7,7 @@
#include <boost/redis/adapter/adapt.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/parser.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/assert/source_location.hpp>
@@ -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<const node_view> 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<node_view> 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<node_view> 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<node_view> 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<node_view> 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<node_view> 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<node_view> 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<node_view> 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<node_view> 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<node_view> 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<node_view> 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<node_view> 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();
}

View File

@@ -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 <boost/redis/adapter/adapt.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/response.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
#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<node_view> 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();
}

View File

@@ -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"});
}