From b6ae42492b06950d583b0d5bdcdefaa892137aa4 Mon Sep 17 00:00:00 2001 From: Stanislav Motylkov Date: Sun, 1 Sep 2024 15:22:29 +0300 Subject: [PATCH] [INTEROP_UNITTEST] Add interoperability tests for localized strings (#7129) Finally, an automated way to verify all these localized strings that must be in sync between multiple modules. CORE-18893 --- modules/rostests/unittests/CMakeLists.txt | 1 + .../rostests/unittests/interop/CMakeLists.txt | 16 + .../unittests/interop/LocaleTests.cpp | 369 ++++++++++++++++++ modules/rostests/unittests/interop/interop.h | 12 + modules/rostests/unittests/interop/testlist.c | 18 + 5 files changed, 416 insertions(+) create mode 100644 modules/rostests/unittests/interop/CMakeLists.txt create mode 100644 modules/rostests/unittests/interop/LocaleTests.cpp create mode 100644 modules/rostests/unittests/interop/interop.h create mode 100644 modules/rostests/unittests/interop/testlist.c diff --git a/modules/rostests/unittests/CMakeLists.txt b/modules/rostests/unittests/CMakeLists.txt index 65ab7eb4c20..cdf63636002 100644 --- a/modules/rostests/unittests/CMakeLists.txt +++ b/modules/rostests/unittests/CMakeLists.txt @@ -1,4 +1,5 @@ +add_subdirectory(interop) if(ISAPNP_ENABLE) add_subdirectory(isapnp) endif() diff --git a/modules/rostests/unittests/interop/CMakeLists.txt b/modules/rostests/unittests/interop/CMakeLists.txt new file mode 100644 index 00000000000..68eece5682c --- /dev/null +++ b/modules/rostests/unittests/interop/CMakeLists.txt @@ -0,0 +1,16 @@ + +PROJECT(interop_unittest) + +include_directories(../../apitests/include) + +list(APPEND SOURCE + LocaleTests.cpp + testlist.c) + +add_executable(interop_unittest ${SOURCE}) + +target_link_libraries(interop_unittest cppstl) +set_module_type(interop_unittest win32cui) +target_compile_definitions(interop_unittest PRIVATE UNICODE _UNICODE _STLP_NO_EXCEPTIONS) +add_importlibs(interop_unittest shell32 user32 msvcrt kernel32) +add_rostests_file(TARGET interop_unittest) diff --git a/modules/rostests/unittests/interop/LocaleTests.cpp b/modules/rostests/unittests/interop/LocaleTests.cpp new file mode 100644 index 00000000000..ab2dd665dad --- /dev/null +++ b/modules/rostests/unittests/interop/LocaleTests.cpp @@ -0,0 +1,369 @@ +/* + * PROJECT: ReactOS interoperability tests + * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) + * PURPOSE: Formal locale verification tests + * COPYRIGHT: Copyright 2024 Stanislav Motylkov + * Copyright 2024 Katayama Hirofumi MZ + */ + +#include "interop.h" + +#include +#include +#include + +#include +#include + +enum E_MODULE +{ + shell32, + userenv, + syssetup, + mmsys, + explorer_old, +}; + +enum E_STRING +{ + SH32_PROGRAMS, + SH32_STARTUP, + SH32_STARTMENU, + SH32_PROGRAM_FILES, + SH32_PROGRAM_FILES_COMMON, + SH32_ADMINTOOLS, + UENV_STARTMENU, + UENV_PROGRAMS, + UENV_STARTUP, + SYSS_PROGRAMFILES, + SYSS_COMMONFILES, + MMSY_STARTMENU, + EOLD_PROGRAMS, +}; + +typedef struct PART_TEST +{ + E_MODULE eModule; + UINT id; + SIZE_T nParts; + SIZE_T gotParts; +} PART_TEST; + +typedef struct PART +{ + E_STRING Num; + UINT Idx; +} PART; + +typedef struct PART_MATCH +{ + PART p1, p2; +} PART_MATCH; + +DWORD dwVersion; +LCID curLcid = 0; +std::set langs; +std::map mod; +std::map parts; + +struct PART_PAIR +{ + E_STRING eString; + PART_TEST part_test; +}; + +static void InitParts(void) +{ + static const PART_PAIR s_pairs[] = + { + // { eString, { eModule, id, nParts } } + { SH32_PROGRAMS, { shell32, 45 /* IDS_PROGRAMS "Start Menu\Programs" */, 2 } }, + { SH32_STARTUP, { shell32, 48 /* IDS_STARTUP "Start Menu\Programs\StartUp" */, 3 } }, + { SH32_STARTMENU, { shell32, 51 /* IDS_STARTMENU "Start Menu" */, 1 } }, + { SH32_PROGRAM_FILES, { shell32, 63 /* IDS_PROGRAM_FILES "Program Files" */, 1 } }, + { SH32_PROGRAM_FILES_COMMON, { shell32, 65 /* IDS_PROGRAM_FILES_COMMON "Program Files\Common Files" */, 2 } }, + { SH32_ADMINTOOLS, { shell32, 67 /* IDS_ADMINTOOLS "Start Menu\Programs\Administrative Tools" */, 3 } }, + { UENV_STARTMENU, { userenv, 11 /* IDS_STARTMENU "Start Menu" */, 1 } }, + { UENV_PROGRAMS, { userenv, 12 /* IDS_PROGRAMS "Start Menu\Programs" */, 2 } }, + { UENV_STARTUP, { userenv, 13 /* IDS_STARTUP "Start Menu\Programs\StartUp" */, 3 } }, + { SYSS_PROGRAMFILES, { syssetup, 3600 /* IDS_PROGRAMFILES "%SystemDrive%\Program Files" */, 2 } }, + { SYSS_COMMONFILES, { syssetup, 3601 /* IDS_COMMONFILES "Common Files" */, 1 } }, + { MMSY_STARTMENU, { mmsys, 5851 /* IDS_STARTMENU "Start Menu" */, 1 } }, + { EOLD_PROGRAMS, { explorer_old, 10 /* IDS_PROGRAMS "Programs" */, 1 } }, + }; + for (auto& pair : s_pairs) + { + parts.insert(std::make_pair(pair.eString, pair.part_test)); + } +} + +static PART_MATCH PartMatches[] = +{ + // Start Menu + { { SH32_PROGRAMS, 0 }, { SH32_STARTUP, 0 } }, + { { SH32_PROGRAMS, 0 }, { SH32_STARTMENU, 0 } }, + { { SH32_PROGRAMS, 0 }, { SH32_ADMINTOOLS, 0 } }, + { { SH32_PROGRAMS, 0 }, { UENV_STARTMENU, 0 } }, + { { SH32_PROGRAMS, 0 }, { UENV_PROGRAMS, 0 } }, + { { SH32_PROGRAMS, 0 }, { UENV_STARTUP, 0 } }, + { { SH32_PROGRAMS, 0 }, { MMSY_STARTMENU, 0 } }, + // Programs + { { SH32_PROGRAMS, 1 }, { SH32_STARTUP, 1 } }, + { { SH32_PROGRAMS, 1 }, { SH32_ADMINTOOLS, 1 } }, + { { SH32_PROGRAMS, 1 }, { UENV_PROGRAMS, 1 } }, + { { SH32_PROGRAMS, 1 }, { UENV_STARTUP, 1 } }, + { { SH32_PROGRAMS, 1 }, { EOLD_PROGRAMS, 0 } }, + // StartUp + { { SH32_STARTUP, 2 }, { UENV_STARTUP, 2 } }, + // Program Files + { { SH32_PROGRAM_FILES, 0 }, { SH32_PROGRAM_FILES_COMMON, 0 } }, + { { SH32_PROGRAM_FILES, 0 }, { SYSS_PROGRAMFILES, 1 } }, + // Common Files + { { SH32_PROGRAM_FILES_COMMON, 1 }, { SYSS_COMMONFILES, 0 } }, +}; + +static int GetLocalisedText(_In_opt_ HINSTANCE hInstance, _In_ UINT uID, _Out_ LPWSTR lpBuffer, _In_ int cchBufferMax) +{ + HRSRC hRes = FindResourceExW(hInstance, (LPWSTR)RT_STRING, + MAKEINTRESOURCEW((uID >> 4) + 1), curLcid); + + if (!hRes) + hRes = FindResourceExW(hInstance, (LPWSTR)RT_STRING, + MAKEINTRESOURCEW((uID >> 4) + 1), + MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_DEFAULT), SORT_DEFAULT)); + + if (!hRes) + return 0; + + HGLOBAL hMem = LoadResource(hInstance, hRes); + if (!hMem) + return 0; + + PWCHAR p = (PWCHAR)LockResource(hMem); + for (UINT i = 0; i < (uID & 0x0F); i++) p += *p + 1; + + int len = (*p > cchBufferMax ? cchBufferMax : *p); + memcpy(lpBuffer, p + 1, len * sizeof(WCHAR)); + lpBuffer[len] = UNICODE_NULL; + return len; +} + +static int LoadStringWrapW(_In_opt_ HINSTANCE hInstance, _In_ UINT uID, _Out_ LPWSTR lpBuffer, _In_ int cchBufferMax) +{ + if (dwVersion < _WIN32_WINNT_WS03) + // Windows XP or lower: SetThreadLocale doesn't select user interface language + return GetLocalisedText(hInstance, uID, lpBuffer, cchBufferMax); + else + return LoadStringW(hInstance, uID, lpBuffer, cchBufferMax); +} + +static DWORD CountParts(_In_ LPWSTR str) +{ + DWORD count = 0; + LPWSTR ptr = str; + + if (*ptr == UNICODE_NULL) + return 0; + + while ((ptr = wcschr(ptr, L'\\'))) + { + count++; + ptr++; + } + + return count + 1; +} + +static LPWSTR GetPart(_In_ LPWSTR str, _In_ SIZE_T num, _Out_ SIZE_T* len) +{ + DWORD count = 0; + LPWSTR ptr = str, next; + + while (count < num && (ptr = wcschr(ptr, L'\\')) != NULL) + { + count++; + ptr++; + } + + if (!ptr) + ptr = str; + + next = wcschr(ptr, L'\\'); + *len = next ? next - ptr : wcslen(ptr); + return ptr; +} + +static BOOL CALLBACK find_locale_id_callback( + _In_ HMODULE hModule, _In_ LPCWSTR type, _In_ LPCWSTR name, _In_ LANGID lang, _In_ LPARAM lParam) +{ + langs.insert(lang); + return TRUE; +} + +static void SetLocale(_In_ LCID lcid) +{ + SetThreadLocale(lcid); + SetThreadUILanguage(lcid); + curLcid = lcid; +} + +static void TEST_NumParts(void) +{ + for (auto& p : parts) + { + E_MODULE m = p.second.eModule; + + if (!mod[m]) + { + skip("No module for test %d\n", p.first); + continue; + } + + WCHAR szBuffer[MAX_PATH]; + + LoadStringWrapW(mod[m], p.second.id, szBuffer, _countof(szBuffer)); + p.second.gotParts = CountParts(szBuffer); + + ok(p.second.nParts == p.second.gotParts, "Locale 0x%lX: Num parts mismatch %d - expected %lu, got %lu\n", + curLcid, p.first, p.second.nParts, p.second.gotParts); + } +} + +static BOOL LoadPart(_In_ PART* p, _Out_ LPWSTR str, _In_ SIZE_T size) +{ + auto s = parts[p->Num]; + E_MODULE m = s.eModule; + + if (!mod[m]) + { + SetLastError(ERROR_FILE_NOT_FOUND); + return FALSE; + } + + if (s.nParts != s.gotParts) + { + SetLastError(ERROR_INVALID_DATA); + return FALSE; + } + + WCHAR szBuffer[MAX_PATH]; + LPWSTR szPart; + SIZE_T len; + + LoadStringWrapW(mod[m], s.id, szBuffer, _countof(szBuffer)); + szPart = GetPart(szBuffer, p->Idx, &len); + StringCchCopyNW(str, size, szPart, len); + + return TRUE; +} + +static void TEST_PartMatches(void) +{ + for (auto& match : PartMatches) + { + WCHAR szP1[MAX_PATH], szP2[MAX_PATH]; + + if (!LoadPart(&match.p1, szP1, _countof(szP1))) + { + skip("%s for match test %d (pair 1)\n", GetLastError() == ERROR_FILE_NOT_FOUND + ? "No module" : "Invalid data", match.p1.Num); + continue; + } + + if (!LoadPart(&match.p2, szP2, _countof(szP2))) + { + skip("%s for match test %d (pair 2)\n", GetLastError() == ERROR_FILE_NOT_FOUND + ? "No module" : "Invalid data", match.p2.Num); + continue; + } + + ok(wcscmp(szP1, szP2) == 0, "Locale 0x%lX: Mismatching pairs %u:%u / %u:%u '%S' vs. '%S'\n", + curLcid, match.p1.Num, match.p1.Idx, match.p2.Num, match.p2.Idx, szP1, szP2); + } +} + +static void TEST_LocaleTests(void) +{ + // Initialization + InitParts(); + + OSVERSIONINFOEXW osvi; + memset(&osvi, 0, sizeof(osvi)); + osvi.dwOSVersionInfoSize = sizeof(osvi); + + GetVersionExW((LPOSVERSIONINFOW)&osvi); + dwVersion = (osvi.dwMajorVersion << 8) | osvi.dwMinorVersion; + + WCHAR szOldDir[MAX_PATH], szBuffer[MAX_PATH]; + GetCurrentDirectoryW(_countof(szOldDir), szOldDir); + + std::map lib; +#define ADD_LIB(eModule, pszPath) lib.insert(std::make_pair(eModule, pszPath)) + + GetModuleFileNameW(NULL, szBuffer, _countof(szBuffer)); + LPCWSTR pszFind = StrStrW(szBuffer, L"modules\\rostests\\unittests"); + if (pszFind) + { + // We're running in ReactOS output folder + WCHAR szNewDir[MAX_PATH]; + + StringCchCopyNW(szNewDir, _countof(szNewDir), szBuffer, pszFind - szBuffer); + SetCurrentDirectoryW(szNewDir); + + ADD_LIB(shell32, L"dll\\win32\\shell32\\shell32.dll"); + ADD_LIB(userenv, L"dll\\win32\\userenv\\userenv.dll"); + ADD_LIB(syssetup, L"dll\\win32\\syssetup\\syssetup.dll"); + ADD_LIB(mmsys, L"dll\\cpl\\mmsys\\mmsys.cpl"); + ADD_LIB(explorer_old, L"modules\\rosapps\\applications\\explorer-old\\explorer_old.exe"); + } + else + { + ADD_LIB(shell32, L"shell32.dll"); + ADD_LIB(userenv, L"userenv.dll"); + ADD_LIB(syssetup, L"syssetup.dll"); + ADD_LIB(mmsys, L"mmsys.cpl"); + ADD_LIB(explorer_old, L"explorer_old.exe"); + } +#undef ADD_LIB + + for (auto& lb : lib) + { + E_MODULE m = lb.first; + + mod[m] = LoadLibraryExW(lib[m], NULL, LOAD_LIBRARY_AS_DATAFILE); + if (!mod[m]) + { + trace("Failed to load '%S', error %lu\n", lib[m], GetLastError()); + continue; + } + + EnumResourceLanguagesW(mod[m], (LPCWSTR)RT_STRING, (LPCWSTR)LOCALE_ILANGUAGE, + find_locale_id_callback, NULL); + } + + // Actual tests + for (auto& lang : langs) + { + SetLocale(MAKELCID(lang, SORT_DEFAULT)); + + TEST_NumParts(); + TEST_PartMatches(); + } + + // Perform cleanup + for (auto& m : mod) + { + if (!m.second) + continue; + + FreeLibrary(m.second); + m.second = NULL; + } + + SetCurrentDirectoryW(szOldDir); +} + +START_TEST(LocaleTests) +{ + TEST_LocaleTests(); +} diff --git a/modules/rostests/unittests/interop/interop.h b/modules/rostests/unittests/interop/interop.h new file mode 100644 index 00000000000..077234af306 --- /dev/null +++ b/modules/rostests/unittests/interop/interop.h @@ -0,0 +1,12 @@ +/* + * PROJECT: ReactOS interoperability tests + * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) + * PURPOSE: Main header file + * COPYRIGHT: Copyright 2024 Stanislav Motylkov + */ + +#pragma once + +#include +#define WIN32_NO_STATUS +#include diff --git a/modules/rostests/unittests/interop/testlist.c b/modules/rostests/unittests/interop/testlist.c new file mode 100644 index 00000000000..c098a49a6f9 --- /dev/null +++ b/modules/rostests/unittests/interop/testlist.c @@ -0,0 +1,18 @@ +/* + * PROJECT: ReactOS interoperability tests + * LICENSE: GPL-2.0-or-later (https://spdx.org/licenses/GPL-2.0-or-later) + * PURPOSE: List of tests + * COPYRIGHT: Copyright 2024 Stanislav Motylkov + */ + +#define STANDALONE +#include + +extern void func_LocaleTests(void); + +const struct test winetest_testlist[] = +{ + { "LocaleTests", func_LocaleTests }, + + { 0, 0 } +};