Files
transmission/tests/libtransmission/serializer-tests.cc

458 lines
14 KiB
C++

// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <climits>
#include <cstdint>
#include <filesystem>
#include <list>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <libtransmission/net.h>
#include <libtransmission/log.h>
#include <libtransmission/quark.h>
#include <libtransmission/serializer.h>
#include <libtransmission/variant.h>
#include "test-fixtures.h"
using SerializerTest = ::tr::test::TransmissionTest;
using namespace std::literals;
using tr::serializer::Converters;
namespace
{
[[nodiscard]] std::string toString(std::u8string const& value)
{
return { reinterpret_cast<char const*>(std::data(value)), std::size(value) };
}
struct Rect
{
int x = 0;
int y = 0;
int width = 0;
int height = 0;
[[nodiscard]] bool operator==(Rect const& that) const noexcept
{
return x == that.x && y == that.y && width == that.width && height == that.height;
}
};
void registerRectConverter()
{
static auto const ToRect = [](tr_variant const& src, Rect* tgt)
{
auto const* const v = src.get_if<tr_variant::Vector>();
if (v == nullptr || std::size(*v) != 4U)
{
return false;
}
auto const x = (*v)[0].value_if<int64_t>();
auto const y = (*v)[1].value_if<int64_t>();
auto const w = (*v)[2].value_if<int64_t>();
auto const h = (*v)[3].value_if<int64_t>();
if (!x || !y || !w || !h)
{
return false;
}
*tgt = Rect{
.x = static_cast<int>(*x),
.y = static_cast<int>(*y),
.width = static_cast<int>(*w),
.height = static_cast<int>(*h),
};
return true;
};
static auto const FromRect = [](Rect const& r) -> tr_variant
{
auto v = tr_variant::Vector{};
v.reserve(4U);
v.emplace_back(int64_t{ r.x });
v.emplace_back(int64_t{ r.y });
v.emplace_back(int64_t{ r.width });
v.emplace_back(int64_t{ r.height });
return v;
};
static std::once_flag once;
std::call_once(once, [] { Converters::add<Rect>(ToRect, FromRect); });
}
TEST_F(SerializerTest, usesBuiltins)
{
{
auto const var = Converters::serialize(true);
EXPECT_TRUE(var.holds_alternative<bool>());
auto out = false;
EXPECT_TRUE(Converters::deserialize(var, &out));
EXPECT_EQ(out, true);
}
{
auto const var = Converters::serialize(3.5);
EXPECT_TRUE(var.holds_alternative<double>());
auto out = 0.0;
EXPECT_TRUE(Converters::deserialize(var, &out));
EXPECT_DOUBLE_EQ(out, 3.5);
}
{
auto const s = "hello"s;
auto const var = Converters::serialize(s);
EXPECT_TRUE(var.holds_alternative<std::string_view>());
EXPECT_EQ(var.value_if<std::string_view>().value_or(""sv), "hello"sv);
auto out = std::string{};
EXPECT_TRUE(Converters::deserialize(var, &out));
EXPECT_EQ(out, s);
}
{
auto const s = std::optional<std::string>{ "opt"s };
auto const var = Converters::serialize(s);
EXPECT_TRUE(var.holds_alternative<std::string_view>());
EXPECT_EQ(var.value_if<std::string_view>().value_or(""sv), "opt"sv);
auto out = std::optional<std::string>{};
EXPECT_TRUE(Converters::deserialize(var, &out));
ASSERT_TRUE(out.has_value());
EXPECT_EQ(*out, *s);
}
{
auto const s = std::optional<std::string>{};
auto const var = Converters::serialize(s);
EXPECT_TRUE(var.holds_alternative<std::nullptr_t>());
auto out = std::optional<std::string>{ "will reset"s };
EXPECT_TRUE(Converters::deserialize(var, &out));
EXPECT_FALSE(out.has_value());
}
{
auto const expected = uint64_t{ 12345678901234ULL };
auto const var = Converters::serialize(expected);
EXPECT_TRUE(var.holds_alternative<int64_t>());
auto out = uint64_t{};
EXPECT_TRUE(Converters::deserialize(var, &out));
EXPECT_EQ(out, expected);
}
}
TEST_F(SerializerTest, usesU8String)
{
auto const expected = std::u8string{ u8"hello" };
auto const var = Converters::serialize(expected);
EXPECT_TRUE(var.holds_alternative<std::string_view>());
EXPECT_EQ(var.value_if<std::string_view>().value_or(""sv), "hello"sv);
auto actual = std::u8string{};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(toString(actual), toString(expected));
}
TEST_F(SerializerTest, usesFsPath)
{
auto const expected = std::filesystem::path{ std::u8string{ u8"foo/βar" } };
auto const var = Converters::serialize(expected);
EXPECT_TRUE(var.holds_alternative<std::string_view>());
EXPECT_EQ(var.value_if<std::string_view>().value_or(""sv), "foo/βar"sv);
auto actual = std::filesystem::path{};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(toString(actual.u8string()), toString(expected.u8string()));
}
TEST_F(SerializerTest, usesTrPex)
{
static auto constexpr CompactIp = std::array{ '\x7F', '\0', '\0', '\1', '\x73', '\x1A' }; // 127.0.0.1:6771
static_assert(CompactIp.size() == tr_socket_address::CompactSockAddrBytes[TR_AF_INET]);
auto const expected_flags = static_cast<uint8_t>(tr_rand_int(0x100U));
auto const expected_sockaddr = tr_socket_address::from_compact_ipv4(reinterpret_cast<std::byte const*>(CompactIp.data()))
.first;
auto const var = Converters::serialize(tr_pex{ expected_sockaddr, expected_flags });
auto* const map = var.get_if<tr_variant::Map>();
ASSERT_NE(map, nullptr);
auto const compact_ip = map->value_if<std::string_view>(TR_KEY_socket_address);
ASSERT_TRUE(compact_ip);
EXPECT_EQ(
std::lexicographical_compare_three_way(compact_ip->begin(), compact_ip->end(), CompactIp.begin(), CompactIp.end()),
std::strong_ordering::equivalent);
EXPECT_EQ(map->value_if<int64_t>(TR_KEY_flags), expected_flags);
auto actual = tr_pex{};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual.socket_address, expected_sockaddr);
EXPECT_EQ(actual.flags, expected_flags);
}
TEST_F(SerializerTest, u8StringWarnsOnInvalidUtf8)
{
auto const bad = std::string{ static_cast<char>(0xC3), static_cast<char>(0x28) };
auto const var = tr_variant{ std::string_view{ bad } };
auto const old_level = tr_logGetLevel();
tr_logSetLevel(TR_LOG_WARN);
tr_logSetQueueEnabled(true);
tr_logFreeQueue(tr_logGetQueue());
auto actual = std::u8string{};
EXPECT_TRUE(Converters::deserialize(var, &actual));
auto* const msgs = tr_logGetQueue();
auto warned = false;
for (auto* msg = msgs; msg != nullptr; msg = msg->next)
{
if (msg->level == TR_LOG_WARN && msg->message.find("contains invalid UTF-8") != std::string::npos)
{
warned = true;
break;
}
}
tr_logFreeQueue(msgs);
tr_logSetQueueEnabled(false);
tr_logSetLevel(old_level);
EXPECT_TRUE(warned);
}
TEST_F(SerializerTest, usesCustomTypes)
{
registerRectConverter();
static constexpr Rect Expected{ .x = 10, .y = 20, .width = 640, .height = 480 };
auto const var = Converters::serialize(Expected);
auto actual = Rect{};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual, Expected);
}
TEST_F(SerializerTest, usesLists)
{
auto const expected = std::list<std::string>{ "apple", "ball", "cat" };
auto const var = Converters::serialize(expected);
auto const* const l = var.get_if<tr_variant::Vector>();
ASSERT_NE(l, nullptr);
ASSERT_EQ(std::size(*l), 3U);
EXPECT_EQ((*l)[0].value_if<std::string_view>().value_or(""sv), "apple"sv);
EXPECT_EQ((*l)[1].value_if<std::string_view>().value_or(""sv), "ball"sv);
EXPECT_EQ((*l)[2].value_if<std::string_view>().value_or(""sv), "cat"sv);
auto actual = decltype(expected){};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual, expected);
}
TEST_F(SerializerTest, usesVectors)
{
auto const expected = std::vector<std::string>{ "apple", "ball", "cat" };
auto const var = Converters::serialize(expected);
auto const* const l = var.get_if<tr_variant::Vector>();
ASSERT_NE(l, nullptr);
ASSERT_EQ(std::size(*l), 3U);
EXPECT_EQ((*l)[0].value_if<std::string_view>().value_or(""sv), "apple"sv);
EXPECT_EQ((*l)[1].value_if<std::string_view>().value_or(""sv), "ball"sv);
EXPECT_EQ((*l)[2].value_if<std::string_view>().value_or(""sv), "cat"sv);
auto actual = decltype(expected){};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual, expected);
}
TEST_F(SerializerTest, usesVectorsOfCustom)
{
registerRectConverter();
auto const expected = std::vector<Rect>{
{ .x = 1, .y = 2, .width = 3, .height = 4 },
{ .x = 10, .y = 20, .width = 640, .height = 480 },
};
auto const var = Converters::serialize(expected);
auto actual = decltype(expected){};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual, expected);
}
TEST_F(SerializerTest, usesNestedVectors)
{
auto const expected = std::vector<std::vector<std::string>>{ { "a", "b" }, { "c" } };
auto const var = Converters::serialize(expected);
auto const* const outer = var.get_if<tr_variant::Vector>();
ASSERT_NE(outer, nullptr);
ASSERT_EQ(std::size(*outer), 2U);
auto const* const inner0 = (*outer)[0].get_if<tr_variant::Vector>();
ASSERT_NE(inner0, nullptr);
ASSERT_EQ(std::size(*inner0), 2U);
EXPECT_EQ((*inner0)[0].value_if<std::string_view>().value_or(""sv), "a"sv);
EXPECT_EQ((*inner0)[1].value_if<std::string_view>().value_or(""sv), "b"sv);
auto const* const inner1 = (*outer)[1].get_if<tr_variant::Vector>();
ASSERT_NE(inner1, nullptr);
ASSERT_EQ(std::size(*inner1), 1U);
EXPECT_EQ((*inner1)[0].value_if<std::string_view>().value_or(""sv), "c"sv);
auto actual = decltype(expected){};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual, expected);
}
TEST_F(SerializerTest, vectorRejectsWrongType)
{
auto const var = tr_variant{ true };
auto out = std::vector<std::string>{ "keep" };
EXPECT_FALSE(Converters::deserialize(var, &out));
EXPECT_EQ(out, (std::vector<std::string>{ "keep" }));
}
TEST_F(SerializerTest, vectorIsNondestructiveOnPartialFailure)
{
auto list = tr_variant::Vector{};
list.reserve(3U);
list.emplace_back("ok"sv);
list.emplace_back(nullptr);
list.emplace_back("ok"sv);
auto const var = tr_variant{ std::move(list) };
auto out = std::vector<std::string>{ "keep" };
EXPECT_FALSE(Converters::deserialize(var, &out));
EXPECT_EQ(out, (std::vector<std::string>{ "keep" }));
}
TEST_F(SerializerTest, usesOptional)
{
auto const expected = std::optional{ "apple"s };
auto const var = Converters::serialize(expected);
auto const sv = var.value_if<std::string_view>();
ASSERT_EQ(sv, "apple"sv);
auto actual = decltype(expected){};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual, expected);
}
TEST_F(SerializerTest, usesNullOptional)
{
auto const expected = std::optional<std::string>{};
auto const var = Converters::serialize(expected);
auto const sv = var.value_if<std::string_view>();
ASSERT_FALSE(sv);
auto actual = decltype(expected){ "discard"s };
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual, expected);
}
TEST_F(SerializerTest, usesOptionalOfCustom)
{
registerRectConverter();
constexpr auto Expected = std::optional{ Rect{ .x = 1, .y = 2, .width = 3, .height = 4 } };
auto const var = Converters::serialize(Expected);
auto actual = decltype(Expected){};
EXPECT_TRUE(Converters::deserialize(var, &actual));
EXPECT_EQ(actual, Expected);
}
TEST_F(SerializerTest, optionalRejectsWrongType)
{
auto const var = tr_variant{ true };
auto out = std::optional{ "keep"s };
EXPECT_FALSE(Converters::deserialize(var, &out));
EXPECT_EQ(out, "keep"s);
}
// ---
using tr::serializer::Field;
using tr::serializer::load;
using tr::serializer::save;
struct Endpoint
{
std::string address;
tr_port port;
static constexpr auto Fields = std::tuple{
Field<&Endpoint::address>{ TR_KEY_address },
Field<&Endpoint::port>{ TR_KEY_port },
};
[[nodiscard]] bool operator==(Endpoint const& that) const noexcept
{
return address == that.address && port == that.port;
}
// C++17 requires explicit operator!=; C++20 would auto-generate from operator==
[[nodiscard]] bool operator!=(Endpoint const& that) const noexcept
{
return !(*this == that);
}
};
TEST_F(SerializerTest, fieldSaveLoad)
{
auto const expected = Endpoint{ .address = "localhost", .port = tr_port::from_host(51413) };
// Save to variant
auto constexpr Expected = R"({"address":"localhost","port":51413})"sv;
auto const var = tr_variant{ save(expected, Endpoint::Fields) };
EXPECT_EQ(Expected, tr_variant_serde::json().compact().to_string(var));
// Load back into a new instance
auto actual = Endpoint{};
EXPECT_NE(actual, expected);
load(actual, Endpoint::Fields, var);
EXPECT_EQ(actual, expected);
}
TEST_F(SerializerTest, fieldLoadIgnoresMissingKeys)
{
auto endpoint = Endpoint{ .address = "default", .port = tr_port::from_host(9999) };
auto const original = endpoint;
load(endpoint, Endpoint::Fields, tr_variant::make_map());
// Should remain unchanged
EXPECT_EQ(original, endpoint);
}
TEST_F(SerializerTest, fieldLoadIgnoresNonMap)
{
auto endpoint = Endpoint{ .address = "default", .port = tr_port::from_host(9999) };
auto const original = endpoint;
load(endpoint, Endpoint::Fields, tr_variant{ 42 });
// Should remain unchanged
EXPECT_EQ(original, endpoint);
}
} // namespace