8 Commits

Author SHA1 Message Date
Mirtle
07eca7256d chore: update docs and bump version to 2026-03-26 2026-03-26 18:41:18 +08:00
Mirtle
ff1da70fbd chore: clean code 2026-03-26 17:52:31 +08:00
mirtlecn
17a04afe39 test: add lint scripts. (#1512)
* ci: add lint scripts
* fix(lint): handle luacheck summary with ansi colors
* fix(ci): use luarocks to install luacheck
* fix(ci): add sudo
* test: 调整用例顺序
2026-03-26 17:20:57 +08:00
Mirtle
3a3e76c82d ci: run only when repo is iDvel/rime-ice 2026-03-26 13:57:40 +08:00
Mirtle
dd67c351d0 fix: require opt-in for destructive smoke cleanup 2026-03-26 13:56:56 +08:00
mirtlecn
fbcd597726 test: add rime smoke framework (#1511)
* test: add config smoke framework
* ci: add smoke test before packing
* ci: add cache
2026-03-26 13:02:38 +08:00
Mi Ramon
4e54d7edb1 fix(lua): 修正 PM / AM 的时间展示 (#1507) 2026-03-23 11:31:57 +08:00
Karlbaey
20b21a0ebe fix(lua): 修复时区问题(#1501)
* fix: 修复时区错误显示的问题
2026-03-21 03:24:58 +08:00
24 changed files with 1311 additions and 74 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,49 @@ 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: Install lint dependencies
run: |
python3 -m pip install yamllint
sudo apt install -y luarocks
sudo luarocks install luacheck
- 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 lint
run: make -C others/script lint
- 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: make -C others/script smoke
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 +92,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 +115,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 +144,4 @@ jobs:
git push
else
echo "No changes to commit."
fi
fi

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

@@ -0,0 +1,47 @@
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
if: github.repository == 'iDvel/rime-ice'
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install lint dependencies
run: |
python3 -m pip install yamllint
sudo apt install -y luarocks
sudo luarocks install luacheck
- 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 lint
run: make -C others/script lint
- 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: make -C others/script smoke

View File

@@ -7,7 +7,7 @@
为了让这个 Lua 同时适配全拼与双拼,使用 `spelling_hints` 生成的 comment全拼拼音作为通用的判断条件。
感谢大佬@[Shewer Lu](https://github.com/shewer)提供的思路。
容错词在 cn_dicts/others.dict.yaml 中,有新增建议可以提个 issue
--]]

View File

@@ -61,11 +61,11 @@ function M.func(input, seg, env)
yield_cand(seg, os.date('%H:%M', current_time))
yield_cand(seg, os.date('%H:%M:%S', current_time))
yield_cand(seg, period_name .. " " .. os.date("%H:%M", current_time))
yield_cand(seg, os.date("%H:%M %p", current_time))
yield_cand(seg, period_name .. " " .. os.date("%I:%M", current_time))
yield_cand(seg, os.date("%I:%M %p", current_time))
-- 带上时间划分时,很少有带秒数的,暂时注释掉
-- yield_cand(seg, period_name .. " " .. os.date("%H:%M:%S", current_time))
-- yield_cand(seg, os.date("%H:%M:%S %p", current_time))
-- yield_cand(seg, period_name .. " " .. os.date("%I:%M:%S", current_time))
-- yield_cand(seg, os.date("%I:%M:%S %p", current_time))
-- 星期
elseif (input == M.week) then
@@ -76,10 +76,21 @@ function M.func(input, seg, env)
yield_cand(seg, '礼拜' .. text)
yield_cand(seg, '' .. text)
-- ISO 8601/RFC 3339 的时间格式 (固定东八区)(示例 2022-01-07T20:42:51+08:00
-- ISO 8601/RFC 3339 的时间格式
elseif (input == M.datetime) then
local current_time = os.time()
yield_cand(seg, os.date('%Y-%m-%dT%H:%M:%S+08:00', current_time))
-- 获取偏移量,例如 "+0800" 或 "+0000"
local tz = os.date("%z", current_time)
-- 格式化时区
-- 如果是零时区 (+0000/-0000),根据规范输出 "Z"
-- 否则将 "+0800" 转换为 "+08:00"
local iso_tz = (tz == "+0000" or tz == "-0000")
and "Z"
or tz:gsub("(%d%d)$", ":%1")
yield_cand(seg, os.date('%Y-%m-%dT%H:%M:%S', current_time) .. iso_tz)
yield_cand(seg, os.date('%Y-%m-%d %H:%M:%S', current_time))
yield_cand(seg, os.date('%Y%m%d%H%M%S', current_time))
@@ -116,7 +127,7 @@ function M.func(input, seg, env)
-- suffix = "rd"
-- end
yield_cand(seg, string.format("%d %s %s", day, month_names_long[month], year)) -- en_UK
yield_cand(seg, string.format("%d %s %s", day, month_names_long[month], year)) -- en_UK
yield_cand(seg, string.format("%s %d, %s", month_names_long[month], day, year)) -- en_US
end

View File

@@ -1,6 +1,5 @@
-- 英文词条上屏自动添加空格
-- 在 engine/filters 的倒数第二个位置,增加 - lua_filter@*en_spacer
--
-- 英文后,再输入英文单词(必须为候选项)自动添加空格
local F = {}

View File

@@ -63,8 +63,6 @@ end
-- 数值转换为中文
local function number2cnChar(num, flag, digitUnit, wordFigure) --flag=0中文小写反之为大写
local result = ""
if tonumber(flag) < 1 then
digitUnit = digitUnit or { [1] = "", [2] = "亿" }
wordFigure = wordFigure or { [1] = "", [2] = "", [3] = "", [4] = "" }
@@ -73,6 +71,7 @@ local function number2cnChar(num, flag, digitUnit, wordFigure) --flag=0中文小
wordFigure = wordFigure or { [1] = "", [2] = "", [3] = "", [4] = "" }
end
local lens = string.len(num)
local result
if lens < 5 then
result = formatNum(num, flag)
elseif lens < 9 then

View File

@@ -126,9 +126,9 @@ function M.init(env)
-- 额外处理包含空格的 preedit增加最后一个拼音的首字母和 zh, ch, sh 的简码
if preedit:find(" ") then
local preceding_part, last_part = preedit:match("^(.+)%s(%S+)$")
local p1, p2 = "", ""
local p2 = ""
-- p1 生成最后一个拼音的首字母简码拼写(最后一个空格后的首字母),如 ni hao 生成 nih
p1 = preceding_part:gsub(" ", "") .. last_part:sub(1, 1)
local p1 = preceding_part:gsub(" ", "") .. last_part:sub(1, 1)
-- p2 生成最后一个拼音的 zh, ch, sh 的简码拼写(最后一个空格后以 zh ch sh 开头),如 zhi chi 生成 zhich
if last_part:match("^[zcs]h") then
p2 = preceding_part:gsub(" ", "") .. last_part:sub(1, 2)

View File

@@ -45,9 +45,7 @@ local function update_dict_entry( s, code, mem, proj )
local code_convert = code:sub( i, i + 1 )
local p = proj:apply( code_convert, true )
if p and #p > 0 then code_convert = p end
if code_convert == 'dian' and pos[loop] then
-- Ignored
else
if code_convert ~= 'dian' or not pos[loop] then
table.insert( custom_code, code_convert )
end
loop = loop + 1
@@ -283,12 +281,12 @@ function f.func( input, env )
end
-- 上屏其余的候选
for i, cand in ipairs( long_word_cands ) do yield( cand ) end
if env.show_other_cands then for i, cand in ipairs( other_cand ) do yield( cand ) end end
for _, cand in ipairs( long_word_cands ) do yield( cand ) end
if env.show_other_cands then for _, cand in ipairs( other_cand ) do yield( cand ) end end
end
function f.tags_match( seg, env )
for i, v in ipairs( env.tag ) do if seg.tags[v] then return true end end
for _, v in ipairs( env.tag ) do if seg.tags[v] then return true end end
return false
end

View File

@@ -28,7 +28,7 @@ function select.func(key, env)
local selected_candidate = context:get_selected_candidate()
selected_candidate = selected_candidate and selected_candidate.text or input
local selected_char = ""
local selected_char
if (key:repr() == env.first_key) then
selected_char = selected_candidate:sub(1, utf8.offset(selected_candidate, 2) - 1)
elseif (key:repr() == env.last_key) then

View File

@@ -26,8 +26,8 @@ local function unicode(input, seg, env)
yield(Candidate("unicode", seg.start, seg._end, text, string.format("U%x", code)))
if code < 0x10000 then
for i = 0, 15 do
local text = utf8.char(code * 16 + i)
yield(Candidate("unicode", seg.start, seg._end, text, string.format("U%x~%x", code, i)))
local next_text = utf8.char(code * 16 + i)
yield(Candidate("unicode", seg.start, seg._end, next_text, string.format("U%x~%x", code, i)))
end
end
end

View File

@@ -2,11 +2,35 @@
除日常更新词库外的一些主要更新 🆕、破坏性变更 ⚠️。
---
*2026.03.26 Release*
## 2026-03-26
- test: 新增自动化测试和 yaml / lua 语法检查。用例覆盖主流程,在提交时运行。
## 2026-03-21
- fix(lua): 修复时区问题(#1501)
## 2026-03-11
- dict(radical):拆字词典覆盖拓展区 b - j 汉字
## 2026-03-08
- feat: 加入一些时间和日期的自定义展示 [#1485](https://github.com/iDvel/rime-ice/pull/1485)
- 中英日期触发关键字,全拼默认为 `rqzh``rqen`,双拼默认为 `datezh``dateen`
## 2026-01-26
- dict(symbol):删除了 V/v 模式中的 emoji 集合
## 2026-01-05
- docs: 添加在线试用链接,由 @Mintimate 友情构建
---
*2025.12.08 Release*

View File

@@ -53,6 +53,7 @@
### 金钱符号
¥ 人民币 日元 日币
$ 美元 美刀
¢ 美分
HK$ 港元 港币
MOP$ 澳门元 澳门币 葡币
S$ 新加坡元 新加坡币
@@ -63,7 +64,6 @@ S$ 新加坡元 新加坡币
₨ 卢比
₹ 印度卢比
### Smileys & Emotion
# face-smiling
😀 笑脸

24
others/script/Makefile Normal file
View File

@@ -0,0 +1,24 @@
.DEFAULT_GOAL := default
.PHONY: default build lint lint-yaml lint-lua lint-lua-format smoke
default:
@:
build:
go mod tidy && go run . --rime_path "$$(cd ../.. && pwd)" --auto_confirm
lint:
bash lint/run.sh all
lint-yaml:
bash lint/run.sh yaml-lint
lint-lua:
bash lint/run.sh lua-lint
lint-lua-format:
bash lint/run.sh lua-format-check
smoke:
bash ./smoke/run.sh rime_ice

View File

@@ -0,0 +1,37 @@
std = "lua54"
globals = {
"Candidate",
"Component",
"ConfigValue",
"DictEntry",
"KeyEvent",
"KeySequence",
"LevelDb",
"Memory",
"Opencc",
"Projection",
"ReverseLookup",
"Schema",
"Segment",
"Set",
"ShadowCandidate",
"Spans",
"Switcher",
"TableDb",
"UniquifiedCandidate",
"UserDb",
"yield",
"log",
"rime_api",
}
ignore = {
"111",
"121",
"142",
"143",
"211",
"212",
"631",
}

View File

@@ -0,0 +1,20 @@
extends: default
rules:
braces: disable
brackets: disable
colons: disable
commas: disable
comments: disable
comments-indentation: disable
document-start: disable
empty-lines:
max: 2
max-end: 1
max-start: 0
hyphens: disable
indentation:
indent-sequences: consistent
line-length: disable
new-line-at-end-of-file: disable
truthy: disable

View File

@@ -0,0 +1,44 @@
# YAML and Lua lint entrypoint
This directory contains the lightweight lint entrypoint used by local commands.
## Dependencies
Current stage:
- `yamllint`
- `luacheck`
Example install on macOS:
```bash
brew install yamllint luacheck
```
## Usage
From the repository root:
```bash
bash others/script/lint/run.sh yaml-lint
bash others/script/lint/run.sh lua-lint
bash others/script/lint/run.sh all
make -C others/script lint-yaml
make -C others/script lint-lua
make -C others/script lint
make -C others/script smoke
```
## Notes
- The current implementation enables YAML linting and Lua linting.
- The first stage checks repository-root business YAML files and root
`*.schema.yaml` files.
- `*.schema.yaml` files are preprocessed before linting so the `pin_cand_filter`
tab-separated list does not break generic YAML parsing.
- `lua-lint` runs `luacheck` with a repository-local configuration derived from
the librime-lua globals currently used by this repository.
- `lua-format-check` is reserved for a later stage.
- `smoke` mirrors the current CI smoke invocation and runs
`bash ./others/script/smoke/run.sh rime_ice` through Make.
- GitHub Actions are intentionally not changed in this stage.

230
others/script/lint/run.sh Normal file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
YAMLLINT_CONFIG="${SCRIPT_DIR}/.yamllint.yml"
LUACHECK_CONFIG="${SCRIPT_DIR}/.luacheckrc"
log() {
printf '[lint] %s\n' "$*"
}
log_step() {
printf '[lint] 🔹 %s\n' "$*"
}
log_pass() {
printf '[lint] ✅ %s\n' "$*"
}
log_warn() {
printf '[lint] ⚠️ %s\n' "$*"
}
log_fail() {
printf '[lint] 🔴 %s\n' "$*" >&2
}
find_business_yaml_files() {
(
cd "${REPO_ROOT}"
find . \
\( -path "./.git" -o -path "./.git/*" -o -path "./.github" -o -path "./.github/*" -o -path "./others" -o -path "./others/*" \) -prune \
-o \
\( -maxdepth 1 -type f \( -name "*.yaml" -o -name "*.yml" \) ! -name "*.dict.yaml" -print \)
) | sort
}
sanitize_schema_yaml() {
local source_file="$1"
local output_file="$2"
awk '
function indent_width(line) {
match(line, /^ */)
return RLENGTH
}
BEGIN {
in_pin_cand_filter = 0
pin_indent = -1
}
{
if (!in_pin_cand_filter) {
if ($0 ~ /^ *pin_cand_filter:[[:space:]]*$/) {
in_pin_cand_filter = 1
pin_indent = indent_width($0)
}
print
next
}
if ($0 ~ /^[[:space:]]*$/ || $0 ~ /^[[:space:]]*#/) {
print
next
}
current_indent = indent_width($0)
if (current_indent <= pin_indent) {
in_pin_cand_filter = 0
print
next
}
if ($0 ~ /^[[:space:]]*-[[:space:]]/) {
print substr($0, 1, current_indent) "- __PIN_CAND_FILTER_PLACEHOLDER__"
next
}
print
}
' "${source_file}" > "${output_file}"
}
require_tool() {
local tool_name="$1"
if ! command -v "${tool_name}" >/dev/null 2>&1; then
log_fail "missing required tool: ${tool_name}"
return 1
fi
}
run_yaml_lint() {
require_tool yamllint
local yaml_files=()
local temp_dir=''
local lint_status=0
local total_start_time
total_start_time="$(date +%s)"
while IFS= read -r yaml_file; do
yaml_files+=("${yaml_file}")
done < <(find_business_yaml_files)
if [[ "${#yaml_files[@]}" -eq 0 ]]; then
log_warn 'no business YAML files found'
return 0
fi
temp_dir="$(mktemp -d)"
trap 'if [[ -n "${temp_dir:-}" ]]; then rm -rf "${temp_dir}"; fi' RETURN
log_step "yaml-lint checking ${#yaml_files[@]} file(s)"
for yaml_file in "${yaml_files[@]}"; do
local lint_target="${yaml_file}"
local lint_output=''
local file_start_time
local file_elapsed
file_start_time="$(date +%s)"
log_step "yaml-lint checking ${yaml_file}"
if [[ "${yaml_file}" == *.schema.yaml ]]; then
lint_target="${temp_dir}/$(basename "${yaml_file}")"
sanitize_schema_yaml "${REPO_ROOT}/${yaml_file#./}" "${lint_target}"
fi
if ! lint_output="$(cd "${REPO_ROOT}" && yamllint -f parsable -c "${YAMLLINT_CONFIG}" "${lint_target}" 2>&1)"; then
lint_status=1
if [[ "${lint_target}" != "${yaml_file}" ]]; then
lint_output="${lint_output//${lint_target}/${yaml_file}}"
fi
printf '%s\n' "${lint_output}"
log_fail "yaml-lint failed for ${yaml_file}"
fi
file_elapsed="$(( $(date +%s) - file_start_time ))"
log_pass "yaml-lint finished ${yaml_file} in ${file_elapsed}s"
done
log_pass "yaml-lint completed in $(( $(date +%s) - total_start_time ))s"
return "${lint_status}"
}
run_lua_lint() {
require_tool luacheck
local lint_output=''
local lint_status=0
local last_line=''
local normalized_last_line=''
log_step 'lua-lint checking lua/**/*.lua'
set +e
lint_output="$(
(
cd "${REPO_ROOT}"
luacheck lua --no-default-config --config "${LUACHECK_CONFIG}" --codes --ranges
) 2>&1
)"
lint_status=$?
set -e
printf '%s\n' "${lint_output}"
last_line="$(printf '%s\n' "${lint_output}" | tail -n 1)"
normalized_last_line="$(printf '%s\n' "${last_line}" | sed -E $'s/\x1B\\[[0-9;]*[[:alpha:]]//g')"
if [[ "${lint_status}" -eq 0 ]]; then
log_pass 'lua-lint passed'
return 0
fi
if printf '%s\n' "${normalized_last_line}" | grep -Eq '0 errors?'; then
log_warn 'lua-lint completed with warnings'
return 0
fi
log_fail 'lua-lint completed with errors'
return 1
}
run_lua_format_check() {
log_warn 'lua-format-check is not enabled yet'
return 1
}
run_all() {
run_yaml_lint
run_lua_lint
}
usage() {
cat <<'EOF'
Usage: bash others/script/lint/run.sh <command>
Commands:
yaml-lint
lua-lint
lua-format-check
all
EOF
}
main() {
local command="${1:-}"
case "${command}" in
yaml-lint)
run_yaml_lint
;;
lua-lint)
run_lua_lint
;;
lua-format-check)
run_lua_format_check
;;
all)
run_all
;;
help|-h|--help)
usage
;;
*)
usage >&2
return 1
;;
esac
}
main "$@"

View File

@@ -0,0 +1,54 @@
# 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
- `SMOKE_ALLOW_DESTRUCTIVE=1`: required for local runs because the smoke suite removes `${RIME_CONFIG_ROOT:-repo}/build` and `${RIME_CONFIG_ROOT:-repo}/*.userdb`
## Destructive Cleanup
The smoke suite removes the following paths under `RIME_CONFIG_ROOT` before deployment:
- `build/`
- `*.userdb/`
This cleanup is allowed automatically in CI. Local runs must opt in explicitly:
```bash
SMOKE_ALLOW_DESTRUCTIVE=1 ./others/script/smoke/run.sh
```
## 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,224 @@
# ------------------
# 方案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} 一
插件:英文全大写 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 ig{space} 一个
基础:中文输入 double_pinyin wusspnyn{space} 雾凇拼音
基础:英文输入 double_pinyin hello{space} hello
基础:英文派生 double_pinyin PtwoP{space} P2P
基础:中英混输 double_pinyin Xgd{space} X光
基础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 ig{space} 一个
# 插件:英文降权 double_pinyin_abc bail{space} 不确定
# 插件:英文降权 double_pinyin_abc bail{2} 不确定
基础:中文输入 double_pinyin_abc wusspcyc{space} 雾凇拼音
基础:英文输入 double_pinyin_abc hello{space} hello
基础:英文派生 double_pinyin_abc PtwoP{space} P2P
基础:中英混输 double_pinyin_abc Xgt{space} X光
基础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 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 ig{space} 一个
基础:中文输入 double_pinyin_mspy wusspnyn{space} 雾凇拼音
基础:英文输入 double_pinyin_mspy hello{space} hello
基础:英文派生 double_pinyin_mspy PtwoP{space} P2P
基础:中英混输 double_pinyin_mspy Xgd{space} X光
基础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 ig{space} 一个
基础:中文输入 double_pinyin_sogou wusspnyn{space} 雾凇拼音
基础:英文输入 double_pinyin_sogou hello{space} hello
基础:英文派生 double_pinyin_sogou PtwoP{space} P2P
基础:中英混输 double_pinyin_sogou Xgd{space} X光
基础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 ig{space} 一个
基础:中文输入 double_pinyin_ziguang wushpyyy{space} 雾凇拼音
基础:英文输入 double_pinyin_ziguang hello{space} hello
基础:英文派生 double_pinyin_ziguang PtwoP{space} P2P
基础:中英混输 double_pinyin_ziguang Xgg{space} X光
基础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 ig{space} 一个
# 插件:英文降权 double_pinyin_jiajia bail{space} 不确定
# 插件:英文降权 double_pinyin_jiajia bail{2} 不确定
基础:中文输入 double_pinyin_jiajia wusyplyl{space} 雾凇拼音
基础:英文输入 double_pinyin_jiajia hello{space} hello
基础:英文派生 double_pinyin_jiajia PtwoP{space} P2P
基础:中英混输 double_pinyin_jiajia Xgh{space} X光
基础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 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 ig{space} 一个
基础Emoji输入 double_pinyin_flypy nihc{2} 👋
基础:中文输入 double_pinyin_flypy wusspbyb{space} 雾凇拼音
基础:英文输入 double_pinyin_flypy hello{space} hello
基础:英文派生 double_pinyin_flypy PtwoP{space} P2P
基础:中英混输 double_pinyin_flypy Xgl{space} X光
基础uU拆字反查 double_pinyin_flypy uUrenrenren{space} 众
基础v键符号输入 double_pinyin_flypy V1{space} 一
插件:英文全大写 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,273 @@
#!/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}"
}
require_destructive_cleanup_approval() {
local config_root="$1"
local build_dir="${config_root}/build"
if [[ "${CI:-}" == "true" || "${GITHUB_ACTIONS:-}" == "true" ]]; then
return 0
fi
if [[ "${SMOKE_ALLOW_DESTRUCTIVE:-}" == "1" ]]; then
log_warn "destructive cleanup approved by SMOKE_ALLOW_DESTRUCTIVE=1"
return 0
fi
fail "smoke test will remove ${build_dir} and ${config_root}/*.userdb; rerun with SMOKE_ALLOW_DESTRUCTIVE=1 for local execution, or run in CI"
}
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,231 @@
#!/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"
require_destructive_cleanup_approval "${config_root}"
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}"
}

View File

@@ -8,6 +8,14 @@
# 鼠须管界面配置指南: https://github.com/LEOYoon-Tsaw/Rime_collections/blob/master/鼠鬚管介面配置指南.md
# 鼠须管作者写的图形化的皮肤设计器: https://github.com/LEOYoon-Tsaw/Squirrel-Designer
# Squirrel 部分命令行选项:
# squirrel="/Library/Input Methods/Squirrel.app/Contents/MacOS/Squirrel"
# --quit退出所有 Squirrel 进程
# --reload重新部署
# --sync触发同步用户数据
# --build构建
# --help帮助输出帮助文本
# 要比共享目录的同名文件的 config_version 大才可以生效
config_version: '2026-01-31' # 对应的鼠须管版本: 1.1.2