mirror of
https://github.com/transmission/transmission.git
synced 2026-04-19 00:12:26 +01:00
458 lines
14 KiB
C++
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
|