`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是 product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次 搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、 miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。 Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、 macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。 词库仍按 product-lock.json 在构建时取发布产物。 引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游 commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。 verify-contracts / verify-checkout 随之移除,verify-published 保留。 CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除 列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job; engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily 改为 monthly。 README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录 文档同步说明新的位置与边界。
225 lines
9.0 KiB
C++
225 lines
9.0 KiB
C++
#include <metasequoia/dictionary_state.h>
|
|
#include <metasequoia/session.h>
|
|
#include "../../core/data_path.h"
|
|
#include "../../contracts/assets/assets.h"
|
|
#include "../../english/english_dictionary.h"
|
|
#include "../../user_dictionary/user_dictionary_journal.h"
|
|
#include "test_directory_cleanup.h"
|
|
#include <sqlite3.h>
|
|
#include <chrono>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
|
|
using namespace metasequoia;
|
|
namespace
|
|
{
|
|
void check(bool value, const char *message)
|
|
{
|
|
if (!value)
|
|
throw std::runtime_error(message);
|
|
}
|
|
void sql(const std::filesystem::path &path, const char *query)
|
|
{
|
|
sqlite3 *db = nullptr;
|
|
check(sqlite3_open(path_to_utf8(path).c_str(), &db) == SQLITE_OK, "open fixture");
|
|
int status = sqlite3_exec(db, query, nullptr, nullptr, nullptr);
|
|
sqlite3_close(db);
|
|
check(status == SQLITE_OK, "fixture SQL");
|
|
}
|
|
RuntimePaths stage(const std::filesystem::path &resources, const std::filesystem::path &root,
|
|
const std::vector<DictionaryStateRecord> &records)
|
|
{
|
|
std::size_t i = 0;
|
|
return stage_dictionary_state(resources, root, "fixture", [&](DictionaryStateRecord &out) {
|
|
if (i == records.size())
|
|
return false;
|
|
out = records[i++];
|
|
return true;
|
|
});
|
|
}
|
|
void run(bool capacity)
|
|
{
|
|
const auto root =
|
|
std::filesystem::temp_directory_path() /
|
|
("dictionary-state-" + std::to_string(std::chrono::high_resolution_clock::now().time_since_epoch().count()));
|
|
test::ScopedDataDirectoryCleanup cleanup(root);
|
|
const auto resources = root / "resources";
|
|
std::filesystem::create_directories(resources);
|
|
sql(resources / assets::main_dictionary,
|
|
"CREATE TABLE tbl_2_n(key TEXT,jp TEXT,value TEXT,weight INTEGER);"
|
|
"INSERT INTO tbl_2_n VALUES('ni''hao','nh','你好',100),('ni''hao','nh','拟好',80);"
|
|
"CREATE TABLE wubi86(key TEXT,value TEXT,weight INTEGER);"
|
|
"CREATE TABLE quick_parases(key TEXT,value TEXT,weight INTEGER);"
|
|
"CREATE INDEX idx_quick_parases_key_weight ON quick_parases(key,weight DESC);");
|
|
check(EnglishDictionary::ensure_schema(path_to_utf8(resources / assets::english_dictionary)), "English schema");
|
|
if (capacity)
|
|
{
|
|
std::size_t count = 0;
|
|
const auto staged =
|
|
stage_dictionary_state(resources, root / "capacity", "fixture", [&](DictionaryStateRecord &out) {
|
|
if (count == 100000)
|
|
return false;
|
|
const auto suffix = std::to_string(++count);
|
|
out = DictionaryStateEntry{
|
|
PersonalDictionaryKind::QuickPhrase, "bulk" + suffix, "合成短语" + suffix, 100, "", false, true};
|
|
return true;
|
|
});
|
|
std::size_t received = 0;
|
|
stream_dictionary_state(staged, [&](const auto &record) {
|
|
const auto *entry = std::get_if<DictionaryStateEntry>(&record);
|
|
check(entry && entry->user_inserted && entry->weight == 100, "capacity state damaged");
|
|
++received;
|
|
return true;
|
|
});
|
|
check(received == 100000, "capacity state truncated");
|
|
return;
|
|
}
|
|
std::vector<DictionaryStateRecord> records{
|
|
DictionaryStateEntry{PersonalDictionaryKind::Pinyin, "ni'hao", "你好", 0, "", true, true},
|
|
DictionaryStateEntry{PersonalDictionaryKind::Pinyin, "ni'hao", "拟好", 200, "", false, false},
|
|
DictionaryStateEntry{PersonalDictionaryKind::Pinyin, "ni'hao", "拟蒿", 150, "", false, true},
|
|
DictionaryStateEntry{PersonalDictionaryKind::QuickPhrase, "fixture", "合成短语", 100, "", false, true},
|
|
DictionaryStateEntry{PersonalDictionaryKind::English, "cloudfixture", "Cloudfixture", 100, "Cloudfixture",
|
|
false, true},
|
|
DictionaryStatePosition{"ni'hao", "ni'hao", "拟蒿", 1},
|
|
DictionaryStateSelection{"ni'hao", "ni'hao", "拟好", 1}};
|
|
const auto first = stage(resources, root / "first", records);
|
|
sql(first.user(assets::user_journal), "PRAGMA journal_mode=WAL");
|
|
bool wrote = false;
|
|
std::size_t snapshot_count = 0;
|
|
stream_dictionary_state(first, [&](const auto &) {
|
|
++snapshot_count;
|
|
if (!wrote)
|
|
{
|
|
wrote = true;
|
|
check(user_dictionary::set_fixed_position(path_to_utf8(first.user(assets::user_journal)), "ni'hao",
|
|
"ni'hao", "拟好", 2),
|
|
"concurrent writer failed");
|
|
}
|
|
return true;
|
|
});
|
|
check(snapshot_count == records.size(), "export mixed concurrent journal versions");
|
|
check(user_dictionary::clear_fixed_position(path_to_utf8(first.user(assets::user_journal)), "ni'hao", "ni'hao",
|
|
"拟好"),
|
|
"clear concurrent fixture");
|
|
std::vector<DictionaryStateRecord> exported;
|
|
stream_dictionary_state(first, [&](const auto &record) {
|
|
exported.push_back(record);
|
|
return true;
|
|
});
|
|
check(exported.size() == records.size(), "complete state not exported");
|
|
const auto second = stage(resources, root / "second", exported);
|
|
std::vector<DictionaryStateRecord> roundtrip;
|
|
stream_dictionary_state(second, [&](const auto &record) {
|
|
roundtrip.push_back(record);
|
|
return true;
|
|
});
|
|
check(roundtrip.size() == records.size(), "state record lost");
|
|
bool selection = false, ownership = false, deleted_ownership = false;
|
|
for (const auto &record : roundtrip)
|
|
{
|
|
if (auto p = std::get_if<DictionaryStateSelection>(&record))
|
|
selection = p->count == 1;
|
|
if (auto p = std::get_if<DictionaryStateEntry>(&record); p && p->value == "拟蒿")
|
|
ownership = p->user_inserted;
|
|
if (auto p = std::get_if<DictionaryStateEntry>(&record); p && p->value == "你好")
|
|
deleted_ownership = p->deleted && p->user_inserted;
|
|
}
|
|
check(selection && ownership && deleted_ownership, "counter or ownership lost");
|
|
SessionOptions options;
|
|
options.paths = second;
|
|
options.learning = false;
|
|
Session session(options);
|
|
for (char ch : std::string("nihao"))
|
|
session.character(ch);
|
|
const auto candidates = session.snapshot().candidates;
|
|
check(!candidates.empty() && candidates.front().word == "拟蒿", "fixed position not applied");
|
|
for (const auto &candidate : candidates)
|
|
check(candidate.word != "你好", "deleted candidate returned");
|
|
// A complete replacement starts from resources, not the previously overlaid dictionaries.
|
|
const auto empty = stage(resources, root / "empty", {});
|
|
SessionOptions plain;
|
|
plain.paths = empty;
|
|
plain.learning = false;
|
|
Session baseline(plain);
|
|
for (char ch : std::string("nihao"))
|
|
baseline.character(ch);
|
|
check(baseline.snapshot().candidates.front().word == "你好", "old deletion leaked into replacement");
|
|
auto rejected = [&](const char *name, std::vector<DictionaryStateRecord> invalid) {
|
|
const auto path = root / name;
|
|
bool failed = false;
|
|
try
|
|
{
|
|
stage(resources, path, invalid);
|
|
}
|
|
catch (const std::exception &)
|
|
{
|
|
failed = true;
|
|
}
|
|
check(failed && !std::filesystem::exists(path), "failed staging left partial generation");
|
|
};
|
|
bool limited = false;
|
|
try
|
|
{
|
|
std::size_t index = 0;
|
|
stage_dictionary_state(
|
|
resources, root / "limited", "fixture",
|
|
[&](DictionaryStateRecord &out) {
|
|
if (index == records.size())
|
|
return false;
|
|
out = records[index++];
|
|
return true;
|
|
},
|
|
1);
|
|
}
|
|
catch (const std::exception &)
|
|
{
|
|
limited = true;
|
|
}
|
|
check(limited && !std::filesystem::exists(root / "limited"), "explicit record limit ignored");
|
|
auto duplicate = records;
|
|
duplicate.push_back(records.front());
|
|
rejected("duplicate", duplicate);
|
|
auto duplicate_slot = records;
|
|
duplicate_slot.push_back(DictionaryStatePosition{"ni'hao", "ni'hao", "拟好", 1});
|
|
rejected("slot", duplicate_slot);
|
|
rejected("bad-position", {DictionaryStatePosition{"ni'hao", "ni'hao", "拟好", 6}});
|
|
bool failed = false;
|
|
try
|
|
{
|
|
stage(resources, root / "first", {});
|
|
}
|
|
catch (const std::exception &)
|
|
{
|
|
failed = true;
|
|
}
|
|
check(failed && std::filesystem::exists(first.user(assets::user_journal)), "existing generation was overwritten");
|
|
failed = false;
|
|
try
|
|
{
|
|
stage_dictionary_state(resources, root / "cancelled", "fixture", [](DictionaryStateRecord &) -> bool {
|
|
throw std::runtime_error("cancelled or invalid transport checksum");
|
|
});
|
|
}
|
|
catch (const std::exception &)
|
|
{
|
|
failed = true;
|
|
}
|
|
check(failed && !std::filesystem::exists(root / "cancelled"), "cancelled generation leaked");
|
|
}
|
|
} // namespace
|
|
int main(int argc, char **argv)
|
|
{
|
|
try
|
|
{
|
|
run(argc == 2 && std::string(argv[1]) == "--capacity");
|
|
std::cout << "Dictionary state tests passed\n";
|
|
return 0;
|
|
}
|
|
catch (const std::exception &e)
|
|
{
|
|
std::cerr << e.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|