diff --git a/README.md b/README.md index e93b4532..b3b8a594 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/ShadowsocksX-NG2/README.md b/ShadowsocksX-NG2/README.md index 2991bae5..ec63e0b5 100644 --- a/ShadowsocksX-NG2/README.md +++ b/ShadowsocksX-NG2/README.md @@ -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/-<随机后缀>/`,开始与结束均输出结果绝对路径。`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 diff --git a/ShadowsocksX-NG2/Taskfile.dist.yml b/ShadowsocksX-NG2/Taskfile.dist.yml index 6bbe4929..bb449da7 100644 --- a/ShadowsocksX-NG2/Taskfile.dist.yml +++ b/ShadowsocksX-NG2/Taskfile.dist.yml @@ -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: