From fbcd5977261a4809de1c2e6957494d4430c9302d Mon Sep 17 00:00:00 2001 From: mirtlecn Date: Thu, 26 Mar 2026 13:02:38 +0800 Subject: [PATCH] test: add rime smoke framework (#1511) * test: add config smoke framework * ci: add smoke test before packing * ci: add cache --- .github/workflows/pages.yml | 45 --- .github/workflows/release.yml | 39 ++- .github/workflows/test.yml | 37 +++ others/script/smoke/README.md | 40 +++ .../smoke/cases/rime_ice/input_cases.tsv | 242 +++++++++++++++++ others/script/smoke/lib/common.sh | 257 ++++++++++++++++++ others/script/smoke/run.sh | 23 ++ others/script/smoke/suites/config_repo.sh | 230 ++++++++++++++++ 8 files changed, 862 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/pages.yml create mode 100644 .github/workflows/test.yml create mode 100644 others/script/smoke/README.md create mode 100644 others/script/smoke/cases/rime_ice/input_cases.tsv create mode 100644 others/script/smoke/lib/common.sh create mode 100644 others/script/smoke/run.sh create mode 100644 others/script/smoke/suites/config_repo.sh diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100644 index 9c4b7d6..0000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Deploy rime-ice with fcitx5-rime.js - -on: - # push: - # branches: - # - main - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Build rime-ice - uses: rimeinn/deploy-schema@master - with: - user-recipe-list: |- - iDvel/rime-ice:others/recipes/full - shared-recipe-list: - package-items: |- - build - lua - opencc - custom_phrase.txt - - - name: Download fcitx5-rime.js - run: | - curl -L -o fcitx5-rime.tgz https://github.com/rimeinn/fcitx5-rime.js/releases/download/latest/fcitx5-rime.tgz - mkdir -p fcitx5-rime - tar -xzvf fcitx5-rime.tgz -C fcitx5-rime - - - name: Move files to publish directory - run: | - mkdir -p ./public/dist - mv /tmp/deploy-schema/artifact.zip ./public/rime-ice.zip - mv fcitx5-rime/package/dist/* ./public/dist - cp others/pages/index.html ./public/index.html - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./public diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f14134c..af5b517 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,13 +14,40 @@ on: workflow_dispatch: jobs: - Release: - runs-on: ubuntu-latest + smoke: + name: Smoke test before release + runs-on: ubuntu-24.04 + # if: github.repository == 'iDvel/rime-ice' + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Cache rime cli bundle + uses: actions/cache@v5 + with: + path: ${{ runner.temp }}/rime-cli-cache/rime-cli-linux-1.6.1.zip + key: rime-cli-linux-1.6.1 + + - name: Run smoke test suite + env: + RIME_CLI_URL: https://github.com/mirtlecn/public/releases/download/v1.6.1/rime-cli-linux-1.6.1.zip + RIME_CLI_CACHE_PATH: ${{ runner.temp }}/rime-cli-cache/rime-cli-linux-1.6.1.zip + run: bash ./others/script/smoke/run.sh rime_ice + + package: + name: Package and release + runs-on: ubuntu-24.04 if: github.repository == 'iDvel/rime-ice' + needs: + - smoke steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Go if: ${{ contains(github.event.head_commit.message, ' [build]') || github.event_name == 'workflow_dispatch' }} @@ -56,7 +83,7 @@ jobs: - name: Create nightly release if: ${{ github.ref == 'refs/heads/main' }} - uses: "softprops/action-gh-release@v2" + uses: softprops/action-gh-release@v2 with: body: | ## 说明 @@ -79,7 +106,7 @@ jobs: - name: Create stable release if: startsWith(github.ref, 'refs/tags/') - uses: "softprops/action-gh-release@v2" + uses: softprops/action-gh-release@v2 with: body: | ## 说明 @@ -108,4 +135,4 @@ jobs: git push else echo "No changes to commit." - fi \ No newline at end of file + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2e3f17a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Smoke test + +on: + push: + branches-ignore: + - main + paths: + - "**/**" + - "!**.md" + - "!**.gitignore" + - "!.github/**" + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + smoke-linux: + name: Rime smoke test + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Cache rime cli bundle + uses: actions/cache@v5 + with: + path: ${{ runner.temp }}/rime-cli-cache/rime-cli-linux-1.6.1.zip + key: rime-cli-linux-1.6.1 + + - name: Run smoke test suite + env: + RIME_CLI_URL: https://github.com/mirtlecn/public/releases/download/v1.6.1/rime-cli-linux-1.6.1.zip + RIME_CLI_CACHE_PATH: ${{ runner.temp }}/rime-cli-cache/rime-cli-linux-1.6.1.zip + run: bash ./others/script/smoke/run.sh rime_ice diff --git a/others/script/smoke/README.md b/others/script/smoke/README.md new file mode 100644 index 0000000..3d2d3c0 --- /dev/null +++ b/others/script/smoke/README.md @@ -0,0 +1,40 @@ +# Smoke Test Framework + +This directory contains the shell-based smoke test framework for the current repository. + +## Layout + +- `run.sh`: suite entrypoint +- `lib/common.sh`: shared shell helpers +- `suites/config_repo.sh`: generic suite implementation for the current config repository +- `cases/rime_ice/input_cases.tsv`: data-driven input cases + +## Current Flow + +- uses local `rime_deployer` and `rime_api_console` from `PATH` when available +- otherwise downloads the public Linux CLI bundle when `RIME_CLI_URL` is set +- deploys the current repository with `rime_deployer --build` +- runs `rime_api_console` +- verifies basic pinyin commit and a stable Lua-driven Unicode commit + +## Environment Variables + +- `RIME_CLI_URL`: optional public CLI bundle URL +- `RIME_CONFIG_ROOT`: optional repository root override + +## Extending + +Add more rows to `cases/rime_ice/input_cases.tsv`. +The current tab-separated case format is: + +- `case_id` +- `schema_id` +- `key_sequence` +- `expected_text` + +`expected_text` also supports: + +- `@today:`, for example `@today:%Y-%m-%d` +- `@regex:`, matched against the normalized stdout log + +`rime_api_console` is used as the default runner because it is more reliable than `rime_console` for smoke tests that reuse an already deployed workspace. diff --git a/others/script/smoke/cases/rime_ice/input_cases.tsv b/others/script/smoke/cases/rime_ice/input_cases.tsv new file mode 100644 index 0000000..1daee69 --- /dev/null +++ b/others/script/smoke/cases/rime_ice/input_cases.tsv @@ -0,0 +1,242 @@ +# ------------------ +# 方案:rime_ice +# ------ 基础 ------ +# case_id schema_id key_sequence expected_text +基础:中文输入 rime_ice wusongpinyin{space} 雾凇拼音 +基础:英文输入 rime_ice hello{space} hello +基础:英文派生 rime_ice PtwoP{space} P2P +基础:中英混输 rime_ice Xguang{space} X光 +基础:自定义短语 rime_ice ig{space} 一个 +基础:Emoji输入 rime_ice nihao{2} 👋 +基础:uU拆字反查 rime_ice uUrenrenren{space} 众 +基础:v键符号输入 rime_ice v1{space} 一 + +# ------ 插件 ------ +# case_id schema_id key_sequence expected_text +插件:英文全大写 rime_ice HELLO{space} HELLO +插件:英文首字母大写 rime_ice Hello{space} Hello +插件:置顶候选 rime_ice d{space} 的 +插件:Unicode输入 rime_ice U4f60{space} 你 +插件:日期输入 rime_ice rq{space} @today:%Y-%m-%d +插件:UUID输入 rime_ice uuid{space} @regex:^commit: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ +插件:英文降权 rime_ice ranch{space} 染成 +插件:英文降权 rime_ice ranch{2} ranch +插件:数字输入 rime_ice R123{space} 一百二十三 +插件:计算器 rime_ice cC1+2{space} 3 +插件:农历输入:农历 rime_ice N19700101{space} 一九六九年冬月廿四 +插件:农历输入:干支 rime_ice N19700101{2} 己酉年(鸡)冬月廿四 +插件:以词定字:左中括号取首字 rime_ice nihao{bracketleft} 你 +插件:以词定字:右中括号取尾字 rime_ice nihao{bracketright} 好 +插件:部件辅码 rime_ice ni`ren{space} 你 +插件:错字错音提示 rime_ice qikefu{Control+Shift+Return} 契诃(hē)夫 + +# ------------------ +# 方案:自然码双拼 +# ------ 基础 ------ +基础:中文输入 double_pinyin wusspnyn{space} 雾凇拼音 +基础:英文输入 double_pinyin hello{space} hello +基础:英文派生 double_pinyin PtwoP{space} P2P +基础:中英混输 double_pinyin Xgd{space} X光 +# 基础:自定义短语 double_pinyin ig{space} 一个 +基础:Emoji输入 double_pinyin nihk{2} 👋 +基础:uU拆字反查 double_pinyin uUrenrenren{space} 众 +基础:v键符号输入 double_pinyin V1{space} 一 + +# ------ 插件 ------ +插件:英文全大写 double_pinyin HELLO{space} HELLO +插件:英文首字母大写 double_pinyin Hello{space} Hello +插件:置顶候选 double_pinyin d{space} 的 +插件:Unicode输入 double_pinyin U4f60{space} 你 +插件:日期输入 double_pinyin date{space} @today:%Y-%m-%d +插件:UUID输入 double_pinyin uuid{space} @regex:^commit: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ +插件:英文降权 double_pinyin bail{space} 把拆 +插件:英文降权 double_pinyin bail{2} bail +插件:数字输入 double_pinyin R123{space} 一百二十三 +插件:计算器 double_pinyin cC1+2{space} 3 +插件:农历输入:农历 double_pinyin N19700101{space} 一九六九年冬月廿四 +插件:农历输入:干支 double_pinyin N19700101{2} 己酉年(鸡)冬月廿四 +插件:以词定字:左中括号取首字 double_pinyin nihk{bracketleft} 你 +插件:以词定字:右中括号取尾字 double_pinyin nihk{bracketright} 好 +插件:部件辅码 double_pinyin ni`ren{space} 你 +插件:错字错音提示 double_pinyin qikefu{Control+Shift+Return} 契诃(hē)夫 + +# ------------------ +# 方案:智能 ABC 双拼 +# ------ 基础 ------ +基础:中文输入 double_pinyin_abc wusspcyc{space} 雾凇拼音 +基础:英文输入 double_pinyin_abc hello{space} hello +基础:英文派生 double_pinyin_abc PtwoP{space} P2P +基础:中英混输 double_pinyin_abc Xgt{space} X光 +# 基础:自定义短语 double_pinyin_abc ig{space} 一个 +基础:Emoji输入 double_pinyin_abc nihk{2} 👋 +基础:uU拆字反查 double_pinyin_abc uUrenrenren{space} 众 +基础:v键符号输入 double_pinyin_abc V1{space} 一 + +# ------ 插件 ------ +插件:英文全大写 double_pinyin_abc HELLO{space} HELLO +插件:英文首字母大写 double_pinyin_abc Hello{space} Hello +插件:置顶候选 double_pinyin_abc d{space} 的 +插件:Unicode输入 double_pinyin_abc U4f60{space} 你 +插件:日期输入 double_pinyin_abc date{space} @today:%Y-%m-%d +插件:UUID输入 double_pinyin_abc uuid{space} @regex:^commit: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ +# 插件:英文降权 double_pinyin_abc bail{space} 不确定 +# 插件:英文降权 double_pinyin_abc bail{2} 不确定 +插件:数字输入 double_pinyin_abc R123{space} 一百二十三 +插件:计算器 double_pinyin_abc cC1+2{space} 3 +插件:农历输入:农历 double_pinyin_abc N19700101{space} 一九六九年冬月廿四 +插件:农历输入:干支 double_pinyin_abc N19700101{2} 己酉年(鸡)冬月廿四 +插件:以词定字:左中括号取首字 double_pinyin_abc nihk{bracketleft} 你 +插件:以词定字:右中括号取尾字 double_pinyin_abc nihk{bracketright} 好 +插件:部件辅码 double_pinyin_abc ni`ren{space} 你 +插件:错字错音提示 double_pinyin_abc qikefu{Control+Shift+Return} 契诃(hē)夫 + +# ------------------ +# 方案:微软双拼 +# ------ 基础 ------ +基础:中文输入 double_pinyin_mspy wusspnyn{space} 雾凇拼音 +基础:英文输入 double_pinyin_mspy hello{space} hello +基础:英文派生 double_pinyin_mspy PtwoP{space} P2P +基础:中英混输 double_pinyin_mspy Xgd{space} X光 +# 基础:自定义短语 double_pinyin_mspy ig{space} 一个 +基础:Emoji输入 double_pinyin_mspy nihk{2} 👋 +基础:uU拆字反查 double_pinyin_mspy uUrenrenren{space} 众 +基础:v键符号输入 double_pinyin_mspy V1{space} 一 + +# ------ 插件 ------ +插件:英文全大写 double_pinyin_mspy HELLO{space} HELLO +插件:英文首字母大写 double_pinyin_mspy Hello{space} Hello +插件:置顶候选 double_pinyin_mspy d{space} 的 +插件:Unicode输入 double_pinyin_mspy U4f60{space} 你 +插件:日期输入 double_pinyin_mspy date{space} @today:%Y-%m-%d +插件:UUID输入 double_pinyin_mspy uuid{space} @regex:^commit: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ +插件:英文降权 double_pinyin_mspy bail{space} 把拆 +插件:英文降权 double_pinyin_mspy bail{2} bail +插件:数字输入 double_pinyin_mspy R123{space} 一百二十三 +插件:计算器 double_pinyin_mspy cC1+2{space} 3 +插件:农历输入:农历 double_pinyin_mspy N19700101{space} 一九六九年冬月廿四 +插件:农历输入:干支 double_pinyin_mspy N19700101{2} 己酉年(鸡)冬月廿四 +插件:以词定字:左中括号取首字 double_pinyin_mspy nihk{bracketleft} 你 +插件:以词定字:右中括号取尾字 double_pinyin_mspy nihk{bracketright} 好 +插件:部件辅码 double_pinyin_mspy ni`ren{space} 你 +插件:错字错音提示 double_pinyin_mspy qikefu{Control+Shift+Return} 契诃(hē)夫 + +# ------------------ +# 方案:搜狗双拼 +# ------ 基础 ------ +基础:中文输入 double_pinyin_sogou wusspnyn{space} 雾凇拼音 +基础:英文输入 double_pinyin_sogou hello{space} hello +基础:英文派生 double_pinyin_sogou PtwoP{space} P2P +基础:中英混输 double_pinyin_sogou Xgd{space} X光 +# 基础:自定义短语 double_pinyin_sogou ig{space} 一个 +基础:Emoji输入 double_pinyin_sogou nihk{2} 👋 +基础:uU拆字反查 double_pinyin_sogou uUrenrenren{space} 众 +基础:v键符号输入 double_pinyin_sogou V1{space} 一 + +# ------ 插件 ------ +插件:英文全大写 double_pinyin_sogou HELLO{space} HELLO +插件:英文首字母大写 double_pinyin_sogou Hello{space} Hello +插件:置顶候选 double_pinyin_sogou d{space} 的 +插件:Unicode输入 double_pinyin_sogou U4f60{space} 你 +插件:日期输入 double_pinyin_sogou date{space} @today:%Y-%m-%d +插件:UUID输入 double_pinyin_sogou uuid{space} @regex:^commit: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ +插件:英文降权 double_pinyin_sogou bail{space} 把拆 +插件:英文降权 double_pinyin_sogou bail{2} bail +插件:数字输入 double_pinyin_sogou R123{space} 一百二十三 +插件:计算器 double_pinyin_sogou cC1+2{space} 3 +插件:农历输入:农历 double_pinyin_sogou N19700101{space} 一九六九年冬月廿四 +插件:农历输入:干支 double_pinyin_sogou N19700101{2} 己酉年(鸡)冬月廿四 +插件:以词定字:左中括号取首字 double_pinyin_sogou nihk{bracketleft} 你 +插件:以词定字:右中括号取尾字 double_pinyin_sogou nihk{bracketright} 好 +插件:部件辅码 double_pinyin_sogou ni`ren{space} 你 +插件:错字错音提示 double_pinyin_sogou qikefu{Control+Shift+Return} 契诃(hē)夫 + +# ------------------ +# 方案:紫光双拼 +# ------ 基础 ------ +基础:中文输入 double_pinyin_ziguang wushpyyy{space} 雾凇拼音 +基础:英文输入 double_pinyin_ziguang hello{space} hello +基础:英文派生 double_pinyin_ziguang PtwoP{space} P2P +基础:中英混输 double_pinyin_ziguang Xgg{space} X光 +# 基础:自定义短语 double_pinyin_ziguang ig{space} 一个 +基础:Emoji输入 double_pinyin_ziguang nihq{2} 👋 +基础:uU拆字反查 double_pinyin_ziguang uUrenrenren{space} 众 +基础:v键符号输入 double_pinyin_ziguang V1{space} 一 + +# ------ 插件 ------ +插件:英文全大写 double_pinyin_ziguang HELLO{space} HELLO +插件:英文首字母大写 double_pinyin_ziguang Hello{space} Hello +插件:置顶候选 double_pinyin_ziguang d{space} 的 +插件:Unicode输入 double_pinyin_ziguang U4f60{space} 你 +插件:日期输入 double_pinyin_ziguang date{space} @today:%Y-%m-%d +插件:UUID输入 double_pinyin_ziguang uuid{space} @regex:^commit: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ +插件:英文降权 double_pinyin_ziguang bail{space} 把栓 +插件:英文降权 double_pinyin_ziguang bail{2} bail +插件:数字输入 double_pinyin_ziguang R123{space} 一百二十三 +插件:计算器 double_pinyin_ziguang cC1+2{space} 3 +插件:农历输入:农历 double_pinyin_ziguang N19700101{space} 一九六九年冬月廿四 +插件:农历输入:干支 double_pinyin_ziguang N19700101{2} 己酉年(鸡)冬月廿四 +插件:以词定字:左中括号取首字 double_pinyin_ziguang nihq{bracketleft} 你 +插件:以词定字:右中括号取尾字 double_pinyin_ziguang nihq{bracketright} 好 +插件:部件辅码 double_pinyin_ziguang ni`ren{space} 你 +插件:错字错音提示 double_pinyin_ziguang qikefu{Control+Shift+Return} 契诃(hē)夫 + +# ------------------ +# 方案:拼音加加双拼 +# ------ 基础 ------ +基础:中文输入 double_pinyin_jiajia wusyplyl{space} 雾凇拼音 +基础:英文输入 double_pinyin_jiajia hello{space} hello +基础:英文派生 double_pinyin_jiajia PtwoP{space} P2P +基础:中英混输 double_pinyin_jiajia Xgh{space} X光 +# 基础:自定义短语 double_pinyin_jiajia ig{space} 一个 +基础:Emoji输入 double_pinyin_jiajia nihd{2} 👋 +基础:uU拆字反查 double_pinyin_jiajia uUrenrenren{space} 众 +基础:v键符号输入 double_pinyin_jiajia V1{space} 一 + +# ------ 插件 ------ +插件:英文全大写 double_pinyin_jiajia HELLO{space} HELLO +插件:英文首字母大写 double_pinyin_jiajia Hello{space} Hello +插件:置顶候选 double_pinyin_jiajia d{space} 的 +插件:Unicode输入 double_pinyin_jiajia U4f60{space} 你 +插件:日期输入 double_pinyin_jiajia date{space} @today:%Y-%m-%d +插件:UUID输入 double_pinyin_jiajia uuid{space} @regex:^commit: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ +# 插件:英文降权 double_pinyin_jiajia bail{space} 不确定 +# 插件:英文降权 double_pinyin_jiajia bail{2} 不确定 +插件:数字输入 double_pinyin_jiajia R123{space} 一百二十三 +插件:计算器 double_pinyin_jiajia cC1+2{space} 3 +插件:农历输入:农历 double_pinyin_jiajia N19700101{space} 一九六九年冬月廿四 +插件:农历输入:干支 double_pinyin_jiajia N19700101{2} 己酉年(鸡)冬月廿四 +插件:以词定字:左中括号取首字 double_pinyin_jiajia nihd{bracketleft} 你 +插件:以词定字:右中括号取尾字 double_pinyin_jiajia nihd{bracketright} 好 +插件:部件辅码 double_pinyin_jiajia ni`ren{space} 你 +插件:错字错音提示 double_pinyin_jiajia qikefu{Control+Shift+Return} 契诃(hē)夫 + +# ------------------ +# 方案:小鹤双拼 +# ------ 基础 ------ +基础:中文输入 double_pinyin_flypy wusspbyb{space} 雾凇拼音 +基础:英文输入 double_pinyin_flypy hello{space} hello +基础:英文派生 double_pinyin_flypy PtwoP{space} P2P +基础:中英混输 double_pinyin_flypy Xgl{space} X光 +# 基础:自定义短语 double_pinyin_flypy ig{space} 一个 +基础:Emoji输入 double_pinyin_flypy nihc{2} 👋 +基础:uU拆字反查 double_pinyin_flypy uUrenrenren{space} 众 +基础:v键符号输入 double_pinyin_flypy V1{space} 一 + +# ------ 插件 ------ +# case_id schema_id key_sequence expected_text +插件:英文全大写 double_pinyin_flypy HELLO{space} HELLO +插件:英文首字母大写 double_pinyin_flypy Hello{space} Hello +插件:置顶候选 double_pinyin_flypy d{space} 的 +插件:Unicode输入 double_pinyin_flypy U4f60{space} 你 +插件:日期输入 double_pinyin_flypy date{space} @today:%Y-%m-%d +插件:UUID输入 double_pinyin_flypy uuid{space} @regex:^commit: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ +插件:英文降权 double_pinyin_flypy bail{space} 把床 +插件:英文降权 double_pinyin_flypy bail{2} bail +插件:数字输入 double_pinyin_flypy R123{space} 一百二十三 +插件:计算器 double_pinyin_flypy cC1+2{space} 3 +插件:农历输入:农历 double_pinyin_flypy N19700101{space} 一九六九年冬月廿四 +插件:农历输入:干支 double_pinyin_flypy N19700101{2} 己酉年(鸡)冬月廿四 +插件:以词定字:左中括号取首字 double_pinyin_flypy nihc{bracketleft} 你 +插件:以词定字:右中括号取尾字 double_pinyin_flypy nihc{bracketright} 好 +插件:部件辅码 double_pinyin_flypy ni`ren{space} 你 +插件:错字错音提示 double_pinyin_flypy qikefu{Control+Shift+Return} 契诃(hē)夫 diff --git a/others/script/smoke/lib/common.sh b/others/script/smoke/lib/common.sh new file mode 100644 index 0000000..a3f528a --- /dev/null +++ b/others/script/smoke/lib/common.sh @@ -0,0 +1,257 @@ +#!/bin/bash + +if [[ -z "${SMOKE_ROOT:-}" ]]; then + SMOKE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" +fi +if [[ -z "${SMOKE_REPO_ROOT:-}" ]]; then + SMOKE_REPO_ROOT="$(cd "${SMOKE_ROOT}/../../.."; pwd)" +fi +if [[ -z "${SMOKE_WORK_ROOT:-}" ]]; then + SMOKE_WORK_ROOT="${RUNNER_TEMP:-/tmp}/rime-ice-smoke" +fi +if [[ -z "${SMOKE_LOG_ROOT:-}" ]]; then + SMOKE_LOG_ROOT="${SMOKE_WORK_ROOT}/logs" +fi + +SMOKE_CURRENT_LABEL="${SMOKE_CURRENT_LABEL:-}" +SMOKE_CURRENT_LOG_FILE="${SMOKE_CURRENT_LOG_FILE:-}" + +log() { + printf '[smoke] %s\n' "$*" +} + +log_step() { + printf '[smoke] 🔹 %s\n' "$*" +} + +log_pass() { + printf '[smoke] ✅ %s\n' "$*" +} + +log_warn() { + printf '[smoke] ⚠️ %s\n' "$*" +} + +fail() { + printf '[smoke] 🔴 %s\n' "$*" >&2 + if [[ -n "${SMOKE_CURRENT_LABEL}" ]]; then + printf '[smoke][context] %s\n' "${SMOKE_CURRENT_LABEL}" >&2 + fi + if [[ -n "${SMOKE_CURRENT_LOG_FILE}" && -f "${SMOKE_CURRENT_LOG_FILE}" ]]; then + printf '[smoke][log] begin %s\n' "${SMOKE_CURRENT_LOG_FILE}" >&2 + sed -n '1,240p' "${SMOKE_CURRENT_LOG_FILE}" >&2 + printf '[smoke][log] end %s\n' "${SMOKE_CURRENT_LOG_FILE}" >&2 + fi + exit 1 +} + +set_failure_context() { + local label="$1" + local log_file="$2" + SMOKE_CURRENT_LABEL="${label}" + SMOKE_CURRENT_LOG_FILE="${log_file}" +} + +clear_failure_context() { + SMOKE_CURRENT_LABEL="" + SMOKE_CURRENT_LOG_FILE="" +} + +require_command() { + local command_name="$1" + command -v "${command_name}" >/dev/null 2>&1 || + fail "required command not found: ${command_name}" +} + +ensure_clean_dir() { + local dir_path="$1" + rm -rf "${dir_path}" + mkdir -p "${dir_path}" +} + +clean_config_artifacts() { + local config_root="$1" + local build_dir="${config_root}/build" + + if [[ -d "${build_dir}" ]]; then + log_step "removing ${build_dir}" + rm -rf "${build_dir}" + fi + + find "${config_root}" -mindepth 1 -maxdepth 1 -type d -name '*.userdb' -print0 | + while IFS= read -r -d '' userdb_dir; do + log_step "removing ${userdb_dir}" + rm -rf "${userdb_dir}" + done +} + +download_file() { + local file_url="$1" + local output_path="$2" + curl -fsSL --retry 3 -o "${output_path}" "${file_url}" +} + +prepare_cli_archive() { + local cli_url="$1" + local output_path="$2" + local cache_path="${RIME_CLI_CACHE_PATH:-}" + + if [[ -n "${cache_path}" && -f "${cache_path}" ]]; then + log_step "using cached rime cli bundle ${cache_path}" >&2 + cp "${cache_path}" "${output_path}" + return 0 + fi + + log_step "downloading rime cli bundle" >&2 + download_file "${cli_url}" "${output_path}" + + if [[ -n "${cache_path}" ]]; then + mkdir -p "$(dirname "${cache_path}")" + cp "${output_path}" "${cache_path}" + fi +} + +extract_zip() { + local archive_path="$1" + local output_dir="$2" + unzip -q "${archive_path}" -d "${output_dir}" +} + +prepare_cli_bundle() { + local archive_path="$1" + local output_dir="$2" + local nested_archive_path + local bundle_root + + extract_zip "${archive_path}" "${output_dir}" + + nested_archive_path="$(find "${output_dir}" -maxdepth 2 -type f \( -name 'rime_cli-*.zip' -o -name 'rime-cli-*.zip' \) | head -n 1)" + if [[ -n "${nested_archive_path}" ]]; then + local nested_root="${output_dir}/nested" + mkdir -p "${nested_root}" + extract_zip "${nested_archive_path}" "${nested_root}" + output_dir="${nested_root}" + fi + + bundle_root="$(find "${output_dir}" -maxdepth 3 -type f -path '*/bin/rime_deployer' | head -n 1)" + [[ -n "${bundle_root}" ]] || fail "rime_deployer not found in bundle" + dirname "$(dirname "${bundle_root}")" +} + +resolve_cli_commands() { + local cli_url="${1:-}" + local work_root="$2" + local deployer_path + local api_console_path + local cli_archive + local extract_root + local bundle_root + + if [[ -n "${cli_url}" ]]; then + require_command curl + require_command unzip + + cli_archive="${work_root}/rime-cli.zip" + extract_root="${work_root}/cli" + + prepare_cli_archive "${cli_url}" "${cli_archive}" + log_step "extracting rime cli bundle" >&2 + bundle_root="$(prepare_cli_bundle "${cli_archive}" "${extract_root}")" + deployer_path="${bundle_root}/bin/rime_deployer" + api_console_path="${bundle_root}/bin/rime_api_console" + else + deployer_path="$(command -v rime_deployer || true)" + api_console_path="$(command -v rime_api_console || true)" + [[ -n "${deployer_path}" ]] || fail "RIME_CLI_URL is not set and local rime_deployer was not found in PATH" + [[ -n "${api_console_path}" ]] || fail "RIME_CLI_URL is not set and local rime_api_console was not found in PATH" + log_step "using local rime cli commands from PATH" >&2 + fi + + [[ -x "${deployer_path}" ]] || fail "rime_deployer is not executable: ${deployer_path}" + [[ -x "${api_console_path}" ]] || fail "rime_api_console is not executable: ${api_console_path}" + printf '%s\n%s\n' "${deployer_path}" "${api_console_path}" +} + +combine_logs() { + local stdout_path="$1" + local stderr_path="$2" + local output_path="$3" + { + printf '== stdout ==\n' + cat "${stdout_path}" + printf '\n== stderr ==\n' + cat "${stderr_path}" + } >"${output_path}" +} + +normalize_console_output() { + local input_path="$1" + local output_path="$2" + tr -d '\r' <"${input_path}" >"${output_path}" +} + +assert_file_exists() { + local file_path="$1" + [[ -f "${file_path}" ]] || fail "expected file not found: ${file_path}" +} + +assert_file_contains() { + local file_path="$1" + local expected_text="$2" + grep -F -- "${expected_text}" "${file_path}" >/dev/null || + fail "expected '${expected_text}' in ${file_path}" +} + +assert_file_matches() { + local file_path="$1" + local expected_pattern="$2" + grep -E -- "${expected_pattern}" "${file_path}" >/dev/null || + fail "expected pattern '${expected_pattern}' in ${file_path}" +} + +resolve_expected_text() { + local expected_text="$1" + if [[ "${expected_text}" == @today:* ]]; then + local date_format="${expected_text#@today:}" + date +"${date_format}" + return 0 + fi + printf '%s\n' "${expected_text}" +} + +assert_no_error_lines() { + local file_path="$1" + local match_path="${file_path}.errors" + if grep -En '(^E[0-9]{4}[[:space:]])|(^F[0-9]{4}[[:space:]])|(^ERROR[:[:space:]])|(^Error([^[:alpha:]]|$))' "${file_path}" >"${match_path}"; then + cat "${match_path}" >&2 + fail "unexpected error output detected in ${file_path}" + fi + rm -f "${match_path}" +} + +collect_warning_lines() { + local file_path="$1" + local output_path="$2" + grep -En '(^W[0-9]{4}[[:space:]])|(^WARNING[:[:space:]])|([Ww]arning)' "${file_path}" >"${output_path}" || true +} + +run_deployer() { + local deployer_path="$1" + local user_data_dir="$2" + local shared_data_dir="$3" + local stdout_path="$4" + local stderr_path="$5" + "${deployer_path}" --build "${user_data_dir}" "${shared_data_dir}" >"${stdout_path}" 2>"${stderr_path}" +} + +run_api_console_script() { + local api_console_path="$1" + local shared_data_dir="$2" + local user_data_dir="$3" + local input_path="$4" + local stdout_path="$5" + local stderr_path="$6" + RIME_SHARED_DATA_DIR="${shared_data_dir}" \ + RIME_USER_DATA_DIR="${user_data_dir}" \ + "${api_console_path}" <"${input_path}" >"${stdout_path}" 2>"${stderr_path}" +} diff --git a/others/script/smoke/run.sh b/others/script/smoke/run.sh new file mode 100644 index 0000000..953ed31 --- /dev/null +++ b/others/script/smoke/run.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" +export SMOKE_ROOT="${script_dir}" +export SMOKE_REPO_ROOT="$(cd "${SMOKE_ROOT}/../../.."; pwd)" +export SMOKE_WORK_ROOT="${SMOKE_WORK_ROOT:-${RUNNER_TEMP:-/tmp}/rime-ice-smoke}" +export SMOKE_LOG_ROOT="${SMOKE_LOG_ROOT:-${SMOKE_WORK_ROOT}/logs}" + +source "${SMOKE_ROOT}/lib/common.sh" + +suite_name="${1:-rime_ice}" + +case "${suite_name}" in + rime_ice) + source "${SMOKE_ROOT}/suites/config_repo.sh" + run_config_repo_suite "${suite_name}" + ;; + *) + fail "unknown smoke suite: ${suite_name}" + ;; +esac diff --git a/others/script/smoke/suites/config_repo.sh b/others/script/smoke/suites/config_repo.sh new file mode 100644 index 0000000..570e373 --- /dev/null +++ b/others/script/smoke/suites/config_repo.sh @@ -0,0 +1,230 @@ +#!/bin/bash + +run_config_repo_suite() { + local suite_name="$1" + local suite_root="${SMOKE_ROOT}/cases/${suite_name}" + local work_root="${SMOKE_WORK_ROOT}/${suite_name}" + local log_root="${SMOKE_LOG_ROOT}/${suite_name}" + local cli_url="${RIME_CLI_URL:-}" + local config_root="${RIME_CONFIG_ROOT:-${SMOKE_REPO_ROOT}}" + local cli_paths_file="${work_root}/cli-paths.txt" + local deployer_path + local api_console_path + local user_data_dir="${work_root}/user-data" + local deploy_stdout="${log_root}/deploy.stdout.log" + local deploy_stderr="${log_root}/deploy.stderr.log" + local deploy_combined="${log_root}/deploy.combined.log" + local deploy_warnings="${log_root}/deploy.warnings.log" + + ensure_clean_dir "${work_root}" + ensure_clean_dir "${log_root}" + mkdir -p "${user_data_dir}" + + assert_file_exists "${config_root}/default.yaml" + assert_file_exists "${config_root}/${suite_name}.schema.yaml" + clean_config_artifacts "${config_root}" + + resolve_cli_commands "${cli_url}" "${work_root}" >"${cli_paths_file}" + deployer_path="$(sed -n '1p' "${cli_paths_file}")" + api_console_path="$(sed -n '2p' "${cli_paths_file}")" + + log_step "using config root ${config_root}" + log_step "running deployer" + if ! run_deployer "${deployer_path}" "${user_data_dir}" "${config_root}" "${deploy_stdout}" "${deploy_stderr}"; then + combine_logs "${deploy_stdout}" "${deploy_stderr}" "${deploy_combined}" + set_failure_context "deployment" "${deploy_combined}" + fail "rime_deployer exited with non-zero status" + fi + + combine_logs "${deploy_stdout}" "${deploy_stderr}" "${deploy_combined}" + set_failure_context "deployment" "${deploy_combined}" + assert_no_error_lines "${deploy_combined}" + collect_warning_lines "${deploy_combined}" "${deploy_warnings}" + assert_file_exists "${user_data_dir}/build/default.yaml" + assert_file_exists "${user_data_dir}/build/${suite_name}.schema.yaml" + log_pass "deployment passed" + + if [[ -s "${deploy_warnings}" ]]; then + log_warn "deployment warnings detected" + cat "${deploy_warnings}" + fi + clear_failure_context + + run_config_input_cases "${suite_root}/input_cases.tsv" "${api_console_path}" "${config_root}" "${user_data_dir}" "${log_root}" +} + +run_config_input_cases() { + local case_file="$1" + local api_console_path="$2" + local shared_data_dir="$3" + local user_data_dir="$4" + local log_root="$5" + local raw_line + local pending_schema_id="" + local pending_case_file="${log_root}/pending_cases.tsv" + + [[ -f "${case_file}" ]] || fail "case file not found: ${case_file}" + : >"${pending_case_file}" + + while IFS= read -r raw_line || [[ -n "${raw_line}" ]]; do + if [[ -z "${raw_line}" ]]; then + flush_schema_case_group \ + "${pending_schema_id}" \ + "${pending_case_file}" \ + "${api_console_path}" \ + "${shared_data_dir}" \ + "${user_data_dir}" \ + "${log_root}" + pending_schema_id="" + : >"${pending_case_file}" + continue + fi + if [[ "${raw_line}" == \#* ]]; then + flush_schema_case_group \ + "${pending_schema_id}" \ + "${pending_case_file}" \ + "${api_console_path}" \ + "${shared_data_dir}" \ + "${user_data_dir}" \ + "${log_root}" + pending_schema_id="" + : >"${pending_case_file}" + printf '%s\n' "${raw_line}" + continue + fi + + local case_id + local schema_id + local key_sequence + local expected_text + IFS=$'\t' read -r case_id schema_id key_sequence expected_text <<<"${raw_line}" + if [[ -z "${case_id}" ]]; then + continue + fi + + if [[ -n "${pending_schema_id}" && "${schema_id}" != "${pending_schema_id}" ]]; then + flush_schema_case_group \ + "${pending_schema_id}" \ + "${pending_case_file}" \ + "${api_console_path}" \ + "${shared_data_dir}" \ + "${user_data_dir}" \ + "${log_root}" + : >"${pending_case_file}" + fi + + pending_schema_id="${schema_id}" + printf '%s\t%s\t%s\n' "${case_id}" "${key_sequence}" "${expected_text}" >>"${pending_case_file}" + done <"${case_file}" + + flush_schema_case_group \ + "${pending_schema_id}" \ + "${pending_case_file}" \ + "${api_console_path}" \ + "${shared_data_dir}" \ + "${user_data_dir}" \ + "${log_root}" +} + +flush_schema_case_group() { + local schema_id="$1" + local case_group_file="$2" + local api_console_path="$3" + local shared_data_dir="$4" + local user_data_dir="$5" + local log_root="$6" + + [[ -n "${schema_id}" ]] || return 0 + [[ -s "${case_group_file}" ]] || return 0 + + local group_name="${schema_id}" + local group_input_path="${log_root}/${group_name}.group.input.txt" + local group_stdout_path="${log_root}/${group_name}.group.stdout.log" + local group_stderr_path="${log_root}/${group_name}.group.stderr.log" + local group_combined_path="${log_root}/${group_name}.group.combined.log" + local group_normalized_path="${log_root}/${group_name}.group.normalized.log" + + build_schema_group_input "${schema_id}" "${case_group_file}" "${group_input_path}" + log_step "running schema group ${schema_id}" + if ! run_api_console_script "${api_console_path}" "${shared_data_dir}" "${user_data_dir}" "${group_input_path}" "${group_stdout_path}" "${group_stderr_path}"; then + combine_logs "${group_stdout_path}" "${group_stderr_path}" "${group_combined_path}" + set_failure_context "schema group ${schema_id}" "${group_combined_path}" + fail "rime_api_console exited with non-zero status" + fi + + combine_logs "${group_stdout_path}" "${group_stderr_path}" "${group_combined_path}" + set_failure_context "schema group ${schema_id}" "${group_combined_path}" + assert_no_error_lines "${group_combined_path}" + normalize_console_output "${group_stdout_path}" "${group_normalized_path}" + clear_failure_context + + assert_schema_group_cases "${schema_id}" "${case_group_file}" "${group_normalized_path}" "${group_combined_path}" "${log_root}" +} + +build_schema_group_input() { + local schema_id="$1" + local case_group_file="$2" + local output_path="$3" + local case_id + local key_sequence + local expected_text + + : >"${output_path}" + while IFS=$'\t' read -r case_id key_sequence expected_text; do + [[ -n "${case_id}" ]] || continue + printf 'select schema %s\n%s\n' "${schema_id}" "${key_sequence}" >>"${output_path}" + done <"${case_group_file}" + printf 'exit\n' >>"${output_path}" +} + +extract_case_output_block() { + local normalized_path="$1" + local schema_id="$2" + local occurrence="$3" + local output_path="$4" + awk -v marker="selected schema: [${schema_id}]" -v target="${occurrence}" ' + $0 == marker { + count++ + if (count == target) { + printing = 1 + } else if (count > target && printing) { + exit + } + } + printing { + print + } + ' "${normalized_path}" >"${output_path}" +} + +assert_schema_group_cases() { + local schema_id="$1" + local case_group_file="$2" + local group_normalized_path="$3" + local group_combined_path="$4" + local log_root="$5" + local case_index=0 + local case_id + local key_sequence + local expected_text + local resolved_expected_text + + while IFS=$'\t' read -r case_id key_sequence expected_text; do + [[ -n "${case_id}" ]] || continue + ((case_index += 1)) + resolved_expected_text="$(resolve_expected_text "${expected_text}")" + + local case_stdout_path="${log_root}/${case_id}.stdout.log" + extract_case_output_block "${group_normalized_path}" "${schema_id}" "${case_index}" "${case_stdout_path}" + + set_failure_context "case ${case_id}" "${group_combined_path}" + assert_file_contains "${case_stdout_path}" "selected schema: [${schema_id}]" + if [[ "${expected_text}" == @regex:* ]]; then + assert_file_matches "${case_stdout_path}" "${expected_text#@regex:}" + else + assert_file_contains "${case_stdout_path}" "commit: ${resolved_expected_text}" + fi + log_pass "input case passed: ${case_id}" + clear_failure_context + done <"${case_group_file}" +}