From e9cd70e143d6cbe8ba9a9bbb320c34f658c8a264 Mon Sep 17 00:00:00 2001 From: Frank Lin Date: Fri, 3 Jul 2026 16:56:44 -0700 Subject: [PATCH] Add librime OpenCC compatibility tests Protect librime's existing OpenCC integration by covering GetConversionChain access, exact word expansion, and prefix fallback threading through conversion chains. --- src/ConfigTest.cpp | 144 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/ConfigTest.cpp b/src/ConfigTest.cpp index 71e20db9d..7d1fd3de4 100644 --- a/src/ConfigTest.cpp +++ b/src/ConfigTest.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -33,10 +34,12 @@ #include "ConfigTestBase.hpp" #include "ConversionChain.hpp" #include "Converter.hpp" +#include "Dict.hpp" #include "Segments.hpp" #include "Exception.hpp" #include "ResourceProvider.hpp" #include "TestUtilsUTF8.hpp" +#include "UTF8Util.hpp" namespace opencc { namespace { @@ -188,6 +191,78 @@ std::string InlineSingleStepConfig(const std::string& segmentationEntries, "}\n"; } +std::string ChainConfig(const std::vector& dicts) { + std::string config = + "{\n" + " \"name\": \"Rime Compatibility Test\",\n" + " \"conversion_chain\": [\n"; + for (size_t i = 0; i < dicts.size(); i++) { + config += + " {\n" + " \"dict\": " + + dicts[i] + + "\n" + " }"; + config += (i + 1 == dicts.size()) ? "\n" : ",\n"; + } + config += + " ]\n" + "}\n"; + return config; +} + +bool ConvertWordLikeRime(const ConversionChainPtr& chain, + const std::string& input, + std::vector* converted) { + std::vector current{input}; + bool hasExactMatch = false; + + const auto AddIfNew = [](const std::string& value, + std::vector* values, + std::set* seen) { + if (seen->insert(value).second) { + values->push_back(value); + } + }; + + for (const ConversionPtr& conversion : chain->GetConversions()) { + const DictPtr dict = conversion->GetDict(); + std::vector next; + std::set seen; + + for (const std::string& word : current) { + const Optional exact = dict->Match(word); + if (!exact.IsNull()) { + hasExactMatch = true; + for (const std::string& value : exact.Get()->Values()) { + AddIfNew(value, &next, &seen); + } + continue; + } + + std::string fallback; + for (const char* pstr = word.c_str(); *pstr != '\0';) { + const Optional prefix = dict->MatchPrefix(pstr); + size_t matchedLength; + if (prefix.IsNull()) { + matchedLength = UTF8Util::NextCharLength(pstr); + fallback.append(pstr, matchedLength); + } else { + matchedLength = prefix.Get()->KeyLength(); + fallback += prefix.Get()->GetDefault(); + } + pstr += matchedLength; + } + AddIfNew(fallback, &next, &seen); + } + + current = std::move(next); + } + + *converted = current; + return hasExactMatch; +} + std::string FindOcd2DictionaryDir(const std::string& configTestDirPath) { std::vector candidates; if (!PACKAGE_DATA_DIRECTORY.empty()) { @@ -1060,10 +1135,79 @@ TEST_F(ConfigTest, NormalizationGetConversionChainIsNonNull) { const ConverterPtr conv = c.NewFromString(config, CONFIG_TEST_DIR_PATH); const ConversionChainPtr chain = conv->GetConversionChain(); ASSERT_NE(nullptr, chain); + const std::list conversions = chain->GetConversions(); + ASSERT_FALSE(conversions.empty()); + ASSERT_NE(nullptr, conversions.front()->GetDict()); + EXPECT_FALSE(conversions.front()->GetDict()->Match(utf8("乙")).IsNull()); + EXPECT_TRUE(conversions.front()->GetDict()->Match(utf8("甲")).IsNull()); + // Verify it is the main chain: 乙 → 丙 (not the normalization chain 甲 → 乙). const SegmentsPtr result = chain->Convert(SegmentsPtr(new Segments{utf8("乙")})); ASSERT_EQ(1u, result->Length()); EXPECT_EQ(utf8("丙"), result->At(0)); } +TEST_F(ConfigTest, RimeConvertWordExactExpansionThreadsThroughChain) { + const fs::path tempDir = MakeTempDir("opencc-rime-exact-expansion-test"); + WriteFile(tempDir / "first.txt", utf8("里\t裏 里 哩\n")); + + const std::string config = ChainConfig({ + "{\"type\": \"text\", \"file\": \"first.txt\"}", + "{\n" + " \"type\": \"inline\",\n" + " \"entries\": {\"裏\": \"裡\"}\n" + " }", + }); + Config c; + const ConverterPtr conv = c.NewFromString(config, PathString(tempDir)); + + std::vector converted; + EXPECT_TRUE(ConvertWordLikeRime(conv->GetConversionChain(), utf8("里"), + &converted)); + EXPECT_EQ((std::vector{utf8("裡"), utf8("里"), utf8("哩")}), + converted); + + fs::remove_all(tempDir); +} + +TEST_F(ConfigTest, RimeConvertWordPartialFallbackContinuesToNextStage) { + const std::string config = ChainConfig({ + "{\n" + " \"type\": \"inline\",\n" + " \"entries\": {\"内\": \"內\"}\n" + " }", + "{\n" + " \"type\": \"inline\",\n" + " \"entries\": {\"內存\": \"記憶體\"}\n" + " }", + }); + Config c; + const ConverterPtr conv = c.NewFromString(config, CONFIG_TEST_DIR_PATH); + + std::vector converted; + EXPECT_TRUE(ConvertWordLikeRime(conv->GetConversionChain(), utf8("内存"), + &converted)); + EXPECT_EQ((std::vector{utf8("記憶體")}), converted); +} + +TEST_F(ConfigTest, RimeConvertWordReturnsFalseWithoutAnyExactMatch) { + const std::string config = ChainConfig({ + "{\n" + " \"type\": \"inline\",\n" + " \"entries\": {\"a\": \"A\"}\n" + " }", + "{\n" + " \"type\": \"inline\",\n" + " \"entries\": {\"z\": \"Z\"}\n" + " }", + }); + Config c; + const ConverterPtr conv = c.NewFromString(config, CONFIG_TEST_DIR_PATH); + + std::vector converted; + EXPECT_FALSE(ConvertWordLikeRime(conv->GetConversionChain(), "abc", + &converted)); + EXPECT_EQ((std::vector{"Abc"}), converted); +} + } // namespace opencc