test: add unit tests for Prefs (#8112)

* fix: hicpp-use-auto,modernize-use-auto

* refactor: make Prefs::getKey() a static method

refactor: make Prefs::isCore() a static method

refactor: make Prefs::type() a static method

* refactor: Application takes a Prefs& arg, not a std::unique_ptr<Prefs> arg

* fix: bugprone-exception-escape

save settings by calling prefs.save() from main()

* refactor: load settings by calling prefs.load() from main()

* refactor: use preferred declaration order in Prefs

* fixup! fix: bugprone-exception-escape

* refactor: add Prefs::current_values()

* refactor: clean up namespace use in Prefs.cc

* feat: add QString, QDateTime serializers

* test: add scaffolding for testing Qt code

test: add tests for Prefs

* refactor: remove unused #includes

* build: add clang-tidy rules to tests/qt/

* refactor: clean up the new test code a little

* chore: add missing copyright statement

* ci: ensure Qt6Test is installed

build: check for QTest when ENABLE_TESTS + ENABLE_QT are ON

* fixup! feat: add QString, QDateTime serializers

* fix: Wswitch warning

* build: do not disable tests in release/windows/build-qt5.psl, build-qt6.psl

* ci: set QT_QPA_PLATFORM for running new Qt tests

* test: build cleanly in Qt 5.15

* fixup! fixup! feat: add QString, QDateTime serializers

fix QDateTime serializer on macOS

* fixup! ci: set QT_QPA_PLATFORM for running new Qt tests

install xcb-util-cursor on alpine
This commit is contained in:
Charles Kerr
2026-01-11 19:23:00 -06:00
committed by GitHub
parent cf0a596a45
commit d177f9f903
19 changed files with 750 additions and 321 deletions

View File

@@ -461,7 +461,7 @@ jobs:
run: apk add --upgrade glibmm-dev gtkmm3-dev
- name: Get Dependencies (Qt6)
if: ${{ needs.what-to-make.outputs.make-qt == 'true' }}
run: apk add --upgrade qt6-qttools-dev qt6-qtsvg-dev
run: apk add --upgrade qt6-qtbase-dev qt6-qttools-dev qt6-qtsvg-dev xcb-util-cursor
- name: Get Source
uses: actions/checkout@v4
with:
@@ -494,6 +494,7 @@ jobs:
if: ${{ needs.what-to-make.outputs.make-tests == 'true' }}
env:
TMPDIR: /private/tmp
QT_QPA_PLATFORM: offscreen
run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure
- name: Install
run: cmake --build obj --config RelWithDebInfo --target install/strip
@@ -765,6 +766,8 @@ jobs:
run: cmake --build obj --config RelWithDebInfo
- name: Test
if: ${{ needs.what-to-make.outputs.make-tests == 'true' }}
env:
QT_QPA_PLATFORM: offscreen
run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure
- name: Install
run: cmake --build obj --config RelWithDebInfo --target install/strip
@@ -814,7 +817,7 @@ jobs:
run: dnf install -y glibmm2.68-devel gtkmm4.0-devel
- name: Get Dependencies (Qt6)
if: ${{ needs.what-to-make.outputs.make-qt == 'true' }}
run: dnf install -y qt6-qtbase-devel qt6-qtsvg-devel qt6-qttools-devel
run: dnf install -y qt6-qtbase-devel qt6-qtsvg-devel qt6-qttools-devel xcb-util-cursor
- name: Get Source
uses: actions/download-artifact@v4
with:
@@ -845,6 +848,8 @@ jobs:
run: cmake --build obj --config RelWithDebInfo
- name: Test
if: ${{ needs.what-to-make.outputs.make-tests == 'true' }}
env:
QT_QPA_PLATFORM: offscreen
run: cmake -E chdir obj ctest -j $(nproc) --build-config RelWithDebInfo --output-on-failure
- name: Install
run: cmake --build obj --config RelWithDebInfo --target install/strip
@@ -1014,7 +1019,7 @@ jobs:
run: sudo apt-get install -y --no-install-recommends libglibmm-2.4-dev libgtkmm-3.0-dev
- name: Get Dependencies (Qt6)
if: ${{ needs.what-to-make.outputs.make-qt == 'true' }}
run: sudo apt-get install -y --no-install-recommends qt6-svg-dev qt6-tools-dev
run: sudo apt-get install -y --no-install-recommends qt6-base-dev qt6-svg-dev qt6-tools-dev
- name: Get Source
uses: actions/checkout@v4
with:

View File

@@ -409,6 +409,9 @@ if(ENABLE_QT)
Network
Svg
LinguistTools)
if(ENABLE_TESTS)
list(APPEND QT_REQUIRED_MODULES Test)
endif()
set(QT_OPTIONAL_MODULES
DBus
AxContainer
@@ -429,7 +432,7 @@ if(ENABLE_QT)
foreach(M ${QT_REQUIRED_MODULES})
find_package(Qt${Qt_VERSION_MAJOR}${M} ${QT_MINIMUM} QUIET)
if(Qt${Qt_VERSION_MAJOR}${M}_FOUND)
if(NOT M STREQUAL "LinguistTools")
if(NOT M STREQUAL "LinguistTools" AND NOT M STREQUAL "Test")
list(APPEND QT_TARGETS Qt${Qt_VERSION_MAJOR}::${M})
endif()
else()
@@ -811,11 +814,6 @@ if(RUN_CLANG_TIDY)
endif()
endif()
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
function(tr_install_web DST_DIR)
if(INSTALL_WEB)
install(
@@ -844,6 +842,11 @@ foreach(P cli daemon gtk mac qt utils)
endif()
endforeach()
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
if(ENABLE_DAEMON OR ENABLE_GTK OR ENABLE_QT)
tr_install_web(${CMAKE_INSTALL_DATAROOTDIR}/${TR_NAME})
endif()

View File

@@ -128,14 +128,14 @@ QAccessibleInterface* accessibleFactory(QString const& className, QObject* objec
} // namespace
Application::Application(
std::unique_ptr<Prefs> prefs,
Prefs& prefs,
bool minimized,
QString const& config_dir,
QStringList const& filenames,
int& argc,
char** argv)
: QApplication{ argc, argv }
, prefs_(std::move(prefs))
, prefs_{ prefs }
{
setApplicationName(ConfigName);
loadTranslations();
@@ -162,9 +162,9 @@ Application::Application(
QAccessible::installFactory(&accessibleFactory);
#endif
session_ = std::make_unique<Session>(config_dir, *prefs_);
model_ = std::make_unique<TorrentModel>(*prefs_);
window_ = std::make_unique<MainWindow>(*session_, *prefs_, *model_, minimized);
session_ = std::make_unique<Session>(config_dir, prefs_);
model_ = std::make_unique<TorrentModel>(prefs_);
window_ = std::make_unique<MainWindow>(*session_, prefs_, *model_, minimized);
watch_dir_ = std::make_unique<WatchDir>(*model_);
connect(this, &QCoreApplication::aboutToQuit, this, &Application::saveGeometry);
@@ -172,7 +172,7 @@ Application::Application(
connect(model_.get(), &TorrentModel::torrentsCompleted, this, &Application::onTorrentsCompleted);
connect(model_.get(), &TorrentModel::torrentsEdited, this, &Application::onTorrentsEdited);
connect(model_.get(), &TorrentModel::torrentsNeedInfo, this, &Application::onTorrentsNeedInfo);
connect(prefs_.get(), &Prefs::changed, this, &Application::refreshPref);
connect(&prefs_, &Prefs::changed, this, &Application::refreshPref);
connect(session_.get(), &Session::sourceChanged, this, &Application::onSessionSourceChanged);
connect(session_.get(), &Session::torrentsRemoved, model_.get(), &TorrentModel::removeTorrents);
connect(session_.get(), &Session::torrentsUpdated, model_.get(), &TorrentModel::updateTorrents);
@@ -295,7 +295,7 @@ QStringList Application::getNames(torrent_ids_t const& torrent_ids) const
void Application::onTorrentsAdded(torrent_ids_t const& torrent_ids) const
{
if (!prefs_->get<bool>(Prefs::SHOW_NOTIFICATION_ON_ADD))
if (!prefs_.get<bool>(Prefs::SHOW_NOTIFICATION_ON_ADD))
{
return;
}
@@ -308,19 +308,19 @@ void Application::onTorrentsAdded(torrent_ids_t const& torrent_ids) const
void Application::onTorrentsCompleted(torrent_ids_t const& torrent_ids) const
{
if (prefs_->get<bool>(Prefs::SHOW_NOTIFICATION_ON_COMPLETE))
if (prefs_.get<bool>(Prefs::SHOW_NOTIFICATION_ON_COMPLETE))
{
auto const title = tr("Torrent(s) Completed", nullptr, static_cast<int>(std::size(torrent_ids)));
auto const body = getNames(torrent_ids).join(QStringLiteral("\n"));
notifyApp(title, body);
}
if (prefs_->get<bool>(Prefs::COMPLETE_SOUND_ENABLED))
if (prefs_.get<bool>(Prefs::COMPLETE_SOUND_ENABLED))
{
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
beep();
#else
auto args = prefs_->get<QStringList>(Prefs::COMPLETE_SOUND_COMMAND);
auto args = prefs_.get<QStringList>(Prefs::COMPLETE_SOUND_COMMAND);
auto const command = args.takeFirst();
QProcess::execute(command, args);
#endif
@@ -346,13 +346,13 @@ void Application::notifyTorrentAdded(Torrent const* tor) const
void Application::saveGeometry() const
{
if (prefs_ != nullptr && window_ != nullptr)
if (window_ != nullptr)
{
auto const geometry = window_->geometry();
prefs_->set(Prefs::MAIN_WINDOW_HEIGHT, std::max(100, geometry.height()));
prefs_->set(Prefs::MAIN_WINDOW_WIDTH, std::max(100, geometry.width()));
prefs_->set(Prefs::MAIN_WINDOW_X, geometry.x());
prefs_->set(Prefs::MAIN_WINDOW_Y, geometry.y());
prefs_.set(Prefs::MAIN_WINDOW_HEIGHT, std::max(100, geometry.height()));
prefs_.set(Prefs::MAIN_WINDOW_WIDTH, std::max(100, geometry.width()));
prefs_.set(Prefs::MAIN_WINDOW_X, geometry.x());
prefs_.set(Prefs::MAIN_WINDOW_Y, geometry.y());
}
}
@@ -368,7 +368,7 @@ void Application::refreshPref(int key) const
case Prefs::DIR_WATCH:
case Prefs::DIR_WATCH_ENABLED:
watch_dir_->setPath(prefs_->get<QString>(Prefs::DIR_WATCH), prefs_->get<bool>(Prefs::DIR_WATCH_ENABLED));
watch_dir_->setPath(prefs_.get<QString>(Prefs::DIR_WATCH), prefs_.get<bool>(Prefs::DIR_WATCH_ENABLED));
break;
default:
@@ -378,19 +378,19 @@ void Application::refreshPref(int key) const
void Application::maybeUpdateBlocklist() const
{
if (!prefs_->get<bool>(Prefs::BLOCKLIST_UPDATES_ENABLED))
if (!prefs_.get<bool>(Prefs::BLOCKLIST_UPDATES_ENABLED))
{
return;
}
QDateTime const last_updated_at = prefs_->get<QDateTime>(Prefs::BLOCKLIST_DATE);
QDateTime const next_update_at = last_updated_at.addDays(7);
QDateTime const now = QDateTime::currentDateTime();
auto const last_updated_at = prefs_.get<QDateTime>(Prefs::BLOCKLIST_DATE);
auto const next_update_at = last_updated_at.addDays(7);
auto const now = QDateTime::currentDateTime();
if (now < next_update_at)
{
session_->updateBlocklist();
prefs_->set(Prefs::BLOCKLIST_DATE, now);
prefs_.set(Prefs::BLOCKLIST_DATE, now);
}
}
@@ -426,7 +426,7 @@ void Application::refreshTorrents()
void Application::addWatchdirTorrent(QString const& filename) const
{
auto add_data = AddData{ filename };
auto const disposal = prefs_->get<bool>(Prefs::TRASH_ORIGINAL) ? AddData::FilenameDisposal::Delete :
auto const disposal = prefs_.get<bool>(Prefs::TRASH_ORIGINAL) ? AddData::FilenameDisposal::Delete :
AddData::FilenameDisposal::Rename;
add_data.setFileDisposal(disposal);
addTorrent(std::move(add_data));
@@ -441,18 +441,18 @@ void Application::addTorrent(AddData addme) const
// if there's not already a disposal action set,
// then honor the `trash original` preference setting
if (!addme.fileDisposal() && prefs_->get<bool>(Prefs::TRASH_ORIGINAL))
if (!addme.fileDisposal() && prefs_.get<bool>(Prefs::TRASH_ORIGINAL))
{
addme.setFileDisposal(AddData::FilenameDisposal::Delete);
}
if (!prefs_->get<bool>(Prefs::OPTIONS_PROMPT))
if (!prefs_.get<bool>(Prefs::OPTIONS_PROMPT))
{
session_->addTorrent(addme);
}
else
{
auto* o = new OptionsDialog{ *session_, *prefs_, addme, window_.get() };
auto* o = new OptionsDialog{ *session_, prefs_, addme, window_.get() };
o->show();
}

View File

@@ -36,13 +36,7 @@ class Application : public QApplication
Q_OBJECT
public:
Application(
std::unique_ptr<Prefs> prefs,
bool minimized,
QString const& config_dir,
QStringList const& filenames,
int& argc,
char** argv);
Application(Prefs& prefs, bool minimized, QString const& config_dir, QStringList const& filenames, int& argc, char** argv);
Application(Application&&) = delete;
Application(Application const&) = delete;
Application& operator=(Application&&) = delete;
@@ -107,7 +101,7 @@ private:
std::unordered_set<QString> interned_strings_;
std::unique_ptr<Prefs> prefs_;
Prefs& prefs_;
std::unique_ptr<Session> session_;
std::unique_ptr<TorrentModel> model_;
std::unique_ptr<MainWindow> window_;

View File

@@ -3,9 +3,9 @@ set_property(GLOBAL PROPERTY AUTOGEN_SOURCE_GROUP "Generated Files")
# https://doc.qt.io/qt-6/macos.html#supported-versions
set(CMAKE_OSX_DEPLOYMENT_TARGET 11)
add_executable(${TR_NAME}-qt WIN32)
add_library(${TR_NAME}-qt-lib STATIC)
target_sources(${TR_NAME}-qt
target_sources(${TR_NAME}-qt-lib
PRIVATE
AboutDialog.cc
AboutDialog.h
@@ -56,7 +56,6 @@ target_sources(${TR_NAME}-qt
InteropObject.h
LicenseDialog.cc
LicenseDialog.h
main.cc
MainWindow.cc
MainWindow.h
MakeDialog.cc
@@ -107,6 +106,8 @@ target_sources(${TR_NAME}-qt
TrackerModel.h
TrackerModelFilter.cc
TrackerModelFilter.h
TrQtInit.cc
TrQtInit.h
Typedefs.h
Utils.cc
Utils.h
@@ -121,7 +122,7 @@ tr_allow_compile_if(
[=[[ENABLE_QT_DBUS_INTEROP]]=]
DBusInteropHelper.cc)
target_sources(${TR_NAME}-qt
target_sources(${TR_NAME}-qt-lib
PRIVATE
AboutDialog.ui
DetailsDialog.ui
@@ -139,7 +140,7 @@ target_sources(${TR_NAME}-qt
source_group(Ui
REGULAR_EXPRESSION [[.*\.ui$]])
target_sources(${TR_NAME}-qt
target_sources(${TR_NAME}-qt-lib
PRIVATE
application.qrc)
@@ -196,35 +197,49 @@ if(TS_FILES)
tr_qt_add_translation(QM_FILES ${TS_FILES})
endif()
target_sources(${TR_NAME}-qt
target_sources(${TR_NAME}-qt-lib
PRIVATE
${QM_FILES})
if(ENABLE_QT_COM_INTEROP)
tr_target_idl_files(${TR_NAME}-qt
tr_target_idl_files(${TR_NAME}-qt-lib
transmission-qt.idl)
endif()
target_include_directories(${TR_NAME}-qt
PRIVATE
target_include_directories(${TR_NAME}-qt-lib
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(${TR_NAME}-qt
PRIVATE
target_link_libraries(${TR_NAME}-qt-lib
PUBLIC
${TR_NAME}-app
${TR_NAME}
transmission::qt_impl)
target_compile_definitions(${TR_NAME}-qt
target_compile_definitions(${TR_NAME}-qt-lib
PRIVATE
"TRANSLATIONS_DIR=\"${CMAKE_INSTALL_FULL_DATADIR}/${TR_NAME}/translations\""
QT_NO_CAST_FROM_ASCII
$<$<BOOL:${ENABLE_QT_COM_INTEROP}>:ENABLE_COM_INTEROP>
$<$<BOOL:${ENABLE_QT_DBUS_INTEROP}>:ENABLE_DBUS_INTEROP>)
if(MSVC)
tr_append_target_property(${TR_NAME}-qt LINK_FLAGS "/ENTRY:mainCRTStartup")
endif()
set_target_properties(
${TR_NAME}-qt-lib
PROPERTIES
AUTOMOC ON
AUTORCC ON
AUTOUIC ON)
add_executable(${TR_NAME}-qt WIN32)
target_sources(${TR_NAME}-qt
PRIVATE
main.cc
application.qrc)
target_link_libraries(${TR_NAME}-qt
PRIVATE
${TR_NAME}-qt-lib)
set_target_properties(
${TR_NAME}-qt
@@ -233,6 +248,10 @@ set_target_properties(
AUTORCC ON
AUTOUIC ON)
if(MSVC)
tr_append_target_property(${TR_NAME}-qt LINK_FLAGS "/ENTRY:mainCRTStartup")
endif()
tr_win32_app_info(${TR_NAME}-qt
"Transmission Qt Client"
"${TR_NAME}-qt"

View File

@@ -3,20 +3,14 @@
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include <algorithm>
#include <array>
#include <cassert>
#include <optional>
#include <string_view>
#include <utility>
#include <QDateTime>
#include <QDir>
#include <QFile>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QStringDecoder>
#else
#include <QTextCodec>
#endif
#include <libtransmission/transmission.h>
@@ -27,42 +21,108 @@
#include "CustomVariantType.h"
#include "Filters.h"
#include "Prefs.h"
#include "VariantHelpers.h"
namespace api_compat = libtransmission::api_compat;
using libtransmission::serializer::to_value;
using libtransmission::serializer::to_variant;
using ::trqt::variant_helpers::dictAdd;
using ::trqt::variant_helpers::getValue;
namespace ser = libtransmission::serializer;
using namespace std::string_view_literals;
// ---
namespace
{
void ensureSoundCommandIsAList(tr_variant* dict)
template<typename T>
[[nodiscard]] QVariant qvarFromOptional(std::optional<T> const& val)
{
tr_quark const key = TR_KEY_torrent_complete_sound_command;
if (tr_variant* list = nullptr; tr_variantDictFindList(dict, key, &list))
{
return;
return val ? QVariant::fromValue(*val) : QVariant{};
}
tr_variantDictRemove(dict, key);
dictAdd(
dict,
key,
std::array<std::string_view, 5>{
"canberra-gtk-play",
[[nodiscard]] QVariant qvarFromTVar(tr_variant const& var, int const qt_metatype)
{
switch (qt_metatype)
{
case QMetaType::Int:
return qvarFromOptional(ser::to_value<int64_t>(var));
case CustomVariantType::EncryptionModeType:
return qvarFromOptional(ser::to_value<tr_encryption_mode>(var));
case CustomVariantType::SortModeType:
return qvarFromOptional(ser::to_value<SortMode>(var));
case CustomVariantType::ShowModeType:
return qvarFromOptional(ser::to_value<ShowMode>(var));
case QMetaType::QString:
return qvarFromOptional(ser::to_value<QString>(var));
case QMetaType::QStringList:
return qvarFromOptional(ser::to_value<QStringList>(var));
case QMetaType::Bool:
return qvarFromOptional(ser::to_value<bool>(var));
case QMetaType::Double:
return qvarFromOptional(ser::to_value<double>(var));
case QMetaType::QDateTime:
return qvarFromOptional(ser::to_value<QDateTime>(var));
default:
assert(false && "unhandled type");
return {};
}
}
[[nodiscard]] tr_variant trvarFromQVar(QVariant const& var, int const qt_metatype)
{
switch (qt_metatype)
{
case QMetaType::Int:
return ser::to_variant(var.value<int>());
case CustomVariantType::EncryptionModeType:
return ser::to_variant(var.value<tr_encryption_mode>());
case CustomVariantType::SortModeType:
return ser::to_variant(var.value<SortMode>());
case CustomVariantType::ShowModeType:
return ser::to_variant(var.value<ShowMode>());
case QMetaType::QString:
return ser::to_variant(var.value<QString>());
case QMetaType::QStringList:
return ser::to_variant(var.value<QStringList>());
case QMetaType::Bool:
return ser::to_variant(var.value<bool>());
case QMetaType::Double:
return ser::to_variant(var.value<double>());
case QMetaType::QDateTime:
return ser::to_variant(var.value<QDateTime>());
default:
assert(false && "unhandled type");
return {};
}
}
void ensureSoundCommandIsAList(tr_variant::Map& map)
{
auto constexpr Key = TR_KEY_torrent_complete_sound_command;
auto constexpr DefaultVal = std::array<std::string_view, 5U>{ "canberra-gtk-play",
"-i",
"complete-download",
"-d",
"transmission torrent downloaded",
});
"transmission torrent downloaded" };
if (map.find_if<tr_variant::Vector>(Key) == nullptr)
{
map.insert_or_assign(Key, ser::to_variant(DefaultVal));
}
}
} // namespace
std::array<Prefs::PrefItem, Prefs::PREFS_COUNT> const Prefs::Items{
@@ -182,224 +242,119 @@ namespace
}
} // namespace
/***
****
***/
// ---
Prefs::Prefs(QString config_dir)
: config_dir_{ std::move(config_dir) }
Prefs::Prefs()
{
static_assert(sizeof(Items) / sizeof(Items[0]) == PREFS_COUNT);
#ifndef NDEBUG
for (int i = 0; i < PREFS_COUNT; ++i)
{
assert(Items[i].id == i);
}
#endif
auto const app_defaults = get_default_app_settings();
auto settings = tr_sessionLoadSettings(config_dir_.toStdString(), &app_defaults);
ensureSoundCommandIsAList(&settings);
for (int i = 0; i < PREFS_COUNT; ++i)
{
tr_variant const* b = tr_variantDictFind(&settings, getKey(i));
switch (Items[i].type)
{
case QMetaType::Int:
if (auto const value = getValue<int64_t>(b); value)
{
values_[i].setValue(*value);
load(defaults());
}
break;
case CustomVariantType::EncryptionModeType:
if (auto const val = to_value<tr_encryption_mode>(*b))
void Prefs::loadFromConfigDir(QString const dir)
{
values_[i] = QVariant::fromValue(*val);
}
break;
case CustomVariantType::SortModeType:
if (auto const val = to_value<SortMode>(*b))
auto settings = tr_sessionLoadSettings(dir.toStdString());
if (auto* const map = settings.get_if<tr_variant::Map>())
{
values_[i] = QVariant::fromValue(*val);
ensureSoundCommandIsAList(*map);
load(*map);
}
}
break;
case CustomVariantType::ShowModeType:
if (auto const val = to_value<ShowMode>(*b))
void Prefs::load(tr_variant::Map const& settings)
{
values_[i] = QVariant::fromValue(*val);
}
break;
case QMetaType::QString:
if (auto const value = getValue<QString>(b); value)
for (int idx = 0; idx < PREFS_COUNT; ++idx)
{
values_[i].setValue(*value);
}
break;
case QMetaType::QStringList:
if (auto const value = getValue<QStringList>(b); value)
if (auto const iter = settings.find(getKey(idx)); iter != settings.end())
{
values_[i].setValue(*value);
}
break;
case QMetaType::Bool:
if (auto const value = getValue<bool>(b); value)
{
values_[i].setValue(*value);
}
break;
case QMetaType::Double:
if (auto const value = getValue<double>(b); value)
{
values_[i].setValue(*value);
}
break;
case QMetaType::QDateTime:
if (auto const value = getValue<time_t>(b); value)
{
values_[i].setValue(QDateTime::fromSecsSinceEpoch(*value));
}
break;
default:
assert(false && "unhandled type");
break;
values_[idx] = qvarFromTVar(iter->second, Items[idx].type);
}
}
}
Prefs::~Prefs()
tr_variant::Map Prefs::current_settings() const
{
// make a dict from settings.json
tr_variant current_settings;
tr_variantInitDict(&current_settings, PREFS_COUNT);
auto map = tr_variant::Map{ PREFS_COUNT };
for (int i = 0; i < PREFS_COUNT; ++i)
for (int idx = 0; idx < PREFS_COUNT; ++idx)
{
if (!prefIsSavable(i))
if (prefIsSavable(idx))
{
continue;
}
auto const key = getKey(i);
auto const& val = values_[i];
switch (Items[i].type)
{
case QMetaType::Int:
dictAdd(&current_settings, key, val.toInt());
break;
case CustomVariantType::EncryptionModeType:
*tr_variantDictAdd(&current_settings, key) = to_variant(val.value<tr_encryption_mode>());
break;
case CustomVariantType::SortModeType:
*tr_variantDictAdd(&current_settings, key) = to_variant(val.value<SortMode>());
break;
case CustomVariantType::ShowModeType:
*tr_variantDictAdd(&current_settings, key) = to_variant(val.value<ShowMode>());
break;
case QMetaType::QString:
dictAdd(&current_settings, key, val.toString());
break;
case QMetaType::QStringList:
dictAdd(&current_settings, key, val.toStringList());
break;
case QMetaType::Bool:
dictAdd(&current_settings, key, val.toBool());
break;
case QMetaType::Double:
dictAdd(&current_settings, key, val.toDouble());
break;
case QMetaType::QDateTime:
dictAdd(&current_settings, key, int64_t{ val.toDateTime().toSecsSinceEpoch() });
break;
default:
assert(false && "unhandled type");
break;
map.try_emplace(Items[idx].key, trvarFromQVar(values_[idx], Items[idx].type));
}
}
// update settings.json with our settings
return map;
}
void Prefs::save(QString const& filename) const
{
auto const filename_str = filename.toStdString();
auto serde = tr_variant_serde::json();
auto const file = QFile{ QDir{ config_dir_ }.absoluteFilePath(QStringLiteral("settings.json")) };
auto const filename = file.fileName().toStdString();
auto settings = tr_variant::make_map(PREFS_COUNT);
if (auto const file_settings = serde.parse_file(filename); file_settings)
{
settings.merge(*file_settings);
}
settings.merge(current_settings);
auto settings = tr_variant::make_map(PREFS_COUNT);
if (auto const var = serde.parse_file(filename_str))
{
settings.merge(*var);
}
settings.merge(tr_variant{ current_settings() });
api_compat::convert_outgoing_data(settings);
serde.to_file(settings, filename);
serde.to_file(settings, filename_str);
}
/**
* This is where we initialize the preferences file with the default values.
* If you add a new preferences key, you /must/ add a default value here.
*/
tr_variant Prefs::get_default_app_settings()
// static
tr_variant::Map Prefs::defaults()
{
auto const download_dir = tr_getDefaultDownloadDir();
auto settings = tr_variant::Map{ 64U };
settings.try_emplace(TR_KEY_blocklist_date, 0);
settings.try_emplace(TR_KEY_blocklist_updates_enabled, true);
settings.try_emplace(TR_KEY_compact_view, false);
settings.try_emplace(TR_KEY_download_dir, download_dir);
settings.try_emplace(TR_KEY_filter_mode, to_variant(DefaultShowMode));
settings.try_emplace(TR_KEY_inhibit_desktop_hibernation, false);
settings.try_emplace(TR_KEY_main_window_height, 500);
settings.try_emplace(TR_KEY_main_window_layout_order, tr_variant::unmanaged_string("menu,toolbar,filter,list,statusbar"sv));
settings.try_emplace(TR_KEY_main_window_width, 600);
settings.try_emplace(TR_KEY_main_window_x, 50);
settings.try_emplace(TR_KEY_main_window_y, 50);
settings.try_emplace(TR_KEY_open_dialog_dir, QDir::home().absolutePath().toStdString());
settings.try_emplace(TR_KEY_prompt_before_exit, true);
settings.try_emplace(TR_KEY_read_clipboard, false);
settings.try_emplace(TR_KEY_remote_session_enabled, false);
settings.try_emplace(TR_KEY_remote_session_host, tr_variant::unmanaged_string("localhost"sv));
settings.try_emplace(TR_KEY_remote_session_https, false);
settings.try_emplace(TR_KEY_remote_session_password, tr_variant::unmanaged_string(""sv));
settings.try_emplace(TR_KEY_remote_session_port, TrDefaultRpcPort);
settings.try_emplace(TR_KEY_remote_session_requires_authentication, false);
settings.try_emplace(TR_KEY_remote_session_url_base_path, tr_variant::unmanaged_string(TrHttpServerDefaultBasePath));
settings.try_emplace(TR_KEY_remote_session_username, tr_variant::unmanaged_string(""sv));
settings.try_emplace(TR_KEY_show_backup_trackers, false);
settings.try_emplace(TR_KEY_show_filterbar, true);
settings.try_emplace(TR_KEY_show_notification_area_icon, false);
settings.try_emplace(TR_KEY_show_options_window, true);
settings.try_emplace(TR_KEY_show_statusbar, true);
settings.try_emplace(TR_KEY_show_toolbar, true);
settings.try_emplace(TR_KEY_show_tracker_scrapes, false);
settings.try_emplace(TR_KEY_sort_mode, to_variant(DefaultSortMode));
settings.try_emplace(TR_KEY_sort_reversed, false);
settings.try_emplace(TR_KEY_start_minimized, false);
settings.try_emplace(TR_KEY_statusbar_stats, tr_variant::unmanaged_string("total-ratio"));
settings.try_emplace(TR_KEY_torrent_added_notification_enabled, true);
settings.try_emplace(TR_KEY_torrent_complete_notification_enabled, true);
settings.try_emplace(TR_KEY_torrent_complete_sound_enabled, true);
settings.try_emplace(TR_KEY_watch_dir, download_dir);
settings.try_emplace(TR_KEY_watch_dir_enabled, false);
return tr_variant{ std::move(settings) };
auto map = tr_variant::Map{ 64U };
map.try_emplace(TR_KEY_blocklist_date, 0);
map.try_emplace(TR_KEY_blocklist_updates_enabled, true);
map.try_emplace(TR_KEY_compact_view, false);
map.try_emplace(TR_KEY_download_dir, download_dir);
map.try_emplace(TR_KEY_filter_mode, ser::to_variant(DefaultShowMode));
map.try_emplace(TR_KEY_inhibit_desktop_hibernation, false);
map.try_emplace(TR_KEY_main_window_height, 500);
map.try_emplace(TR_KEY_main_window_layout_order, tr_variant::unmanaged_string("menu,toolbar,filter,list,statusbar"sv));
map.try_emplace(TR_KEY_main_window_width, 600);
map.try_emplace(TR_KEY_main_window_x, 50);
map.try_emplace(TR_KEY_main_window_y, 50);
map.try_emplace(TR_KEY_open_dialog_dir, QDir::home().absolutePath().toStdString());
map.try_emplace(TR_KEY_prompt_before_exit, true);
map.try_emplace(TR_KEY_read_clipboard, false);
map.try_emplace(TR_KEY_remote_session_enabled, false);
map.try_emplace(TR_KEY_remote_session_host, tr_variant::unmanaged_string("localhost"sv));
map.try_emplace(TR_KEY_remote_session_https, false);
map.try_emplace(TR_KEY_remote_session_password, tr_variant::unmanaged_string(""sv));
map.try_emplace(TR_KEY_remote_session_port, TrDefaultRpcPort);
map.try_emplace(TR_KEY_remote_session_requires_authentication, false);
map.try_emplace(TR_KEY_remote_session_url_base_path, tr_variant::unmanaged_string(TrHttpServerDefaultBasePath));
map.try_emplace(TR_KEY_remote_session_username, tr_variant::unmanaged_string(""sv));
map.try_emplace(TR_KEY_show_backup_trackers, false);
map.try_emplace(TR_KEY_show_filterbar, true);
map.try_emplace(TR_KEY_show_notification_area_icon, false);
map.try_emplace(TR_KEY_show_options_window, true);
map.try_emplace(TR_KEY_show_statusbar, true);
map.try_emplace(TR_KEY_show_toolbar, true);
map.try_emplace(TR_KEY_show_tracker_scrapes, false);
map.try_emplace(TR_KEY_sort_mode, ser::to_variant(DefaultSortMode));
map.try_emplace(TR_KEY_sort_reversed, false);
map.try_emplace(TR_KEY_start_minimized, false);
map.try_emplace(TR_KEY_statusbar_stats, tr_variant::unmanaged_string("total-ratio"));
map.try_emplace(TR_KEY_torrent_added_notification_enabled, true);
map.try_emplace(TR_KEY_torrent_complete_notification_enabled, true);
map.try_emplace(TR_KEY_torrent_complete_sound_enabled, true);
map.try_emplace(TR_KEY_watch_dir, download_dir);
map.try_emplace(TR_KEY_watch_dir_enabled, false);
return map;
}

View File

@@ -12,16 +12,10 @@
#include <QVariant>
#include <libtransmission/quark.h>
#include <libtransmission/variant.h>
#include <libtransmission-app/display-modes.h>
class QDateTime;
extern "C"
{
struct tr_variant;
}
class Prefs : public QObject
{
Q_OBJECT
@@ -131,37 +125,36 @@ public:
PREFS_COUNT
};
explicit Prefs(QString config_dir);
Prefs();
Prefs(Prefs&&) = delete;
Prefs(Prefs const&) = delete;
Prefs& operator=(Prefs&&) = delete;
Prefs& operator=(Prefs const&) = delete;
~Prefs() override;
~Prefs() override = default;
[[nodiscard]] constexpr auto isCore(int key) const noexcept
[[nodiscard]] static auto constexpr isCore(int const idx)
{
return FIRST_CORE_PREF <= key && key <= LAST_CORE_PREF;
return FIRST_CORE_PREF <= idx && idx <= LAST_CORE_PREF;
}
[[nodiscard]] constexpr auto getKey(int i) const noexcept
[[nodiscard]] static auto constexpr getKey(int const idx)
{
return Items[i].key;
return Items[idx].key;
}
[[nodiscard]] constexpr auto type(int i) const noexcept
[[nodiscard]] static auto constexpr type(int const idx)
{
return Items[i].type;
return Items[idx].type;
}
[[nodiscard]] constexpr auto const& variant(int i) const noexcept
{
return values_[i];
}
void loadFromConfigDir(QString dir);
template<typename T>
[[nodiscard]] T get(int const key) const
void load(tr_variant::Map const& settings);
// DEPRECATED
[[nodiscard]] constexpr auto const& variant(int const idx) const noexcept
{
return values_[key].value<T>();
return values_[idx];
}
template<typename T>
@@ -177,8 +170,18 @@ public:
}
}
template<typename T>
[[nodiscard]] T get(int const idx) const
{
return values_[idx].value<T>();
}
[[nodiscard]] tr_variant::Map current_settings() const;
void save(QString const& filename) const;
signals:
void changed(int key);
void changed(int idx);
private:
struct PrefItem
@@ -188,13 +191,11 @@ private:
int type;
};
[[nodiscard]] static tr_variant get_default_app_settings();
static std::array<PrefItem, PREFS_COUNT> const Items;
[[nodiscard]] static tr_variant::Map defaults();
void set(int key, char const* value) = delete;
QString const config_dir_;
std::array<QVariant, PREFS_COUNT> mutable values_;
static std::array<PrefItem, PREFS_COUNT> const Items;
};

View File

@@ -63,7 +63,7 @@ void Session::sessionSet(tr_quark const key, QVariant const& value)
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
switch (value.typeId())
#else
switch (static_cast<QMetaType::Type>(value.type()))
switch (value.userType())
#endif
{
case QMetaType::Bool:
@@ -166,7 +166,7 @@ void Session::copyMagnetLinkToClipboard(int torrent_id)
void Session::updatePref(int key)
{
if (prefs_.isCore(key))
if (Prefs::isCore(key))
{
switch (key)
{
@@ -209,11 +209,11 @@ void Session::updatePref(int key)
case Prefs::USPEED:
case Prefs::USPEED_ENABLED:
case Prefs::UTP_ENABLED:
sessionSet(prefs_.getKey(key), prefs_.variant(key));
sessionSet(Prefs::getKey(key), prefs_.variant(key));
break;
case Prefs::DOWNLOAD_DIR:
sessionSet(prefs_.getKey(key), prefs_.variant(key));
sessionSet(Prefs::getKey(key), prefs_.variant(key));
/* this will change the 'freespace' argument, so refresh */
refreshSessionInfo();
break;
@@ -890,14 +890,14 @@ void Session::updateInfo(tr_variant* args_dict)
for (int i = Prefs::FIRST_CORE_PREF; i <= Prefs::LAST_CORE_PREF; ++i)
{
tr_variant const* b(tr_variantDictFind(args_dict, prefs_.getKey(i)));
tr_variant const* b(tr_variantDictFind(args_dict, Prefs::getKey(i)));
if (b == nullptr)
{
continue;
}
switch (prefs_.type(i))
switch (Prefs::type(i))
{
case QMetaType::Int:
if (auto const value = getValue<int>(b); value)

21
qt/TrQtInit.cc Normal file
View File

@@ -0,0 +1,21 @@
// 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 "TrQtInit.h"
#include <libtransmission-app/app.h>
#include "VariantHelpers.h"
namespace trqt
{
void trqt_init()
{
transmission::app::init();
trqt::variant_helpers::register_qt_converters();
}
} // namespace trqt

13
qt/TrQtInit.h Normal file
View File

@@ -0,0 +1,13 @@
// 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.
#pragma once
namespace trqt
{
void trqt_init();
} // namespace trqt

View File

@@ -43,7 +43,7 @@ QIcon Utils::getIconFromIndex(QModelIndex const& index)
#if QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)
switch (variant.typeId())
#else
switch (static_cast<QMetaType::Type>(variant.type()))
switch (variant.userType())
#endif
{
case QMetaType::QIcon:

View File

@@ -11,6 +11,7 @@
#include <mutex>
#include <string_view>
#include <QDateTime>
#include <QUrl>
#include <libtransmission/serializer.h>
@@ -19,6 +20,8 @@
#include "Speed.h"
#include "Torrent.h"
namespace ser = libtransmission::serializer;
namespace trqt::variant_helpers
{
@@ -253,6 +256,42 @@ tr_variant fromInt(int const& val)
{
return static_cast<int64_t>(val);
}
// ---
bool toQDateTime(tr_variant const& src, QDateTime* tgt)
{
if (auto const val = ser::to_value<int64_t>(src))
{
*tgt = QDateTime::fromSecsSinceEpoch(*val);
return true;
}
return false;
}
tr_variant fromQDateTime(QDateTime const& src)
{
return ser::to_variant(int64_t{ src.toSecsSinceEpoch() });
}
// ---
bool toQString(tr_variant const& src, QString* tgt)
{
if (auto const val = src.value_if<std::string_view>())
{
*tgt = QString::fromUtf8(std::data(*val), std::size(*val));
return true;
}
return false;
}
tr_variant fromQString(QString const& val)
{
return val.toStdString();
}
} // namespace
void register_qt_converters()
@@ -264,6 +303,8 @@ void register_qt_converters()
{
using namespace libtransmission::serializer;
Converters::add(toInt, fromInt);
Converters::add(toQDateTime, fromQDateTime);
Converters::add(toQString, fromQString);
});
}

View File

@@ -7,6 +7,8 @@
#include <memory>
#include <string_view>
#include <QDir>
#include <fmt/format.h>
#include <libtransmission/transmission.h>
@@ -203,40 +205,41 @@ int tr_main(int argc, char** argv)
}
// initialize the prefs
auto prefs = std::make_unique<Prefs>(config_dir);
auto prefs = Prefs{};
prefs.loadFromConfigDir(config_dir);
if (!host.isNull())
{
prefs->set(Prefs::SESSION_REMOTE_HOST, host);
prefs.set(Prefs::SESSION_REMOTE_HOST, host);
}
if (!port.isNull())
{
prefs->set(Prefs::SESSION_REMOTE_PORT, port.toUInt());
prefs.set(Prefs::SESSION_REMOTE_PORT, port.toUInt());
}
if (!username.isNull())
{
prefs->set(Prefs::SESSION_REMOTE_USERNAME, username);
prefs.set(Prefs::SESSION_REMOTE_USERNAME, username);
}
if (!password.isNull())
{
prefs->set(Prefs::SESSION_REMOTE_PASSWORD, password);
prefs.set(Prefs::SESSION_REMOTE_PASSWORD, password);
}
if (!host.isNull() || !port.isNull() || !username.isNull() || !password.isNull())
{
prefs->set(Prefs::SESSION_IS_REMOTE, true);
prefs.set(Prefs::SESSION_IS_REMOTE, true);
}
if (prefs->get<bool>(Prefs::START_MINIMIZED))
if (prefs.get<bool>(Prefs::START_MINIMIZED))
{
minimized = true;
}
// start as minimized only if the system tray present
if (!prefs->get<bool>(Prefs::SHOW_TRAY_ICON))
if (!prefs.get<bool>(Prefs::SHOW_TRAY_ICON))
{
minimized = false;
}
@@ -247,8 +250,14 @@ int tr_main(int argc, char** argv)
qt_argv.insert(qt_argv.end(), &argv[qt_args_start_idx], &argv[argc]);
}
// run the app
auto qt_argc = static_cast<int>(std::size(qt_argv));
auto const app = Application{ prefs, minimized, config_dir, filenames, qt_argc, std::data(qt_argv) };
auto const ret = QApplication::exec();
Application const app(std::move(prefs), minimized, config_dir, filenames, qt_argc, std::data(qt_argv));
return QApplication::exec();
// save prefs before exiting
auto const filename = QDir{ config_dir }.absoluteFilePath(QStringLiteral("settings.json"));
prefs.save(filename);
return ret;
}

View File

@@ -58,8 +58,6 @@ function global:Build-Qt5([string] $PrefixDir, [string] $Arch, [string] $DepsPre
'-no-sql-sqlite2'
'-no-sql-tds'
'-nomake'; 'examples'
'-nomake'; 'tests'
'-nomake'; 'tools'
'-I'; (Join-Path $DepsPrefixDir include)
'-L'; (Join-Path $DepsPrefixDir lib)
)

View File

@@ -89,7 +89,6 @@ function global:Build-Qt6([string] $PrefixDir, [string] $Arch, [string] $DepsPre
'-no-feature-syntaxhighlighter'
'-no-feature-systemsemaphore'
'-no-feature-tablewidget'
'-no-feature-testlib'
'-no-feature-textmarkdownreader'
'-no-feature-textmarkdownwriter'
'-no-feature-textodfwriter'

View File

@@ -3,3 +3,25 @@ add_subdirectory(libtransmission)
if(ENABLE_UTILS)
add_subdirectory(utils)
endif()
if(ENABLE_QT)
add_subdirectory(qt)
endif()
add_custom_target(all-tests)
set_property(
TARGET all-tests
PROPERTY FOLDER "tests")
if(TARGET libtransmission-test)
add_dependencies(all-tests libtransmission-test)
endif()
if(TARGET qt-tests)
add_dependencies(all-tests qt-tests)
endif()
if(TARGET transmission-show)
add_dependencies(all-tests transmission-show)
endif()

60
tests/qt/.clang-tidy Normal file
View File

@@ -0,0 +1,60 @@
---
HeaderFilterRegex: .*/libtransmission/.*
# TODO: Enable `portability-template-virtual-member-function` after https://github.com/llvm/llvm-project/issues/139031 is fixed
# TODO: Enable `cppcoreguidelines-pro-bounds-pointer-arithmetic` after converting all pointers to std::span
# PRs welcome to fix & re-enable any of these explicitly-disabled checks
Checks: >
bugprone-*,
-bugprone-branch-clone,
-bugprone-easily-swappable-parameters,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-narrowing-conversions,
cert-*,
-cert-err58-cpp,
-cert-int09-c,
clang-analyzer-*,
cppcoreguidelines-*,
-cppcoreguidelines-avoid-c-arrays,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-avoid-non-const-global-variables,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-narrowing-conversions,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-pro-type-const-cast,
-cppcoreguidelines-pro-type-reinterpret-cast,
-cppcoreguidelines-pro-type-union-access,
-cppcoreguidelines-pro-type-vararg,
google-explicit-constructor,
misc-*,
-misc-include-cleaner,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
modernize-*,
-modernize-use-trailing-return-type,
performance-*,
-performance-move-const-arg,
portability-*,
-portability-template-virtual-member-function,
readability-*,
-readability-enum-initial-value,
-readability-function-cognitive-complexity,
-readability-identifier-length,
-readability-magic-numbers,
-readability-qualified-auto,
CheckOptions:
- { key: cppcoreguidelines-avoid-do-while.IgnoreMacros, value: true }
- { key: cppcoreguidelines-rvalue-reference-param-not-moved.IgnoreUnnamedParams, value: true }
- { key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor, value: true }
- { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase }
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
- { key: readability-identifier-naming.VariableCase, value: lower_case }
- { key: readability-implicit-bool-conversion.UseUpperCaseLiteralSuffix, value: true }

27
tests/qt/CMakeLists.txt Normal file
View File

@@ -0,0 +1,27 @@
find_package(Qt${Qt_VERSION_MAJOR}Test ${QT_MINIMUM} REQUIRED)
set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES
${CMAKE_BINARY_DIR}/qt/${TR_NAME}-qt-lib_autogen/include)
set_property(DIRECTORY PROPERTY LINK_LIBRARIES
${TR_NAME}-qt-lib
Qt${Qt_VERSION_MAJOR}::Test)
function(add_trqt_test source)
get_filename_component(name_no_ext ${source} NAME_WE)
string(REGEX REPLACE "-test$" "" test_name ${name_no_ext})
set(target qt-test-${test_name})
add_executable(${target} ${source} ${CMAKE_CURRENT_SOURCE_DIR}/../../qt/application.qrc)
set_property(TARGET ${target} PROPERTY AUTOMOC ON)
set_property(TARGET ${target} PROPERTY AUTORCC ON)
add_test(NAME QT.${test_name} COMMAND ${target})
endfunction()
add_trqt_test(prefs-test.cc)
add_custom_target(qt-tests
DEPENDS
qt-test-prefs)
set_property(
TARGET qt-tests
PROPERTY FOLDER "tests")

262
tests/qt/prefs-test.cc Normal file
View File

@@ -0,0 +1,262 @@
// 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 <string_view>
#include <QApplication>
#include <QDateTime>
#include <QSignalSpy>
#include <QString>
#include <QStringList>
#include <QTest>
#include <fmt/format.h>
#include <libtransmission/quark.h>
#include <libtransmission/utils.h>
#include <libtransmission/variant.h>
#include "CustomVariantType.h"
#include "Filters.h"
#include "Prefs.h"
#include "TrQtInit.h"
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
#define QCOMPARE_EQ(actual, expected) QCOMPARE(actual, expected)
#define QCOMPARE_NE(actual, expected) QVERIFY((actual) != (expected))
#endif
using namespace std::literals;
class PrefsTest : public QObject
{
Q_OBJECT
[[nodiscard]] static std::string get_json_member_str(int const idx, std::string_view const valstr)
{
auto const json_key = tr_quark_get_string_view(Prefs::getKey(idx));
return fmt::format(R"("{:s}":{:s})", json_key, valstr);
}
static void verify_json_contains(tr_variant const& var, std::string_view const substr)
{
auto serde = tr_variant_serde::json();
serde.compact();
auto const str = serde.to_string(var);
QVERIFY2(tr_strv_contains(str, substr), str.c_str());
}
static void verify_json_contains(tr_variant const& var, int const idx, std::string_view const val)
{
auto serde = tr_variant_serde::json();
serde.compact();
auto const str = serde.to_string(var);
auto const substr = get_json_member_str(idx, val);
QVERIFY2(tr_strv_contains(str, substr), str.c_str());
}
template<typename T>
void verify_get_set_by_property(Prefs& prefs, int const idx, T const& val1, T const& val2)
{
QCOMPARE_NE(val1, val2);
prefs.set(idx, val1);
QCOMPARE_EQ(prefs.get<T>(idx), val1);
QCOMPARE_NE(prefs.get<T>(idx), val2);
prefs.set(idx, val2);
QCOMPARE_NE(prefs.get<T>(idx), val1);
QCOMPARE_EQ(prefs.get<T>(idx), val2);
}
template<typename T>
void verify_get_by_json(Prefs& prefs, int const idx, T const& val, std::string_view const valstr)
{
prefs.set(idx, val);
QCOMPARE_EQ(prefs.get<T>(idx), val);
verify_json_contains(prefs.current_settings(), idx, valstr);
}
template<typename T>
void verify_set_by_json(Prefs& prefs, int const idx, T const& val, std::string_view const valstr)
{
auto const json_object_str = fmt::format(R"({{{:s}}})", get_json_member_str(idx, valstr));
auto serde = tr_variant_serde::json();
auto const var = serde.parse(json_object_str);
QVERIFY(var.has_value());
// IDK why clang-tidy doesn't see the QVERIFY check above?
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
auto const* const map = var->get_if<tr_variant::Map>();
QVERIFY(map != nullptr);
prefs.load(*map);
QCOMPARE_EQ(prefs.get<T>(idx), val);
}
private slots:
void handles_bool()
{
auto constexpr Idx = Prefs::SORT_REVERSED;
auto constexpr ValA = false;
auto constexpr ValAStr = "false"sv;
auto constexpr ValB = true;
auto constexpr ValBStr = "true"sv;
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, ValA, ValB);
verify_set_by_json(prefs, Idx, ValA, ValAStr);
verify_get_by_json(prefs, Idx, ValB, ValBStr);
}
void handles_int()
{
auto constexpr Idx = Prefs::MAIN_WINDOW_HEIGHT;
auto constexpr ValA = 4242;
auto constexpr ValAStr = "4242"sv;
auto constexpr ValB = 2323;
auto constexpr ValBStr = "2323"sv;
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, ValA, ValB);
verify_set_by_json(prefs, Idx, ValA, ValAStr);
verify_get_by_json(prefs, Idx, ValB, ValBStr);
}
void handles_double()
{
auto constexpr Idx = Prefs::RATIO;
auto constexpr ValA = 1.234;
auto constexpr ValB = 5.678;
auto const val_a_str = fmt::format("{}", ValA);
auto const val_b_str = fmt::format("{}", ValB);
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, ValA, ValB);
verify_set_by_json(prefs, Idx, ValA, val_a_str);
verify_get_by_json(prefs, Idx, ValB, val_b_str);
}
void handles_qstring()
{
auto constexpr Idx = Prefs::DOWNLOAD_DIR;
auto constexpr ValAStr = R"("/tmp/transmission-test-download-dir")"sv;
auto constexpr ValBStr = R"("/tmp/transmission-test-download-dir-b")"sv;
auto const val_a = QStringLiteral("/tmp/transmission-test-download-dir");
auto const val_b = QStringLiteral("/tmp/transmission-test-download-dir-b");
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, val_a, val_b);
verify_set_by_json(prefs, Idx, val_a, ValAStr);
verify_get_by_json(prefs, Idx, val_b, ValBStr);
}
void handles_qstringlist()
{
auto constexpr Idx = Prefs::COMPLETE_SOUND_COMMAND;
auto constexpr ValAStr = R"(["one","two","three"])"sv;
auto constexpr ValBStr = R"(["alpha","beta"])"sv;
auto const val_a = QStringList{ QStringLiteral("one"), QStringLiteral("two"), QStringLiteral("three") };
auto const val_b = QStringList{ QStringLiteral("alpha"), QStringLiteral("beta") };
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, val_a, val_b);
verify_set_by_json(prefs, Idx, val_a, ValAStr);
verify_get_by_json(prefs, Idx, val_b, ValBStr);
}
void handles_qdatetime()
{
auto constexpr Idx = Prefs::BLOCKLIST_DATE;
auto const val_a = QDateTime::fromMSecsSinceEpoch(1700000000000LL).toUTC();
auto const val_a_str = fmt::format("{}", val_a.toSecsSinceEpoch());
auto const val_b = QDateTime::fromMSecsSinceEpoch(1700000000000LL + 123000LL).toUTC();
auto const val_b_str = fmt::format("{}", val_b.toSecsSinceEpoch());
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, val_a, val_b);
verify_set_by_json(prefs, Idx, val_a, val_a_str);
verify_get_by_json(prefs, Idx, val_b, val_b_str);
}
void handles_sortmode()
{
auto constexpr Idx = Prefs::SORT_MODE;
auto constexpr ValA = SortMode::SortBySize;
auto constexpr ValAStr = R"("sort_by_size")"sv;
auto constexpr ValB = SortMode::SortByName;
auto constexpr ValBStr = R"("sort_by_name")"sv;
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, ValA, ValB);
verify_set_by_json(prefs, Idx, ValA, ValAStr);
verify_get_by_json(prefs, Idx, ValB, ValBStr);
}
void handles_showmode()
{
auto constexpr Idx = Prefs::FILTER_MODE;
auto constexpr ValA = ShowMode::ShowAll;
auto constexpr ValAStr = R"("show_all")"sv;
auto constexpr ValB = ShowMode::ShowActive;
auto constexpr ValBStr = R"("show_active")"sv;
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, ValA, ValB);
verify_set_by_json(prefs, Idx, ValA, ValAStr);
verify_get_by_json(prefs, Idx, ValB, ValBStr);
}
void handles_encryptionmode()
{
auto constexpr Idx = Prefs::ENCRYPTION;
auto constexpr ValA = TR_ENCRYPTION_REQUIRED;
auto constexpr ValAStr = R"("required")"sv;
auto constexpr ValB = TR_ENCRYPTION_PREFERRED;
auto constexpr ValBStr = R"("preferred")"sv;
auto prefs = Prefs{};
verify_get_set_by_property(prefs, Idx, ValA, ValB);
verify_set_by_json(prefs, Idx, ValA, ValAStr);
verify_get_by_json(prefs, Idx, ValB, ValBStr);
}
// ---
static void changed_signal_emits_when_change()
{
static auto constexpr Idx = Prefs::SORT_REVERSED;
auto prefs = Prefs{};
auto const spy = QSignalSpy{ &prefs, &Prefs::changed };
auto const old_value = prefs.get<bool>(Idx);
auto const new_value = !old_value;
prefs.set(Idx, new_value);
QCOMPARE(spy.count(), 1);
auto const& signal_args = spy.first();
QCOMPARE(signal_args.at(0).toInt(), Idx);
}
static void changed_signal_does_not_emit_when_unchanged()
{
static auto constexpr Idx = Prefs::SORT_REVERSED;
auto prefs = Prefs{};
auto const spy = QSignalSpy{ &prefs, &Prefs::changed };
auto const current_value = prefs.get<bool>(Idx);
prefs.set(Idx, current_value);
QCOMPARE(spy.count(), 0);
}
};
int main(int argc, char** argv)
{
trqt::trqt_init();
auto const app = QApplication{ argc, argv };
auto test = PrefsTest{};
return QTest::qExec(&test, argc, argv);
}
#include "prefs-test.moc"