test: add rime smoke framework (#1511)

* test: add config smoke framework
* ci: add smoke test before packing
* ci: add cache
This commit is contained in:
mirtlecn
2026-03-26 13:02:38 +08:00
committed by GitHub
parent 4e54d7edb1
commit fbcd597726
8 changed files with 862 additions and 51 deletions

View File

@@ -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

View File

@@ -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
fi

37
.github/workflows/test.yml vendored Normal file
View File

@@ -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

View File

@@ -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:<date format>`, for example `@today:%Y-%m-%d`
- `@regex:<pattern>`, 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.

View File

@@ -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ē)夫
Can't render this file because it has a wrong number of fields in line 4.

View File

@@ -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}"
}

View File

@@ -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

View File

@@ -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}"
}