Files
msime-windows/engine/tests/src/test_temporary_input_session.cpp
T
Jia Sui 4fde941291 fix(engine): 测试的 main 再补一层 catch (...)
只接 std::exception 的话,第三方库或跨 FFI 边界抛出来的东西照样会逃出 main 走 abort,
和上一个 commit 修掉的是同一类故障:ctest 看不到原因,栈也不展开。引擎内现有的抛出确实
都是 std::exception 派生,但这一层缺的是「无论什么异常都到不了 terminate」的保证,补上它
才让 main 的边界真正闭合。

只加 catch 分支,输出通道沿用各文件原来那一个(fprintf / cerr / fmt::println),
不改任何断言。
2026-09-30 21:34:12 +08:00

264 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "../../core/data_path.h"
#include "../../core/input_session.h"
#include "test_directory_cleanup.h"
#include <sqlite3.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <cstdio>
namespace
{
class Database
{
public:
explicit Database(const std::filesystem::path &path)
{
if (sqlite3_open(metasequoia::path_to_utf8(path).c_str(), &database_) != SQLITE_OK)
{
throw std::runtime_error("Failed to create a temporary-mode test dictionary.");
}
}
~Database()
{
sqlite3_close(database_);
}
void execute(const char *sql)
{
char *error = nullptr;
if (sqlite3_exec(database_, sql, nullptr, nullptr, &error) != SQLITE_OK)
{
const std::string message = error == nullptr ? "SQLite operation failed." : error;
sqlite3_free(error);
throw std::runtime_error(message);
}
}
private:
sqlite3 *database_ = nullptr;
};
void require(bool condition, const char *message)
{
if (!condition)
{
throw std::runtime_error(message);
}
}
bool contains(const std::vector<WordItem> &candidates, const std::string &word)
{
return std::any_of(candidates.begin(), candidates.end(),
[&](const WordItem &candidate) { return candidate.word == word; });
}
void type(metasequoia::InputSession &session, const std::string &text)
{
for (const char character : text)
{
require(session.handle_character(character).handled, "A temporary-mode test character was not handled.");
}
}
} // namespace
int run_test()
{
const auto suffix = std::to_string(std::chrono::high_resolution_clock::now().time_since_epoch().count());
const auto data_directory = std::filesystem::temp_directory_path() / ("metasequoia-temporary-mode-" + suffix);
metasequoia::test::ScopedDataDirectoryCleanup cleanup(data_directory);
std::filesystem::create_directories(data_directory);
#ifdef _WIN32
if (_wputenv_s(L"METASEQUOIA_IME_DATA_DIR", data_directory.c_str()) != 0)
#else
if (setenv("METASEQUOIA_IME_DATA_DIR", metasequoia::path_to_utf8(data_directory).c_str(), 1) != 0)
#endif
{
throw std::runtime_error("Failed to set the temporary-mode test data directory.");
}
{
Database main_database(data_directory / "msime.db");
Database english_database(data_directory / "english.db");
english_database.execute("CREATE TABLE english_words(word TEXT COLLATE BINARY NOT NULL,display TEXT NOT NULL,"
"weight INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(word,display)) WITHOUT ROWID;"
"CREATE TABLE en_zh_glosses(english TEXT PRIMARY KEY,chinese_gloss TEXT NOT NULL);"
"CREATE TABLE zh_en_glosses(chinese TEXT PRIMARY KEY,english_gloss TEXT NOT NULL);"
"INSERT INTO english_words VALUES('he','HE',110);"
"INSERT INTO english_words VALUES('hello','Hello',100);"
"INSERT INTO english_words VALUES('help','Help',90);");
}
metasequoia::InputSession english(SchemeType::Quanpin);
require(english.handle_character('Y', true).handled &&
english.local_input_mode() == metasequoia::LocalInputMode::TemporaryEnglish &&
english.preedit() == "Y" && english.candidates().empty(),
"Shift+Y did not enter an empty temporary English composition.");
const auto bare_english_punctuation = english.handle_punctuation(',');
require(bare_english_punctuation.handled && bare_english_punctuation.commit == "," &&
english.local_input_mode() == metasequoia::LocalInputMode::None && !english.has_composition(),
"Engine punctuation committed or retained the bare temporary-English display prefix.");
require(english.handle_character('Y', true).handled,
"Temporary English could not restart after engine punctuation.");
type(english, "he");
require(english.preedit() == "Yhe" && english.candidates().size() == 3 && english.candidates()[0].word == "he" &&
english.candidates()[0].source == CandidateSource::Generated &&
english.candidates()[1].word == "Hello" && english.candidates()[2].word == "Help" &&
std::none_of(english.candidates().begin() + 1, english.candidates().end(),
[](const WordItem &candidate) { return candidate.word == "HE"; }),
"Temporary English did not put raw input before deduplicated completions.");
const auto english_commit = english.select_candidate(2);
require(english_commit.handled && english_commit.commit == "Help" &&
english.local_input_mode() == metasequoia::LocalInputMode::None &&
english.scheme() == SchemeType::Quanpin,
"Temporary English selection did not commit and return to Quanpin.");
require(english.handle_character('Y', true).handled &&
english.handle_command(metasequoia::Command::Backspace).handled &&
english.local_input_mode() == metasequoia::LocalInputMode::None,
"Backspace on a bare Y prefix did not leave temporary English mode.");
require(english.handle_character('Y', true).handled, "Temporary English could not start for a bare-prefix commit.");
const auto bare_english_commit = english.handle_command(metasequoia::Command::CommitCandidate);
require(bare_english_commit.handled && !bare_english_commit.commit.has_value() &&
english.local_input_mode() == metasequoia::LocalInputMode::None,
"A bare temporary-English display prefix escaped into committed text.");
require(english.handle_character('Y', true).handled, "Temporary English could not be re-entered.");
type(english, "he");
require(english.handle_command(metasequoia::Command::Cancel).handled &&
english.local_input_mode() == metasequoia::LocalInputMode::None,
"Cancel did not leave temporary English mode.");
require(english.handle_character('Y', true).handled, "Temporary English could not start before a scheme switch.");
english.switch_scheme(SchemeType::Shuangpin);
require(english.scheme() == SchemeType::Shuangpin &&
english.local_input_mode() == metasequoia::LocalInputMode::None && !english.has_composition(),
"A scheme switch did not clear temporary English mode.");
metasequoia::InputSession english_enter(SchemeType::Quanpin);
require(english_enter.handle_character('Y', true).handled,
"Temporary English could not start for raw Enter commit.");
type(english_enter, "he");
const auto english_raw = english_enter.handle_command(metasequoia::Command::CommitRaw);
require(english_raw.handled && english_raw.commit == "he" &&
english_enter.local_input_mode() == metasequoia::LocalInputMode::None,
"Enter committed the display-only Y prefix with temporary English raw input.");
metasequoia::InputSession english_frequency(SchemeType::Quanpin);
require(english_frequency.set_frequency_adjustment({metasequoia::FrequencyAdjustmentMode::Promote, 1, 1}),
"A valid temporary-English frequency configuration was rejected.");
require(english_frequency.handle_character('Y', true).handled,
"Temporary English could not start for frequency learning.");
type(english_frequency, "he");
require(english_frequency.select_candidate(std::string("Help")).commit == "Help",
"Temporary English could not select a completion for frequency learning.");
metasequoia::InputSession english_reopened(SchemeType::Quanpin);
require(english_reopened.handle_character('Y', true).handled,
"Temporary English could not reopen after frequency learning.");
type(english_reopened, "he");
require(english_reopened.candidates().size() == 3 && english_reopened.candidates()[0].word == "he" &&
english_reopened.candidates()[1].word == "Help" && english_reopened.candidates()[2].word == "Hello",
"Temporary English frequency learning included the fixed raw candidate in ranking.");
metasequoia::InputSession japanese(SchemeType::Quanpin);
require(japanese.handle_character('R', true).handled &&
japanese.local_input_mode() == metasequoia::LocalInputMode::TemporaryJapanese &&
japanese.preedit() == "R" && japanese.scheme() == SchemeType::Quanpin &&
japanese.scheme_type() == SchemeType::Quanpin,
"Shift+R did not enter a temporary Japanese session with a visible prefix.");
const auto bare_japanese_punctuation = japanese.handle_punctuation(',');
require(bare_japanese_punctuation.handled && bare_japanese_punctuation.commit == "," &&
japanese.local_input_mode() == metasequoia::LocalInputMode::None &&
japanese.scheme() == SchemeType::Quanpin && japanese.scheme_type() == SchemeType::Quanpin,
"Engine punctuation committed the bare R prefix or failed to restore Quanpin.");
require(japanese.handle_character('R', true).handled,
"Temporary Japanese could not restart after engine punctuation.");
type(japanese, "ka");
require(japanese.preedit() == "Rka" && contains(japanese.candidates(), "か"),
"Temporary Japanese did not reuse Romaji conversion candidates.");
const auto japanese_commit = japanese.select_candidate(std::string("か"));
require(japanese_commit.handled && japanese_commit.commit == "か" &&
japanese.local_input_mode() == metasequoia::LocalInputMode::None &&
japanese.scheme() == SchemeType::Quanpin,
"Temporary Japanese selection did not restore the original Chinese scheme.");
require(japanese.handle_character('R', true).handled &&
japanese.handle_command(metasequoia::Command::Backspace).handled &&
japanese.local_input_mode() == metasequoia::LocalInputMode::None &&
japanese.scheme() == SchemeType::Quanpin,
"Backspace on a bare R prefix did not restore Quanpin.");
require(japanese.handle_character('R', true).handled,
"Temporary Japanese could not start for a bare-prefix commit.");
const auto bare_japanese_commit = japanese.handle_command(metasequoia::Command::CommitCandidate);
require(bare_japanese_commit.handled && !bare_japanese_commit.commit.has_value() &&
japanese.local_input_mode() == metasequoia::LocalInputMode::None &&
japanese.scheme() == SchemeType::Quanpin,
"A bare temporary-Japanese display prefix escaped into committed text.");
require(japanese.handle_character('R', true).handled, "Temporary Japanese could not start for a flush.");
const auto empty_flush = japanese.finish_composition();
require(empty_flush.handled && !empty_flush.commit.has_value() && !japanese.has_composition(),
"Flushing a bare local-mode prefix produced a spurious client insertion.");
require(japanese.handle_character('R', true).handled, "Temporary Japanese could not be re-entered.");
type(japanese, "ka");
require(japanese.handle_command(metasequoia::Command::Cancel).handled && japanese.scheme() == SchemeType::Quanpin &&
japanese.local_input_mode() == metasequoia::LocalInputMode::None,
"Cancel did not restore the original scheme from temporary Japanese.");
require(japanese.handle_character('R', true).handled, "Temporary Japanese could not start before a scheme switch.");
japanese.switch_scheme(SchemeType::Shuangpin);
require(japanese.scheme() == SchemeType::Shuangpin &&
japanese.local_input_mode() == metasequoia::LocalInputMode::None && !japanese.has_composition(),
"An explicit scheme switch did not supersede temporary Japanese restoration.");
metasequoia::InputSession japanese_enter(SchemeType::Quanpin);
require(japanese_enter.handle_character('R', true).handled,
"Temporary Japanese could not start for raw Enter commit.");
type(japanese_enter, "ka");
const auto japanese_raw = japanese_enter.handle_command(metasequoia::Command::CommitRaw);
require(japanese_raw.handled && japanese_raw.commit == "ka" &&
japanese_enter.local_input_mode() == metasequoia::LocalInputMode::None &&
japanese_enter.scheme() == SchemeType::Quanpin,
"Enter committed the display-only R prefix or failed to restore Quanpin.");
metasequoia::InputSession shuangpin_japanese(SchemeType::Shuangpin);
require(shuangpin_japanese.handle_character('R', true).handled, "Temporary Japanese did not start from Shuangpin.");
type(shuangpin_japanese, "ka");
require(shuangpin_japanese.select_candidate(std::string("か")).commit == "か" &&
shuangpin_japanese.scheme() == SchemeType::Shuangpin,
"Temporary Japanese did not return to the original Shuangpin scheme.");
metasequoia::LocalModeOptions disabled_options;
disabled_options.temporary_english = false;
disabled_options.temporary_japanese = false;
metasequoia::InputSession disabled(SchemeType::Quanpin);
disabled.set_local_mode_options(disabled_options);
require(!disabled.handle_character('Y', true).handled && !disabled.has_composition() &&
disabled.local_input_mode() == metasequoia::LocalInputMode::None,
"Disabled temporary English swallowed Shift+Y.");
require(!disabled.handle_character('R', true).handled && !disabled.has_composition() &&
disabled.local_input_mode() == metasequoia::LocalInputMode::None,
"Disabled temporary Japanese swallowed Shift+R.");
return 0;
}
int main()
{
try
{
return run_test();
}
catch (const std::exception &error)
{
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
catch (...)
{
std::fprintf(stderr, "An exception escaped the test body.\n");
return 1;
}
}