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:
committed by
GitHub
parent
7750a6b126
commit
3b07119e54
@@ -77,6 +77,7 @@ if (BOOST_REDIS_MAIN_PROJECT)
|
||||
test
|
||||
json
|
||||
endian
|
||||
compat
|
||||
)
|
||||
|
||||
foreach(dep IN LISTS deps)
|
||||
|
||||
@@ -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&)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,6 +70,7 @@ local tests =
|
||||
test_parse_sentinel_response
|
||||
test_update_sentinel_list
|
||||
test_flat_tree
|
||||
test_generic_flat_response
|
||||
test_read_buffer
|
||||
;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
119
test/test_generic_flat_response.cpp
Normal file
119
test/test_generic_flat_response.cpp
Normal 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();
|
||||
}
|
||||
@@ -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"});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user