Add tests

This commit is contained in:
fanlusky committed 2026-06-20 22:11:00 +08:00
1 parent 6d606580e0
commit fca15d898d
12 files changed
+427 -80

No files matched your search

+4 -3
View File
@@ -1,9 +1,10 @@
build/
Testing/
log.*
# /.clangd
# /CMakeLists.txt
# /CMakePresets.json
/tests/CMakeLists.txt
/tests/CMakePresets.json
# /tests/CMakeLists.txt
# /tests/CMakePresets.json
/.vs
build-release/
build-release/
+6
View File
@@ -5,6 +5,8 @@ set(PROJECT_NAME "MetasequoiaImeServer")
cmake_policy(SET CMP0167 NEW)
project(${PROJECT_NAME} VERSION 1.0)
include(CTest)
# Boost
set(Boost_ROOT "C:/Users/sonnycalcr/scoop/apps/boost/current")
set (Boost_USE_STATIC_LIBS ON)
@@ -146,3 +148,7 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
$<$<CONFIG:Release>:${WEBVIEW2_VCPKG_ROOT}/bin/WebView2Loader.dll>
$<TARGET_FILE_DIR:${PROJECT_NAME}>
)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
+2 -1
View File
@@ -1,4 +1,5 @@
build/
Testing/
.vs/
out/
*.db
*.db
+61
View File
@@ -0,0 +1,61 @@
set(TEST_EXECUTABLE_NAME "MetasequoiaImeServerTests")
set(TEST_SOURCE_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_shuangpin_scheme.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_quanpin_scheme.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_shuangpin_query.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_engine_shuangpin_session.cpp"
"${CMAKE_SOURCE_DIR}/src/config/ime_config.cpp"
"${CMAKE_SOURCE_DIR}/src/session/engine_input_session.cpp"
"${CMAKE_SOURCE_DIR}/src/utils/common_utils.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/core/ime_session.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/common/string_utils.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/providers/provider_registry.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/providers/pinyin_candidate_provider.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/quanpin/engine.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/quanpin/quanpin_dictionary.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/schemes/shuangpin_scheme.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/schemes/quanpin_scheme.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/quanpin/quanpin_query.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/shuangpin/engine.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/shuangpin/shuangpin_dictionary.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/shuangpin/shuangpin_query.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/shuangpin/shuangpin_utils.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/dictbuilder.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/dictlist.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/dicttrie.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/lpicache.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/matrixsearch.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/mystdlib.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/ngram.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/pinyinime.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/searchutility.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/spellingtable.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/spellingtrie.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/splparser.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/sync.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/userdict.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/utf16char.cpp"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/googlepinyinime-rev/src/share/utf16reader.cpp"
)
add_executable(${TEST_EXECUTABLE_NAME} ${TEST_SOURCE_FILES})
target_include_directories(${TEST_EXECUTABLE_NAME}
PRIVATE
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/src"
"${CMAKE_SOURCE_DIR}/MetasequoiaImeEngine/utfcpp/source"
)
target_link_libraries(${TEST_EXECUTABLE_NAME}
PRIVATE
fmt::fmt
spdlog::spdlog
Boost::locale
Boost::json
unofficial::sqlite3::sqlite3
)
add_test(NAME ${TEST_EXECUTABLE_NAME} COMMAND ${TEST_EXECUTABLE_NAME})
+90 -2
View File
@@ -1,3 +1,91 @@
# About
# Tests
Test.
This project now includes a lightweight regression test target wired into the main CMake build.
## What It Covers
Current tests focus on high-value IME behaviors that are easy to accidentally break during refactors:
- `shuangpin` scheme request construction
- `quanpin` scheme request construction
- case-preserving shuangpin segmentation
- full-helpcode detection
- `engine + shuangpin` continuation behavior
- cloud suggestion trigger timing for shuangpin
The goal is not to fully simulate the whole IME UI pipeline yet. The goal is to protect the core input rules and
session behavior.
## Build The Tests
From the repository root:
```powershell
cmake --build build --config Debug --target MetasequoiaImeServerTests
```
## Run The Tests
Recommended:
```powershell
ctest --test-dir build -C Debug --output-on-failure
```
You can also run from inside the build directory:
```powershell
cd build
ctest -C Debug --output-on-failure
```
## Common Pitfall
If you run `ctest` from the source root directly, you may see:
```text
No tests were found!!!
```
That usually means `ctest` is looking at the source directory instead of the build directory.
Use one of these instead:
```powershell
ctest --test-dir build -C Debug --output-on-failure
```
or
```powershell
cd build
ctest -C Debug --output-on-failure
```
## Test Layout
- `tests/src/main.cpp`
- tiny custom test runner
- `tests/includes/test_framework.h`
- lightweight assertion helpers
- `tests/src/test_shuangpin_scheme.cpp`
- shuangpin scheme unit tests
- `tests/src/test_quanpin_scheme.cpp`
- quanpin scheme unit tests
- `tests/src/test_shuangpin_query.cpp`
- query/helper function tests
- `tests/src/test_engine_shuangpin_session.cpp`
- session-level behavior tests
## Recommended Practice
When you change input behavior, add a regression test if the change affects:
- input segmentation
- space/enter/backspace behavior
- helpcode handling
- continuation after candidate selection
- create-word flow
- cloud suggestion timing
The best tests in this project are the ones that capture a concrete input example that previously broke.
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <functional>
#include <stdexcept>
#include <string>
#include <vector>
namespace test
{
struct TestCase
{
std::string name;
std::function<void()> fn;
};
std::vector<TestCase> &registry();
struct Registrar
{
Registrar(const char *name, std::function<void()> fn);
};
}
#define TEST_CASE(name) \
static void name(); \
static test::Registrar name##_registrar(#name, name); \
static void name()
#define REQUIRE(expr) \
do \
{ \
if (!(expr)) \
{ \
throw std::runtime_error("Requirement failed: " #expr); \
} \
} while (false)
#define REQUIRE_EQ(lhs, rhs) \
do \
{ \
const auto &_lhs = (lhs); \
const auto &_rhs = (rhs); \
if (!(_lhs == _rhs)) \
{ \
throw std::runtime_error("Requirement failed: " #lhs " == " #rhs); \
} \
} while (false)
+34 -52
View File
@@ -1,67 +1,49 @@
#include <exception>
#include <fmt/core.h>
#include <iostream>
#include <sqlite3.h>
#include "tests/includes/test_framework.h"
int main(int argc, char *argv[])
namespace test
{
std::cout << "This is a code template project for Windows cpp!" << '\n';
fmt::print("Hello World from fmt!!\n");
std::vector<TestCase> &registry()
{
static std::vector<TestCase> tests;
return tests;
}
sqlite3 *db = nullptr;
char *errMsg = nullptr;
Registrar::Registrar(const char *name, std::function<void()> fn)
{
registry().push_back({name, std::move(fn)});
}
} // namespace test
// Create or open database
if (sqlite3_open("test.db", &db) != SQLITE_OK)
int main()
{
int failures = 0;
for (const auto &test_case : test::registry())
{
std::cerr << "Cannot open database: " << sqlite3_errmsg(db) << std::endl;
return 1;
}
// Create table
const char *createTableSQL = "CREATE TABLE IF NOT EXISTS users ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL,"
"age INTEGER);";
if (sqlite3_exec(db, createTableSQL, nullptr, nullptr, &errMsg) != SQLITE_OK)
{
std::cerr << "Failed to create table: " << errMsg << std::endl;
sqlite3_free(errMsg);
sqlite3_close(db);
return 1;
}
// Insert data
const char *insertSQL = "INSERT INTO users (name, age) VALUES ('Alice', 25);";
if (sqlite3_exec(db, insertSQL, nullptr, nullptr, &errMsg) != SQLITE_OK)
{
std::cerr << "Failed to insert data: " << errMsg << std::endl;
sqlite3_free(errMsg);
sqlite3_close(db);
return 1;
}
// Query data
const char *querySQL = "SELECT id, name, age FROM users;";
auto callback = [](void *, int argc, char **argv, char **colName) -> int {
for (int i = 0; i < argc; ++i)
try
{
std::cout << colName[i] << ": " << (argv[i] ? argv[i] : "NULL") << " | ";
test_case.fn();
fmt::print("[PASS] {}\n", test_case.name);
}
std::cout << "\n";
return 0;
};
catch (const std::exception &ex)
{
++failures;
fmt::print(stderr, "[FAIL] {}: {}\n", test_case.name, ex.what());
}
catch (...)
{
++failures;
fmt::print(stderr, "[FAIL] {}: unknown exception\n", test_case.name);
}
}
if (sqlite3_exec(db, querySQL, callback, nullptr, &errMsg) != SQLITE_OK)
if (failures > 0)
{
std::cerr << "Failed to query data: " << errMsg << std::endl;
sqlite3_free(errMsg);
sqlite3_close(db);
fmt::print(stderr, "{} test(s) failed\n", failures);
return 1;
}
// Close database
sqlite3_close(db);
std::cout << "All done.\n";
fmt::print("{} test(s) passed\n", test::registry().size());
return 0;
}
@@ -0,0 +1,69 @@
#include "tests/includes/test_framework.h"
#include "src/session/engine_input_session.h"
namespace
{
void InputLetters(EngineInputSession &session, const std::string &keys)
{
for (const char ch : keys)
{
const bool is_upper = ch >= 'A' && ch <= 'Z';
const char upper = is_upper ? ch : static_cast<char>(ch - ('a' - 'A'));
session.handle_key(static_cast<UINT>(upper), 0, static_cast<WCHAR>(ch));
}
}
}
TEST_CASE(EngineShuangpinSessionContinuesCompositionWithoutHelpcode)
{
EngineInputSession session(SchemeType::Shuangpin);
InputLetters(session, "xitele");
const auto transition = session.advance_composition_after_selection("xi", "西");
REQUIRE(transition.continues_composition);
REQUIRE_EQ(session.get_pinyin_sequence(), std::string("tele"));
}
TEST_CASE(EngineShuangpinSessionContinuesCompositionWithSingleHelpcode)
{
EngineInputSession session(SchemeType::Shuangpin);
InputLetters(session, "xitelea");
const auto transition = session.advance_composition_after_selection("xi", "西");
REQUIRE(transition.continues_composition);
REQUIRE_EQ(session.get_pinyin_sequence(), std::string("tele"));
}
TEST_CASE(EngineShuangpinSessionContinuesCompositionWithDoubleHelpcode)
{
EngineInputSession session(SchemeType::Shuangpin);
InputLetters(session, "xiteleaA");
const auto transition = session.advance_composition_after_selection("xi", "西");
REQUIRE(transition.continues_composition);
REQUIRE_EQ(session.get_pinyin_sequence(), std::string("tele"));
}
TEST_CASE(EngineShuangpinSessionCloudQueryMatchesLegacyTiming)
{
EngineInputSession session(SchemeType::Shuangpin);
InputLetters(session, "xi");
auto state = session.get_cloud_query_state();
REQUIRE(state.should_query);
session.reset_state();
InputLetters(session, "xia");
state = session.get_cloud_query_state();
REQUIRE(!state.should_query);
session.reset_state();
InputLetters(session, "xiA");
state = session.get_cloud_query_state();
REQUIRE(!state.should_query);
session.reset_state();
InputLetters(session, "xI");
state = session.get_cloud_query_state();
REQUIRE(!state.should_query);
}
+36
View File
@@ -0,0 +1,36 @@
#include "tests/includes/test_framework.h"
#include "MetasequoiaImeEngine/schemes/quanpin_scheme.h"
namespace
{
void InputKey(QuanpinScheme &scheme, UINT vk, WCHAR wch, UINT modifiers_down = 0)
{
scheme.handle_key(vk, modifiers_down, wch);
}
}
TEST_CASE(QuanpinSchemeSpaceDoesNotResetComposition)
{
QuanpinScheme scheme;
InputKey(scheme, 'N', L'n');
InputKey(scheme, 'I', L'i');
InputKey(scheme, VK_SPACE, L' ');
const QueryRequest request = scheme.build_request();
REQUIRE(request.valid);
REQUIRE_EQ(request.raw_input, std::string("ni"));
REQUIRE_EQ(request.normalized_input, std::string("ni"));
}
TEST_CASE(QuanpinSchemeApostropheIsPreservedInRawInput)
{
QuanpinScheme scheme;
InputKey(scheme, 'X', L'x');
InputKey(scheme, VK_OEM_7, L'\'');
InputKey(scheme, 'I', L'i');
const QueryRequest request = scheme.build_request();
REQUIRE(request.valid);
REQUIRE_EQ(request.raw_input, std::string("x'i"));
REQUIRE_EQ(request.normalized_input, std::string("xi"));
}
-22
View File
@@ -1,22 +0,0 @@
#include <fmt/core.h>
#include <sqlite3.h>
#include "fmt/base.h"
#include "MetasequoiaImeEngine/shuangpin/shuangpin_dictionary.h"
#include <chrono>
int main(int argc, char *argv[])
{
DictionaryUlPb dict;
std::vector<DictionaryUlPb::WordItem> result = dict.generate("ceui", "ce'ui");
std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
result = dict.generate("name", "na'me");
std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
fmt::println("Time: {} us", duration.count());
fmt::println("Word count: {}", result.size());
for (const auto &[pinyin, word, weight] : result)
{
fmt::println("Word: {}", word);
}
return 0;
}
+17
View File
@@ -0,0 +1,17 @@
#include "tests/includes/test_framework.h"
#include "MetasequoiaImeEngine/shuangpin/shuangpin_query.h"
#include "MetasequoiaImeEngine/shuangpin/shuangpin_utils.h"
TEST_CASE(ApplySegmentationCasesPreservesUppercaseMarkers)
{
const std::string segmented = "xi'te'le'aa";
const std::string cased = "xiteleaA";
REQUIRE_EQ(shuangpin::apply_segmentation_cases(segmented, cased), std::string("xi'te'le'aA"));
}
TEST_CASE(IsFullHelpModeRecognizesDoubleHelpcodePattern)
{
REQUIRE(ShuangpinUtil::IsFullHelpMode("xiteleaA"));
REQUIRE(!ShuangpinUtil::IsFullHelpMode("xitelea"));
REQUIRE(!ShuangpinUtil::IsFullHelpMode("xi"));
}
@@ -0,0 +1,61 @@
#include "tests/includes/test_framework.h"
#include "MetasequoiaImeEngine/schemes/shuangpin_scheme.h"
namespace
{
void InputKey(ShuangpinScheme &scheme, UINT vk, WCHAR wch, UINT modifiers_down = 0)
{
scheme.handle_key(vk, modifiers_down, wch);
}
}
TEST_CASE(ShuangpinSchemeBuildRequestPreservesCaseAndNormalizesQuery)
{
ShuangpinScheme scheme;
InputKey(scheme, 'X', L'X', 1);
InputKey(scheme, 'I', L'i');
const QueryRequest request = scheme.build_request();
REQUIRE(request.valid);
REQUIRE_EQ(request.raw_input, std::string("xi"));
REQUIRE_EQ(request.raw_input_with_cases, std::string("Xi"));
REQUIRE_EQ(request.raw_segmentation, std::string("Xi"));
REQUIRE_EQ(request.normalized_input, std::string("xi"));
}
TEST_CASE(ShuangpinSchemeSpaceDoesNotResetComposition)
{
ShuangpinScheme scheme;
InputKey(scheme, 'X', L'x');
InputKey(scheme, 'I', L'i');
InputKey(scheme, VK_SPACE, L' ');
const QueryRequest request = scheme.build_request();
REQUIRE(request.valid);
REQUIRE_EQ(request.raw_input, std::string("xi"));
}
TEST_CASE(ShuangpinSchemeEnterResetsComposition)
{
ShuangpinScheme scheme;
InputKey(scheme, 'X', L'x');
InputKey(scheme, 'I', L'i');
InputKey(scheme, VK_RETURN, 0);
const QueryRequest request = scheme.build_request();
REQUIRE(!request.valid);
REQUIRE_EQ(request.raw_input, std::string(""));
}
TEST_CASE(ShuangpinSchemeBackspaceRemovesLastInput)
{
ShuangpinScheme scheme;
InputKey(scheme, 'X', L'x');
InputKey(scheme, 'I', L'i');
InputKey(scheme, VK_BACK, 0);
const QueryRequest request = scheme.build_request();
REQUIRE(request.valid);
REQUIRE_EQ(request.raw_input, std::string("x"));
REQUIRE_EQ(request.raw_input_with_cases, std::string("x"));
}