build: add fail-fast validation task with retained evidence

This commit is contained in:
Qiu Yuzhou
2026-10-19 03:24:22 +08:00
parent e0db8b1e13
commit b8c6f4d5b4
3 changed files with 120 additions and 0 deletions
+2
View File
@@ -13,6 +13,8 @@
旧版工程源码已从本仓库移除,归档在 [shadowsocks/ShadowsocksX-NG](https://github.com/shadowsocks/ShadowsocksX-NG) 的 [`legacy-v1`](https://github.com/shadowsocks/ShadowsocksX-NG/tree/legacy-v1) 分支,仅供参考,不保证可构建;其功能说明与使用文档见 [legacy-v1 的 README](https://github.com/shadowsocks/ShadowsocksX-NG/blob/legacy-v1/README.md)。
构建与统一验证入口见 [`ShadowsocksX-NG2/README.md` 的「开发、构建与测试」](ShadowsocksX-NG2/README.md#开发构建与测试)。
## 开发钩子
新 clone 后,在仓库根目录启用 Git hooks:
+18
View File
@@ -79,6 +79,24 @@ ShadowsocksX-NG 的现代化重写版本。新工程的全部源码与构建配
## 开发、构建与测试
统一验证入口(从仓库根目录调用):
```bash
task --exit-code --taskfile ShadowsocksX-NG2/Taskfile.dist.yml validate
# 按指定顺序只运行所需关卡:
task --exit-code --taskfile ShadowsocksX-NG2/Taskfile.dist.yml validate -- lint test
```
在本目录内可用 `task --exit-code validate`。从任意目录可用 `task --exit-code --taskfile /path/to/ShadowsocksX-NG/ShadowsocksX-NG2/Taskfile.dist.yml validate`。入口按 Taskfile 所在位置定位工程目录,内部调用发行 Taskfile 中的单项任务。
默认按 `lint → test → test:smoke → build` 执行,也可按指定顺序选择子集,每关最多一次;任一关失败立即停止,后续关标记 `not_run`,进程返回非零。`--exit-code` 让 Task 透传关卡退出码;省略时 Task 仍返回非零,但可能使用自己的错误码。入口不自动重试,不修改测试并行设置,也不执行会修改源码的格式化。
每次运行创建独立的 `.build/validation/<UTC时间>-<随机后缀>/`,开始与结束均输出结果绝对路径。`summary.txt` 保存各关状态、退出码与证据状态;每个已执行关卡在同名子目录下保留合并 stdout/stderr 的 `raw.log` 和 `status.txt`(`test:smoke` 对应 `test-smoke/`),Xcode 关卡显式保存 `result.xcresult`。控制台只显示进度和摘要,需要完整错误时直接读取对应日志。所有产物均在现有忽略目录内;`task clean` 会删除它们,需要长期留存时先复制出去。
测试关通过本机 `xcrun xcresulttool get test-results summary` 和系统 `plutil` 尽力读取计数,无需新增脚本或工具依赖。`test-summary.json` 保存原始摘要,`test-summary.stderr.log` 保存 xcresulttool 错误,`count-reader.log` 保存计数读取错误。汇总展示通过、失败、跳过、预期失败及总数(Xcode 汇总口径,参数化测试的实际执行次数可能不同)。关卡与总体退出码由原命令决定,命令失败始终保留失败。结果包缺失或摘要不可读时显著标注 `evidence incomplete`(证据不完整)、计数未知,不改变命令结果,也不把缺失计数写成零。构建关不读取测试计数;未执行的关卡不等同于测试跳过。
统一入口成功表示所选关卡的命令通过,仍需核对证据是否完整;hostless、真实进程 smoke、原生 UI 和真实系统代理的验收边界仍分别成立,发布打包门槛见下文。
在本目录内,日常格式化、静态检查、工程生成、构建和测试优先使用 `Taskfile.dist.yml` 定义的任务:
```bash
+100
View File
@@ -7,6 +7,103 @@ vars:
DERIVED_DATA_DIR: "{{ .BUILD_DIR }}/derivedData"
tasks:
validate:
desc: 'Run validation gates in order and retain logs/results; stop on first failure'
dir: '{{.TASKFILE_DIR}}'
silent: true
vars:
GATES: '{{default "lint test test:smoke build" .CLI_ARGS | trim}}'
RUN_DIR:
sh: |
mkdir -p {{.BUILD_DIR | shellQuote}}/validation
mktemp -d {{.BUILD_DIR | shellQuote}}/validation/$(date -u +%Y%m%dT%H%M%SZ)-XXXXXX
preconditions:
- sh: '{{if regexMatch "^(lint|test|test:smoke|build)( (lint|test|test:smoke|build))*$" .GATES}}true{{else}}false{{end}}'
msg: 'Choose validation gates: lint, test, test:smoke, build (space separated).'
- sh: '{{if eq (len (splitList " " .GATES)) (len (uniq (splitList " " .GATES)))}}true{{else}}false{{end}}'
msg: 'Choose each validation gate at most once.'
cmds:
- defer:
task: validate:summary
vars:
RUN_DIR: '{{.RUN_DIR}}'
GATES: '{{.GATES}}'
CODE: '{{default "0" .EXIT_CODE}}'
- "printf '%s\n' {{printf \"Validation results: %s\" .RUN_DIR | shellQuote}}"
- for: {var: GATES, split: ' '}
task: validate:gate
vars:
RUN_DIR: '{{.RUN_DIR}}'
GATE: '{{.ITEM}}'
validate:gate:
internal: true
dir: '{{.TASKFILE_DIR}}'
silent: true
vars:
GATE_DIR: '{{.RUN_DIR}}/{{.GATE | replace ":" "-"}}'
cmds:
- mkdir -p {{.GATE_DIR | shellQuote}}
- defer:
task: validate:result
vars:
GATE: '{{.GATE}}'
GATE_DIR: '{{.GATE_DIR}}'
CODE: '{{default "0" .EXIT_CODE}}'
- "printf '%s\n' {{printf \"[%s] running; log: %s/raw.log\" .GATE .GATE_DIR | shellQuote}}"
- >-
task --exit-code --taskfile {{.TASKFILE_DIR | shellQuote}}/Taskfile.dist.yml {{.GATE | shellQuote}}
RESULT_BUNDLE_PATH={{.GATE_DIR | shellQuote}}/result.xcresult
> {{.GATE_DIR | shellQuote}}/raw.log 2>&1
validate:result:
internal: true
silent: true
cmds:
- |
status=failed
if [ {{.CODE | shellQuote}} = 0 ]; then status=passed; fi
echo "[{{.GATE}}] $status; exit={{.CODE}}" > {{.GATE_DIR | shellQuote}}/status.txt
- cmd: |
{{if ne .GATE "lint"}}
if [ ! -d {{.GATE_DIR | shellQuote}}/result.xcresult ]; then
echo '[{{.GATE}}] evidence incomplete: result bundle missing; test counts unknown' >> {{.GATE_DIR | shellQuote}}/status.txt
{{if ne .GATE "build"}}
elif xcrun xcresulttool get test-results summary --path {{.GATE_DIR | shellQuote}}/result.xcresult > {{.GATE_DIR | shellQuote}}/test-summary.json 2> {{.GATE_DIR | shellQuote}}/test-summary.stderr.log; then
if passed=$(plutil -extract passedTests raw -expect integer {{.GATE_DIR | shellQuote}}/test-summary.json) &&
failed=$(plutil -extract failedTests raw -expect integer {{.GATE_DIR | shellQuote}}/test-summary.json) &&
skipped=$(plutil -extract skippedTests raw -expect integer {{.GATE_DIR | shellQuote}}/test-summary.json) &&
expected=$(plutil -extract expectedFailures raw -expect integer {{.GATE_DIR | shellQuote}}/test-summary.json) &&
total=$(plutil -extract totalTestCount raw -expect integer {{.GATE_DIR | shellQuote}}/test-summary.json); then
echo "[{{.GATE}}] tests: passed=$passed failed=$failed skipped=$skipped expectedFailures=$expected total=$total" >> {{.GATE_DIR | shellQuote}}/status.txt
else
printf '%s\n' "$passed" "$failed" "$skipped" "$expected" "$total" >> {{.GATE_DIR | shellQuote}}/count-reader.log
echo '[{{.GATE}}] evidence incomplete: invalid test summary; test counts unknown (see count-reader.log)' >> {{.GATE_DIR | shellQuote}}/status.txt
fi 2> {{.GATE_DIR | shellQuote}}/count-reader.log
else
echo '[{{.GATE}}] evidence incomplete: xcresulttool failed; test counts unknown (see test-summary.stderr.log)' >> {{.GATE_DIR | shellQuote}}/status.txt
{{end}}
fi
{{end}}
- cat {{.GATE_DIR | shellQuote}}/status.txt
validate:summary:
internal: true
silent: true
cmds:
- |
echo 'Validation command exit={{.CODE}}' > {{.RUN_DIR | shellQuote}}/summary.txt
echo 'Selected gates: {{.GATES}}' >> {{.RUN_DIR | shellQuote}}/summary.txt
- for: {var: GATES, split: ' '}
cmd: |
if [ -f {{.RUN_DIR | shellQuote}}/{{.ITEM | replace ":" "-"}}/status.txt ]; then
cat {{.RUN_DIR | shellQuote}}/{{.ITEM | replace ":" "-"}}/status.txt >> {{.RUN_DIR | shellQuote}}/summary.txt
else
echo '[{{.ITEM}}] not_run' >> {{.RUN_DIR | shellQuote}}/summary.txt
fi
- cat {{.RUN_DIR | shellQuote}}/summary.txt
- "printf '%s\n' {{printf \"Summary: %s/summary.txt\" .RUN_DIR | shellQuote}}"
gen:prj:
desc: 'Generate xcode project by xcodegen'
cmds:
@@ -24,6 +121,7 @@ tasks:
-configuration Debug
-destination 'platform=macOS,arch=arm64'
-derivedDataPath {{ .DERIVED_DATA_DIR }}
{{if .RESULT_BUNDLE_PATH}}-resultBundlePath {{.RESULT_BUNDLE_PATH | shellQuote}}{{end}}
build
build:release:
@@ -68,6 +166,7 @@ tasks:
-scheme ShadowsocksX-NG2
-destination 'platform=macOS,arch=arm64'
-derivedDataPath {{ .DERIVED_DATA_DIR }}
{{if .RESULT_BUNDLE_PATH}}-resultBundlePath {{.RESULT_BUNDLE_PATH | shellQuote}}{{end}}
test
test:binaries:
@@ -92,6 +191,7 @@ tasks:
-scheme ShadowsocksX-NG2Smoke
-destination 'platform=macOS,arch=arm64'
-derivedDataPath {{ .DERIVED_DATA_DIR }}
{{if .RESULT_BUNDLE_PATH}}-resultBundlePath {{.RESULT_BUNDLE_PATH | shellQuote}}{{end}}
test
clean: