diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 1760d86b8..df603eb71 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -798,18 +798,6 @@ if(Qt6LinguistTools_FOUND) elseif(WIN32) install(FILES ${cockatrice_QM} DESTINATION ${COCKATRICE_WIN32_QM_INSTALL_DIR}) endif() - - # Qt attaches the .qm files to the default build target only, so building the executable on its own - # (CLion, --target cockatrice) leaves it without any translations: nothing is translated and the - # language selector stays empty. - # Depend on Qt's own lrelease target instead of attaching the .qm custom commands to a second - # target: two independent targets owning the same outputs race on MSBuild/Makefiles and are an - # error on Xcode. Older Qt versions already add this dependency themselves. - foreach(_lrelease_target Cockatrice_lrelease cockatrice_lrelease) - if(TARGET ${_lrelease_target}) - add_dependencies(cockatrice ${_lrelease_target}) - endif() - endforeach() endif() qt6_finalize_target(cockatrice) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index f401dfd00..260167bc8 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #if defined(Q_OS_MACOS) #include @@ -27,29 +26,6 @@ #define GIT_SHORT_HASH_LEN 7 -namespace -{ - -#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) -// QSysInfo::productVersion() is not guaranteed to start with a number, e.g. it returns -// "Server 2022" on Windows Server. Use the first numeric token as the major host version. -std::optional getProductVersionMajor() -{ - const QString versionString = QSysInfo::productVersion(); - bool ok = false; - static const QRegularExpression firstNumber(R"((\d+))"); - const auto match = firstNumber.match(versionString); - const int version = match.hasMatch() ? match.captured(1).toInt(&ok) : 0; - if (!ok) { - qCWarning(ReleaseChannelLog) << "Unable to determine OS version from" << versionString; - return std::nullopt; - } - return version; -} -#endif - -} // namespace - ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr) { } @@ -67,93 +43,55 @@ void ReleaseChannel::checkForUpdates() connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished); } -// Find assets compatible with host platform (Linux is not supported by in-client updater) -std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &fileName) +// Different release channel checking functions for different operating systems +bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName) { -#if defined(Q_OS_WIN) && Q_PROCESSOR_WORDSIZE == 4 - // The published Windows assets are 64-bit only - Q_UNUSED(fileName); - return std::nullopt; -#endif - - const QRegularExpression *regex = nullptr; - - // Only platforms shipping updater assets (macOS, Windows) need the host version - static const std::optional systemVersion = -#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) - getProductVersionMajor(); -#else - std::nullopt; -#endif - - if (!systemVersion) { - return std::nullopt; +#if defined(Q_OS_MACOS) + static QRegularExpression version_regex("macOS(\\d+)"); + auto match = version_regex.match(fileName); + if (!match.hasMatch()) { + return false; } -#if defined(Q_OS_MACOS) - static const bool isIntel = [] { + auto getSystemVersion = [] { // QSysInfo does not go through translation layers // We need to use sysctl to reliably detect the underlying architecture char arch[255]; size_t len = sizeof(arch); if (sysctlbyname("machdep.cpu.brand_string", arch, &len, nullptr, 0) == 0) { - return QString::fromUtf8(arch).contains("Intel"); + // Intel mac is only supported on macOS 13 versions + if (QString::fromUtf8(arch).contains("Intel")) { + return 13; + } } - return false; - }(); - // Apple (ARM) --> Cockatrice-3.0.0-macOS15.dmg - // Apple (x86) --> Cockatrice-3.0.0-macOS15_Intel.dmg - static const QRegularExpression macIntelRegex(R"(-macOS(\d+)_Intel\.[^.]+$)", - QRegularExpression::CaseInsensitiveOption); - static const QRegularExpression macRegex(R"(-macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); - regex = isIntel ? &macIntelRegex : &macRegex; + return QSysInfo::productVersion().split(".")[0].toInt(); + }; + + // older(smaller) releases are compatible with a newer or the same system version + int sys_maj = getSystemVersion(); + int rel_maj = match.captured(1).toInt(); + return rel_maj == sys_maj; #elif defined(Q_OS_WIN) - // Windows (x86) --> Cockatrice-3.0.0-Windows10.exe - static const QRegularExpression winRegex(R"(-(?:Win|Windows)(\d+)\.[^.]+$)", - QRegularExpression::CaseInsensitiveOption); - regex = &winRegex; - -#else // if the OS doesn't fit one of the above #defines, then it will never match +#if Q_PROCESSOR_WORDSIZE == 4 + return fileName.contains("32bit"); +#elif Q_PROCESSOR_WORDSIZE == 8 + const QString &version = QSysInfo::productVersion(); + if (version.startsWith("7") || version.startsWith("8")) { + return fileName.contains("Win7"); + } else { + return fileName.contains("Win10"); + } +#else Q_UNUSED(fileName); - return std::nullopt; + return false; #endif - // Any asset targeting an OS version up to the current host version works - auto match = regex->match(fileName); - if (!match.hasMatch()) { - return std::nullopt; - } - const int targetVersion = match.captured(1).toInt(); - if (targetVersion > *systemVersion) { - return std::nullopt; - } - return targetVersion; -} - -// Pick newest targeted version (highest number) amongst all compatible ones -QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets) -{ - QString bestUrl; - int bestVersion = -1; - for (const auto &rawAsset : assets) { - QVariantMap asset = rawAsset.toMap(); - QString name = asset["name"].toString(); - QString url = asset["browser_download_url"].toString(); - auto version = getTargetVersionForCurrentOS(name); - if (!version) { - continue; - } - if (*version > bestVersion) { - bestVersion = *version; - bestUrl = url; - } - } - if (!bestUrl.isEmpty()) { - qCInfo(ReleaseChannelLog) << "Best compatible asset=" << bestUrl; - } - return bestUrl; +#else // If the OS doesn't fit one of the above #defines, then it will never match + Q_UNUSED(fileName); + return false; +#endif } QString StableReleaseChannel::getManualDownloadUrl() const @@ -200,9 +138,16 @@ void StableReleaseChannel::releaseListFinished() lastRelease->setPublishDate(resultMap["published_at"].toDate()); if (resultMap.contains("assets")) { - auto url = findBestDownloadUrl(resultMap["assets"].toList()); - if (!url.isEmpty()) { - lastRelease->setDownloadUrl(url); + auto rawAssets = resultMap["assets"].toList(); + for (const auto &rawAsset : rawAssets) { + QVariantMap asset = rawAsset.toMap(); + QString name = asset["name"].toString(); + QString url = asset["browser_download_url"].toString(); + + if (downloadMatchesCurrentOS(name)) { + lastRelease->setDownloadUrl(url); + break; + } } } @@ -344,10 +289,21 @@ void BetaReleaseChannel::fileListFinished() bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0); bool compatibleVersion = false; - QString downloadUrl = findBestDownloadUrl(resultList); - if (!downloadUrl.isEmpty()) { - compatibleVersion = true; - lastRelease->setDownloadUrl(downloadUrl); + QStringList resultUrlList{}; + for (QVariant file : resultList) { + QVariantMap map = file.toMap(); + resultUrlList << map["browser_download_url"].toString(); + } + + resultUrlList.sort(); + // iterate in reverse so the first item is the latest os version + for (auto url = resultUrlList.rbegin(); url < resultUrlList.rend(); ++url) { + if (downloadMatchesCurrentOS(*url)) { + compatibleVersion = true; + lastRelease->setDownloadUrl(*url); + qCInfo(ReleaseChannelLog) << "Found compatible version url=" << *url; + break; + } } emit finishedCheck(needToUpdate, compatibleVersion, lastRelease); diff --git a/cockatrice/src/client/network/update/client/release_channel.h b/cockatrice/src/client/network/update/client/release_channel.h index e038cb2f4..c56d0cfce 100644 --- a/cockatrice/src/client/network/update/client/release_channel.h +++ b/cockatrice/src/client/network/update/client/release_channel.h @@ -12,7 +12,6 @@ #include #include #include -#include #include inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel"); @@ -97,8 +96,7 @@ protected: Release *lastRelease; protected: - static std::optional getTargetVersionForCurrentOS(const QString &fileName); - static QString findBestDownloadUrl(const QVariantList &assets); + static bool downloadMatchesCurrentOS(const QString &fileName); [[nodiscard]] virtual QString getReleaseChannelUrl() const = 0; public: diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 0f8693f2d..eaa5d96ac 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -110,16 +110,6 @@ SettingsCache::SettingsCache() settings = new QSettings(settingsPath + "global.ini", QSettings::IniFormat, this); - // Both migrations below overwrite per-domain files in place, so snapshot the whole settings - // folder first: global.ini is preserved as global.ini.old, but pre-existing per-domain files - // (and a re-migration after a downgrade) would otherwise have no way back. - if (SettingsMigration::isMigrationPending(settingsPath)) { - const QString backupPath = SettingsMigration::backupSettingsDirectory(settingsPath); - if (!backupPath.isEmpty()) { - qCInfo(SettingsCacheLog) << "[SettingsCache] Settings backed up before migration:" << backupPath; - } - } - // Migrate from legacy NativeFormat settings to per-class INI files (runs at most once) if (!isPortableBuild) { SettingsMigration::migrateLegacySettings(settingsPath); diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp index 57099fb4a..b0fd0e018 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -21,7 +21,6 @@ #include #include #include -#include enum startupCardUpdateCheckBehaviorIndex { @@ -334,18 +333,22 @@ GeneralSettingsPage::GeneralSettingsPage() QStringList GeneralSettingsPage::findQmFiles() { - return TranslationLoader::availableLanguages(translationPrefix, - TranslationLoader::applicationTranslationPaths(translationPath)); + QDir dir(translationPath); + QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name); + fileNames.replaceInStrings(QRegularExpression(translationPrefix + "_(.*)\\.qm"), "\\1"); + return fileNames; } QString GeneralSettingsPage::languageName(const QString &lang) { QTranslator qTranslator; - // Only the language name is needed here, so the file is loaded without logging a line per - // language: findQmFiles() asked for languages whose files exist in the first place. - TranslationLoader::loadFrom(qTranslator, translationPrefix + "_" + lang, - TranslationLoader::applicationTranslationPaths(translationPath)); + QString appNameHint = translationPrefix + "_" + lang; + bool appTranslationLoaded = qTranslator.load(appNameHint, translationPath); + if (!appTranslationLoaded) { + qCWarning(GeneralSettingsPageLog) + << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath; + } return qTranslator.translate("i18n", DEFAULT_LANG_NAME); } diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index af519f436..b9c30d1ad 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -54,7 +55,6 @@ #include #include #include -#include QTranslator *translator, *qtTranslator; RNG_Abstract *rng; @@ -141,13 +141,26 @@ void installNewTranslator() { QString lang = SettingsCache::instance().personal().getLang(); - // Qt's own strings and ours need a translator each: QTranslator discards its previous contents - // on every load, so loading both files into one translator silently drops the first of them. - TranslationLoader::loadQt(*qtTranslator, lang); + QString qtNameHint = "qt_" + lang; + QString qtTranslationPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); + + bool qtTranslationLoaded = qtTranslator->load(qtNameHint, qtTranslationPath); + if (!qtTranslationLoaded) { + qCWarning(QtTranslatorDebug) << "Unable to load qt translation" << qtNameHint << "at" << qtTranslationPath; + } else { + qCInfo(QtTranslatorDebug) << "Loaded qt translation" << qtNameHint << "at" << qtTranslationPath; + } qApp->installTranslator(qtTranslator); - TranslationLoader::loadApplication(*translator, translationPrefix, lang, - TranslationLoader::applicationTranslationPaths(translationPath)); + QString appNameHint = translationPrefix + "_" + lang; + bool appTranslationLoaded = qtTranslator->load(appNameHint, translationPath); + if (!appTranslationLoaded) { + qCWarning(QtTranslatorDebug) << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" + << translationPath; + } else { + qCInfo(QtTranslatorDebug) << "Loaded" << translationPrefix << "translation" << appNameHint << "at" + << translationPath; + } qApp->installTranslator(translator); } @@ -377,11 +390,8 @@ int main(int argc, char *argv[]) qCInfo(MainLog) << "MainWindow constructor finished"; ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice"))); - // Base name of the desktop file installed next to the binary, without the ".desktop" - // extension. Wayland reads the window icon from it, and xdg-desktop-portal looks up - // org.freedesktop.host.portal.Registry by this app ID, so it has to match the file name - // exactly, down to the case. - QGuiApplication::setDesktopFileName("cockatrice"); + // Set name of the app desktop file; used by wayland to load the window icon + QGuiApplication::setDesktopFileName("Cockatrice"); SettingsCache::instance().network().setClientID(generateClientID()); diff --git a/cockatrice/src/main.h b/cockatrice/src/main.h index eee262d2c..ef75a41f4 100644 --- a/cockatrice/src/main.h +++ b/cockatrice/src/main.h @@ -10,6 +10,7 @@ #include inline Q_LOGGING_CATEGORY(MainLog, "main"); +inline Q_LOGGING_CATEGORY(QtTranslatorDebug, "qt_translator"); class CardDatabase; class QString; diff --git a/doc/doxygen/extra-pages/user_documentation/index.md b/doc/doxygen/extra-pages/user_documentation/index.md index f4ecbe8a1..b55d00fcd 100644 --- a/doc/doxygen/extra-pages/user_documentation/index.md +++ b/doc/doxygen/extra-pages/user_documentation/index.md @@ -11,10 +11,6 @@ - @subpage beta_release -## Settings - -- @subpage settings_folder - ## Card Pictures - @subpage custom_card_pictures diff --git a/doc/doxygen/extra-pages/user_documentation/settings/settings_folder.md b/doc/doxygen/extra-pages/user_documentation/settings/settings_folder.md deleted file mode 100644 index 5b3426e45..000000000 --- a/doc/doxygen/extra-pages/user_documentation/settings/settings_folder.md +++ /dev/null @@ -1,43 +0,0 @@ -@page settings_folder Settings Folder and Backups - -Cockatrice keeps all of its preferences in a settings folder on your computer, one `.ini` file per -group of settings (appearance, sound, decks, and so on). Open it from the main window via -Help → Open Settings Folder. - -# The Per-File Migration - -Older versions of Cockatrice stored every setting in a single `global.ini` file. The settings were -split into per-group `.ini` files so that unrelated groups no longer share one file. The first time -you start a version that uses the new layout, your settings are moved into the new files -automatically and nothing is lost. The old `global.ini` is kept next to the new files as -`global.ini.old`. - -# Automatic Backups - -Both migrations overwrite files in the settings folder in place, so before either one runs, -Cockatrice copies the whole folder to a timestamped backup folder: - -``` -/settings-backup-20261002-193000/ -``` - -The backup folder sits next to the settings folder, not inside it. Every backup is kept, so if -settings ever end up wrong you can compare against, or copy files back from, the earlier snapshot. -Backups are only created when a migration actually has work to do, so a normal start creates -nothing. - -# Downgrading - -Settings migrations only run forwards. If you ever go back to an older version of Cockatrice, that -version does not read the new per-group files, so it will start with default settings. - -Before downgrading, copy your settings folder somewhere safe, for example: - -- Linux: `~/.local/share/Cockatrice/settings/` -- macOS: `~/Library/Application Support/Cockatrice/settings/` -- Windows: `%LOCALAPPDATA%\Cockatrice\settings\` - -Portable installs keep the settings folder in the `data` folder next to the executable. - -Copying that folder back after downgrading restores your settings. The automatic backup folder from -the migration is there as a second copy of the same data if you need it. \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp index c22bd3c02..f6e107dad 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp +++ b/libcockatrice_settings/libcockatrice/settings/settings_migration.cpp @@ -1,44 +1,12 @@ #include "settings_migration.h" -#include -#include -#include #include -#include -#include #include #include #include -#include - -inline Q_LOGGING_CATEGORY(SettingsMigrationLog, "settings_migration"); static const QString MIGRATION_SENTINEL_KEY = QStringLiteral("migration/perfile_complete"); -/** - * True when global.ini holds keys that migrateSettingsFromGlobalIni() would still move, i.e. the - * exact condition that function uses to decide whether it has work to do. If a user runs an older - * Cockatrice build that writes to global.ini (non-sentinel keys) and then upgrades back, those new - * keys would otherwise be silently ignored, so they are re-migrated instead. - */ -static bool hasPendingGlobalIniSettings(const QString &settingsPath) -{ - if (!QFile::exists(settingsPath + "global.ini")) { - return false; - } - - QSettings globalIni(settingsPath + "global.ini", QSettings::IniFormat); - if (!globalIni.value(MIGRATION_SENTINEL_KEY, false).toBool()) { - return true; - } - - // The sentinel is set, so any key besides it was written after the migration, i.e. by an - // older build sharing this file. Those are exactly the keys to re-migrate. - QStringList otherKeys = globalIni.allKeys(); - otherKeys.removeAll(MIGRATION_SENTINEL_KEY); - return !otherKeys.isEmpty(); -} - static void migrateTabsSettings(const QString &settingsPath, QSettings &globalIni) { if (!globalIni.contains("tabs/visualDeckStorage") && !globalIni.contains("tabs/server") && @@ -690,11 +658,21 @@ bool SettingsMigration::migrateLegacySettings(const QString &settingsPath) bool SettingsMigration::migrateSettingsFromGlobalIni(const QString &settingsPath) { - if (!hasPendingGlobalIniSettings(settingsPath)) { + if (!QFile::exists(settingsPath + "global.ini")) { return false; } QSettings globalIni(settingsPath + "global.ini", QSettings::IniFormat); + if (globalIni.value(MIGRATION_SENTINEL_KEY, false).toBool()) { + // If a user runs an older Cockatrice build that writes to global.ini (non-sentinel keys) + // and then upgrades back, those new keys will be silently ignored. Re-migrate them. + globalIni.sync(); + auto allKeys = globalIni.allKeys(); + allKeys.removeAll(MIGRATION_SENTINEL_KEY); + if (allKeys.isEmpty()) { + return false; + } + } migrateTabsSettings(settingsPath, globalIni); migrateSoundSettings(settingsPath, globalIni); @@ -722,78 +700,3 @@ bool SettingsMigration::migrateSettingsFromGlobalIni(const QString &settingsPath return true; } - -bool SettingsMigration::isMigrationPending(const QString &settingsPath) -{ - if (hasPendingGlobalIniSettings(settingsPath)) { - return true; - } - - QSettings personalIni(settingsPath + "personal.ini", QSettings::IniFormat); - if (personalIni.value("migration/legacy_complete", false).toBool()) { - return false; - } - - // migrateLegacySettings() also runs on an empty profile, where it only writes its sentinel. - // Reporting that as pending would leave a backup folder behind on every fresh install, so - // only count it when there really are legacy keys left to move. - return !QSettings().allKeys().isEmpty(); -} - -QString SettingsMigration::backupSettingsDirectory(const QString &settingsPath) -{ - QDir settingsDir(QDir::cleanPath(settingsPath)); - if (!settingsDir.exists()) { - return QString(); - } - - const QDir::Filters fileFilters = QDir::Files | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot; - - // A profile holding nothing but empty files has nothing to lose, and backing it up would only - // leave clutter next to the settings folder. - const QFileInfoList files = settingsDir.entryInfoList(fileFilters); - const bool hasContent = - std::any_of(files.cbegin(), files.cend(), [](const QFileInfo &file) { return file.size() > 0; }); - if (!hasContent) { - return QString(); - } - - // Keep the backup a sibling of the settings folder rather than a child: a folder that is never - // copied into its own backup, and one the "Open settings folder" action does not have to skip. - QDir backupParentDir(settingsDir.absolutePath()); - if (!backupParentDir.cdUp()) { - qCWarning(SettingsMigrationLog) << "[SettingsMigration] Could not resolve the parent of the settings folder:" - << settingsDir.absolutePath(); - return QString(); - } - - const QString backupName = - QStringLiteral("settings-backup-") + QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss"); - QString backupPath = backupParentDir.absoluteFilePath(backupName); - // Two migrations inside the same second must not merge into a single folder. - for (int suffix = 1; QFileInfo::exists(backupPath); ++suffix) { - backupPath = backupParentDir.absoluteFilePath(backupName + '-' + QString::number(suffix)); - } - - if (!QDir().mkpath(backupPath)) { - qCWarning(SettingsMigrationLog) << "[SettingsMigration] Could not create the settings backup folder:" - << backupPath; - return QString(); - } - - QDir backupDir(backupPath); - QDirIterator it(settingsDir.absolutePath(), fileFilters, QDirIterator::Subdirectories); - while (it.hasNext()) { - it.next(); - - const QString target = backupDir.absoluteFilePath(settingsDir.relativeFilePath(it.filePath())); - QDir().mkpath(QFileInfo(target).absolutePath()); - if (!QFile::copy(it.filePath(), target)) { - // A partially copied backup still beats none: report it and let the migration run. - qCWarning(SettingsMigrationLog) << "[SettingsMigration] Could not back up settings file:" << it.filePath(); - } - } - - qCInfo(SettingsMigrationLog) << "[SettingsMigration] Settings backed up to" << backupPath; - return backupPath; -} diff --git a/libcockatrice_settings/libcockatrice/settings/settings_migration.h b/libcockatrice_settings/libcockatrice/settings/settings_migration.h index 78f9c8a3f..f596dba75 100644 --- a/libcockatrice_settings/libcockatrice/settings/settings_migration.h +++ b/libcockatrice_settings/libcockatrice/settings/settings_migration.h @@ -8,19 +8,6 @@ class SettingsMigration public: static bool migrateSettingsFromGlobalIni(const QString &settingsPath); static bool migrateLegacySettings(const QString &settingsPath); - - /** - * Returns true when at least one of the two migrations would still do work, i.e. when a - * backup of the settings directory is worth taking before they run. - */ - static bool isMigrationPending(const QString &settingsPath); - - /** - * Copies the whole settings directory to a timestamped sibling folder, so a migration that - * overwrites per-domain files can be undone by hand. Returns the backup path, or an empty - * string when there was nothing to back up or the copy failed. Never aborts the migration. - */ - static QString backupSettingsDirectory(const QString &settingsPath); }; #endif // SETTINGS_MIGRATION_H diff --git a/libcockatrice_utility/CMakeLists.txt b/libcockatrice_utility/CMakeLists.txt index 41f1bfa0f..ab647258c 100644 --- a/libcockatrice_utility/CMakeLists.txt +++ b/libcockatrice_utility/CMakeLists.txt @@ -12,7 +12,6 @@ set(UTILITY_SOURCES libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/report_categories.cpp libcockatrice/utility/server_rate_limiter.cpp - libcockatrice/utility/translation_loader.cpp libcockatrice/utility/warning_categories.cpp ) @@ -33,7 +32,6 @@ set(UTILITY_HEADERS libcockatrice/utility/zone_names.h libcockatrice/utility/days_years_between.h libcockatrice/utility/server_rate_limiter.h - libcockatrice/utility/translation_loader.h libcockatrice/utility/warning_categories.h ) diff --git a/libcockatrice_utility/libcockatrice/utility/translation_loader.cpp b/libcockatrice_utility/libcockatrice/utility/translation_loader.cpp deleted file mode 100644 index b06bcffc6..000000000 --- a/libcockatrice_utility/libcockatrice/utility/translation_loader.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "translation_loader.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -// Kept under the historical category name so existing QT_LOGGING_RULES filters keep working. -Q_LOGGING_CATEGORY(TranslationLoaderLog, "qt_translator") - -QList TranslationLoader::qtTranslationCandidates() -{ - QStringList candidates; - -#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) - // Qt 6.8 exposes every path listed in qt.conf, path() only returns the first one. - candidates << QLibraryInfo::paths(QLibraryInfo::TranslationsPath); -#else - candidates << QLibraryInfo::path(QLibraryInfo::TranslationsPath); -#endif - - // A relocated Qt can report a prefix-relative path that does not exist, so probe the usual - // install layouts as well instead of trusting that single report. - const QString prefix = QLibraryInfo::path(QLibraryInfo::PrefixPath); - const QString dataPath = QLibraryInfo::path(QLibraryInfo::DataPath); - candidates << prefix + QStringLiteral("/share/qt6/translations") - << prefix + QStringLiteral("/share/qt/translations") << prefix + QStringLiteral("/translations") - << dataPath + QStringLiteral("/translations"); - - QList uniqueCandidates; - for (const QString &candidate : candidates) { - if (!candidate.isEmpty() && !uniqueCandidates.contains(candidate)) { - uniqueCandidates << candidate; - } - } - return uniqueCandidates; -} - -QList TranslationLoader::qtTranslationPaths() -{ - QList existingPaths; - for (const QString &candidate : qtTranslationCandidates()) { - if (QFileInfo::exists(candidate)) { - existingPaths << candidate; - } - } - return existingPaths; -} - -QList TranslationLoader::applicationTranslationPaths(const QString &_installedPath) -{ - // A build tree keeps the .qm files next to the executable, the installed layout puts them in - // _installedPath. Searching both keeps a developer build translated as well. - const QString executableDirectory = QCoreApplication::applicationDirPath(); - - QList paths; - for (const QString &path : {_installedPath, executableDirectory}) { - if (!path.isEmpty() && !paths.contains(path)) { - paths << path; - } - } - return paths; -} - -QList TranslationLoader::availableLanguages(const QString &_prefix, const QList &_directories) -{ - const QRegularExpression filePattern( - QRegularExpression::anchoredPattern(QRegularExpression::escape(_prefix) + QStringLiteral("_(.*)\\.qm"))); - - QSet languages; - for (const QString &directory : _directories) { - if (directory.isEmpty()) { - continue; - } - const QDir dir(directory); - for (const QString &fileName : dir.entryList(QDir::Files)) { - const auto match = filePattern.match(fileName); - if (match.hasMatch()) { - languages.insert(match.captured(1)); - } - } - } - - QList sortedLanguages(languages.cbegin(), languages.cend()); - std::sort(sortedLanguages.begin(), sortedLanguages.end()); - return sortedLanguages; -} - -TranslationLoadResult -TranslationLoader::loadFrom(QTranslator &_translator, const QString &_nameHint, const QList &_directories) -{ - bool anyDirectoryExists = false; - - for (const QString &directory : _directories) { - if (directory.isEmpty() || !QFileInfo::exists(directory)) { - continue; - } - anyDirectoryExists = true; - if (_translator.load(_nameHint, directory)) { - return TranslationLoadResult::Loaded; - } - } - - return anyDirectoryExists ? TranslationLoadResult::Missing : TranslationLoadResult::NoDirectory; -} - -TranslationLoadResult TranslationLoader::loadQt(QTranslator &_translator, const QString &_lang) -{ - // An unset language is the default for English, which has no translation file to begin with. - if (_lang.isEmpty()) { - return TranslationLoadResult::Missing; - } - - const QString nameHint = QStringLiteral("qt_") + _lang; - const auto result = loadFrom(_translator, nameHint, qtTranslationPaths()); - - switch (result) { - case TranslationLoadResult::Loaded: - qCInfo(TranslationLoaderLog) << "Loaded Qt translation" << nameHint << "at" << _translator.filePath(); - break; - case TranslationLoadResult::Missing: - qCDebug(TranslationLoaderLog) - << "No Qt translation" << nameHint << "in" << QStringList(qtTranslationPaths()); - break; - case TranslationLoadResult::NoDirectory: - qCWarning(TranslationLoaderLog) - << "No Qt translations directory exists, looked in" << QStringList(qtTranslationPaths()); - break; - } - - return result; -} - -TranslationLoadResult TranslationLoader::loadApplication(QTranslator &_translator, - const QString &_prefix, - const QString &_lang, - const QList &_directories) -{ - if (_lang.isEmpty()) { - return TranslationLoadResult::Missing; - } - - const QString nameHint = _prefix + QLatin1Char('_') + _lang; - const auto result = loadFrom(_translator, nameHint, _directories); - - switch (result) { - case TranslationLoadResult::Loaded: - qCInfo(TranslationLoaderLog) << "Loaded" << _prefix << "translation" << nameHint << "at" - << _translator.filePath(); - break; - case TranslationLoadResult::Missing: - qCDebug(TranslationLoaderLog) - << "No" << _prefix << "translation" << nameHint << "in" << QStringList(_directories); - break; - case TranslationLoadResult::NoDirectory: - qCWarning(TranslationLoaderLog) - << "No translation directory exists, looked in" << QStringList(_directories); - break; - } - - return result; -} diff --git a/libcockatrice_utility/libcockatrice/utility/translation_loader.h b/libcockatrice_utility/libcockatrice/utility/translation_loader.h deleted file mode 100644 index 645489696..000000000 --- a/libcockatrice_utility/libcockatrice/utility/translation_loader.h +++ /dev/null @@ -1,92 +0,0 @@ -#ifndef TRANSLATION_LOADER_H -#define TRANSLATION_LOADER_H - -#include -#include -#include - -class QTranslator; - -Q_DECLARE_LOGGING_CATEGORY(TranslationLoaderLog) - -/** @brief The outcome of a translation load attempt. */ -enum class TranslationLoadResult -{ - Loaded, /**< A translation file was found and loaded into the translator. */ - Missing, /**< The search directories exist but hold no file for this language. */ - NoDirectory /**< Not a single search directory exists, so no lookup could be attempted. */ -}; - -namespace TranslationLoader -{ -/** - * @brief Every directory that may hold Qt's own translations, in order of preference. - * - * More than one entry is needed because a relocated Qt (Flatpak, AppImage, a bundled SDK) can report - * a prefix-relative translations path that does not exist: the KDE runtime reports /usr/translations - * while its qt_*.qm files live in /usr/share/qt6/translations. - */ -QList qtTranslationCandidates(); - -/** - * @brief The candidates that actually exist, most likely first, so no caller searches a path that is - * not there. - */ -QList qtTranslationPaths(); - -/** - * @brief The directories to search for an application's own translation files, in order of preference. - * - * @param _installedPath The location the platform installs the translation files in, derived from the - * executable's path. - * @return The installed location, followed by the executable's own directory: a build tree keeps the - * .qm files next to the binary instead of in the installed layout, so a developer build would - * otherwise find no translations at all. - */ -QList applicationTranslationPaths(const QString &_installedPath); - -/** - * @brief Loads the first _nameHint + language translation file found in _directories into _translator. - * - * QTranslator discards whatever it held before every load, so each translation file needs its own - * translator; loading a second file into the first one silently drops the first. - * - * @param _translator The translator to load into. - * @param _nameHint The translation file name without its language and suffix, e.g. "qt_de" or "cockatrice_de". - * @param _directories The directories to search, in order of preference. - * @return What the search found. - */ -TranslationLoadResult loadFrom(QTranslator &_translator, const QString &_nameHint, const QList &_directories); - -/** - * @brief Loads Qt's own translation for _lang, which covers the strings of Qt's own dialogs. - * - * @param _translator The translator to load into; must not also be used for an application translation. - * @param _lang The language code, e.g. "de". An empty language loads nothing. - * @return What the search found. - */ -TranslationLoadResult loadQt(QTranslator &_translator, const QString &_lang); - -/** - * @brief The language codes of the <_prefix>_.qm files in _directories, sorted by code. - * - * Lets a language selector offer exactly the languages that can be loaded. - */ -QList availableLanguages(const QString &_prefix, const QList &_directories); - -/** - * @brief Loads the <_prefix>_<_lang>.qm translation file from _directories into _translator. - * - * @param _translator The translator to load into; must not also be used for Qt's own translation. - * @param _prefix The translation file prefix, e.g. "cockatrice" or "oracle". - * @param _lang The language code, e.g. "de". An empty language loads nothing. - * @param _directories The directories to search, in order of preference. - * @return What the search found. - */ -TranslationLoadResult loadApplication(QTranslator &_translator, - const QString &_prefix, - const QString &_lang, - const QList &_directories); -} // namespace TranslationLoader - -#endif // TRANSLATION_LOADER_H diff --git a/oracle/CMakeLists.txt b/oracle/CMakeLists.txt index 0003b77ac..d8f30dcad 100644 --- a/oracle/CMakeLists.txt +++ b/oracle/CMakeLists.txt @@ -299,18 +299,6 @@ if(Qt6LinguistTools_FOUND) elseif(WIN32) install(FILES ${oracle_QM} DESTINATION ${ORACLE_WIN32_QM_INSTALL_DIR}) endif() - - # Qt attaches the .qm files to the default build target only, so building the executable on its own - # (CLion, --target oracle) leaves it without any translations: nothing is translated and the - # language selector in the wizard stays empty. - # Depend on Qt's own lrelease target instead of attaching the .qm custom commands to a second - # target: two independent targets owning the same outputs race on MSBuild/Makefiles and are an - # error on Xcode. Older Qt versions already add this dependency themselves. - foreach(_lrelease_target Oracle_lrelease oracle_lrelease) - if(TARGET ${_lrelease_target}) - add_dependencies(oracle ${_lrelease_target}) - endif() - endforeach() endif() qt6_finalize_target(oracle) diff --git a/oracle/src/main.cpp b/oracle/src/main.cpp index 103c72784..0f962f9be 100644 --- a/oracle/src/main.cpp +++ b/oracle/src/main.cpp @@ -7,10 +7,10 @@ #include #include #include +#include #include #include #include -#include QTranslator *translator, *qtTranslator; ThemeManager *themeManager; @@ -24,13 +24,24 @@ void installNewTranslator() { QString lang = SettingsCache::instance().personal().getLang(); - // Qt's own strings and ours need a translator each: QTranslator discards its previous contents - // on every load, so loading both files into one translator silently drops the first of them. - TranslationLoader::loadQt(*qtTranslator, lang); + QString qtNameHint = "qt_" + lang; + QString qtTranslationPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); + + bool qtTranslationLoaded = qtTranslator->load(qtNameHint, qtTranslationPath); + if (!qtTranslationLoaded) { + qDebug() << "Unable to load qt translation" << qtNameHint << "at" << qtTranslationPath; + } else { + qDebug() << "Loaded qt translation" << qtNameHint << "at" << qtTranslationPath; + } qApp->installTranslator(qtTranslator); - TranslationLoader::loadApplication(*translator, translationPrefix, lang, - TranslationLoader::applicationTranslationPaths(translationPath)); + QString appNameHint = translationPrefix + "_" + lang; + bool appTranslationLoaded = qtTranslator->load(appNameHint, translationPath); + if (!appTranslationLoaded) { + qDebug() << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath; + } else { + qDebug() << "Loaded" << translationPrefix << "translation" << appNameHint << "at" << translationPath; + } qApp->installTranslator(translator); } @@ -71,9 +82,8 @@ int main(int argc, char *argv[]) QIcon icon("theme:appicon.svg"); wizard.setWindowIcon(icon); - // Base name of the installed oracle.desktop; wayland reads the window icon from it and - // xdg-desktop-portal registers it as the app ID, so the case has to match the file name. - QGuiApplication::setDesktopFileName("oracle"); + // set name of the app desktop file; used by wayland to load the window icon + QGuiApplication::setDesktopFileName("Oracle"); wizard.show(); diff --git a/oracle/src/pages.cpp b/oracle/src/pages.cpp index 566c88520..df0c3f51d 100644 --- a/oracle/src/pages.cpp +++ b/oracle/src/pages.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #ifdef HAS_LZMA #include "lzma/decompress.h" @@ -213,18 +212,21 @@ void IntroPage::initializePage() QStringList IntroPage::findQmFiles() { - return TranslationLoader::availableLanguages(translationPrefix, - TranslationLoader::applicationTranslationPaths(translationPath)); + QDir dir(translationPath); + QStringList fileNames = dir.entryList(QStringList(translationPrefix + "_*.qm"), QDir::Files, QDir::Name); + fileNames.replaceInStrings(QRegularExpression(translationPrefix + "_(.*)\\.qm"), "\\1"); + return fileNames; } QString IntroPage::languageName(const QString &lang) { QTranslator qTranslator; - // Only the language name is needed here, so the file is loaded without logging a line per - // language: findQmFiles() asked for languages whose files exist in the first place. - TranslationLoader::loadFrom(qTranslator, translationPrefix + "_" + lang, - TranslationLoader::applicationTranslationPaths(translationPath)); + QString appNameHint = translationPrefix + "_" + lang; + bool appTranslationLoaded = qTranslator.load(appNameHint, translationPath); + if (!appTranslationLoaded) { + qDebug() << "Unable to load" << translationPrefix << "translation" << appNameHint << "at" << translationPath; + } return qTranslator.translate("i18n", DEFAULT_LANG_NAME); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b51d10560..5c12957fb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,10 +14,6 @@ add_test(NAME server_rate_limiter_test COMMAND server_rate_limiter_test) add_test(NAME server_developer_role_test COMMAND server_developer_role_test) add_test(NAME server_game_join_test COMMAND server_game_join_test) add_test(NAME warning_categories_test COMMAND warning_categories_test) -# The translation loader test compiles a .qm fixture, which needs lrelease from Qt Linguist. -if(TARGET Qt6::lrelease) - add_test(NAME translation_loader_test COMMAND translation_loader_test) -endif() add_test(NAME report_categories_test COMMAND report_categories_test) add_test(NAME lag_monitor_test COMMAND lag_monitor_test) add_test(NAME latency_tracker_test COMMAND latency_tracker_test) @@ -42,34 +38,6 @@ add_executable(server_rate_limiter_test server_rate_limiter_test.cpp) add_executable(server_developer_role_test server_developer_role_test.cpp) add_executable(server_game_join_test server_game_join_test.cpp) add_executable(warning_categories_test warning_categories_test.cpp) -if(TARGET Qt6::lrelease) - add_executable(translation_loader_test translation_loader_test.cpp) - - # QTranslator only accepts real .qm files, so the translation loader test gets one compiled from its - # own .ts fixture rather than a stub. - set(TRANSLATION_LOADER_TEST_QM "${CMAKE_CURRENT_BINARY_DIR}/translation_loader_test_de.qm") - add_custom_command( - OUTPUT "${TRANSLATION_LOADER_TEST_QM}" - COMMAND Qt6::lrelease "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts" -qm "${TRANSLATION_LOADER_TEST_QM}" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/translation_loader_test.ts" - COMMENT "Compiling translation_loader_test.ts" - ) - add_custom_target(translation_loader_test_qm DEPENDS "${TRANSLATION_LOADER_TEST_QM}") - add_dependencies(translation_loader_test translation_loader_test_qm) - target_compile_definitions(translation_loader_test PRIVATE TRANSLATION_LOADER_TEST_DIR="${CMAKE_CURRENT_BINARY_DIR}") - - # A multi-config generator (MSBuild, Xcode) puts the executable in a per-config subdirectory while - # the .qm stays in the current binary directory, so copy the fixture next to the binary: that is the - # layout ABuildTreeFindsTheTranslationsNextToTheExecutable is about, and the only one it finds on - # those generators. - add_custom_command( - TARGET translation_loader_test - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${TRANSLATION_LOADER_TEST_QM}" - "$/" - VERBATIM - ) -endif() add_executable(report_categories_test report_categories_test.cpp) add_executable(lag_monitor_test ${CMAKE_SOURCE_DIR}/cockatrice/src/client/lag_monitor.cpp lag_monitor_test.cpp) target_include_directories(lag_monitor_test PRIVATE ${CMAKE_SOURCE_DIR}/cockatrice/src) @@ -136,9 +104,6 @@ if(NOT GTEST_FOUND) add_dependencies(server_developer_role_test gtest) add_dependencies(server_game_join_test gtest) add_dependencies(warning_categories_test gtest) - if(TARGET translation_loader_test) - add_dependencies(translation_loader_test gtest) - endif() add_dependencies(report_categories_test gtest) add_dependencies(lag_monitor_test gtest) add_dependencies(latency_tracker_test gtest) @@ -187,11 +152,6 @@ target_link_libraries( target_link_libraries( warning_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) -if(TARGET translation_loader_test) - target_link_libraries( - translation_loader_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} - ) -endif() target_link_libraries( report_categories_test libcockatrice_utility Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} ) diff --git a/tests/settings/settings_migration_test.cpp b/tests/settings/settings_migration_test.cpp index 84b948d2e..4fa843abf 100644 --- a/tests/settings/settings_migration_test.cpp +++ b/tests/settings/settings_migration_test.cpp @@ -20,11 +20,7 @@ protected: 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); + settingsPath = tempDir.path() + "/"; // 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 @@ -68,25 +64,6 @@ protected: } 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) @@ -748,168 +725,6 @@ TEST_F(SettingsMigrationTest, RenameFailureWithExistingBackup) 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)); -} - } // namespace int main(int argc, char **argv) diff --git a/tests/translation_loader_test.cpp b/tests/translation_loader_test.cpp deleted file mode 100644 index d54001ad3..000000000 --- a/tests/translation_loader_test.cpp +++ /dev/null @@ -1,262 +0,0 @@ -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include -#include -#include - -// The directory holding translation_loader_test_de.qm, compiled from translation_loader_test.ts by the -// build. QTranslator rejects anything that is not a real .qm file, so a stub cannot stand in here. -#ifndef TRANSLATION_LOADER_TEST_DIR -#define TRANSLATION_LOADER_TEST_DIR "" -#endif - -namespace -{ -const QString testContext = QStringLiteral("TranslationLoaderTest"); -const QString firstMessage = QStringLiteral("Load into the first translator"); -const QString secondMessage = QStringLiteral("Load into the second translator"); -const QString germanFirstMessage = QStringLiteral("In den ersten Übersetzer laden"); -const QString germanSecondMessage = QStringLiteral("In den zweiten Übersetzer laden"); - -QString translate(const QTranslator &_translator, const QString &_sourceText) -{ - return _translator.translate(testContext.toUtf8().constData(), _sourceText.toUtf8().constData()); -} - -QString testTranslationDirectory() -{ - return QString(TRANSLATION_LOADER_TEST_DIR); -} - -// GTest's main() creates no QCoreApplication, which the search paths are derived from. -const QCoreApplication &application() -{ - static int argc = 1; - static char executable[] = "translation_loader_test"; - static char *argv[] = {executable, nullptr}; - static const QCoreApplication app(argc, argv); - return app; -} -} // namespace - -TEST(TranslationLoaderTest, LoadsFromTheFirstDirectoryHoldingTheFile) -{ - QTemporaryDir emptyDir; - ASSERT_TRUE(emptyDir.isValid()); - - QTranslator translator; - - // The first directory is searched but has no file, the second one has. - EXPECT_EQ(TranslationLoadResult::Loaded, - TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), - {emptyDir.path(), testTranslationDirectory()})); - EXPECT_EQ(germanFirstMessage, translate(translator, firstMessage)); -} - -TEST(TranslationLoaderTest, LoadedTranslatorResolvesTheTranslatedString) -{ - QTranslator translator; - - ASSERT_EQ(TranslationLoadResult::Loaded, - TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), - {testTranslationDirectory()})); - - EXPECT_EQ(germanFirstMessage, translate(translator, firstMessage)); - EXPECT_EQ(germanSecondMessage, translate(translator, secondMessage)); -} - -TEST(TranslationLoaderTest, SeparateTranslatorsKeepTheirOwnFiles) -{ - // QTranslator discards its previous contents on every load, so a caller reusing one translator - // for two files keeps only the last of them. That is why Qt's translation and the application - // translation are loaded into translators of their own. - QTranslator firstTranslator; - QTranslator secondTranslator; - - ASSERT_EQ(TranslationLoadResult::Loaded, - TranslationLoader::loadFrom(firstTranslator, QStringLiteral("translation_loader_test_de"), - {testTranslationDirectory()})); - ASSERT_EQ(TranslationLoadResult::Loaded, - TranslationLoader::loadFrom(secondTranslator, QStringLiteral("translation_loader_test_de"), - {testTranslationDirectory()})); - - EXPECT_FALSE(firstTranslator.isEmpty()); - EXPECT_FALSE(secondTranslator.isEmpty()); - EXPECT_EQ(germanFirstMessage, translate(firstTranslator, firstMessage)); - EXPECT_EQ(germanSecondMessage, translate(secondTranslator, secondMessage)); -} - -TEST(TranslationLoaderTest, ExistingDirectoryWithoutTheFileIsMissing) -{ - QTemporaryDir emptyDir; - ASSERT_TRUE(emptyDir.isValid()); - - QTranslator translator; - - EXPECT_EQ(TranslationLoadResult::Missing, - TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), {emptyDir.path()})); - EXPECT_TRUE(translator.isEmpty()); -} - -TEST(TranslationLoaderTest, NonexistentDirectoriesAreNotSearched) -{ - QTemporaryDir emptyDir; - ASSERT_TRUE(emptyDir.isValid()); - - QTranslator translator; - const QString missingDirectory = emptyDir.path() + QStringLiteral("/does-not-exist"); - - EXPECT_EQ( - TranslationLoadResult::NoDirectory, - TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), {missingDirectory})); - EXPECT_EQ(TranslationLoadResult::NoDirectory, - TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), {})); - EXPECT_TRUE(translator.isEmpty()); -} - -TEST(TranslationLoaderTest, EmptyDirectoryEntriesAreSkipped) -{ - QTranslator translator; - - EXPECT_EQ(TranslationLoadResult::Loaded, - TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de"), - {QString(), testTranslationDirectory()})); -} - -TEST(TranslationLoaderTest, LanguageVariantsFallBackToTheBaseLanguage) -{ - QTranslator translator; - - // Users pick languages such as en_US or zh-Hans that need not have a translation file of their - // own; a language without one falls back to the file without its country or script part. - EXPECT_EQ(TranslationLoadResult::Loaded, - TranslationLoader::loadFrom(translator, QStringLiteral("translation_loader_test_de_CH"), - {testTranslationDirectory()})); -} - -TEST(TranslationLoaderTest, QtTranslationCandidatesCoverTheUsualInstallLayouts) -{ - const QList candidates = TranslationLoader::qtTranslationCandidates(); - - EXPECT_FALSE(candidates.isEmpty()); - for (const QString &candidate : candidates) { - EXPECT_FALSE(candidate.isEmpty()); - } - EXPECT_EQ(candidates.size(), QSet(candidates.cbegin(), candidates.cend()).size()); - - // A relocated Qt can report a prefix-relative translations path that does not exist (the KDE - // runtime reports /usr/translations while its files live in /usr/share/qt6/translations), so the - // layout Qt actually installs into has to be among the candidates. - const QString prefix = QLibraryInfo::path(QLibraryInfo::PrefixPath); - EXPECT_TRUE(candidates.contains(prefix + QStringLiteral("/share/qt6/translations"))); -} - -TEST(TranslationLoaderTest, QtTranslationPathsOnlyHoldExistingDirectories) -{ - const QList candidates = TranslationLoader::qtTranslationCandidates(); - const QList paths = TranslationLoader::qtTranslationPaths(); - - for (const QString &path : paths) { - EXPECT_FALSE(path.isEmpty()); - EXPECT_TRUE(QDir(path).exists()) << path.toStdString(); - } - - EXPECT_EQ(paths.size(), QSet(paths.cbegin(), paths.cend()).size()); - - // Every candidate that exists is searched, and nothing else is. - for (const QString &candidate : candidates) { - if (QDir(candidate).exists()) { - EXPECT_TRUE(paths.contains(candidate)) << candidate.toStdString(); - } - } -} - -TEST(TranslationLoaderTest, LoadApplicationUsesThePrefixAndTheLanguage) -{ - QTranslator translator; - - ASSERT_EQ(TranslationLoadResult::Loaded, - TranslationLoader::loadApplication(translator, QStringLiteral("translation_loader_test"), - QStringLiteral("de"), {testTranslationDirectory()})); - EXPECT_EQ(germanSecondMessage, translate(translator, secondMessage)); -} - -TEST(TranslationLoaderTest, ABuildTreeFindsTheTranslationsNextToTheExecutable) -{ - // The regression this guards: a build tree keeps the .qm files next to the executable rather than - // in the installed layout, so a developer build loaded no translation and offered no language. - // This test binary's .qm file sits in its own directory, just like cockatrice's ones do. - const QString executableDirectory = application().applicationDirPath(); - const QList paths = TranslationLoader::applicationTranslationPaths( - executableDirectory + QStringLiteral("/../share/cockatrice/translations")); - - ASSERT_EQ(2, paths.size()); - EXPECT_EQ(executableDirectory + QStringLiteral("/../share/cockatrice/translations"), paths.at(0)); - EXPECT_EQ(executableDirectory, paths.at(1)); - - const QList languages = - TranslationLoader::availableLanguages(QStringLiteral("translation_loader_test"), paths); - EXPECT_EQ(QStringList({QStringLiteral("de")}), QStringList(languages.cbegin(), languages.cend())); -} - -TEST(TranslationLoaderTest, ApplicationTranslationPathsHoldNoDuplicates) -{ - const QString executableDirectory = application().applicationDirPath(); - - EXPECT_EQ(1, TranslationLoader::applicationTranslationPaths(executableDirectory).size()); - - // Without an installed layout to search there is nothing left but the executable's directory. - const QList withoutInstalledPath = TranslationLoader::applicationTranslationPaths(QString()); - ASSERT_EQ(1, withoutInstalledPath.size()); - EXPECT_EQ(executableDirectory, withoutInstalledPath.at(0)); -} - -TEST(TranslationLoaderTest, AvailableLanguagesListsTheTranslationsThatCanBeLoaded) -{ - const QList languages = - TranslationLoader::availableLanguages(QStringLiteral("translation_loader_test"), {testTranslationDirectory()}); - - EXPECT_EQ(QStringList({QStringLiteral("de")}), QStringList(languages.cbegin(), languages.cend())); - - // Files of another application and files without a language code are not languages. - EXPECT_TRUE(TranslationLoader::availableLanguages(QStringLiteral("some_other_app"), {testTranslationDirectory()}) - .isEmpty()); - EXPECT_TRUE(TranslationLoader::availableLanguages(QStringLiteral("translation_loader_test"), {}).isEmpty()); -} - -TEST(TranslationLoaderTest, LoadApplicationReportsAMissingTranslationDirectory) -{ - QTemporaryDir emptyDir; - ASSERT_TRUE(emptyDir.isValid()); - - QTranslator translator; - - EXPECT_EQ(TranslationLoadResult::NoDirectory, - TranslationLoader::loadApplication(translator, QStringLiteral("translation_loader_test"), - QStringLiteral("de"), - {emptyDir.path() + QStringLiteral("/translations")})); -} - -TEST(TranslationLoaderTest, UnsetLanguageLoadsNothing) -{ - QTranslator translator; - - // An unset language is the default for English, which has no translation file to begin with: - // nothing is searched for and the translator is left untouched. - EXPECT_EQ(TranslationLoadResult::Missing, - TranslationLoader::loadApplication(translator, QStringLiteral("translation_loader_test"), QString(), - {testTranslationDirectory()})); - EXPECT_EQ(TranslationLoadResult::Missing, TranslationLoader::loadQt(translator, QString())); - EXPECT_TRUE(translator.isEmpty()); - EXPECT_TRUE(translator.filePath().isEmpty()); -} - -int main(int argc, char **argv) -{ - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/tests/translation_loader_test.ts b/tests/translation_loader_test.ts deleted file mode 100644 index a02b2e753..000000000 --- a/tests/translation_loader_test.ts +++ /dev/null @@ -1,15 +0,0 @@ - - - - - TranslationLoaderTest - - Load into the first translator - In den ersten Übersetzer laden - - - Load into the second translator - In den zweiten Übersetzer laden - - -