做一道拦住合并的门禁
目标
使用 shell 编写测试运行器、覆盖率门禁、重试和隔离机制,并将这些信号合并成一个是否允许合并的判定。
为什么这很重要
门禁的价值不在于放行,而在于阻止。因此,运行器必须准确统计失败,覆盖率比较不能在边界值上出错,重试也必须设有上限。无限重试是在掩盖失败;隔离机制则是一种折中,它避免不稳定测试阻塞整个团队,同时必须在报告中留下记录。最终报告之所以重要,是因为读取该判定并阻止合并的不是人,而是分支保护中的 required status checks 等自动化机制。数值与判定不一致的报告会让门禁失去意义。
步骤
- 创建
/root/ci2/run-tests.sh <디렉터리>并赋予执行权限。逐个运行参数所指定目录中的*.sh文件,最后用一行输出摘要passed=<수> failed=<수> skipped=<수>。全部通过时退出码为 0。测试文件可能没有执行权限,因此要使用bash "$f"的形式运行。 - 即使其中有失败的测试,也要继续运行其余测试直至结束,最后以非 0 状态码退出。若有 2 个通过、1 个失败,摘要应为
passed=2 failed=1。 - 收到空目录时,输出
passed=0 failed=0,退出码为 0。此时不要在目标目录内创建日志等文件。 - 创建
/root/ci2/gate-coverage.sh <커버리지파일> <임계값>。覆盖率文件只有一行,格式为covered=80 total=100。百分比为covered * 100 / total;达到或超过阈值时退出码为 0,低于阈值时返回非 0 状态码(相等时通过)。失败时输出实际百分比值。 - 创建
/root/ci2/retry.sh <최대시도횟수> <명령...>。命令成功后立即停止并返回 0;达到最大次数后仍全部失败,则返回非 0 状态码。实际尝试次数绝不能超过最大次数。 - 如果目标目录中存在
quarantine.txt,则其中列出的文件名(每行一个)不执行,而计入skipped。被隔离的测试不计入failed;其余测试全部通过时,退出码为 0。 - 创建
/root/ci2/gate.sh <테스트디렉터리> <커버리지파일> <임계값>。只有测试和覆盖率两个条件都通过时才返回退出码 0;任一条件失败都返回非 0 状态码。 - 创建
/root/ci2/gate-report.json。字段包括tests.passed、tests.failed、tests.skipped、coverage.pct、coverage.threshold、coverage.ok(布尔值 true/false)以及decision(字符串merge或block)。如果 failed 大于等于 1,或者 pct 低于 threshold,则 decision 必须为block;其他情况必须为merge。
参考
- 摘要行会在移除空格后接受检查,因此不要在等号两侧加入空格:
passed=2 failed=0 skipped=0。 - 如果在中间进度中多次输出
passed=,检查会混淆。汇总行只能在最后输出一次。 - 评分创建的测试文件没有执行权限。如果直接用
"$f"执行,全部都会被统计为失败。 - 使用 shell 整数运算比较覆盖率会截断小数,在边界处产生错误。请用
awk进行浮点数比较。 - 常见错误:第一次失败时运行器就退出,导致无法统计其余测试;将隔离测试计入 failed;手工填写 decision,导致它与数值不一致。
创建测试运行器
创建 /root/ci2/run-tests.sh <디렉터리> 并赋予执行权限。逐个运行参数所指定目录中的 *.sh 文件,最后用一行输出摘要 passed=<수> failed=<수> skipped=<수>。全部通过时退出码为 0。测试文件可能没有执行权限,因此要使用 bash "$f" 的形式运行。
调用方式为 /root/ci2/run-tests.sh <디렉터리>。逐个运行目录中的 *.sh,最后只输出一行摘要。评分创建的测试文件没有执行权限,因此必须使用 bash "$f" 运行。
即使失败也统计到最后
即使其中有失败的测试,也要继续运行其余测试直至结束,最后以非 0 状态码退出。若有 2 个通过、1 个失败,摘要应为 passed=2 failed=1。
第一次失败时就停止,便无法看到全貌。请运行到最后,再以非 0 状态码退出。如果开启 set -e 后直接调用测试,运行器本身会终止。请使用 if bash "$f"; then ... else ... fi 直接处理退出码。
安全处理空输入
收到空目录时,输出 passed=0 failed=0,退出码为 0。此时不要在目标目录内创建日志等文件。
没有任何测试并不算失败。当 glob 未匹配任何内容时,请检查文件是否存在,避免把 *.sh 字符串本身作为文件名传入。不要在目标目录中创建日志文件。
覆盖率阈值门禁
创建 /root/ci2/gate-coverage.sh <커버리지파일> <임계값>。覆盖率文件只有一行,格式为 covered=80 total=100。百分比为 covered * 100 / total;达到或超过阈值时退出码为 0,低于阈值时返回非 0 状态码(相等时通过)。失败时输出实际百分比值。
格式为 gate-coverage.sh <파일> <임계값>,文件中只有一行 covered=80 total=100。边界值(相等时)应通过。使用整数除法会在边界处出错,请像 awk 'BEGIN{exit !(p+0 >= t+0)}' 这样进行浮点数比较。
有上限的重试
创建 /root/ci2/retry.sh <최대시도횟수> <명령...>。命令成功后立即停止并返回 0;达到最大次数后仍全部失败,则返回非 0 状态码。实际尝试次数绝不能超过最大次数。
格式为 retry.sh <최대횟수> <명령...>。成功后立即停止,绝不能超过最大次数。无限重试不是稳定化,而是在掩盖失败,因此全部尝试失败后必须返回非 0 状态码。
隔离不稳定测试
如果目标目录中存在 quarantine.txt,则其中列出的文件名(每行一个)不执行,而计入 skipped。被隔离的测试不计入 failed;其余测试全部通过时,退出码为 0。
目标目录的 quarantine.txt 中每行写有一个文件名。不要执行这些文件,而应计入 skipped。隔离测试不计入 failed,因此其余测试通过时退出码为 0。
复合门禁
创建 /root/ci2/gate.sh <테스트디렉터리> <커버리지파일> <임계값>。只有测试和覆盖率两个条件都通过时才返回退出码 0;任一条件失败都返回非 0 状态码。
gate.sh <테스트디렉터리> <커버리지파일> <임계값> 只需调用前面创建的两个脚本并合并判定。任一失败即为失败。将各次调用的退出码保存到变量中,还可以输出是哪一项阻止了通过。
判定与数值一致的报告
创建 /root/ci2/gate-report.json。字段包括 tests.passed、tests.failed、tests.skipped、coverage.pct、coverage.threshold、coverage.ok(布尔值 true/false)以及 decision(字符串 merge 或 block)。如果 failed 大于等于 1,或者 pct 低于 threshold,则 decision 必须为 block;其他情况必须为 merge。
/root/ci2/gate-report.json 中的 decision 必须从数值推导。如果 failed 大于等于 1,或 pct 低于 threshold,则为 block,否则为 merge。coverage.ok 应为 JSON 布尔值(true/false),而不是字符串。