Files
scylladb/test/boost/string_format_test.cc
Avi Kivity 3c54d5ec5e test: string_format_test: don't compare std::string with sstring
For unknown reasons, clang 16 rejects equality comparison
(operator==) where the left-hand-side is an std::string and the
right-hand-side is an sstring. gcc and older clang versions first
convert the left-hand-side to an sstring and then call the symmetric
equality operator.

I was able to hack sstring to support this assymetric comparison,
but the solution is quite convoluted, and it may be that it's clang
at fault here. So instead this patch eliminates the three cases where
it happened. With is applied, we can build with clang 16.

Closes #13893
2023-05-16 08:56:16 +03:00

266 lines
9.3 KiB
C++

/*
* Copyright (C) 2023-present ScyllaDB
*/
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#define BOOST_TEST_MODULE string_format
#include <boost/test/unit_test.hpp>
#include <array>
#include <vector>
#include <list>
#include <deque>
#include <set>
#include <unordered_set>
#include <map>
#include <unordered_map>
#include <initializer_list>
#include <boost/range/adaptors.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <fmt/format.h>
#include <seastar/core/print.hh>
#include "utils/to_string.hh"
#include "utils/small_vector.hh"
#include "utils/chunked_vector.hh"
#include "bytes.hh"
// Test scylla's string formatters and printers defined in utils/to_string.hh
namespace {
std::string_view trim(std::string_view sv) {
auto it = sv.begin();
auto end = sv.end();
while (it != end && *it == ' ') {
++it;
}
return std::string_view(it, end);
}
std::string_view cmp_and_remove_prefix(std::string_view sv, std::string_view expected) {
BOOST_TEST_MESSAGE(fmt::format("cmp_and_remove_prefix: {} expected='{}'", sv, expected));
trim(sv);
BOOST_REQUIRE(sv.starts_with(expected));
auto sz = expected.size();
sv = sv.substr(sz, sv.size() - sz);
return trim(sv);
}
// Verify that the formatted string begins and ends
// with a matching pair of supported perenthesis.
void verify_parenthesis(std::string_view sv) {
static std::unordered_map<char, char> paren_map = {{'{', '}'}, {'[', ']'}, {'(', ')'}, {'<', '>'}};
BOOST_REQUIRE(!sv.empty());
char open = sv.front();
char close = sv.back();
auto it = paren_map.find(open);
if (it == paren_map.end()) {
BOOST_FAIL(fmt::format("Unexpected delimiters: '{}' '{}'", open, close));
}
BOOST_REQUIRE_EQUAL(close, it->second);
}
// Verify that formatting a range using seastar::format is compatible
// with formatting using fmt::format,
// And that it produces a valid list, separated
// by the `expected_delim` and has parenthesis iff `expect_parenthesis` is set.
// The output is compared the ordered vector of `expected_strings`.
template <std::ranges::range Range>
void test_format_range(const char* desc, Range x, std::vector<std::string> expected_strings, std::string expected_delim = ",", bool expect_parenthesis = true) {
auto str = seastar::format("{}", x);
BOOST_TEST_MESSAGE(fmt::format("{}: {}", desc, str));
auto fmt_str = fmt::format("{}", x);
BOOST_REQUIRE_EQUAL(str, fmt_str);
auto fmt_to_string = fmt::to_string(x);
BOOST_REQUIRE_EQUAL(str, fmt_to_string);
size_t num_elements = expected_strings.size();
size_t paren_size = expect_parenthesis ? 2 : 0;
size_t min_size = paren_size + (x.begin() == x.end() ? 0 : (num_elements - 1));
BOOST_REQUIRE_GE(str.size(), min_size);
std::string_view sv = str;
if (expect_parenthesis) {
verify_parenthesis(sv);
sv = sv.substr(1, sv.size() - 2);
}
bool first = true;
while (!expected_strings.empty()) {
sv = trim(sv);
if (!std::exchange(first, false)) {
sv = cmp_and_remove_prefix(sv, expected_delim);
}
auto s = *expected_strings.begin();
sv = cmp_and_remove_prefix(sv, s);
expected_strings.erase(expected_strings.begin());
}
BOOST_REQUIRE(sv.empty());
}
// Verify that formatting a range using seastar::format is compatible
// with formatting using fmt::format,
// And that it produces a valid list, separated
// by the `expected_delim` and has parenthesis iff `expect_parenthesis` is set.
// The output is compared the unordered set of `expected_strings`.
template <std::ranges::range Range>
void test_format_range(const char* desc, Range x, std::unordered_set<std::string> expected_strings, std::string expected_delim = ",", bool expect_parenthesis = true) {
auto str = seastar::format("{}", x);
BOOST_TEST_MESSAGE(fmt::format("{}: {}", desc, str));
auto fmt_str = fmt::format("{}", x);
BOOST_REQUIRE_EQUAL(str, fmt_str);
auto fmt_to_string = fmt::to_string(x);
BOOST_REQUIRE_EQUAL(str, fmt_to_string);
size_t num_elements = expected_strings.size();
size_t paren_size = expect_parenthesis ? 2 : 0;
size_t min_size = paren_size + (x.empty() ? 0 : (num_elements - 1));
BOOST_REQUIRE_GE(str.size(), min_size);
std::string_view sv = str;
if (expect_parenthesis) {
verify_parenthesis(sv);
sv = sv.substr(1, sv.size() - 2);
}
bool first = true;
while (!expected_strings.empty()) {
sv = trim(sv);
if (!std::exchange(first, false)) {
sv = cmp_and_remove_prefix(sv, expected_delim);
}
for (auto it = expected_strings.begin(); it != expected_strings.end(); ++it) {
if (sv.starts_with(*it)) {
sv = cmp_and_remove_prefix(sv, *it);
expected_strings.erase(it);
break;
}
}
}
BOOST_REQUIRE(sv.empty());
}
} // namespace
BOOST_AUTO_TEST_CASE(test_vector_format) {
auto ints = {1, 2, 3};
auto ordered_strings = std::vector<std::string>({"1", "2", "3"});
auto unordered_strings = std::unordered_set<std::string>(ordered_strings.begin(), ordered_strings.end());
auto vector = std::vector<int>(ints);
test_format_range("vector", vector, ordered_strings);
auto array = std::array<int, 3>({1, 2, 3});
test_format_range("array", array, ordered_strings);
auto list = std::list<int>(ints);
test_format_range("list", list, ordered_strings);
auto deque = std::deque<int>(ints);
test_format_range("deque", deque, ordered_strings);
auto set = std::set<int>(ints);
test_format_range("set", set, ordered_strings);
auto unordered_set = std::unordered_set<int>(ints);
test_format_range("unordered_set", unordered_set, unordered_strings);
auto small_vector = utils::small_vector<int, 1>(ints);
test_format_range("small_vector", small_vector, ordered_strings);
auto chunked_vector = utils::chunked_vector<int, 131072>(ints);
test_format_range("chunked_vector", chunked_vector, ordered_strings);
test_format_range("initializer_list", std::initializer_list<std::string>{"1", "2", "3"}, ordered_strings);
auto map = std::map<int, std::string>({{1, "one"}, {2, "two"}, {3, "three"}});
auto ordered_map_strings = std::vector<std::string>({"{1, one}", "{2, two}", "{3, three}"});
auto ordered_map_values = std::vector<std::string>({"one", "two", "three"});
test_format_range("map", map, ordered_map_strings);
test_format_range("map | boost::adaptors::map_keys", map | boost::adaptors::map_keys, ordered_strings);
test_format_range("map | boost::adaptors::map_values", map | boost::adaptors::map_values, ordered_map_values);
auto unordered_map = std::unordered_map<int, std::string>(map.begin(), map.end());
// seastar has a specialized print function for unordered_map
// See https://github.com/scylladb/seastar/issues/1544
auto unordered_map_strings = std::unordered_set<std::string>({"{1 -> one}", "{2 -> two}", "{3 -> three}"});
auto unordered_map_values = std::unordered_set<std::string>(ordered_map_values.begin(), ordered_map_values.end());
test_format_range("unordered_map", unordered_map, unordered_map_strings);
test_format_range("unordered_map | boost::adaptors::map_keys", unordered_map | boost::adaptors::map_keys, unordered_strings);
test_format_range("unordered_map | boost::adaptors::map_values", unordered_map | boost::adaptors::map_values, unordered_map_values);
}
BOOST_AUTO_TEST_CASE(test_string_format) {
seastar::sstring sstring = "foo";
auto formatted = fmt::format("{}", sstring);
BOOST_REQUIRE_EQUAL(seastar::sstring(formatted), sstring);
std::string std_string = "foo";
formatted = fmt::format("{}", std_string);
BOOST_REQUIRE_EQUAL(seastar::sstring(formatted), std_string);
std::string_view std_string_view = std_string;
formatted = fmt::format("{}", std_string_view);
BOOST_REQUIRE_EQUAL(formatted, std_string_view);
}
BOOST_AUTO_TEST_CASE(test_bytes_format) {
auto b = to_bytes("f0");
auto formatted = fmt::format("{}", b);
auto expected = to_hex(b);
BOOST_REQUIRE_EQUAL(seastar::sstring(formatted), expected);
}
BOOST_AUTO_TEST_CASE(test_optional_string_format) {
std::optional<std::string> sopt;
auto s = fmt::format("{}", sopt);
BOOST_TEST_MESSAGE(fmt::format("Empty opt: {}", s));
BOOST_REQUIRE_EQUAL(s.size(), 2);
verify_parenthesis(s);
sopt.emplace("foo");
s = fmt::format("{}", sopt);
BOOST_TEST_MESSAGE(fmt::format("Engaged opt: {}", s));
}
BOOST_AUTO_TEST_CASE(test_fs_path_format) {
auto str_path = "/foo/bar";
auto fs_path = std::filesystem::path(str_path);
auto formatted = fmt::format("{}", fs_path);
// fs::path printer quotes the path
auto expected = fmt::format("\"{}\"", str_path);
BOOST_REQUIRE_EQUAL(formatted, expected);
}
BOOST_AUTO_TEST_CASE(test_boost_transformed_range_format) {
auto v = std::vector<int>({1, 2, 3});
test_format_range("boost::adaptors::transformed", v | boost::adaptors::transformed([] (int i) { return fmt::format("{}", i * 11); }),
std::vector<std::string>({"11", "22", "33"}));
auto sv = utils::small_vector<int, 3>({1, 2, 3});
test_format_range("transformed vector", sv | boost::adaptors::transformed([] (int i) { return fmt::format("/{}", i); }),
std::vector<std::string>({"/1", "/2", "/3"}));
}