mirror of
https://github.com/transmission/transmission.git
synced 2026-02-15 07:26:49 +00:00
feat: add serializer support for std::chrono::sys_seconds, std::u8string, std::filesystem::path (#8364)
* chore: rename display-mode-tests.cc as converter-tests.cc
* feat: support std::chrono::sys_seconds in serializers
* feat: support std::u8string, std::filesystem::path in serializer
* build: address review feedback
* chore: remove unnecessary helper function
* Revert "chore: remove unnecessary helper function"
This reverts commit 69ea907836.
std::to_chars() unavailable on macOS < 13.3
We can remove this hack if/when we drop support for macOS < 13.3
This commit is contained in:
@@ -4,7 +4,7 @@ add_executable(libtransmission-app-test)
|
||||
|
||||
target_sources(libtransmission-app-test
|
||||
PRIVATE
|
||||
display-mode-tests.cc
|
||||
converter-tests.cc
|
||||
test-fixtures.h)
|
||||
|
||||
set_property(
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
// License text can be found in the licenses/ folder.
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <regex>
|
||||
#include <string_view>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
@@ -15,12 +18,36 @@
|
||||
|
||||
#include "test-fixtures.h"
|
||||
|
||||
using DisplayModeTest = TransmissionTest;
|
||||
using ConverterTest = TransmissionTest;
|
||||
using namespace std::literals;
|
||||
using tr::serializer::Converters;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int64_t days_from_civil(int year, unsigned month, unsigned day)
|
||||
{
|
||||
year -= month <= 2;
|
||||
auto const era = (year >= 0 ? year : year - 399) / 400;
|
||||
auto const yoe = static_cast<unsigned>(year - era * 400);
|
||||
auto const doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1;
|
||||
auto const doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
return static_cast<int64_t>(era) * 146097 + static_cast<int64_t>(doe) - 719468;
|
||||
}
|
||||
|
||||
constexpr std::chrono::sys_seconds make_sys_seconds(int year, int month, int day, int hour, int minute, int second)
|
||||
{
|
||||
return std::chrono::sys_seconds{ std::chrono::seconds{ days_from_civil(year, month, day) * 86400 } } +
|
||||
std::chrono::hours{ hour } + std::chrono::minutes{ minute } + std::chrono::seconds{ second };
|
||||
}
|
||||
|
||||
void expect_sys_seconds_eq(std::optional<std::chrono::sys_seconds> const& actual, std::chrono::sys_seconds expected)
|
||||
{
|
||||
EXPECT_TRUE(actual.has_value());
|
||||
if (actual.has_value())
|
||||
{
|
||||
EXPECT_EQ(actual->time_since_epoch().count(), expected.time_since_epoch().count());
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T, size_t N>
|
||||
void testModeRoundtrip(std::array<std::pair<std::string_view, T>, N> const& items)
|
||||
@@ -39,7 +66,7 @@ void testModeRoundtrip(std::array<std::pair<std::string_view, T>, N> const& item
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_F(DisplayModeTest, showModeStringsRoundtrip)
|
||||
TEST_F(ConverterTest, showModeStringsRoundtrip)
|
||||
{
|
||||
auto constexpr Items = std::array<std::pair<std::string_view, tr::app::ShowMode>, tr::app::ShowModeCount>{ {
|
||||
{ "show_active", tr::app::ShowMode::ShowActive },
|
||||
@@ -55,7 +82,7 @@ TEST_F(DisplayModeTest, showModeStringsRoundtrip)
|
||||
testModeRoundtrip(Items);
|
||||
}
|
||||
|
||||
TEST_F(DisplayModeTest, sortModeStringsRoundtrip)
|
||||
TEST_F(ConverterTest, sortModeStringsRoundtrip)
|
||||
{
|
||||
auto constexpr Items = std::array<std::pair<std::string_view, tr::app::SortMode>, tr::app::SortModeCount>{ {
|
||||
{ "sort_by_activity", tr::app::SortMode::SortByActivity },
|
||||
@@ -73,7 +100,7 @@ TEST_F(DisplayModeTest, sortModeStringsRoundtrip)
|
||||
testModeRoundtrip(Items);
|
||||
}
|
||||
|
||||
TEST_F(DisplayModeTest, statsModeStringsRoundtrip)
|
||||
TEST_F(ConverterTest, statsModeStringsRoundtrip)
|
||||
{
|
||||
auto constexpr Items = std::array<std::pair<std::string_view, tr::app::StatsMode>, tr::app::StatsModeCount>{ {
|
||||
{ "total_ratio", tr::app::StatsMode::TotalRatio },
|
||||
@@ -84,3 +111,33 @@ TEST_F(DisplayModeTest, statsModeStringsRoundtrip)
|
||||
|
||||
testModeRoundtrip(Items);
|
||||
}
|
||||
|
||||
TEST_F(ConverterTest, sysSecondsRoundtrip)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
using namespace tr::serializer;
|
||||
|
||||
auto constexpr Expected = make_sys_seconds(2024, 2, 3, 4, 5, 6);
|
||||
auto const var = Converters::serialize(Expected);
|
||||
EXPECT_TRUE(var.holds_alternative<std::string_view>());
|
||||
auto const serialized = var.value_if<std::string_view>().value_or(""sv);
|
||||
static auto const re = std::regex(R"(^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{4})$)");
|
||||
EXPECT_TRUE(std::regex_match(std::string{ serialized }, re));
|
||||
|
||||
auto actual = to_value<std::chrono::sys_seconds>(tr_variant{ serialized });
|
||||
expect_sys_seconds_eq(actual, Expected);
|
||||
|
||||
actual = to_value<std::chrono::sys_seconds>(tr_variant{ "2024-02-03T04:05:06Z"sv });
|
||||
expect_sys_seconds_eq(actual, Expected);
|
||||
|
||||
actual = to_value<std::chrono::sys_seconds>(tr_variant{ "2024-02-03T04:05:06+00:00"sv });
|
||||
expect_sys_seconds_eq(actual, Expected);
|
||||
|
||||
actual = to_value<std::chrono::sys_seconds>(tr_variant{ "2024-02-03T04:05:06+02:30"sv });
|
||||
expect_sys_seconds_eq(actual, Expected - (hours{ 2 } + minutes{ 30 }));
|
||||
|
||||
auto constexpr Epoch = int64_t{ 1700000000 };
|
||||
auto const epoch_seconds = time_point_cast<seconds>(system_clock::from_time_t(static_cast<time_t>(Epoch)));
|
||||
actual = to_value<std::chrono::sys_seconds>(tr_variant{ Epoch });
|
||||
expect_sys_seconds_eq(actual, epoch_seconds);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -13,6 +14,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include <libtransmission/net.h>
|
||||
#include <libtransmission/log.h>
|
||||
#include <libtransmission/quark.h>
|
||||
#include <libtransmission/serializer.h>
|
||||
#include <libtransmission/variant.h>
|
||||
@@ -26,6 +28,11 @@ 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;
|
||||
@@ -147,6 +154,61 @@ TEST_F(SerializerTest, usesBuiltins)
|
||||
}
|
||||
}
|
||||
|
||||
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, 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();
|
||||
|
||||
@@ -204,6 +204,18 @@ TEST_F(UtilsTest, strvReplaceInvalid)
|
||||
EXPECT_EQ(out, tr_strv_replace_invalid(out));
|
||||
}
|
||||
|
||||
TEST_F(UtilsTest, strvFindInvalidUtf8)
|
||||
{
|
||||
EXPECT_EQ(std::string_view::npos, tr_strv_find_invalid_utf8("hello"sv));
|
||||
EXPECT_EQ(std::string_view::npos, tr_strv_find_invalid_utf8("Трудно быть Богом"sv));
|
||||
|
||||
auto const invalid = std::string{ static_cast<char>(0xC3), static_cast<char>(0x28) };
|
||||
EXPECT_EQ(0U, tr_strv_find_invalid_utf8(invalid));
|
||||
|
||||
auto const mixed = std::string{ "ok " } + invalid + " end";
|
||||
EXPECT_EQ(3U, tr_strv_find_invalid_utf8(mixed));
|
||||
}
|
||||
|
||||
TEST_F(UtilsTest, strvConvertUtf8Fuzz)
|
||||
{
|
||||
auto buf = std::vector<char>{};
|
||||
|
||||
@@ -45,6 +45,9 @@ Q_DECLARE_METATYPE(Style)
|
||||
.arg(dir)
|
||||
};
|
||||
}
|
||||
|
||||
abort();
|
||||
return {};
|
||||
}
|
||||
|
||||
class SessionTest
|
||||
|
||||
Reference in New Issue
Block a user