Skip to content

Simplifier / OpenCC 測試計劃提案 #1189

Closed

Description

@frankslin

事件背景

OpenCC ver.1.3.2 引入了一系列破壞性的 C++ ABI 變更,導致 librime 在運行期載入新版 libopencc 時直接 crash。

技術上,1.3.2 將 Converter 重構為純虛擬介面(abstract class),並引入 PipelineConverter 作為新的具體實作。librime 的 simplifier.cc 直接呼叫:

converter_->GetConversionChain()->GetConversions()

而 PipelineConverter::GetConversionChain() 在有 normalization 設定時會回傳 nullptr,立即導致 null pointer dereference。OpenCC 為此在 ver.1.4.0 中:

  1. 修復了 GetConversionChain() 回傳 nullptr 的 bug(PR #1380)
  2. 將 SOVERSION 從 1.x 改為 1.4,專門用來阻止 librime 誤載入 ABI 不相容的 1.3.2(PR #1377,PR 標題明確寫道 "to prevent librime loading ABI-incompatible v1.3.2 builds")

這次事件暴露的根本問題是:librime 沒有任何自動化測試可以在 OpenCC 升級後驗證繁簡轉換是否仍然正確運作。 問題只能在使用者端被察覺,修復也只能由 OpenCC 上游完成。


現有程式碼的層次結構

simplifier.cc 內部有兩個職責不同的層次:

第一層:rime::Opencc(私有適配器類別)

這是 librime 與 OpenCC C++ API 之間唯一的接觸點,封裝了三個操作:

方法 功能
ConvertWord(text, forms*) 詞條級精確查詞,遍歷轉換鏈,返回所有可能形態(一對多)
ConvertText(text, simplified*) 整句轉換,直接呼叫 converter_->Convert()
RandomConvertText(text, simplified*) 整句轉換,每個前綴匹配隨機選一個變體

其中 ConvertWord 的邏輯完全由 librime 自己維護:它逐層遍歷 ConversionChain,在每個 Dict 上做精確匹配與前綴最長匹配,並在多個 dict 之間累積展開結果。這段邏輯並非對 OpenCC 的直接封裝,而是 librime 基於 OpenCC 底層 dict / conversion chain 介面自行實作的——也正是本次 ABI 破壞直接衝擊到的地方。

若有測試覆蓋這個邊界,GetConversionChain() 回傳 nullptr 就會在 CI 中以 crash 或測試失敗的方式立即暴露,而不是等到使用者回報。

第二層:Simplifier(RIME 過濾器)

使用 rime::Opencc,負責:

  • 路由決策:Convert() 先嘗試 ConvertWord,失敗再 fallback 到 ConvertText;random_ 模式走 RandomConvertText
  • 顯示格式化:PushBack() 處理 tips 顯示級別(無 / 單字 / 全部)、注釋位置(show_in_comment)、comment_formatter 投影
  • 類型過濾:excluded_types_ 讓特定類型的候選詞跳過轉換

這一層深度依賴 RIME 引擎(Engine、Ticket、Candidate、Translation)。


目前的可測試性問題

rime::Opencc 被定義為 simplifier.cc 的內部私有類別,外部無法直接引用,導致:

  • 無法在不啟動完整 RIME Engine 的前提下驗證轉換邏輯
  • 任何對 simplifier.cc 的修改,或對 OpenCC 的版本升級,都缺乏自動化驗證手段
  • ConvertWord 中的鏈式轉換邊界行為(如注釋所說的「s2t 展開 里 為 里 和 裏,再由 t2tw 將 裏 轉為 裡」)完全沒有被測試保護

測試計劃

第一步:提取 rime::Opencc 至獨立標頭

將 rime::Opencc 從 simplifier.cc 移出,建立:

  • src/rime/gear/opencc.h:類別宣告(對外可見)
  • src/rime/gear/opencc.cc:實作

simplifier.cc 改為 #include 這個新標頭。此步驟不改變任何對外行為,是純粹的內部重構,同時讓未來其他模組也能複用這個適配器。

第二步:新增 test/simplifier_test.cc

分兩組測試案例:

組一:OpenccTest(整合測試)

直接測試 rime::Opencc,使用真實的 OpenCC 詞典檔案(t2s.json、s2t.json、s2twp.json)。這組測試是未來再次升級 OpenCC 時的安全網。

測試案例 驗證內容
ConvertWord_ExactMatch 詞條精確匹配,返回正確簡體形態
ConvertWord_MultipleValues 一對多詞條返回所有變體
ConvertWord_NoMatch 無對應詞條時返回 false
ConvertWord_ChainExpansion 鏈式轉換跨 dict 累積結果正確(對應 s2twp 的行為)
ConvertWord_NoChange_ReturnsFalse 轉換前後相同時正確返回 false
ConvertText_FullSentence 整句繁簡轉換結果正確
ConvertText_NoChange_ReturnsFalse 無變化時返回 false
Initialize_InvalidConfigPath 詞典路徑不存在時,三個方法均安全返回 false,不拋出例外、不 crash

最後一個案例直接對應本次事件的根因:rime::Opencc 的初始化錯誤處理必須健壯。

組二:SimplifierConvertTest(單元測試)

以 mock 的 rime::Opencc 測試 Simplifier 的路由與格式化邏輯,不依賴 OpenCC 詞典,也不需要啟動 RIME Engine。

測試案例 驗證內容
ConvertWord_MultiForm_ProducesMultipleCandidates ConvertWord 返回多個形態時,產生對應數量的 ShadowCandidate
ConvertWord_OriginalFormPassesThrough 其中一個形態與原文相同時,直接 push 原 candidate(不包裝)
ConvertText_FallbackWhenConvertWordFails ConvertWord 失敗時,fallback 至 ConvertText
NoConversion_WhenBothFail 兩者均失敗時,Convert() 返回 false
ExcludedType_IsSkipped excluded_types 中的候選詞不觸發轉換
RandomMode_DispatchesToRandomConvert random: true 時走 RandomConvertText 而非 ConvertWord
TipsLevel_None tips: none 時 ShadowCandidate 的 comment 為空
TipsLevel_Char_SingleChar tips: char 對單字顯示原文提示
TipsLevel_Char_MultiChar tips: char 對多字詞不顯示提示
TipsLevel_All tips: all 對所有候選詞顯示原文提示
ShowInComment_SwapsTextAndTips show_in_comment: true 時轉換結果顯示於注釋欄

第三步:在 CI 中確認測試可跑

確認 make test 涵蓋新增的測試案例,在 GitHub Actions 現有 workflow 中通過。


工作量估計

步驟 估計工時
提取 rime::Opencc 至獨立標頭(純重構) 1 小時
撰寫 OpenccTest 整合測試 2–3 小時
撰寫 SimplifierConvertTest 單元測試(含 mock 設計) 3–4 小時
合計 約 6–8 小時

不在本次範圍內

  • SimplifierComponent::Create() 的路徑解析邏輯(需要 deployer 基礎設施,建議另立提案)
  • SimplifiedTranslation 的翻譯包裝層(依賴完整的 Translation 物件,同上)
  • OpenCC 詞典資料本身的正確性(屬於 OpenCC 專案的責任範圍)

Activity

  1. lotem commented on Jul 3, 2026

    @lotem
    Member

    多謝,多謝。真是大動作。
    沒想到 OpenCC 時隔多年,又開始活躍更新了。歡迎拾柴。

  2. frankslin commented on Jul 3, 2026

    @frankslin
    ContributorAuthor

    第一步重構: #1190

  3. frankslin commented on Jul 4, 2026

    @frankslin
    ContributorAuthor

    OpenCC 側的測試已添加: BYVoid/OpenCC#1395

    Rime 的測試見 #1191
    CI 已全部通過。

  4. lotem commented on Jul 4, 2026

    @lotem
    Member

    還需要這項修改嗎 #1184 ?或是要做更大的升級?

  5. frankslin commented on Jul 4, 2026

    @frankslin
    ContributorAuthor

    還需要這項修改嗎 #1184 ?或是要做更大的升級?

    PR #1184 是針對 OpenCC 1.3.2 的 workaround,主要是為了處理當時 1.3.2 的 ABI 不一致問題。這個問題在 OpenCC 1.4.0 中已經透過 ABI version bump 與修正解決。

    因此這個 patch 是否需要合併,取決於 librime 是否仍然希望支援這個壞掉的舊版 OpenCC 1.3.2:

    • 如果保留 1.3.2 相容性:可以合併此 patch,作為防禦性處理
    • 如果將最低版本提升至 1.4.0:則不再需要此 workaround

    以 OpenCC maintainer 的角度,我傾向於建議將最低支援版本提升至 1.4.0,並移除此類針對 1.3.x 的 workaround,以避免未來 ABI 行為分歧持續累積。

    我可以再开一个 issue 討論 OpenCC API 層如何更明確地表達「一對多」 conversion 行為,但這個議題可以獨立於本 PR 處理。

  6. amzxyz commented on Jul 11, 2026

    @amzxyz
    Contributor

    我特别希望OpenCC是其中一个预调用的模块,现在看代码已经实现,但我希望替换这个滤镜更开放一些,支持根据自身内容设置分隔符,支持自定义txt编译到bin在调用OpenCC时候通过参数设置即可,不想调用也能维持正确的键值替换能力

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions