Cockatrice/tests/settings/settings_migration_test.cpp
Lukas Brübach 8c2bfc46ff [Fonts] Retire max font size setting into theme CardText min/max range
The per-card text size lives in the theme now, so a theme can pick the size
it was designed for instead of every user overriding it globally.

- CardText stores family + max point size; CardTextMinSize stores the
  overlay shrink-to-fit floor (omitted from fonts.toml when 1).
- ThemeManager::cardTextMaxSize()/cardTextMinSize() resolve the range with
  the retired defaults (12 max, 1 min), and cardTextMaxSize() falls back to
  the CardText role size that #7541 made live.
- The overlay widget starts at the explicit size when one is set (deck
  preview), else the theme max, and shrinks down to the theme floor.
- The appearance page's "Maximum font size" spin is gone; the range is
  edited in the theme's font grid, whose min/max spins now keep the saved
  range consistent with each other.
- Migration: appearance/maxFontSize is seeded into every user theme folder
  and into the active theme's folder (created when the theme is a shipped
  one that only exists in the system directory). The retired
  game/maxfontsize -> appearance/maxFontSize mapping stays, so users still
  on global.ini keep their value through the chain.
2026-11-03 00:21:48 +01:00

1189 lines
53 KiB
C++

#include "gtest/gtest.h"
#include <QByteArray>
#include <QByteArrayView>
#include <QChar>
#include <QColor>
#include <QCoreApplication>
#include <QDate>
#include <QDir>
#include <QFile>
#include <QIODevice>
#include <QList>
#include <QSettings>
#include <QString>
#include <QStringList>
#include <QStringLiteral>
#include <QTemporaryDir>
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/settings/settings_migration.h>
#include <qnamespace.h>
#include <string>
namespace
{
class SettingsMigrationTest : public ::testing::Test
{
protected:
QTemporaryDir tempDir;
QString settingsPath;
void SetUp() override
{
// Mirror the production layout: the settings folder lives inside the data folder, so
// migration backups (siblings of settings/) stay isolated inside tempDir instead of
// landing in the shared system temp directory.
settingsPath = tempDir.path() + "/settings/";
QDir().mkpath(settingsPath);
// Isolate the settings used by the legacy migration tests inside the temporary
// directory so the tests never read or write the real user config (registry on
// Windows, plist on macOS, .conf on Linux), which would otherwise be shared
// across CI jobs and flaky. Default-format QSettings is forced to IniFormat and
// its UserScope path is redirected here; setPath wins over the XDG_CONFIG_HOME
// default on Unix, so every platform resolves to <tempDir>/config/...
const QString configDir = tempDir.path() + "/config";
QDir().mkpath(configDir);
qputenv("XDG_CONFIG_HOME", configDir.toUtf8());
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, configDir);
}
bool fileExists(const QString &name) const
{
return QFile::exists(settingsPath + name);
}
QVariant readFromIni(const QString &fileName, const QString &key) const
{
QSettings ini(settingsPath + fileName, QSettings::IniFormat);
return ini.value(key);
}
// Checks for the exact legacy key in the raw INI content. On Windows, INI keys are
// case-insensitive, so reading back a lower-case legacy key would match its migrated
// camelCase counterpart and hide regressions. Checking the file bytes directly keeps
// the comparison case-sensitive on every platform.
bool iniFileHasKeyCaseSensitive(const QString &fileName, const QString &key) const
{
QFile f(settingsPath + fileName);
if (!f.open(QIODevice::ReadOnly)) {
return false;
}
const QString valueName = key.section('/', -1);
const QStringList lines = QString::fromUtf8(f.readAll()).split('\n');
for (const auto &line : lines) {
if (line.startsWith(valueName + '=')) {
return true;
}
}
return false;
}
// The migration backups are timestamped, so tests address them by index rather than by name.
QStringList backupFolderNames() const
{
return QDir(tempDir.path())
.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name)
.filter(QStringLiteral("settings-backup-"));
}
QVariant readFromBackup(const QString &folderName, const QString &fileName, const QString &key) const
{
QSettings ini(QDir::cleanPath(tempDir.path() + '/' + folderName + '/' + fileName), QSettings::IniFormat);
return ini.value(key);
}
bool backupFileExists(const QString &folderName, const QString &fileName) const
{
return QFile::exists(QDir::cleanPath(tempDir.path() + '/' + folderName + '/' + fileName));
}
};
TEST_F(SettingsMigrationTest, NoGlobalIniDoesNothing)
{
bool result = SettingsMigration::migrateSettingsFromGlobalIni(settingsPath);
ASSERT_FALSE(result);
}
TEST_F(SettingsMigrationTest, EmptyGlobalIniGetsBackedUp)
{
{
QFile f(settingsPath + "global.ini");
ASSERT_TRUE(f.open(QIODevice::WriteOnly));
f.close();
}
bool result = SettingsMigration::migrateSettingsFromGlobalIni(settingsPath);
ASSERT_TRUE(result);
ASSERT_TRUE(fileExists("global.ini.old"));
ASSERT_TRUE(fileExists("global.ini"));
// No per-file INIs should be created
ASSERT_FALSE(fileExists("tabs.ini"));
ASSERT_FALSE(fileExists("sound.ini"));
// Sentinel should be in the new global.ini
ASSERT_EQ(readFromIni("global.ini", "migration/perfile_complete"), QVariant(true));
}
TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
// tabs
g.setValue("tabs/visualDeckStorage", false);
g.setValue("tabs/server", true);
g.setValue("tabs/account", true);
g.setValue("tabs/deckStorage", true);
g.setValue("tabs/replays", false);
g.setValue("tabs/admin", false);
g.setValue("tabs/log", true);
// sound
g.setValue("sound/enabled", true);
g.setValue("sound/theme", "custom_theme");
g.setValue("sound/mastervolume", 75);
// game
g.beginGroup("game");
g.setValue("maxplayers", 4);
g.setValue("gamedescription", "test game");
g.setValue("remembergamesettings", false);
g.setValue("gametypes", "commander");
g.setValue("onlybuddies", true);
g.endGroup();
// localgameoptions
g.beginGroup("localgameoptions");
g.setValue("maxplayers", 2);
g.setValue("startinglifetotal", 40);
g.endGroup();
// chat
g.beginGroup("chat");
g.setValue("mention", false);
g.setValue("mentioncolor", "FF0000");
g.setValue("showmessagepopups", false);
g.setValue("mentioncompleter", false);
g.setValue("roomhistory", false);
g.setValue("highlightcolor", "00FF00");
g.endGroup();
// legacy highlight words (under [personal])
g.setValue("personal/highlightWords", "alpha beta");
// cache storage (under [personal] group)
g.setValue("personal/pixmapCacheSize", 1024);
g.setValue("personal/networkCacheSize", 2048);
g.setValue("personal/redirectCacheTtl", 5);
g.setValue("personal/cardPictureLoaderCacheMethod", 1);
g.setValue("personal/localCardImageStorageNamingScheme", 2);
// updates (under [personal] group)
g.setValue("personal/startupUpdateCheck", false);
g.setValue("personal/startupCardUpdateCheckPromptForUpdate", false);
g.setValue("personal/startupCardUpdateCheckAlwaysUpdate", true);
g.setValue("personal/cardUpdateCheckInterval", 14);
g.setValue("personal/lastCardUpdateCheck", QDate(2024, 1, 1));
g.setValue("personal/alwaysEnableNewSets", true);
g.setValue("personal/updatenotification", false);
g.setValue("personal/newversionnotification", false);
// personal
g.setValue("personal/lang", "de");
// downloads (previously under [personal])
g.setValue("personal/picturedownload", true);
g.setValue("personal/downloadspoilers", true);
// interface (previously under [personal])
g.setValue("personal/showStatusBar", true);
// theme
g.setValue("theme/name", "custom_theme");
g.setValue("game/maxfontsize", 14);
// appearance
g.setValue("appearance/styleUserList", false);
g.setValue("home/background/displayCardName", false);
g.setValue("menu/showshortcuts", false);
g.setValue("menu/showgameselectorfiltertoolbar", false);
// deck editor
g.setValue("editor/openDeckInNewTab", false);
g.setValue("interface/deckeditortagswidgetvisible", false);
g.setValue("interface/defaultDeckEditorType", 0);
g.setValue("interface/visualdeckeditorsamplehandsize", 5);
// personal home
g.setValue("home/background", "custom_bg");
g.setValue("home/background/shuffleTimer", 30);
// personal tipOfDay
g.setValue("tipOfDay/showTips", false);
g.setValue("tipOfDay/seenTips", QStringList{"1", "2", "3"});
// network
g.setValue("personal/keepalive", 10);
g.setValue("personal/timeout", 30);
g.setValue("personal/clientid", "test-client-id");
g.setValue("personal/clientversion", "test-client-version");
g.setValue("interface/knownmissingfeatures", "feature1,feature2");
// cards
g.setValue("cards/displaycardnames", false);
g.setValue("cards/roundcardcorners", false);
g.setValue("cards/overrideallcardartwithpersonalpreference", true);
g.setValue("cards/bumpsetswithcardsindecktotop", false);
g.setValue("cards/autorotatesidewayslayoutcards", false);
g.setValue("cards/tapanimation", true);
g.setValue("cards/scaleCards", false);
g.setValue("cards/verticalCardOverlapPercent", 42);
g.setValue("cards/cardinfoviewmode", 1);
g.setValue("cards/printingselectorcardsize", 90);
g.setValue("cards/printingselectorsortorder", 3);
g.setValue("cards/printingselectornavigationbuttonsvisible", false);
// card counters (migrate into card_counters.ini)
g.setValue("cards/counters/0/color", QColor(Qt::red));
// interface
g.setValue("interface/usetearoffmenus", true);
g.setValue("interface/cardViewInitialRowsMax", 8);
g.setValue("interface/cardViewExpandedRowsMax", 12);
g.setValue("interface/closeEmptyCardView", false);
g.setValue("interface/focusCardViewSearchBar", false);
g.setValue("interface/keepGameChatFocus", true);
g.setValue("interface/notificationsenabled", false);
g.setValue("interface/specnotificationsenabled", true);
g.setValue("interface/buddyconnectnotificationsenabled", false);
g.setValue("interface/doubleclicktoplay", false);
g.setValue("interface/clickPlaysAllSelected", false);
g.setValue("interface/playtostack", false);
g.setValue("interface/doNotDeleteArrowsInSubPhases", false);
g.setValue("interface/startinghandsize", 5);
g.setValue("interface/annotatetokens", true);
g.setValue("interface/showlassoselectioncount", false);
g.setValue("interface/showpersistentselectioncount", false);
g.setValue("interface/tallyType", 2);
g.setValue("interface/leftjustified", true);
g.setValue("interface/min_players_multicolumn", 6);
g.setValue("interface/deckeditorbannercardcomboboxvisible", false);
// hand/table/replay/zoneview
g.setValue("hand/horizontal", true);
g.setValue("table/invert_vertical", true);
g.setValue("replay/rewindBufferingMs", 6000);
g.setValue("replay/fastForwardSpeed", 5);
g.setValue("zoneview/groupby", 2);
g.setValue("zoneview/sortby", 1);
g.setValue("zoneview/pileview", false);
// paths
g.beginGroup("paths");
g.setValue("decks", "/custom/decks");
g.setValue("pics", "/custom/pics");
g.endGroup();
// visual deck storage (under [interface] group)
g.setValue("interface/visualdeckstoragecardsize", 150);
g.setValue("interface/visualdeckstoragesortingorder", 2);
g.setValue("interface/visualdeckstorageshowfolders", false);
g.setValue("interface/visualdeckstorageshowtagfilter", false);
g.setValue("interface/visualdeckstoragedefaulttagslist", QStringList{"Alpha", "Beta"});
g.setValue("interface/visualdeckstoragesearchfoldernames", false);
g.setValue("interface/visualdeckstorageshowcoloridentity", false);
g.setValue("interface/visualdeckstorageshowbannercardcombobox", false);
g.setValue("interface/visualdeckstorageshowtagsondeckpreviews", false);
g.setValue("interface/visualdeckstoragedrawunusedcoloridentities", false);
g.setValue("interface/visualdeckstorageunusedcoloridentitiesopacity", 35);
g.setValue("interface/visualdeckstoragetooltiptype", 1);
g.setValue("interface/visualdeckstoragepromptforconversion", false);
g.setValue("interface/visualdeckstoragealwaysconvert", true);
g.setValue("interface/visualdeckstorageingame", false);
g.setValue("interface/visualdeckstorageselectionanimation", false);
g.setValue("interface/visualdatabasedisplayfiltertomostrecentsetsenabled", true);
g.setValue("interface/visualdatabasedisplayfiltertomostrecentsetsamount", 25);
// card sizes (migrate into cards_display.ini)
g.setValue("interface/visualdatabasedisplaycardsize", 80);
g.setValue("interface/visualdeckeditorcardsize", 70);
g.setValue("interface/edhreccardsize", 60);
g.setValue("interface/archidektpreviewsize", 50);
g.sync();
}
bool result = SettingsMigration::migrateSettingsFromGlobalIni(settingsPath);
ASSERT_TRUE(result);
// Original global.ini should be backed up
ASSERT_TRUE(fileExists("global.ini.old"));
// Verify each per-file INI was created and has correct values
ASSERT_TRUE(fileExists("tabs.ini"));
ASSERT_EQ(readFromIni("tabs.ini", "tabs/visualDeckStorage"), QVariant(false));
ASSERT_EQ(readFromIni("tabs.ini", "tabs/server"), QVariant(true));
ASSERT_EQ(readFromIni("tabs.ini", "tabs/log"), QVariant(true));
ASSERT_TRUE(fileExists("sound.ini"));
ASSERT_EQ(readFromIni("sound.ini", "sound/enabled"), QVariant(true));
ASSERT_EQ(readFromIni("sound.ini", "sound/theme"), QVariant("custom_theme"));
ASSERT_EQ(readFromIni("sound.ini", "sound/masterVolume"), QVariant(75));
ASSERT_TRUE(fileExists("game.ini"));
ASSERT_EQ(readFromIni("game.ini", "game/maxPlayers"), QVariant(4));
ASSERT_EQ(readFromIni("game.ini", "game/gameDescription"), QVariant("test game"));
ASSERT_EQ(readFromIni("game.ini", "game/rememberGameSettings"), QVariant(false));
ASSERT_EQ(readFromIni("game.ini", "game/gameTypes"), QVariant("commander"));
ASSERT_EQ(readFromIni("game.ini", "game/onlyBuddies"), QVariant(true));
ASSERT_EQ(readFromIni("game.ini", "localgameoptions/maxPlayers"), QVariant(2));
ASSERT_EQ(readFromIni("game.ini", "localgameoptions/startingLifeTotal"), QVariant(40));
ASSERT_TRUE(fileExists("chat.ini"));
ASSERT_EQ(readFromIni("chat.ini", "chat/mention"), QVariant(false));
ASSERT_EQ(readFromIni("chat.ini", "chat/mentionColor"), QVariant("FF0000"));
ASSERT_EQ(readFromIni("chat.ini", "chat/showMessagePopups"), QVariant(false));
ASSERT_EQ(readFromIni("chat.ini", "chat/mentionCompleter"), QVariant(false));
ASSERT_EQ(readFromIni("chat.ini", "chat/roomHistory"), QVariant(false));
ASSERT_EQ(readFromIni("chat.ini", "chat/highlightColor"), QVariant("00FF00"));
ASSERT_EQ(readFromIni("chat.ini", "chat/highlightWords"), QVariant("alpha beta"));
ASSERT_TRUE(fileExists("cache_storage.ini"));
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/pixmapCacheSize"), QVariant(1024));
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/networkCacheSize"), QVariant(2048));
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/redirectCacheTtl"), QVariant(5));
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/cardPictureLoaderCacheMethod"), QVariant(1));
ASSERT_EQ(readFromIni("cache_storage.ini", "cache_storage/localCardImageStorageNamingScheme"), QVariant(2));
ASSERT_TRUE(fileExists("updates.ini"));
ASSERT_EQ(readFromIni("updates.ini", "updates/startupUpdateCheck"), QVariant(false));
ASSERT_EQ(readFromIni("updates.ini", "updates/startupCardUpdateCheckPromptForUpdate"), QVariant(false));
ASSERT_EQ(readFromIni("updates.ini", "updates/startupCardUpdateCheckAlwaysUpdate"), QVariant(true));
ASSERT_EQ(readFromIni("updates.ini", "updates/cardUpdateCheckInterval"), QVariant(14));
ASSERT_EQ(readFromIni("updates.ini", "updates/lastCardUpdateCheck"), QVariant(QDate(2024, 1, 1)));
ASSERT_EQ(readFromIni("updates.ini", "updates/alwaysEnableNewSets"), QVariant(true));
ASSERT_EQ(readFromIni("updates.ini", "updates/updateNotification"), QVariant(false));
ASSERT_EQ(readFromIni("updates.ini", "updates/newVersionNotification"), QVariant(false));
ASSERT_TRUE(fileExists("personal.ini"));
ASSERT_EQ(readFromIni("personal.ini", "personal/lang"), QVariant("de"));
ASSERT_EQ(readFromIni("personal.ini", "tipOfDay/showTips"), QVariant(false));
ASSERT_EQ(readFromIni("personal.ini", "tipOfDay/seenTips"), QVariant(QStringList{"1", "2", "3"}));
ASSERT_TRUE(fileExists("downloads.ini"));
ASSERT_EQ(readFromIni("downloads.ini", "downloads/pictureDownload"), QVariant(true));
ASSERT_EQ(readFromIni("downloads.ini", "downloads/downloadSpoilers"), QVariant(true));
ASSERT_TRUE(fileExists("appearance.ini"));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/themeName"), QVariant("custom_theme"));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/maxFontSize"), QVariant(14));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/styleUserList"), QVariant(false));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundSource"), QVariant("custom_bg"));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabBackgroundShuffleFrequency"), QVariant(30));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/homeTabDisplayCardName"), QVariant(false));
ASSERT_TRUE(fileExists("network.ini"));
ASSERT_EQ(readFromIni("network.ini", "network/keepAlive"), QVariant(10));
ASSERT_EQ(readFromIni("network.ini", "network/timeout"), QVariant(30));
ASSERT_EQ(readFromIni("network.ini", "network/clientId"), QVariant("test-client-id"));
ASSERT_EQ(readFromIni("network.ini", "network/clientVersion"), QVariant("test-client-version"));
ASSERT_EQ(readFromIni("network.ini", "network/knownMissingFeatures"), QVariant("feature1,feature2"));
ASSERT_TRUE(fileExists("cards_display.ini"));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/displayCardNames"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/roundCardCorners"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/overrideAllCardArtWithPersonalPreference"), QVariant(true));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/bumpSetsWithCardsInDeckToTop"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/autoRotateSidewaysLayoutCards"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/tapAnimation"), QVariant(true));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/scaleCards"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/verticalCardOverlapPercent"), QVariant(42));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardInfoViewMode"), QVariant(1));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/printingSelector"), QVariant(90));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/printingSelector/sortOrder"), QVariant(3));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/printingSelector/navigationButtonsVisible"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDeckStorage"), QVariant(150));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDatabaseDisplay"), QVariant(80));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/visualDeckEditor"), QVariant(70));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/edhrec"), QVariant(60));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/archidektPreview"), QVariant(50));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/cardSize/sampleHandSize"), QVariant(5));
ASSERT_TRUE(fileExists("card_counters.ini"));
ASSERT_EQ(readFromIni("card_counters.ini", "cards/counters/0/color").toString(), QColor(Qt::red).name());
ASSERT_FALSE(readFromIni("global.ini", "cards/counters/0/color").isValid());
ASSERT_TRUE(fileExists("interface.ini"));
ASSERT_EQ(readFromIni("interface.ini", "interface/useTearOffMenus"), QVariant(true));
ASSERT_EQ(readFromIni("interface.ini", "interface/cardViewInitialRowsMax"), QVariant(8));
ASSERT_EQ(readFromIni("interface.ini", "interface/cardViewExpandedRowsMax"), QVariant(12));
ASSERT_EQ(readFromIni("interface.ini", "interface/closeEmptyCardView"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/focusCardViewSearchBar"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/keepGameChatFocus"), QVariant(true));
ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/enabled"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/spectatorsEnabled"), QVariant(true));
ASSERT_EQ(readFromIni("interface.ini", "interface/notifications/buddyConnectEnabled"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/doubleClickToPlay"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/clickPlaysAllSelected"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/playToStack"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/doNotDeleteArrowsInSubPhases"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/startingHandSize"), QVariant(5));
ASSERT_EQ(readFromIni("interface.ini", "interface/annotateTokens"), QVariant(true));
ASSERT_EQ(readFromIni("interface.ini", "interface/showLassoSelectionCount"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/showPersistentSelectionCount"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/tallyType"), QVariant(2));
ASSERT_EQ(readFromIni("interface.ini", "interface/leftJustified"), QVariant(true));
ASSERT_EQ(readFromIni("interface.ini", "interface/minPlayersMulticolumn"), QVariant(6));
ASSERT_EQ(readFromIni("interface.ini", "interface/showStatusBar"), QVariant(true));
ASSERT_EQ(readFromIni("interface.ini", "interface/showShortcuts"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "interface/showGameSelectorFilterToolbar"), QVariant(false));
ASSERT_EQ(readFromIni("interface.ini", "hand/horizontal"), QVariant(true));
ASSERT_EQ(readFromIni("interface.ini", "table/invertVertical"), QVariant(true));
ASSERT_EQ(readFromIni("interface.ini", "replay/rewindBufferingMs"), QVariant(6000));
ASSERT_EQ(readFromIni("interface.ini", "replay/fastForwardSpeed"), QVariant(5));
ASSERT_EQ(readFromIni("interface.ini", "zoneview/groupBy"), QVariant(2));
ASSERT_EQ(readFromIni("interface.ini", "zoneview/sortBy"), QVariant(1));
ASSERT_EQ(readFromIni("interface.ini", "zoneview/pileView"), QVariant(false));
ASSERT_TRUE(fileExists("deck_editor.ini"));
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/openDeckInNewTab"), QVariant(false));
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/bannerCardComboBoxVisible"), QVariant(false));
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/tagsWidgetVisible"), QVariant(false));
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/defaultDeckEditorType"), QVariant(0));
ASSERT_TRUE(fileExists("paths.ini"));
ASSERT_EQ(readFromIni("paths.ini", "paths/decks"), QVariant("/custom/decks"));
ASSERT_EQ(readFromIni("paths.ini", "paths/pics"), QVariant("/custom/pics"));
ASSERT_TRUE(fileExists("visual_deck_storage.ini"));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/sortingOrder"), QVariant(2));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showFolders"), QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showTagFilter"), QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/defaultTagsList"),
QVariant(QStringList{"Alpha", "Beta"}));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/searchFolderNames"), QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showColorIdentity"), QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showBannerCardComboBox"),
QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/showTagsOnDeckPreviews"),
QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/drawUnusedColorIdentities"),
QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/unusedColorIdentitiesOpacity"),
QVariant(35));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/tooltipType"), QVariant(1));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/promptForConversion"),
QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/alwaysConvert"), QVariant(true));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/inGame"), QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDeckStorage/selectionAnimation"),
QVariant(false));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDatabaseDisplay/filterToMostRecentSetsEnabled"),
QVariant(true));
ASSERT_EQ(readFromIni("visual_deck_storage.ini", "interface/visualDatabaseDisplay/filterToMostRecentSetsAmount"),
QVariant(25));
// No legacy flat keys should remain in the per-file INIs
ASSERT_FALSE(iniFileHasKeyCaseSensitive("visual_deck_storage.ini", "interface/visualdeckstorageshowfolders"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("visual_deck_storage.ini", "interface/visualdeckstoragecardsize"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("deck_editor.ini", "deckeditor/sampleHandSize"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("deck_editor.ini", "deckeditor/cardSize"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/notificationsenabled"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/printingselectorsortorder"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/visualDeckStorage/cardSize"));
// No legacy non-camelCase keys should remain in the per-file INIs
ASSERT_FALSE(iniFileHasKeyCaseSensitive("game.ini", "game/gamedescription"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("game.ini", "localgameoptions/maxplayers"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("chat.ini", "chat/roomhistory"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("chat.ini", "chat/highlightwords"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("sound.ini", "sound/mastervolume"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("downloads.ini", "downloads/picturedownload"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("network.ini", "network/keepalive"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("network.ini", "network/knownmissingfeatures"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("updates.ini", "updates/updatenotification"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/displaycardnames"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("cards_display.ini", "cards/cardinfoviewmode"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/usetearoffmenus"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/doubleclicktoplay"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "interface/min_players_multicolumn"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "table/invert_vertical"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "zoneview/groupby"));
ASSERT_FALSE(iniFileHasKeyCaseSensitive("interface.ini", "zoneview/pileview"));
// Verify sentinel was written
ASSERT_EQ(readFromIni("global.ini", "migration/perfile_complete"), QVariant(true));
}
TEST_F(SettingsMigrationTest, SecondCallDoesNothing)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("tabs/visualDeckStorage", true);
g.setValue("sound/enabled", true);
g.sync();
}
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
ASSERT_FALSE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
}
TEST_F(SettingsMigrationTest, SecondCallDoesNotOverwriteUserChanges)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("tabs/visualDeckStorage", true);
g.setValue("sound/enabled", true);
g.sync();
}
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
// User changes the value after migration
{
QSettings tabsIni(settingsPath + "tabs.ini", QSettings::IniFormat);
tabsIni.setValue("tabs/visualDeckStorage", false);
tabsIni.sync();
}
// Second migration should NOT overwrite the user's change
ASSERT_FALSE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
ASSERT_EQ(readFromIni("tabs.ini", "tabs/visualDeckStorage"), QVariant(false));
}
TEST_F(SettingsMigrationTest, PartialMigrationOnlyMigratesPresentKeys)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("tabs/visualDeckStorage", true);
g.setValue("tabs/server", false);
g.sync();
}
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
ASSERT_TRUE(fileExists("global.ini.old"));
ASSERT_TRUE(fileExists("tabs.ini"));
ASSERT_EQ(readFromIni("tabs.ini", "tabs/visualDeckStorage"), QVariant(true));
ASSERT_EQ(readFromIni("tabs.ini", "tabs/server"), QVariant(false));
// Other files should NOT exist since no keys were present for them
ASSERT_FALSE(fileExists("sound.ini"));
ASSERT_FALSE(fileExists("game.ini"));
ASSERT_FALSE(fileExists("chat.ini"));
}
TEST_F(SettingsMigrationTest, ThemeNameDefaultIsRewrittenToSystem)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("theme/name", "Default");
g.sync();
}
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
ASSERT_TRUE(fileExists("appearance.ini"));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/themeName"), QVariant("System"));
}
TEST_F(SettingsMigrationTest, OtherThemeNamesAreLeftAlone)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("theme/name", "VelvetMarble");
g.sync();
}
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
ASSERT_TRUE(fileExists("appearance.ini"));
ASSERT_EQ(readFromIni("appearance.ini", "appearance/themeName"), QVariant("VelvetMarble"));
}
TEST_F(SettingsMigrationTest, KeyMapTranslationIsCorrect)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("personal/cardUpdateCheckInterval", 30);
g.setValue("personal/updatereleasechannel", 1);
g.sync();
}
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
ASSERT_TRUE(fileExists("global.ini.old"));
ASSERT_TRUE(fileExists("updates.ini"));
// The key should be translated from "personal/cardUpdateCheckInterval" to "updates/cardUpdateCheckInterval"
ASSERT_EQ(readFromIni("updates.ini", "updates/cardUpdateCheckInterval"), QVariant(30));
ASSERT_EQ(readFromIni("updates.ini", "updates/updateReleaseChannel"), QVariant(1));
}
TEST_F(SettingsMigrationTest, CardsKeysKeepGroupPrefix)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("cards/displaycardnames", false);
g.setValue("interface/deckeditorbannercardcomboboxvisible", true);
g.sync();
}
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
ASSERT_TRUE(fileExists("global.ini.old"));
ASSERT_TRUE(fileExists("cards_display.ini"));
ASSERT_TRUE(fileExists("deck_editor.ini"));
// "cards/displaycardnames" should be stored with its group prefix
ASSERT_EQ(readFromIni("cards_display.ini", "cards/displayCardNames"), QVariant(false));
// deck editor keys belong to the deck editor settings now
ASSERT_EQ(readFromIni("deck_editor.ini", "deckeditor/bannerCardComboBoxVisible"), QVariant(true));
}
TEST_F(SettingsMigrationTest, LegacyMigrationIsIdempotent)
{
{
QSettings nativeSettings;
nativeSettings.setValue("server/previoushostlogin", "test_user");
nativeSettings.setValue("server/auto_connect", true);
nativeSettings.sync();
}
ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath));
// Change the migrated value
{
QSettings serversIni(settingsPath + "servers.ini", QSettings::IniFormat);
serversIni.setValue("server/previousHostLogin", "modified_user");
serversIni.sync();
}
// Second migration should NOT overwrite the change
ASSERT_FALSE(SettingsMigration::migrateLegacySettings(settingsPath));
ASSERT_EQ(readFromIni("servers.ini", "server/previousHostLogin"), QVariant("modified_user"));
}
TEST_F(SettingsMigrationTest, LegacyMigrationCamelCasesKeys)
{
{
QSettings nativeSettings;
nativeSettings.setValue("sets/AAA/sortkey", 2);
nativeSettings.setValue("sets/AAA/enabled", false);
nativeSettings.setValue("sets/AAA/isknown", false);
nativeSettings.setValue("server/previoushostlogin", "legacy_user");
nativeSettings.setValue("server/auto_connect", 1);
nativeSettings.setValue("server/fpport", "5080");
nativeSettings.setValue("messages/count", 1);
nativeSettings.setValue("messages/msg0", "hello");
nativeSettings.setValue("filter_games/hide_full_games", true);
nativeSettings.setValue("filter_games/min_players", 3);
nativeSettings.setValue("filter_games/max_players", 5);
nativeSettings.setValue("filter_games/game_type/deadbeef", true);
nativeSettings.sync();
}
ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath));
ASSERT_TRUE(fileExists("cardDatabase.ini"));
ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/sortKey"), QVariant(2));
ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/enabled"), QVariant(false));
ASSERT_EQ(readFromIni("cardDatabase.ini", "sets/AAA/isKnown"), QVariant(false));
ASSERT_TRUE(fileExists("servers.ini"));
ASSERT_EQ(readFromIni("servers.ini", "server/previousHostLogin"), QVariant("legacy_user"));
ASSERT_EQ(readFromIni("servers.ini", "server/autoConnect"), QVariant(1));
ASSERT_EQ(readFromIni("servers.ini", "server/fpPort"), QVariant("5080"));
ASSERT_TRUE(fileExists("messages.ini"));
ASSERT_EQ(readFromIni("messages.ini", "messages/count"), QVariant(1));
ASSERT_EQ(readFromIni("messages.ini", "messages/msg0"), QVariant("hello"));
ASSERT_TRUE(fileExists("gamefilters.ini"));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hideFullGames"), QVariant(true));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/minPlayers"), QVariant(3));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/maxPlayers"), QVariant(5));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/gameType/deadbeef"), QVariant(true));
}
TEST_F(SettingsMigrationTest, LegacyMigrationMapsCreatorNameFilterToHostNameFilter)
{
{
QSettings nativeSettings;
nativeSettings.setValue("filter_games/creator_name_filter", QStringList({"Alice", "Bob"}));
nativeSettings.sync();
}
ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath));
ASSERT_TRUE(fileExists("gamefilters.ini"));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hostNameFilter"), QVariant(QStringList({"Alice", "Bob"})));
}
TEST_F(SettingsMigrationTest, GameFiltersIniSnakeCaseMigratesToCamelCase)
{
{
QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat);
filtersIni.setValue("filter_games/hide_full_games", true);
filtersIni.setValue("filter_games/hide_games_that_started", true);
filtersIni.setValue("filter_games/game_name_filter", "test game");
filtersIni.setValue("filter_games/creator_name_filter", QStringList({"Alice"}));
filtersIni.setValue("filter_games/game_type/deadbeef", true);
filtersIni.sync();
}
ASSERT_TRUE(SettingsMigration::migrateGameFiltersToCamelCase(settingsPath));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hideFullGames"), QVariant(true));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hideGamesThatStarted"), QVariant(true));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/gameNameFilter"), QVariant("test game"));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hostNameFilter"), QVariant(QStringList({"Alice"})));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/gameType/deadbeef"), QVariant(true));
// Idempotent: a second run has nothing left to do
ASSERT_FALSE(SettingsMigration::migrateGameFiltersToCamelCase(settingsPath));
}
TEST_F(SettingsMigrationTest, GameFiltersIniOrphanCreatorNameFilterMigratesToHostName)
{
{
QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat);
filtersIni.setValue("filter_games/creatorNameFilter", QStringList({"Alice"}));
filtersIni.sync();
}
ASSERT_TRUE(SettingsMigration::migrateGameFiltersToCamelCase(settingsPath));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hostNameFilter"), QVariant(QStringList({"Alice"})));
ASSERT_FALSE(SettingsMigration::migrateGameFiltersToCamelCase(settingsPath));
}
TEST_F(SettingsMigrationTest, GameFiltersIniKeepsNewerCamelCaseValues)
{
{
QSettings filtersIni(settingsPath + "gamefilters.ini", QSettings::IniFormat);
filtersIni.setValue("filter_games/hide_full_games", true);
filtersIni.setValue("filter_games/hideFullGames", false);
filtersIni.sync();
}
// Nothing to do: the current-format key already exists and must win
ASSERT_FALSE(SettingsMigration::migrateGameFiltersToCamelCase(settingsPath));
ASSERT_EQ(readFromIni("gamefilters.ini", "filter_games/hideFullGames"), QVariant(false));
}
TEST_F(SettingsMigrationTest, LegacyMigrationEmptyNativeFormatWritesSentinel)
{
QSettings().clear();
QSettings().sync();
ASSERT_TRUE(SettingsMigration::migrateLegacySettings(settingsPath));
ASSERT_TRUE(fileExists("personal.ini"));
ASSERT_EQ(readFromIni("personal.ini", "migration/legacy_complete"), QVariant(true));
ASSERT_FALSE(fileExists("messages.ini"));
ASSERT_FALSE(fileExists("gamefilters.ini"));
ASSERT_FALSE(fileExists("cardDatabase.ini"));
// Second call should be a no-op
ASSERT_FALSE(SettingsMigration::migrateLegacySettings(settingsPath));
}
// --- migrateSettingsFromGlobalIni rename-failure test ---
TEST_F(SettingsMigrationTest, RenameFailureWithExistingBackup)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("tabs/visualDeckStorage", true);
g.setValue("sound/enabled", true);
g.sync();
}
// Create a stale backup
{
QFile oldBackup(settingsPath + "global.ini.old");
ASSERT_TRUE(oldBackup.open(QIODevice::WriteOnly));
oldBackup.close();
}
// Migration should still succeed
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
// Per-file INIs should be created
ASSERT_TRUE(fileExists("tabs.ini"));
ASSERT_TRUE(fileExists("sound.ini"));
// Sentinel should be set in the new global.ini
ASSERT_EQ(readFromIni("global.ini", "migration/perfile_complete"), QVariant(true));
// Original data should be backed up (stale backup was replaced)
ASSERT_TRUE(fileExists("global.ini.old"));
ASSERT_EQ(readFromIni("global.ini.old", "tabs/visualDeckStorage"), QVariant(true));
// Second call should be a no-op
ASSERT_FALSE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
}
// --- pre-migration backup of the settings folder ---
TEST_F(SettingsMigrationTest, BackupHoldsPreMigrationStateOfPerDomainFiles)
{
// A user already migrated once, changed a setting, then ran an older build that wrote to
// global.ini again. Re-migrating clobbers the per-domain file, so the backup has to hold the
// pre-migration value.
{
QSettings tabs(settingsPath + "tabs.ini", QSettings::IniFormat);
tabs.setValue("tabs/visualDeckStorage", false);
tabs.sync();
}
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("tabs/visualDeckStorage", true);
g.sync();
}
const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath);
ASSERT_FALSE(backupPath.isEmpty());
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
// The migration overwrote the user's change ...
ASSERT_EQ(readFromIni("tabs.ini", "tabs/visualDeckStorage"), QVariant(true));
// ... but the backup still holds it, so it can be restored by hand.
const QStringList backups = backupFolderNames();
ASSERT_EQ(backups.size(), 1);
ASSERT_EQ(readFromBackup(backups.at(0), "tabs.ini", "tabs/visualDeckStorage"), QVariant(false));
ASSERT_EQ(readFromBackup(backups.at(0), "global.ini", "tabs/visualDeckStorage"), QVariant(true));
}
TEST_F(SettingsMigrationTest, BackupIsASiblingOfTheSettingsFolder)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("sound/enabled", true);
g.sync();
}
const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath);
ASSERT_FALSE(backupPath.isEmpty());
const QDir backupDir(backupPath);
ASSERT_TRUE(backupDir.dirName().startsWith("settings-backup-"));
// Same parent as the settings folder, and not inside it, so a backup is never copied into
// another backup and "Open settings folder" shows only live files.
ASSERT_EQ(QDir::cleanPath(backupDir.absolutePath() + "/.."), QDir::cleanPath(tempDir.path()));
ASSERT_FALSE(backupDir.absolutePath().startsWith(QDir::cleanPath(settingsPath) + '/'));
ASSERT_TRUE(backupFileExists(backupDir.dirName(), "global.ini"));
}
TEST_F(SettingsMigrationTest, BackupCopiesNestedFolders)
{
ASSERT_TRUE(QDir().mkpath(settingsPath + "nested"));
{
QFile f(settingsPath + "nested/deep.ini");
ASSERT_TRUE(f.open(QIODevice::WriteOnly));
f.write("[visualDeckStorage]\nfirstDeck=1234\n");
}
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("sound/enabled", true);
g.sync();
}
const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath);
ASSERT_FALSE(backupPath.isEmpty());
const QStringList backups = backupFolderNames();
ASSERT_EQ(backups.size(), 1);
ASSERT_TRUE(backupFileExists(backups.at(0), "nested/deep.ini"));
ASSERT_EQ(readFromBackup(backups.at(0), "nested/deep.ini", "visualDeckStorage/firstDeck"), QVariant("1234"));
}
TEST_F(SettingsMigrationTest, NoBackupWhenSettingsFolderIsEmpty)
{
ASSERT_TRUE(SettingsMigration::backupSettingsDirectory(settingsPath).isEmpty());
ASSERT_TRUE(backupFolderNames().isEmpty());
}
TEST_F(SettingsMigrationTest, NoBackupWhenEverySettingsFileIsEmpty)
{
// A fresh profile: QSettings has created global.ini, but it holds nothing worth preserving.
{
QFile f(settingsPath + "global.ini");
ASSERT_TRUE(f.open(QIODevice::WriteOnly));
f.close();
}
ASSERT_TRUE(SettingsMigration::backupSettingsDirectory(settingsPath).isEmpty());
ASSERT_TRUE(backupFolderNames().isEmpty());
}
TEST_F(SettingsMigrationTest, BackupFailsGracefullyOnMissingSettingsFolder)
{
ASSERT_TRUE(SettingsMigration::backupSettingsDirectory(tempDir.path() + "/does-not-exist/").isEmpty());
}
TEST_F(SettingsMigrationTest, BackupKeepsEveryPreviousSnapshot)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("sound/enabled", true);
g.sync();
}
ASSERT_FALSE(SettingsMigration::backupSettingsDirectory(settingsPath).isEmpty());
ASSERT_FALSE(SettingsMigration::backupSettingsDirectory(settingsPath).isEmpty());
// A second snapshot in the same second must not merge into the first one.
const QStringList backups = backupFolderNames();
ASSERT_EQ(backups.size(), 2);
ASSERT_NE(backups.at(0), backups.at(1));
}
TEST_F(SettingsMigrationTest, IsMigrationPendingWhenGlobalIniHasKeys)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("sound/enabled", true);
g.sync();
}
ASSERT_TRUE(SettingsMigration::isMigrationPending(settingsPath));
}
TEST_F(SettingsMigrationTest, IsNotMigrationPendingAfterSentinelIsSet)
{
{
QSettings g(settingsPath + "global.ini", QSettings::IniFormat);
g.setValue("sound/enabled", true);
g.sync();
}
ASSERT_TRUE(SettingsMigration::migrateSettingsFromGlobalIni(settingsPath));
// The sentinel-only global.ini and the legacy sentinel leave nothing to migrate.
{
QSettings personal(settingsPath + "personal.ini", QSettings::IniFormat);
personal.setValue("migration/legacy_complete", true);
personal.sync();
}
ASSERT_FALSE(SettingsMigration::isMigrationPending(settingsPath));
}
TEST_F(SettingsMigrationTest, IsMigrationPendingWhenOnlyLegacySettingsRemain)
{
QSettings().setValue("server/host", "example.com");
QSettings().sync();
ASSERT_TRUE(SettingsMigration::isMigrationPending(settingsPath));
}
TEST_F(SettingsMigrationTest, IsNotMigrationPendingWithoutLegacyData)
{
// Nothing to migrate: a fresh profile must not leave a backup folder behind on every start.
QSettings().clear();
QSettings().sync();
ASSERT_FALSE(SettingsMigration::isMigrationPending(settingsPath));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_NonDefaultSeedsThemeAndDeletesKey)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
const QString fontsPath = themesPath + "/MyTheme/fonts.toml";
ASSERT_TRUE(QFile::exists(fontsPath));
QFile f(fontsPath);
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
// Size-only: the migration must not pin the family, or every CardText
// surface switches to Serif for users who never chose it.
EXPECT_FALSE(content.contains("Serif"));
ASSERT_FALSE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_SeedsActiveStockTheme)
{
// A stock theme resolves to the system directory, so there is no user theme
// folder to seed. The migration has to create the active theme's folder,
// otherwise the retired size is deleted without landing anywhere.
const QString themesPath = tempDir.path() + "/themes";
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/themeName", "Fabric");
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
const QString fontsPath = themesPath + "/Fabric/fonts.toml";
ASSERT_TRUE(QFile::exists(fontsPath));
QFile f(fontsPath);
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_KeepsExistingActiveTheme)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/Fabric"));
{
QFile f(themesPath + "/Fabric/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nCardText = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/themeName", "Fabric");
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/Fabric/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("Inter, 10"));
EXPECT_FALSE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_DefaultDeletesKeyWithoutFiles)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 12);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
ASSERT_FALSE(readFromIni("appearance.ini", "appearance/maxFontSize").isValid());
ASSERT_FALSE(QFile::exists(themesPath + "/MyTheme/fonts.toml"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_DoesNotOverwriteExplicitMax)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nCardText = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("10"));
EXPECT_FALSE(content.contains("18"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_MergesWithOtherRoles)
{
const QString themesPath = tempDir.path() + "/themes";
ASSERT_TRUE(QDir().mkpath(themesPath + "/MyTheme"));
{
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text));
QTextStream out(&f);
out << "[Fonts]\nInterfaceDefault = \"Inter, 10\"\n";
}
{
QSettings paths(settingsPath + "paths.ini", QSettings::IniFormat);
paths.setValue("paths/themes", themesPath);
paths.sync();
}
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 20);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
QFile f(themesPath + "/MyTheme/fonts.toml");
ASSERT_TRUE(f.open(QIODevice::ReadOnly | QIODevice::Text));
const QString content = QString::fromUtf8(f.readAll());
EXPECT_TRUE(content.contains("InterfaceDefault"));
EXPECT_TRUE(content.contains("CardTextMaxSize"));
EXPECT_TRUE(content.contains("20"));
}
TEST_F(SettingsMigrationTest, AppearanceMaxFontSize_AbsentKeyDoesNothing)
{
ASSERT_FALSE(SettingsMigration::migrateAppearanceMaxFontSizeToTheme(settingsPath));
}
TEST_F(SettingsMigrationTest, IsMigrationPendingWhenMaxFontSizePresent)
{
{
QSettings appearance(settingsPath + "appearance.ini", QSettings::IniFormat);
appearance.setValue("appearance/maxFontSize", 18);
appearance.sync();
}
ASSERT_TRUE(SettingsMigration::isMigrationPending(settingsPath));
}
} // namespace
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
QCoreApplication app(argc, argv);
app.setOrganizationName("CockatriceTest");
app.setApplicationName("SettingsMigrationTest");
QSettings::setDefaultFormat(QSettings::IniFormat);
return RUN_ALL_TESTS();
}