mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-10-29 11:23:23 +00:00
test(locale): test that languages are properly defined (#4220)
This commit is contained in:
parent
14fc19ddbd
commit
2e8f1d4773
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@ -3,6 +3,9 @@ You need to prefix commands with `C:\msys64\msys2_shell.cmd -defterm -here -no-s
|
||||
|
||||
Prefix build directories with `cmake-build-`.
|
||||
|
||||
The test executable is named `test_sunshine`
|
||||
The test executable is named `test_sunshine` and will be located inside the `tests` directory within
|
||||
the build directory.
|
||||
|
||||
The project uses gtest as a test framework.
|
||||
|
||||
Always follow the style guidelines defined in .clang-format for c/c++ code.
|
||||
|
||||
@ -60,6 +60,7 @@ set(CONFIG_TEST_FILES
|
||||
"src_assets/common/assets/web/config.html"
|
||||
"docs/configuration.md"
|
||||
"src_assets/common/assets/web/public/assets/locale/en.json"
|
||||
"src_assets/common/assets/web/configs/tabs/General.vue"
|
||||
)
|
||||
|
||||
foreach(file ${CONFIG_TEST_FILES})
|
||||
@ -70,10 +71,25 @@ foreach(file ${CONFIG_TEST_FILES})
|
||||
)
|
||||
endforeach()
|
||||
|
||||
# Copy all locale files for locale consistency tests
|
||||
# Use a custom command to properly handle both adding and removing files
|
||||
set(LOCALE_SRC_DIR "${CMAKE_SOURCE_DIR}/src_assets/common/assets/web/public/assets/locale")
|
||||
set(LOCALE_DST_DIR "${CMAKE_CURRENT_BINARY_DIR}/src_assets/common/assets/web/public/assets/locale")
|
||||
add_custom_target(sync_locale_files ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${LOCALE_DST_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${LOCALE_DST_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${LOCALE_SRC_DIR}" "${LOCALE_DST_DIR}"
|
||||
COMMENT "Synchronizing locale files for tests"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
foreach(dep ${SUNSHINE_TARGET_DEPENDENCIES})
|
||||
add_dependencies(${PROJECT_NAME} ${dep}) # compile these before sunshine
|
||||
endforeach()
|
||||
|
||||
# Ensure locale files are synchronized before building the test executable
|
||||
add_dependencies(${PROJECT_NAME} sync_locale_files)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 23)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
${SUNSHINE_EXTERNAL_LIBRARIES}
|
||||
|
||||
357
tests/integration/test_locale_consistency.cpp
Normal file
357
tests/integration/test_locale_consistency.cpp
Normal file
@ -0,0 +1,357 @@
|
||||
/**
|
||||
* @file tests/integration/test_locale_consistency.cpp
|
||||
* @brief Test locale consistency across configuration files and locale JSON files
|
||||
*/
|
||||
#include "../tests_common.h"
|
||||
|
||||
// standard includes
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// lib includes
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// local includes
|
||||
#include "src/file_handler.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class LocaleConsistencyTest: public ::testing::Test {
|
||||
protected:
|
||||
// Extract locale options from config.cpp
|
||||
static std::set<std::string, std::less<>> extractConfigCppLocales() {
|
||||
std::set<std::string, std::less<>> locales;
|
||||
const std::string content = file_handler::read_file("src/config.cpp");
|
||||
|
||||
// Find the string_restricted_f call for locale
|
||||
const std::regex localeSection(R"(string_restricted_f\s*\(\s*vars\s*,\s*"locale"[^}]*\{([^}]*)\})");
|
||||
|
||||
if (std::smatch match; std::regex_search(content, match, localeSection)) {
|
||||
const std::string localeList = match[1].str();
|
||||
|
||||
// Extract individual locale codes
|
||||
const std::regex localePattern(R"delimiter("([^"]+)"sv)delimiter");
|
||||
std::sregex_iterator iter(localeList.begin(), localeList.end(), localePattern);
|
||||
|
||||
for (const std::sregex_iterator end; iter != end; ++iter) {
|
||||
locales.insert((*iter)[1].str());
|
||||
}
|
||||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
// Extract locale options from General.vue
|
||||
static std::map<std::string, std::string, std::less<>> extractGeneralVueLocales() {
|
||||
std::map<std::string, std::string, std::less<>> locales;
|
||||
const std::string content = file_handler::read_file("src_assets/common/assets/web/configs/tabs/General.vue");
|
||||
|
||||
// Find the locale select section specifically
|
||||
const std::regex localeSelectPattern("id=\"locale\"[^>]*>([^<]*(?:<option[^>]*>[^<]*</option>[^<]*)*)</select>");
|
||||
|
||||
if (std::smatch selectMatch; std::regex_search(content, selectMatch, localeSelectPattern)) {
|
||||
const std::string localeSection = selectMatch[1].str();
|
||||
|
||||
// Extract option elements with locale codes and display names from the locale section
|
||||
const std::regex optionPattern(R"delimiter(<option\s+value="([^"]+)">([^<]+)</option>)delimiter");
|
||||
std::sregex_iterator iter(localeSection.begin(), localeSection.end(), optionPattern);
|
||||
|
||||
for (const std::sregex_iterator end; iter != end; ++iter) {
|
||||
const std::string localeCode = (*iter)[1].str();
|
||||
const std::string displayName = (*iter)[2].str();
|
||||
locales[localeCode] = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
// Get available locale JSON files
|
||||
static std::set<std::string, std::less<>> getAvailableLocaleFiles() {
|
||||
std::set<std::string, std::less<>> locales;
|
||||
const std::filesystem::path localeDir = "src_assets/common/assets/web/public/assets/locale";
|
||||
|
||||
if (!fs::exists(localeDir)) {
|
||||
return locales;
|
||||
}
|
||||
|
||||
for (const auto &entry : fs::directory_iterator(localeDir)) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".json") {
|
||||
const std::string filename = entry.path().stem().string();
|
||||
locales.insert(filename);
|
||||
}
|
||||
}
|
||||
|
||||
return locales;
|
||||
}
|
||||
|
||||
// Helper function to check if a locale JSON file is valid using nlohmann/json
|
||||
static bool isValidLocaleFile(const std::string &localeCode) {
|
||||
const std::string filePath = std::format("src_assets/common/assets/web/public/assets/locale/{}.json", localeCode);
|
||||
|
||||
if (!fs::exists(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const std::string content = file_handler::read_file(filePath.c_str());
|
||||
|
||||
// Parse JSON using nlohmann/json to validate it's properly formatted
|
||||
const nlohmann::json localeJson = nlohmann::json::parse(content);
|
||||
|
||||
// Basic validation - should be a JSON object with some content
|
||||
return localeJson.is_object() && !localeJson.empty();
|
||||
} catch (const nlohmann::json::parse_error &) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(LocaleConsistencyTest, AllLocaleFilesHaveConfigCppEntries) {
|
||||
const auto configLocales = extractConfigCppLocales();
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
|
||||
std::vector<std::string> missingFromConfig;
|
||||
|
||||
// Check that every locale file has a corresponding entry in config.cpp
|
||||
for (const auto &localeFile : localeFiles) {
|
||||
if (!configLocales.contains(localeFile)) {
|
||||
missingFromConfig.push_back(localeFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingFromConfig.empty()) {
|
||||
std::string errorMsg = "Locale files missing from config.cpp:\n";
|
||||
for (const auto &missing : missingFromConfig) {
|
||||
errorMsg += std::format(" {}.json\n", missing);
|
||||
}
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, AllLocaleFilesHaveGeneralVueEntries) {
|
||||
const auto vueLocales = extractGeneralVueLocales();
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
|
||||
std::vector<std::string> missingFromVue;
|
||||
|
||||
// Check that every locale file has a corresponding entry in General.vue
|
||||
for (const auto &localeFile : localeFiles) {
|
||||
if (!vueLocales.contains(localeFile)) {
|
||||
missingFromVue.push_back(localeFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingFromVue.empty()) {
|
||||
std::string errorMsg = "Locale files missing from General.vue:\n";
|
||||
for (const auto &missing : missingFromVue) {
|
||||
errorMsg += std::format(" {}.json\n", missing);
|
||||
}
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, AllConfigCppLocalesHaveFiles) {
|
||||
const auto configLocales = extractConfigCppLocales();
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
|
||||
std::vector<std::string> missingFiles;
|
||||
|
||||
// Check that every config.cpp locale has a corresponding JSON file
|
||||
for (const auto &configLocale : configLocales) {
|
||||
if (!localeFiles.contains(configLocale)) {
|
||||
missingFiles.push_back(configLocale);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingFiles.empty()) {
|
||||
std::string errorMsg = "config.cpp locales missing JSON files:\n";
|
||||
for (const auto &missing : missingFiles) {
|
||||
errorMsg += std::format(" {}.json\n", missing);
|
||||
}
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, AllGeneralVueLocalesHaveFiles) {
|
||||
const auto vueLocales = extractGeneralVueLocales();
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
|
||||
std::vector<std::string> missingFiles;
|
||||
|
||||
// Check that every General.vue locale has a corresponding JSON file
|
||||
for (const auto &vueLocale : vueLocales | std::views::keys) {
|
||||
if (!localeFiles.contains(vueLocale)) {
|
||||
missingFiles.push_back(vueLocale);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingFiles.empty()) {
|
||||
std::string errorMsg = "General.vue locales missing JSON files:\n";
|
||||
for (const auto &missing : missingFiles) {
|
||||
errorMsg += std::format(" {}.json\n", missing);
|
||||
}
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, ConfigCppAndGeneralVueLocalesMatch) {
|
||||
const auto configLocales = extractConfigCppLocales();
|
||||
const auto vueLocales = extractGeneralVueLocales();
|
||||
|
||||
std::vector<std::string> configOnlyLocales;
|
||||
std::vector<std::string> vueOnlyLocales;
|
||||
|
||||
// Find locales in config.cpp but not in General.vue
|
||||
for (const auto &configLocale : configLocales) {
|
||||
if (!vueLocales.contains(configLocale)) {
|
||||
configOnlyLocales.push_back(configLocale);
|
||||
}
|
||||
}
|
||||
|
||||
// Find locales in General.vue but not in config.cpp
|
||||
for (const auto &vueLocale : vueLocales | std::views::keys) {
|
||||
if (!configLocales.contains(vueLocale)) {
|
||||
vueOnlyLocales.push_back(vueLocale);
|
||||
}
|
||||
}
|
||||
|
||||
std::string errorMsg;
|
||||
|
||||
if (!configOnlyLocales.empty()) {
|
||||
errorMsg += "Locales in config.cpp but not in General.vue:\n";
|
||||
for (const auto &locale : configOnlyLocales) {
|
||||
errorMsg += std::format(" {}\n", locale);
|
||||
}
|
||||
}
|
||||
|
||||
if (!vueOnlyLocales.empty()) {
|
||||
errorMsg += "Locales in General.vue but not in config.cpp:\n";
|
||||
for (const auto &locale : vueOnlyLocales) {
|
||||
errorMsg += std::format(" {}\n", locale);
|
||||
}
|
||||
}
|
||||
|
||||
if (!errorMsg.empty()) {
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, AllLocaleFilesAreValid) {
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
std::vector<std::string> invalidFiles;
|
||||
|
||||
// Check that all locale files are valid JSON
|
||||
for (const auto &localeFile : localeFiles) {
|
||||
if (!isValidLocaleFile(localeFile)) {
|
||||
invalidFiles.push_back(localeFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (!invalidFiles.empty()) {
|
||||
std::string errorMsg = "Invalid locale files found:\n";
|
||||
for (const auto &invalid : invalidFiles) {
|
||||
errorMsg += std::format(" {}.json\n", invalid);
|
||||
}
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, LocaleDisplayNamesAreConsistent) {
|
||||
const auto vueLocales = extractGeneralVueLocales();
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
std::vector<std::string> inconsistentDisplayNames;
|
||||
|
||||
// Check that all locales in General.vue have corresponding JSON files
|
||||
for (const auto &[localeCode, displayName] : vueLocales) {
|
||||
if (!localeFiles.contains(localeCode)) {
|
||||
inconsistentDisplayNames.push_back(
|
||||
std::format("{}: has display name '{}' but no corresponding JSON file exists", localeCode, displayName)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check that locale files that exist have entries in General.vue
|
||||
for (const auto &localeFile : localeFiles) {
|
||||
if (!vueLocales.contains(localeFile)) {
|
||||
inconsistentDisplayNames.push_back(
|
||||
std::format("{}: has JSON file but no display name in General.vue", localeFile)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!inconsistentDisplayNames.empty()) {
|
||||
std::string errorMsg = "Locale display name inconsistencies found:\n";
|
||||
for (const auto &inconsistent : inconsistentDisplayNames) {
|
||||
errorMsg += std::format(" {}\n", inconsistent);
|
||||
}
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, NoOrphanedLocaleReferences) {
|
||||
const auto configLocales = extractConfigCppLocales();
|
||||
const auto vueLocales = extractGeneralVueLocales();
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
|
||||
std::vector<std::string> orphanedReferences;
|
||||
|
||||
// Check for locale references that don't have corresponding files
|
||||
for (const auto &configLocale : configLocales) {
|
||||
if (!localeFiles.contains(configLocale)) {
|
||||
orphanedReferences.push_back(std::format("config.cpp references missing file: {}.json", configLocale));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &vueLocale : vueLocales | std::views::keys) {
|
||||
if (!localeFiles.contains(vueLocale)) {
|
||||
orphanedReferences.push_back(std::format("General.vue references missing file: {}.json", vueLocale));
|
||||
}
|
||||
}
|
||||
|
||||
if (!orphanedReferences.empty()) {
|
||||
std::string errorMsg = "Orphaned locale references found:\n";
|
||||
for (const auto &orphaned : orphanedReferences) {
|
||||
errorMsg += std::format(" {}\n", orphaned);
|
||||
}
|
||||
FAIL() << errorMsg;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(LocaleConsistencyTest, TestFrameworkDetectsLocaleInconsistencies) {
|
||||
// Test the framework by simulating a missing locale scenario
|
||||
const std::string testLocale = "test_framework_validation_locale";
|
||||
|
||||
auto configLocales = extractConfigCppLocales();
|
||||
auto vueLocales = extractGeneralVueLocales();
|
||||
const auto localeFiles = getAvailableLocaleFiles();
|
||||
|
||||
// Add a fake locale to config to simulate a missing file
|
||||
configLocales.insert(testLocale);
|
||||
|
||||
std::vector<std::string> missingFiles;
|
||||
for (const auto &configLocale : configLocales) {
|
||||
if (!localeFiles.contains(configLocale)) {
|
||||
missingFiles.push_back(configLocale);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the test framework detects the missing fake locale
|
||||
bool foundMissingTestLocale = false;
|
||||
for (const auto &missing : missingFiles) {
|
||||
if (missing == testLocale) {
|
||||
foundMissingTestLocale = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(foundMissingTestLocale) << "Test framework failed to detect missing locale file";
|
||||
EXPECT_GE(missingFiles.size(), 1) << "Test framework should detect at least the fake missing locale";
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user