diff --git a/doc/modules/ROOT/pages/reference.adoc b/doc/modules/ROOT/pages/reference.adoc index e87ee7cd8..6281911ee 100644 --- a/doc/modules/ROOT/pages/reference.adoc +++ b/doc/modules/ROOT/pages/reference.adoc @@ -55,6 +55,8 @@ xref:reference:boost/redis/response.adoc[`response`] xref:reference:boost/redis/generic_response.adoc[`generic_response`] +xref:reference:boost/redis/generic_flat_response.adoc[`generic_flat_response`] + xref:reference:boost/redis/consume_one-08.adoc[`consume_one`] @@ -70,25 +72,33 @@ xref:reference:boost/redis/adapter/result.adoc[`adapter::result`] xref:reference:boost/redis/any_adapter.adoc[`any_adapter`] | -xref:reference:boost/redis/resp3/basic_node.adoc[`basic_node`] +xref:reference:boost/redis/resp3/basic_node.adoc[`resp3::basic_node`] + +xref:reference:boost/redis/resp3/node.adoc[`resp3::node`] + +xref:reference:boost/redis/resp3/node_view.adoc[`resp3::node_view`] + +xref:reference:boost/redis/resp3/basic_tree.adoc[`resp3::basic_tree`] + +xref:reference:boost/redis/resp3/tree.adoc[`resp3::tree`] -xref:reference:boost/redis/resp3/node.adoc[`node`] +xref:reference:boost/redis/resp3/view_tree.adoc[`resp3::view_tree`] -xref:reference:boost/redis/resp3/node_view.adoc[`node_view`] +xref:reference:boost/redis/resp3/flat_tree.adoc[`resp3::flat_tree`] xref:reference:boost/redis/resp3/boost_redis_to_bulk-08.adoc[`boost_redis_to_bulk`] -xref:reference:boost/redis/resp3/type.adoc[`type`] +xref:reference:boost/redis/resp3/type.adoc[`resp3::type`] -xref:reference:boost/redis/resp3/is_aggregate.adoc[`is_aggregate`] +xref:reference:boost/redis/resp3/is_aggregate.adoc[`resp3::is_aggregate`] | xref:reference:boost/redis/adapter/adapt2.adoc[`adapter::adapt2`] -xref:reference:boost/redis/resp3/parser.adoc[`parser`] +xref:reference:boost/redis/resp3/parser.adoc[`resp3::parser`] -xref:reference:boost/redis/resp3/parse.adoc[`parse`] +xref:reference:boost/redis/resp3/parse.adoc[`resp3::parse`] |=== \ No newline at end of file diff --git a/doc/modules/ROOT/pages/requests_responses.adoc b/doc/modules/ROOT/pages/requests_responses.adoc index 59938dd5c..7f40fed11 100644 --- a/doc/modules/ROOT/pages/requests_responses.adoc +++ b/doc/modules/ROOT/pages/requests_responses.adoc @@ -278,7 +278,8 @@ struct basic_node { ---- Any response to a Redis command can be parsed into a -xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response]. +xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response] +and its counterpart xref:reference:boost/redis/generic_flat_response.adoc[boost::redis::generic_flat_response]. The vector can be seen as a pre-order view of the response tree. Using it is not different than using other types: @@ -292,7 +293,7 @@ co_await conn->async_exec(req, resp); For example, suppose we want to retrieve a hash data structure from Redis with `HGETALL`, some of the options are -* `boost::redis::generic_response`: always works. +* `boost::redis::generic_response` and `boost::redis::generic_flat_response`: always works. * `std::vector`: efficient and flat, all elements as string. * `std::map`: efficient if you need the data as a `std::map`. * `std::map`: efficient if you are storing serialized data. Avoids temporaries and requires `boost_redis_from_bulk` for `U` and `V`. diff --git a/include/boost/redis/impl/flat_tree.ipp b/include/boost/redis/impl/flat_tree.ipp index 095e48ae6..73aeabe30 100644 --- a/include/boost/redis/impl/flat_tree.ipp +++ b/include/boost/redis/impl/flat_tree.ipp @@ -7,119 +7,183 @@ // #include +#include +#include + #include +#include +#include +#include +#include + namespace boost::redis::resp3 { -flat_tree::flat_tree(flat_tree const& other) -: data_{other.data_} -, view_tree_{other.view_tree_} -, ranges_{other.ranges_} -, pos_{0u} -, reallocs_{0u} -, total_msgs_{other.total_msgs_} +namespace detail { + +// Updates string views by performing pointer arithmetic +inline void rebase_strings(view_tree& nodes, const char* old_base, const char* new_base) { - view_tree_.resize(ranges_.size()); - set_views(); + for (auto& nd : nodes) { + if (!nd.value.empty()) { + const auto offset = nd.value.data() - old_base; + BOOST_ASSERT(offset >= 0); + nd.value = {new_base + offset, nd.value.size()}; + } + } } -flat_tree& -flat_tree::operator=(flat_tree other) +// --- Operations in flat_buffer --- + +// Compute the new capacity upon reallocation. We always use powers of 2, +// starting in 512, to prevent many small allocations +inline std::size_t compute_capacity(std::size_t current, std::size_t requested) { - swap(*this, other); - return *this; + std::size_t res = (std::max)(current, static_cast(512u)); + while (res < requested) + res *= 2u; + return res; } -void flat_tree::reserve(std::size_t bytes, std::size_t nodes) +// Copy construction +inline flat_buffer copy_construct(const flat_buffer& other) { - data_.reserve(bytes); - view_tree_.reserve(nodes); - ranges_.reserve(nodes); + flat_buffer res{{}, other.size, 0u, 0u}; + + if (other.size > 0u) { + const std::size_t capacity = compute_capacity(0u, other.size); + res.data.reset(new char[capacity]); + res.capacity = capacity; + res.reallocs = 1u; + std::copy(other.data.get(), other.data.get() + other.size, res.data.get()); + } + + return res; } -void flat_tree::clear() +// Copy assignment +inline void copy_assign(flat_buffer& buff, const flat_buffer& other) { - pos_ = 0u; - total_msgs_ = 0u; - reallocs_ = 0u; - data_.clear(); - view_tree_.clear(); - ranges_.clear(); + // Make space if required + if (buff.capacity < other.size) { + const std::size_t capacity = compute_capacity(buff.capacity, other.size); + buff.data.reset(new char[capacity]); + buff.capacity = capacity; + ++buff.reallocs; + } + + // Copy the contents + std::copy(other.data.get(), other.data.get() + other.size, buff.data.get()); + buff.size = other.size; } -void flat_tree::set_views() +// Grows the buffer until reaching a target size. +// Might rebase the strings in nodes +inline void grow(flat_buffer& buff, std::size_t new_capacity, view_tree& nodes) { - BOOST_ASSERT_MSG(pos_ < view_tree_.size(), "notify_done called but no nodes added."); - BOOST_ASSERT_MSG(view_tree_.size() == ranges_.size(), "Incompatible sizes."); + if (new_capacity <= buff.capacity) + return; - for (; pos_ < view_tree_.size(); ++pos_) { - auto const& r = ranges_.at(pos_); - view_tree_.at(pos_).value = std::string_view{data_.data() + r.offset, r.size}; - } + // Compute the actual capacity that we will be using + new_capacity = compute_capacity(buff.capacity, new_capacity); + + // Allocate space + std::unique_ptr new_buffer{new char[new_capacity]}; + + // Copy any data into the newly allocated space + const char* data_before = buff.data.get(); + char* data_after = new_buffer.get(); + std::copy(data_before, data_before + buff.size, data_after); + + // Update the string views so they don't dangle + rebase_strings(nodes, data_before, data_after); + + // Replace the buffer. Note that size hasn't changed here + buff.data = std::move(new_buffer); + buff.capacity = new_capacity; + ++buff.reallocs; } -void flat_tree::notify_done() +// 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) { - total_msgs_ += 1; - set_views(); + // If there is nothing to copy, do nothing + if (value.empty()) + return value; + + // Make space for the new string + const std::size_t new_size = buff.size + value.size(); + grow(buff, new_size, nodes); + + // Copy the new value + const std::size_t offset = buff.size; + std::copy(value.data(), value.data() + value.size(), buff.data.get() + offset); + buff.size = new_size; + return {buff.data.get() + offset, value.size()}; } -void flat_tree::push(node_view const& node) -{ - auto data_before = data_.data(); - add_node_impl(node); - auto data_after = data_.data(); +} // namespace detail - if (data_after != data_before) { - pos_ = 0; - reallocs_ += 1; - } +flat_tree::flat_tree(flat_tree const& other) +: data_{detail::copy_construct(other.data_)} +, view_tree_{other.view_tree_} +, total_msgs_{other.total_msgs_} +{ + detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get()); } -void flat_tree::add_node_impl(node_view const& node) +flat_tree& flat_tree::operator=(const flat_tree& other) { - ranges_.push_back({data_.size(), node.value.size()}); + if (this != &other) { + // Copy the data + detail::copy_assign(data_, other.data_); - // This must come after setting the offset above. - data_.insert(data_.end(), node.value.begin(), node.value.end()); + // Copy the nodes + view_tree_ = other.view_tree_; + detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get()); - view_tree_.push_back(node); + // Copy the other fields + total_msgs_ = other.total_msgs_; + } + + return *this; } -void swap(flat_tree& a, flat_tree& b) +void flat_tree::reserve(std::size_t bytes, std::size_t nodes) { - using std::swap; - - swap(a.data_, b.data_); - swap(a.view_tree_, b.view_tree_); - swap(a.ranges_, b.ranges_); - swap(a.pos_, b.pos_); - swap(a.reallocs_, b.reallocs_); - swap(a.total_msgs_, b.total_msgs_); + // Space for the strings + detail::grow(data_, bytes, view_tree_); + + // Space for the nodes + view_tree_.reserve(nodes); } -bool -operator==( - flat_tree::range const& a, - flat_tree::range const& b) +void flat_tree::clear() noexcept { - return a.offset == b.offset && a.size == b.size; + data_.size = 0u; + view_tree_.clear(); + total_msgs_ = 0u; } -bool operator==(flat_tree const& a, flat_tree const& b) +void flat_tree::push(node_view const& nd) { - return - a.data_ == b.data_ && - a.view_tree_ == b.view_tree_ && - a.ranges_ == b.ranges_ && - a.pos_ == b.pos_ && - //a.reallocs_ == b.reallocs_ && - a.total_msgs_ == b.total_msgs_; + // Add the string + const std::string_view str = detail::append(data_, nd.value, view_tree_); + + // Add the node + view_tree_.push_back({ + nd.data_type, + nd.aggregate_size, + nd.depth, + str, + }); } -bool operator!=(flat_tree const& a, flat_tree const& b) +bool operator==(flat_tree const& a, flat_tree const& b) { - return !(a == b); + // data is already taken into account by comparing the nodes. + return a.view_tree_ == b.view_tree_ && a.total_msgs_ == b.total_msgs_; } -} // namespace boost::redis +} // namespace boost::redis::resp3 diff --git a/include/boost/redis/resp3/flat_tree.hpp b/include/boost/redis/resp3/flat_tree.hpp index 4635b95b9..b0bff1014 100644 --- a/include/boost/redis/resp3/flat_tree.hpp +++ b/include/boost/redis/resp3/flat_tree.hpp @@ -12,119 +12,241 @@ #include #include -#include +#include +#include namespace boost::redis { namespace adapter::detail { - template class general_aggregate; -} +template class general_aggregate; +} // namespace adapter::detail namespace resp3 { -/** @brief A generic-response that stores data contiguously +namespace detail { + +struct flat_buffer { + std::unique_ptr data; + std::size_t size = 0u; + std::size_t capacity = 0u; + std::size_t reallocs = 0u; +}; + +} // namespace detail + +/** @brief A generic response that stores data contiguously. * - * Similar to the @ref boost::redis::resp3::tree but data is - * stored contiguously. + * Implements a container of RESP3 nodes. It's similar to @ref boost::redis::resp3::tree, + * but node data is stored contiguously. This allows for amortized no allocations + * when re-using `flat_tree` objects. Like `tree`, it can contain the response + * to several Redis commands or several server pushes. Use @ref get_total_msgs + * to obtain how many responses this object contains. + * + * Objects are typically created by the user and passed to @ref connection::async_exec + * to be used as response containers. Call @ref get_view to access the actual RESP3 nodes. + * Once populated, `flat_tree` can't be modified, except for @ref clear and assignment. + * + * A `flat_tree` is conceptually similar to a pair of `std::vector` objects, one holding + * @ref resp3::node_view objects, and another owning the the string data that these views + * point to. The node capacity and the data capacity are the capacities of these two vectors. */ -struct flat_tree { +class flat_tree { public: - /// Default constructor + /** + * @brief Default constructor. + * + * Constructs an empty tree, with no nodes, zero node capacity and zero data capacity. + * + * @par Exception safety + * No-throw guarantee. + */ flat_tree() = default; - /// Move constructor - flat_tree(flat_tree&&) noexcept = default; + /** + * @brief Move constructor. + * + * Constructs a tree by taking ownership of the nodes in `other`. + * + * @par Object lifetimes + * References to the nodes and strings in `other` remain valid. + * + * @par Exception safety + * No-throw guarantee. + */ + flat_tree(flat_tree&& other) noexcept = default; - /// Copy constructor + /** + * @brief Copy constructor. + * + * Constructs a tree by copying the nodes in `other`. After the copy, + * `*this` and `other` have independent lifetimes (usual copy semantics). + * + * @par Exception safety + * Strong guarantee. Memory allocations might throw. + */ flat_tree(flat_tree const& other); - /// Copy assignment - flat_tree& operator=(flat_tree other); + /** + * @brief Move assignment. + * + * Replaces the nodes in `*this` by taking ownership of the nodes in `other`. + * `other` is left in a valid but unspecified state. + * + * @par Object lifetimes + * References to the nodes and strings in `other` remain valid. + * References to the nodes and strings in `*this` are invalidated. + * + * @par Exception safety + * No-throw guarantee. + */ + flat_tree& operator=(flat_tree&& other) = default; - friend void swap(flat_tree&, flat_tree&); + /** + * @brief Copy assignment. + * + * Replaces the nodes in `*this` by copying the nodes in `other`. + * After the copy, `*this` and `other` have independent lifetimes (usual copy semantics). + * + * @par Object lifetimes + * References to the nodes and strings in `*this` are invalidated. + * + * @par Exception safety + * Basic guarantee. Memory allocations might throw. + */ + flat_tree& operator=(const flat_tree& other); - friend - bool operator==(flat_tree const&, flat_tree const&); + friend bool operator==(flat_tree const&, flat_tree const&); - friend - bool operator!=(flat_tree const&, flat_tree const&); + friend bool operator!=(flat_tree const&, flat_tree const&); - /** @brief Reserve capacity + /** @brief Reserves capacity for incoming data. * - * Reserve memory for incoming data. + * Adding nodes (e.g. by passing the tree to `async_exec`) + * won't cause reallocations until the data or node capacities + * are exceeded, following the usual vector semantics. + * The implementation might reserve more capacity than the one requested. + * + * @par Object lifetimes + * References to the nodes and strings in `*this` are invalidated. * - * @param bytes Number of bytes to reserve for data. - * @param nodes Number of nodes to reserve. + * @par Exception safety + * Basic guarantee. Memory allocations might throw. + * + * @param bytes Number of bytes to reserve for data. + * @param nodes Number of nodes to reserve. */ void reserve(std::size_t bytes, std::size_t nodes); - /** @brief Clear both the data and the node buffers - * - * @Note: A `boost::redis:.flat_tree` can contain the - * response to multiple Redis commands and server pushes. Calling - * this function will erase everything contained in it. + /** @brief Clears the tree so it contains no nodes. + * + * Calling this function removes every node, making + * @ref get_view return empty and @ref get_total_msgs + * return zero. It does not modify the object's capacity. + * + * To re-use a `flat_tree` for several requests, + * use `clear()` before each `async_exec` call. + * + * @par Object lifetimes + * References to the nodes and strings in `*this` are invalidated. + * + * @par Exception safety + * No-throw guarantee. + */ + void clear() noexcept; + + /** @brief Returns the size of the data buffer, in bytes. + * + * You may use this function to calculate how much capacity + * should be reserved for data when calling @ref reserve. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns The number of bytes in use in the data buffer. */ - void clear(); + auto data_size() const noexcept -> std::size_t { return data_.size; } - /// Returns the size of the data buffer - auto data_size() const noexcept -> std::size_t - { return data_.size(); } + /** @brief Returns the capacity of the data buffer, in bytes. + * + * Note that the actual capacity of the data buffer may be bigger + * than the one requested by @ref reserve. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns The capacity of the data buffer, in bytes. + */ + auto data_capacity() const noexcept -> std::size_t { return data_.capacity; } - /// Returns the RESP3 response - auto get_view() const -> view_tree const& - { return view_tree_; } + /** @brief Returns a vector with the nodes in the tree. + * + * This is the main way to access the contents of the tree. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns The nodes in the tree. + */ + auto get_view() const noexcept -> view_tree const& { return view_tree_; } - /** @brief Returns the number of times reallocation took place + /** @brief Returns the number of memory reallocations that took place in the data buffer. + * + * This function returns how many reallocations in the data buffer were performed and + * can be useful to determine how much memory to reserve upfront. + * + * @par Exception safety + * No-throw guarantee. * - * This function returns how many reallocations were performed and - * can be useful to determine how much memory to reserve upfront. + * @returns The number of times that the data buffer reallocated its memory. */ - auto get_reallocs() const noexcept -> std::size_t - { return reallocs_; } + auto get_reallocs() const noexcept -> std::size_t { return data_.reallocs; } - /// Returns the number of complete RESP3 messages contained in this object. - std::size_t get_total_msgs() const noexcept - { return total_msgs_; } + /** @brief Returns the number of complete RESP3 messages contained in this object. + * + * This value is equal to the number of nodes in the tree with a depth of zero. + * + * @par Exception safety + * No-throw guarantee. + * + * @returns The number of complete RESP3 messages contained in this object. + */ + std::size_t get_total_msgs() const noexcept { return total_msgs_; } private: template friend class adapter::detail::general_aggregate; - // Notify the object that all nodes were pushed. - void notify_done(); + void notify_done() { ++total_msgs_; } // Push a new node to the response void push(node_view const& node); - void add_node_impl(node_view const& node); - - void set_views(); - - // Range into the data buffer. - struct range { - std::size_t offset; - std::size_t size; - }; - - friend bool operator==(range const&, range const&); - - std::vector data_; + detail::flat_buffer data_; view_tree view_tree_; - std::vector ranges_; - std::size_t pos_ = 0u; - std::size_t reallocs_ = 0u; std::size_t total_msgs_ = 0u; }; -/// Swaps two responses -void swap(flat_tree&, flat_tree&); - -/// Equality operator +/** + * @brief Equality operator. + * @relates flat_tree + * + * Two trees are equal if they contain the same nodes in the same order. + * Capacities are not taken into account. + * + * @par Exception safety + * No-throw guarantee. + */ bool operator==(flat_tree const&, flat_tree const&); -/// Inequality operator -bool operator!=(flat_tree const&, flat_tree const&); +/** + * @brief Inequality operator. + * @relates flat_tree + * + * @par Exception safety + * No-throw guarantee. + */ +inline bool operator!=(flat_tree const& lhs, flat_tree const& rhs) { return !(lhs == rhs); } -} // resp3 -} // namespace boost::redis +} // namespace resp3 +} // namespace boost::redis #endif // BOOST_REDIS_RESP3_FLAT_TREE_HPP diff --git a/include/boost/redis/resp3/node.hpp b/include/boost/redis/resp3/node.hpp index 1e284dab3..e7a768222 100644 --- a/include/boost/redis/resp3/node.hpp +++ b/include/boost/redis/resp3/node.hpp @@ -43,7 +43,7 @@ struct basic_node { * @param b Right hand side node object. */ template -auto operator==(basic_node const& a, basic_node const& b) +bool operator==(basic_node const& a, basic_node const& b) { // clang-format off return a.aggregate_size == b.aggregate_size @@ -53,9 +53,14 @@ auto operator==(basic_node const& a, basic_node const& b) // clang-format on }; -/// Inequality operator for RESP3 nodes +/** @brief Inequality operator for RESP3 nodes. + * @relates basic_node + * + * @param a Left hand side node object. + * @param b Right hand side node object. + */ template -auto operator!=(basic_node const& a, basic_node const& b) +bool operator!=(basic_node const& a, basic_node const& b) { return !(a == b); }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 58ad78a4f..72a28a29d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -51,6 +51,7 @@ make_test(test_setup_adapter) make_test(test_multiplexer) make_test(test_parse_sentinel_response) make_test(test_update_sentinel_list) +make_test(test_flat_tree) # Tests that require a real Redis server make_test(test_conn_quit) diff --git a/test/Jamfile b/test/Jamfile index 4ac93900a..5db6c0ae4 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -68,6 +68,7 @@ local tests = test_multiplexer test_parse_sentinel_response test_update_sentinel_list + test_flat_tree ; # Build and run the tests diff --git a/test/test_flat_tree.cpp b/test/test_flat_tree.cpp new file mode 100644 index 000000000..095af2c7f --- /dev/null +++ b/test/test_flat_tree.cpp @@ -0,0 +1,881 @@ +/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com) + * + * Distributed under the Boost Software License, Version 1.0. (See + * accompanying file LICENSE.txt) + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include "print_node.hpp" + +#include +#include +#include +#include +#include +#include +#include + +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::resp3::type; +using boost::redis::resp3::detail::deserialize; +using boost::redis::resp3::node; +using boost::redis::resp3::node_view; +using boost::redis::resp3::to_string; +using boost::redis::response; +using boost::system::error_code; + +namespace { + +void add_nodes( + flat_tree& to, + std::string_view data, + boost::source_location loc = BOOST_CURRENT_LOCATION) +{ + error_code ec; + deserialize(data, adapt2(to), ec); + if (!BOOST_TEST_EQ(ec, error_code{})) + std::cerr << "Called from " << loc << std::endl; +} + +void check_nodes( + const flat_tree& tree, + boost::span expected, + boost::source_location loc = BOOST_CURRENT_LOCATION) +{ + if (!BOOST_TEST_ALL_EQ( + tree.get_view().begin(), + tree.get_view().end(), + expected.begin(), + expected.end())) + std::cerr << "Called from " << loc << std::endl; +} + +// --- Adding nodes --- +// Adding nodes works, even when reallocations happen. +// Empty nodes don't cause trouble +void test_add_nodes() +{ + flat_tree t; + + // Add a bunch of nodes. Single allocation. Some nodes are empty. + 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_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + // Capacity will have raised to 512 bytes, at least. Add some more without reallocations + add_nodes(t, "$3\r\nbye\r\n"); + expected_nodes.push_back({type::blob_string, 1u, 0u, "bye"}); + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 13u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 2u); + + // Add nodes above the first reallocation threshold. Node strings are still valid + const std::string long_value(600u, 'a'); + add_nodes(t, "+" + long_value + "\r\n"); + expected_nodes.push_back({type::simple_string, 1u, 0u, long_value}); + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 613u); + BOOST_TEST_EQ(t.data_capacity(), 1024u); + BOOST_TEST_EQ(t.get_reallocs(), 2u); + BOOST_TEST_EQ(t.get_total_msgs(), 3u); + + // Add some more nodes, still within the reallocation threshold + add_nodes(t, "+some_other_value\r\n"); + expected_nodes.push_back({type::simple_string, 1u, 0u, "some_other_value"}); + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 629u); + BOOST_TEST_EQ(t.data_capacity(), 1024u); + BOOST_TEST_EQ(t.get_reallocs(), 2u); + BOOST_TEST_EQ(t.get_total_msgs(), 4u); + + // Add some more, causing another reallocation + add_nodes(t, "+" + long_value + "\r\n"); + expected_nodes.push_back({type::simple_string, 1u, 0u, long_value}); + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 1229u); + BOOST_TEST_EQ(t.data_capacity(), 2048u); + BOOST_TEST_EQ(t.get_reallocs(), 3u); + BOOST_TEST_EQ(t.get_total_msgs(), 5u); +} + +// Strings are really copied into the object +void test_add_nodes_copies() +{ + flat_tree t; + + // Place the message in dynamic memory + constexpr std::string_view const_msg = "+some_long_value_for_a_node\r\n"; + std::unique_ptr data{new char[100]{}}; + std::copy(const_msg.begin(), const_msg.end(), data.get()); + + // Add nodes pointing into this message + add_nodes(t, data.get()); + + // Invalidate the original message + data.reset(); + + // Check + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "some_long_value_for_a_node"}, + }; + check_nodes(t, expected_nodes); +} + +// Reallocations happen only when we would exceed capacity +void test_add_nodes_capacity_limit() +{ + flat_tree t; + + // Add a node to reach capacity 512 + add_nodes(t, "+hello\r\n"); + BOOST_TEST_EQ(t.data_size(), 5u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + + // Fill the rest of the capacity + add_nodes(t, "+" + std::string(507u, 'b') + "\r\n"); + BOOST_TEST_EQ(t.data_size(), 512u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + + // Adding an empty node here doesn't change capacity + add_nodes(t, "_\r\n"); + BOOST_TEST_EQ(t.data_size(), 512u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + + // Adding more data causes a reallocation + add_nodes(t, "+a\r\n"); + BOOST_TEST_EQ(t.data_size(), 513u); + BOOST_TEST_EQ(t.data_capacity(), 1024); + + // Same goes for the next capacity limit + add_nodes(t, "+" + std::string(511u, 'c') + "\r\n"); + BOOST_TEST_EQ(t.data_size(), 1024); + BOOST_TEST_EQ(t.data_capacity(), 1024); + + // Reallocation + add_nodes(t, "+u\r\n"); + BOOST_TEST_EQ(t.data_size(), 1025u); + BOOST_TEST_EQ(t.data_capacity(), 2048u); + + // This would continue + add_nodes(t, "+" + std::string(1024u, 'd') + "\r\n"); + BOOST_TEST_EQ(t.data_size(), 2049u); + BOOST_TEST_EQ(t.data_capacity(), 4096u); +} + +// It's no problem if a node is big enough to surpass several reallocation limits +void test_add_nodes_big_node() +{ + flat_tree t; + + // Add a bunch of nodes. Single allocation. Some nodes are empty. + const std::string long_value(1500u, 'h'); + add_nodes(t, "+" + long_value + "\r\n"); + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, long_value}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 1500u); + BOOST_TEST_EQ(t.data_capacity(), 2048u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// --- Reserving space --- +// The usual case, calling it before using it +void test_reserve() +{ + flat_tree t; + + t.reserve(1024u, 5u); + check_nodes(t, {}); + BOOST_TEST_EQ(t.get_view().capacity(), 5u); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 1024); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); + + // Adding some nodes now works + add_nodes(t, "+hello\r\n"); + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "hello"}, + }; + check_nodes(t, expected_nodes); +} + +// Reserving space uses the same allocation thresholds +void test_reserve_not_power_of_2() +{ + flat_tree t; + + // First threshold at 512 + t.reserve(200u, 5u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + + // Second threshold at 1024 + t.reserve(600u, 5u); + BOOST_TEST_EQ(t.data_capacity(), 1024u); + BOOST_TEST_EQ(t.get_reallocs(), 2u); +} + +// Requesting a capacity below the current one does nothing +void test_reserve_below_current_capacity() +{ + flat_tree t; + + // Reserving with a zero capacity does nothing + t.reserve(0u, 0u); + BOOST_TEST_EQ(t.data_capacity(), 0u); + BOOST_TEST_EQ(t.get_reallocs(), 0u); + + // Increase capacity + t.reserve(400u, 5u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + + // Reserving again does nothing + t.reserve(400u, 5u); + t.reserve(512u, 5u); + t.reserve(0u, 5u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); +} + +// Reserving might reallocate. If there are nodes, strings remain valid +void test_reserve_with_data() +{ + flat_tree t; + + // Add a bunch of nodes, and then reserve + add_nodes(t, "*2\r\n+hello\r\n+world\r\n"); + t.reserve(1000u, 10u); + + // 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() +{ + flat_tree t; + + // Add a bunch of nodes, then clear + add_nodes(t, "*2\r\n+hello\r\n+world\r\n"); + t.clear(); + + // Nodes are no longer there, but memory hasn't been fred + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); +} + +// Clearing an empty tree doesn't cause trouble +void test_clear_empty() +{ + flat_tree t; + t.clear(); + + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 0u); + BOOST_TEST_EQ(t.get_reallocs(), 0u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); +} + +// With clear, memory can be reused +// The response should be reusable. +void test_clear_reuse() +{ + flat_tree t; + + // First use + add_nodes(t, "~6\r\n+orange\r\n+apple\r\n+one\r\n+two\r\n+three\r\n+orange\r\n"); + std::vector expected_nodes{ + {type::set, 6u, 0u, "" }, + {type::simple_string, 1u, 1u, "orange"}, + {type::simple_string, 1u, 1u, "apple" }, + {type::simple_string, 1u, 1u, "one" }, + {type::simple_string, 1u, 1u, "two" }, + {type::simple_string, 1u, 1u, "three" }, + {type::simple_string, 1u, 1u, "orange"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); + + // Second use + t.clear(); + add_nodes(t, "*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_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// --- Default ctor --- +void test_default_constructor() +{ + flat_tree t; + + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.get_reallocs(), 0u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); +} + +// --- Copy ctor --- +void test_copy_ctor() +{ + // Setup + auto t = std::make_unique(); + 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"}, + }; + + // Construct, then destroy the original copy + flat_tree t2{*t}; + t.reset(); + + // Check + check_nodes(t2, expected_nodes); + BOOST_TEST_EQ(t2.data_size(), 10u); + BOOST_TEST_EQ(t2.data_capacity(), 512u); + BOOST_TEST_EQ(t2.get_reallocs(), 1u); + BOOST_TEST_EQ(t2.get_total_msgs(), 1u); +} + +// Copying an empty tree doesn't cause problems +void test_copy_ctor_empty() +{ + flat_tree t; + flat_tree t2{t}; + check_nodes(t2, {}); + BOOST_TEST_EQ(t2.data_size(), 0u); + BOOST_TEST_EQ(t2.data_capacity(), 0u); + BOOST_TEST_EQ(t2.get_reallocs(), 0u); + BOOST_TEST_EQ(t2.get_total_msgs(), 0u); +} + +// Copying an object that has no elements but some capacity doesn't cause trouble +void test_copy_ctor_empty_with_capacity() +{ + flat_tree t; + t.reserve(300u, 8u); + + flat_tree t2{t}; + check_nodes(t2, {}); + BOOST_TEST_EQ(t2.data_size(), 0u); + BOOST_TEST_EQ(t2.data_capacity(), 0u); + BOOST_TEST_EQ(t2.get_reallocs(), 0u); + BOOST_TEST_EQ(t2.get_total_msgs(), 0u); +} + +// Copying an object with more capacity than required adjusts its capacity +void test_copy_ctor_adjust_capacity() +{ + // Setup + flat_tree t; + add_nodes(t, "+hello\r\n"); + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "hello"}, + }; + + // Cause reallocations + t.reserve(1000u, 10u); + t.reserve(2000u, 10u); + t.reserve(4000u, 10u); + + // Copy + flat_tree t2{t}; + + // The target object has the minimum required capacity, + // and the number of reallocs has been reset + check_nodes(t2, expected_nodes); + BOOST_TEST_EQ(t2.data_size(), 5u); + BOOST_TEST_EQ(t2.data_capacity(), 512u); + BOOST_TEST_EQ(t2.get_reallocs(), 1u); + BOOST_TEST_EQ(t2.get_total_msgs(), 1u); +} + +// --- Move ctor --- +void test_move_ctor() +{ + flat_tree t; + add_nodes(t, "*2\r\n+hello\r\n+world\r\n"); + + flat_tree t2{std::move(t)}; + + std::vector expected_nodes{ + {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(), 10u); + BOOST_TEST_EQ(t2.data_capacity(), 512u); + BOOST_TEST_EQ(t2.get_reallocs(), 1u); + BOOST_TEST_EQ(t2.get_total_msgs(), 1u); +} + +// Moving an empty object doesn't cause trouble +void test_move_ctor_empty() +{ + flat_tree t; + + flat_tree t2{std::move(t)}; + + check_nodes(t2, {}); + BOOST_TEST_EQ(t2.data_size(), 0u); + BOOST_TEST_EQ(t2.data_capacity(), 0u); + BOOST_TEST_EQ(t2.get_reallocs(), 0u); + BOOST_TEST_EQ(t2.get_total_msgs(), 0u); +} + +// Moving an object with capacity but no data doesn't cause trouble +void test_move_ctor_with_capacity() +{ + flat_tree t; + t.reserve(1000u, 10u); + + flat_tree t2{std::move(t)}; + + check_nodes(t2, {}); + BOOST_TEST_EQ(t2.data_size(), 0u); + BOOST_TEST_EQ(t2.data_capacity(), 1024u); + BOOST_TEST_EQ(t2.get_reallocs(), 1u); + BOOST_TEST_EQ(t2.get_total_msgs(), 0u); +} + +// --- Copy assignment --- +void test_copy_assign() +{ + flat_tree t; + add_nodes(t, "+some_data\r\n"); + + auto t2 = std::make_unique(); + add_nodes(*t2, "*2\r\n+hello\r\n+world\r\n"); + + t = *t2; + + // Delete the source object, to check that we copied the contents + t2.reset(); + + 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_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// The lhs is empty and doesn't have any capacity +void test_copy_assign_target_empty() +{ + flat_tree t; + + flat_tree t2; + add_nodes(t2, "+hello\r\n"); + + t = t2; + + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "hello"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 5u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// If the target doesn't have enough capacity, a reallocation happens +void test_copy_assign_target_not_enough_capacity() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + const std::string big_node(2000u, 'a'); + flat_tree t2; + add_nodes(t2, "+" + big_node + "\r\n"); + + t = t2; + + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, big_node}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 2000u); + BOOST_TEST_EQ(t.data_capacity(), 2048u); + BOOST_TEST_EQ(t.get_reallocs(), 2u); // initial + assignment + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// If the source of the assignment is empty, nothing bad happens +void test_copy_assign_source_empty() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + flat_tree t2; + + t = t2; + + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 512u); // capacity is kept + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); +} + +// If the source of the assignment has capacity but no data, we're OK +void test_copy_assign_source_with_capacity() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + flat_tree t2; + t2.reserve(1000u, 4u); + t2.reserve(4000u, 8u); + + t = t2; + + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 512u); // capacity is kept + BOOST_TEST_EQ(t.get_reallocs(), 1u); // not propagated + BOOST_TEST_EQ(t.get_total_msgs(), 0u); +} + +// If the source of the assignment has data with extra capacity +// and a reallocation is needed, the minimum amount of space is allocated +void test_copy_assign_source_with_extra_capacity() +{ + flat_tree t; + + flat_tree t2; + add_nodes(t2, "+hello\r\n"); + t2.reserve(4000u, 8u); + + t = t2; + + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "hello"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 5u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +void test_copy_assign_both_empty() +{ + flat_tree t; + flat_tree t2; + + t = t2; + + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 0u); + BOOST_TEST_EQ(t.get_reallocs(), 0u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); +} + +// Self-assignment doesn't cause trouble +void test_copy_assign_self() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + const auto& tref = t; + t = tref; + + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "hello"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 5u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// --- Move assignment --- +void test_move_assign() +{ + flat_tree t; + add_nodes(t, "+some_data\r\n"); + + flat_tree t2; + add_nodes(t2, "*2\r\n+hello\r\n+world\r\n"); + + t = std::move(t2); + + 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_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// The lhs is empty and doesn't have any capacity +void test_move_assign_target_empty() +{ + flat_tree t; + + flat_tree t2; + add_nodes(t2, "+hello\r\n"); + + t = std::move(t2); + + std::vector expected_nodes{ + {type::simple_string, 1u, 0u, "hello"}, + }; + check_nodes(t, expected_nodes); + BOOST_TEST_EQ(t.data_size(), 5u); + BOOST_TEST_EQ(t.data_capacity(), 512u); + BOOST_TEST_EQ(t.get_reallocs(), 1u); + BOOST_TEST_EQ(t.get_total_msgs(), 1u); +} + +// If the source of the assignment is empty, nothing bad happens +void test_move_assign_source_empty() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + flat_tree t2; + + t = std::move(t2); + + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 0u); + BOOST_TEST_EQ(t.get_reallocs(), 0u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); +} + +// If both source and target are empty, nothing bad happens +void test_move_assign_both_empty() +{ + flat_tree t; + + flat_tree t2; + + t = std::move(t2); + + check_nodes(t, {}); + BOOST_TEST_EQ(t.data_size(), 0u); + BOOST_TEST_EQ(t.data_capacity(), 0u); + BOOST_TEST_EQ(t.get_reallocs(), 0u); + BOOST_TEST_EQ(t.get_total_msgs(), 0u); +} + +// --- Comparison --- +void test_comparison_different() +{ + flat_tree t; + add_nodes(t, "+some_data\r\n"); + + flat_tree t2; + add_nodes(t2, "*2\r\n+hello\r\n+world\r\n"); + + BOOST_TEST_NOT(t == t2); + BOOST_TEST(t != t2); + BOOST_TEST_NOT(t2 == t); + BOOST_TEST(t2 != t); +} + +// The only difference is node types +void test_comparison_different_node_types() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + flat_tree t2; + add_nodes(t2, "$5\r\nhello\r\n"); + + BOOST_TEST_NOT(t == t2); + BOOST_TEST(t != t2); +} + +void test_comparison_equal() +{ + flat_tree t; + add_nodes(t, "+some_data\r\n"); + + flat_tree t2; + add_nodes(t2, "+some_data\r\n"); + + BOOST_TEST(t == t2); + BOOST_TEST_NOT(t != t2); +} + +// Allocations are not taken into account when comparing +void test_comparison_equal_reallocations() +{ + const std::string big_node(2000u, 'a'); + flat_tree t; + t.reserve(100u, 5u); + add_nodes(t, "+" + big_node + "\r\n"); + BOOST_TEST_EQ(t.get_reallocs(), 2u); + + flat_tree t2; + t2.reserve(2048u, 5u); + add_nodes(t2, "+" + big_node + "\r\n"); + BOOST_TEST_EQ(t2.get_reallocs(), 1u); + + BOOST_TEST(t == t2); + BOOST_TEST_NOT(t != t2); +} + +// Capacity is not taken into account when comparing +void test_comparison_equal_capacity() +{ + flat_tree t; + add_nodes(t, "+hello\r\n"); + + flat_tree t2; + t2.reserve(2048u, 5u); + add_nodes(t2, "+hello\r\n"); + + BOOST_TEST(t == t2); + BOOST_TEST_NOT(t != t2); +} + +// Empty containers don't cause trouble +void test_comparison_empty() +{ + flat_tree t; + add_nodes(t, "$5\r\nhello\r\n"); + + flat_tree tempty, tempty2; + + BOOST_TEST_NOT(t == tempty); + BOOST_TEST(t != tempty); + + BOOST_TEST_NOT(tempty == t); + BOOST_TEST(tempty != t); + + BOOST_TEST(tempty == tempty2); + BOOST_TEST_NOT(tempty != tempty2); +} + +// Self comparisons don't cause trouble +void test_comparison_self() +{ + flat_tree t; + add_nodes(t, "$5\r\nhello\r\n"); + + flat_tree tempty; + + BOOST_TEST(t == t); + BOOST_TEST_NOT(t != t); + + BOOST_TEST(tempty == tempty); + BOOST_TEST_NOT(tempty != tempty); +} + +} // namespace + +int main() +{ + test_add_nodes(); + test_add_nodes_copies(); + test_add_nodes_capacity_limit(); + test_add_nodes_big_node(); + + test_reserve(); + test_reserve_not_power_of_2(); + test_reserve_below_current_capacity(); + test_reserve_with_data(); + + test_clear(); + test_clear_empty(); + test_clear_reuse(); + + test_default_constructor(); + + test_copy_ctor(); + test_copy_ctor_empty(); + test_copy_ctor_empty_with_capacity(); + test_copy_ctor_adjust_capacity(); + + 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_copy_assign(); + test_copy_assign_target_empty(); + test_copy_assign_target_not_enough_capacity(); + test_copy_assign_source_empty(); + test_copy_assign_source_with_capacity(); + test_copy_assign_source_with_extra_capacity(); + test_copy_assign_both_empty(); + test_copy_assign_self(); + + test_comparison_different(); + test_comparison_different_node_types(); + test_comparison_equal(); + test_comparison_equal_reallocations(); + test_comparison_equal_capacity(); + test_comparison_empty(); + test_comparison_self(); + + 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 687263d5b..f840fa555 100644 --- a/test/test_low_level_sync_sans_io.cpp +++ b/test/test_low_level_sync_sans_io.cpp @@ -13,8 +13,6 @@ #include #include -#include "print_node.hpp" - #define BOOST_TEST_MODULE low_level_sync_sans_io #include @@ -337,148 +335,6 @@ BOOST_AUTO_TEST_CASE(check_counter_adapter) BOOST_CHECK_EQUAL(done, 1); } -namespace boost::redis::resp3 { - -template -std::ostream& operator<<(std::ostream& os, basic_tree const& resp) -{ - for (auto const& e : resp) - os << e << ","; - return os; -} - -} // namespace boost::redis::resp3 - -node from_node_view(node_view const& v) -{ - node ret; - ret.data_type = v.data_type; - ret.aggregate_size = v.aggregate_size; - ret.depth = v.depth; - ret.value = v.value; - return ret; -} - -tree from_flat(flat_tree const& resp) -{ - tree ret; - for (auto const& e : resp.get_view()) - ret.push_back(from_node_view(e)); - - return ret; -} - -tree from_flat(generic_flat_response const& resp) -{ - tree ret; - for (auto const& e : resp.value().get_view()) - ret.push_back(from_node_view(e)); - - return ret; -} - -// Parses the same data into a tree and a -// flat_tree, they should be equal to each other. -BOOST_AUTO_TEST_CASE(flat_tree_views_are_set) -{ - tree resp1; - flat_tree resp2; - generic_flat_response resp3; - - error_code ec; - deserialize(resp3_set, adapt2(resp1), ec); - BOOST_CHECK_EQUAL(ec, error_code{}); - - deserialize(resp3_set, adapt2(resp2), ec); - BOOST_CHECK_EQUAL(ec, error_code{}); - - deserialize(resp3_set, adapt2(resp3), ec); - BOOST_CHECK_EQUAL(ec, error_code{}); - - BOOST_CHECK_EQUAL(resp2.get_reallocs(), 4u); - BOOST_CHECK_EQUAL(resp2.get_total_msgs(), 1u); - - BOOST_CHECK_EQUAL(resp3.value().get_reallocs(), 4u); - BOOST_CHECK_EQUAL(resp3.value().get_total_msgs(), 1u); - - auto const tmp2 = from_flat(resp2); - BOOST_CHECK_EQUAL(resp1, tmp2); - - auto const tmp3 = from_flat(resp3); - BOOST_CHECK_EQUAL(resp1, tmp3); -} - -// The response should be reusable. -BOOST_AUTO_TEST_CASE(flat_tree_reuse) -{ - flat_tree tmp; - - // First use - error_code ec; - deserialize(resp3_set, adapt2(tmp), ec); - BOOST_CHECK_EQUAL(ec, error_code{}); - - BOOST_CHECK_EQUAL(tmp.get_reallocs(), 4u); - BOOST_CHECK_EQUAL(tmp.get_total_msgs(), 1u); - - // Copy to compare after the reuse. - auto const resp1 = tmp.get_view(); - tmp.clear(); - - // Second use - deserialize(resp3_set, adapt2(tmp), ec); - BOOST_CHECK_EQUAL(ec, error_code{}); - - // No reallocation this time - BOOST_CHECK_EQUAL(tmp.get_reallocs(), 0u); - BOOST_CHECK_EQUAL(tmp.get_total_msgs(), 1u); - - BOOST_CHECK_EQUAL(resp1, tmp.get_view()); -} - -BOOST_AUTO_TEST_CASE(flat_tree_copy_assign) -{ - flat_tree ref1; - flat_tree ref2; - flat_tree ref3; - flat_tree ref4; - - error_code ec; - deserialize(resp3_set, adapt2(ref1), ec); - deserialize(resp3_set, adapt2(ref2), ec); - deserialize(resp3_set, adapt2(ref3), ec); - deserialize(resp3_set, adapt2(ref4), ec); - BOOST_CHECK_EQUAL(ec, error_code{}); - - // Copy ctor - resp3::flat_tree copy1{ref1}; - - // Move ctor - resp3::flat_tree move1{std::move(ref2)}; - - // Copy assignment - resp3::flat_tree copy2 = ref1; - - // Move assignment - resp3::flat_tree move2 = std::move(ref3); - - // Assignment - resp3::flat_tree copy3; - copy3 = ref1; - - // Move assignment - resp3::flat_tree move3; - move3 = std::move(ref4); - - BOOST_TEST((copy1 == ref1)); - BOOST_TEST((copy2 == ref1)); - BOOST_TEST((copy3 == ref1)); - - BOOST_TEST((move1 == ref1)); - BOOST_TEST((move2 == ref1)); - BOOST_TEST((move3 == ref1)); -} - BOOST_AUTO_TEST_CASE(generic_flat_response_simple_error) { generic_flat_response resp;