P2-1 时长解析:后缀乘数只作用在整数部分,nsec 没跟着乘。 0.05m 被算成 50ms(应 3s)、1.5m 算成 60.5s(应 90s)、0.5h 算成 0.5s。 修法:sec 与 nsec 都乘 mult(各自带溢出检查,mult==1 行为不变)。 --help 与头注释的示例改为逐个标出等价值(2.5s=2.5s、1.5m=90s、0.5h=1800s), 不再留文档写着、实际算错的例子。 新增 T11:宿主上编译 parser 单测(27 例,含 0.01m/0.05m/1.5m/0.5h)真跑, 并单独点名评审抓到的三条,防止回归。 P2-2 文档数字:§2 产物表 47 708 → 49 676,合计 2 189 472 → 2 191 440; 全表按 MANIFEST 重核(其余七项未漂移),§11.1 补上新的体积/md5/sha256。 P2-3 信号失败不静默:新增 kill_child()(kill(-pgid) 失败时 ESRCH 视为组已消失, 退化为 kill(pid),EPERM 等明确告警)、verify_process_group()(发信号前用 getpgid 核对建组是否生效)、父进程 setpgid 的异常 errno 告警;连兜底 kill 都失败时退出码 125 而不是谎报 124。新增 -DTIMEOUT_TEST_SETPGID_FAIL 故障注入 与 T12(a-d)验证告警路径可达且兜底真的杀掉子进程。 真机复验(md5 aded39e0,49 676 B):124/137/0/孙进程清零/保证挂住的命令 3.01s 被 124 结束/0.01m=0.61s、0.05m=3.01s、2.5s=2.51s/无参 125;收工 bt=0 sleep=0, Music 199164544。回归 44 通过 0 失败。
220 lines
11 KiB
Bash
Executable File
220 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
||
# ============================================================================
|
||
# test_build_devkit.sh — build_devkit.sh 的回归测试(不重编)
|
||
#
|
||
# 耗时:热缓存约 15 秒;若需冷启动基线重建(首次运行/源码未缓存)约 1–2 分钟。
|
||
#
|
||
# 覆盖:
|
||
# r1 P1 回归点 —— stamp 命中(跳过编译)时,--with-sources 必须真正产出 src/
|
||
# r2 P2-1 —— licenses/README 被删后 --with-sources 必须恢复,
|
||
# --verify-only 必须拒绝缺许可证的交付
|
||
# MANIFEST 内容漂移检测、src/ 缺文件与内容漂移自愈
|
||
# M53 —— 本地源码路径:timeout 产物存在/静态/MIPS32r2/零 NEEDED、
|
||
# MANIFEST 有 Apache-2.0 行、license 原文随附、
|
||
# 源码纪律(独立实现、不调 clock_gettime、不用 timerfd、setpgid+kill(-pgid))
|
||
# M53 r2 —— 时长解析真跑回归(宿主编译 parser,覆盖 0.01m/0.05m/1.5m/0.5h)
|
||
# 与信号失败告警路径(注入 setpgid 失败,必须告警且兜底 kill(pid))
|
||
#
|
||
# 前置:MEOW_DEVKIT_WORK 里已有一次构建(脚本会先跑一次增量构建,命中则秒回;
|
||
# 缓存缺失时会联网下载/编译,第一次运行约 1 分钟)。
|
||
#
|
||
# 用法:bash scripts/build/test_build_devkit.sh [-v]
|
||
# 退出码 0 = 全过;非 0 = 用例失败数。
|
||
# ============================================================================
|
||
set -u
|
||
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
SCRIPT="$ROOT/scripts/build/build_devkit.sh"
|
||
export MEOW_DEVKIT_WORK="${MEOW_DEVKIT_WORK:-$HOME/.cache/meow-devkit}"
|
||
OUT="$ROOT/fw_apps/devkit"
|
||
TMP="$MEOW_DEVKIT_WORK/test-tmp"
|
||
|
||
VERBOSE=0
|
||
[ "${1:-}" = "-v" ] && VERBOSE=1
|
||
|
||
PASS=0
|
||
FAILN=0
|
||
pass() { PASS=$((PASS + 1)); printf 'PASS %s\n' "$*"; }
|
||
fail() { FAILN=$((FAILN + 1)); printf 'FAIL %s\n' "$*"; }
|
||
run() { # run <说明> <命令...>
|
||
local desc="$1"; shift
|
||
local log="$TMP/last.log"
|
||
if "$@" >"$log" 2>&1; then
|
||
[ "$VERBOSE" = 1 ] && sed 's/^/ | /' "$log"
|
||
return 0
|
||
fi
|
||
printf ' 命令失败: %s\n' "$*"
|
||
sed 's/^/ | /' "$log" | tail -20
|
||
return 1
|
||
}
|
||
|
||
mkdir -p "$TMP"
|
||
# 无论成败都恢复现场,别把测试的破坏留在产物目录里
|
||
restore() {
|
||
[ -f "$TMP/MANIFEST.txt.bak" ] && cp "$TMP/MANIFEST.txt.bak" "$OUT/MANIFEST.txt"
|
||
rm -rf "$TMP"
|
||
}
|
||
trap restore EXIT
|
||
|
||
echo "== 0) 确保基线构建(幂等;已构建则秒回)"
|
||
run "基线构建" bash "$SCRIPT" || { echo "!!! 基线构建失败,后续用例无意义"; exit 1; }
|
||
pass "基线构建"
|
||
|
||
echo "== 1) P1 回归:stamp 命中 + --with-sources 必须产出 src/"
|
||
rm -rf "$OUT/src"
|
||
run "普通重跑(应命中 stamp 且不建 src/)" bash "$SCRIPT" || fail "T1a 普通重跑"
|
||
[ ! -d "$OUT/src" ] && pass "T1a 普通重跑不创建 src/(符合预期)" || fail "T1a 普通重跑意外创建了 src/"
|
||
run "--with-sources(stamp 命中路径)" bash "$SCRIPT" --with-sources || fail "T1b --with-sources 退出码非 0"
|
||
for f in ncurses-6.5.tar.gz procps-ng-4.0.4.tar.xz lsof-4.99.4.tar.xz build_devkit.sh DEVKIT.md timeout.c; do
|
||
[ -s "$OUT/src/$f" ] && pass "T1b src/$f 存在且非空" || fail "T1b 缺 src/$f(P1 未修好)"
|
||
done
|
||
|
||
echo "== 2) 缺文件自愈:删掉构建脚本后再 --with-sources"
|
||
rm -f "$OUT/src/build_devkit.sh"
|
||
run "--with-sources 补齐" bash "$SCRIPT" --with-sources || fail "T2 --with-sources 退出码非 0"
|
||
[ -s "$OUT/src/build_devkit.sh" ] && pass "T2 src/build_devkit.sh 被补回" || fail "T2 未补回 src/build_devkit.sh"
|
||
|
||
echo "== 3) 内容漂移自愈:把 src 里的源码包改坏"
|
||
printf 'tamper\n' >> "$OUT/src/ncurses-6.5.tar.gz"
|
||
run "--with-sources 重铺" bash "$SCRIPT" --with-sources || fail "T3 --with-sources 退出码非 0"
|
||
if cmp -s "$OUT/src/ncurses-6.5.tar.gz" "$MEOW_DEVKIT_WORK/src/ncurses-6.5.tar.gz"; then
|
||
pass "T3 被改坏的源码包已恢复成钉住版本"
|
||
else
|
||
fail "T3 源码包未恢复"
|
||
fi
|
||
|
||
echo "== 4) --verify-only:正常通过"
|
||
run "--verify-only" bash "$SCRIPT" --verify-only && pass "T4a --verify-only 通过" || fail "T4a --verify-only 失败"
|
||
|
||
echo "== 5) --verify-only:能查出 MANIFEST 内容漂移"
|
||
cp "$OUT/MANIFEST.txt" "$TMP/MANIFEST.txt.bak"
|
||
awk 'BEGIN{done=0} { if (!done && $1=="vmstat") { $NF="0000000000000000000000000000000000000000000000000000000000000000"; done=1 } print }' \
|
||
"$TMP/MANIFEST.txt.bak" > "$OUT/MANIFEST.txt"
|
||
if run "--verify-only(MANIFEST 被篡改)" bash "$SCRIPT" --verify-only; then
|
||
fail "T5 --verify-only 未发现 MANIFEST sha 被篡改"
|
||
else
|
||
pass "T5 --verify-only 正确拒绝被篡改的 MANIFEST"
|
||
fi
|
||
cp "$TMP/MANIFEST.txt.bak" "$OUT/MANIFEST.txt"
|
||
run "--verify-only(恢复后)" bash "$SCRIPT" --verify-only && pass "T5b 恢复后重新通过" || fail "T5b 恢复后仍失败"
|
||
|
||
echo "== 6) 交付完整性:删 licenses/ 后 --with-sources 必须恢复"
|
||
LICENSES="procps-ng-4.0.4-COPYING procps-ng-4.0.4-COPYING.LIB ncurses-6.5-COPYING lsof-4.99.4-COPYING musl-COPYRIGHT meowos-timeout-LICENSE"
|
||
rm -rf "$OUT/licenses"
|
||
run "--with-sources 补 licenses" bash "$SCRIPT" --with-sources || fail "T6 --with-sources 退出码非 0"
|
||
for f in $LICENSES; do
|
||
[ -s "$OUT/licenses/$f" ] && pass "T6 licenses/$f 已恢复" || fail "T6 缺 licenses/$f"
|
||
done
|
||
|
||
echo "== 7) --verify-only 必须拒绝缺 licenses 的交付"
|
||
rm -rf "$OUT/licenses"
|
||
if run "--verify-only(缺 licenses)" bash "$SCRIPT" --verify-only; then
|
||
fail "T7 --verify-only 未发现 licenses/ 缺失"
|
||
else
|
||
pass "T7 --verify-only 正确拒绝缺 licenses 的交付"
|
||
fi
|
||
run "--with-sources 恢复" bash "$SCRIPT" --with-sources || fail "T7 --with-sources 恢复失败"
|
||
run "--verify-only(恢复后)" bash "$SCRIPT" --verify-only && pass "T7b 恢复后通过" || fail "T7b 恢复后仍失败"
|
||
|
||
echo "== 8) 交付完整性:删 README.txt 后 --with-sources 必须恢复"
|
||
rm -f "$OUT/README.txt"
|
||
run "--with-sources 补 README" bash "$SCRIPT" --with-sources || fail "T8 --with-sources 退出码非 0"
|
||
[ -s "$OUT/README.txt" ] && pass "T8 README.txt 已恢复" || fail "T8 README.txt 未恢复"
|
||
|
||
echo "== 9) timeout(本地源码路径):产物存在 + 静态/架构/零 NEEDED"
|
||
if [ -f "$OUT/bin/timeout" ]; then
|
||
pass "T9a bin/timeout 存在"
|
||
DESC="$(file -b "$OUT/bin/timeout")"
|
||
case "$DESC" in *"statically linked"*) pass "T9b 静态链接" ;; *) fail "T9b 非静态: $DESC" ;; esac
|
||
case "$DESC" in *"MIPS32 rel2"*) pass "T9c MIPS32 rel2" ;; *) fail "T9c 架构不符: $DESC" ;; esac
|
||
[ "$(readelf -d "$OUT/bin/timeout" 2>/dev/null | grep -c NEEDED)" = 0 ] \
|
||
&& pass "T9d readelf -d 零 NEEDED" || fail "T9d 有 NEEDED 依赖"
|
||
readelf -h "$OUT/bin/timeout" | grep -q mips32r2 \
|
||
&& pass "T9e ELF Flags 含 mips32r2" || fail "T9e ELF Flags 无 mips32r2"
|
||
else
|
||
fail "T9a 缺 $OUT/bin/timeout(本地源码没编进来)"
|
||
fi
|
||
ROW="$(awk '$1=="timeout" && $(NF-1) ~ /^[0-9]+$/ && length($NF)==64' "$OUT/MANIFEST.txt")"
|
||
if [ -n "$ROW" ]; then
|
||
pass "T9f MANIFEST 有 timeout 表格行(sha256 + 体积)"
|
||
case "$ROW" in *Apache-2.0*) pass "T9g MANIFEST 标注 Apache-2.0" ;; *) fail "T9g MANIFEST 未标 Apache-2.0: $ROW" ;; esac
|
||
else
|
||
fail "T9f MANIFEST 缺 timeout 表格行"
|
||
fi
|
||
head -2 "$OUT/licenses/meowos-timeout-LICENSE" 2>/dev/null | grep -q 'Apache License' \
|
||
&& pass "T9h licenses/meowos-timeout-LICENSE 是 Apache-2.0 原文" \
|
||
|| fail "T9h 缺 Apache-2.0 许可证原文"
|
||
|
||
echo "== 10) timeout 源码纪律:本仓独立实现,且不碰坏掉的 clock_gettime"
|
||
SRC_T="$ROOT/scripts/build/devkit-src/timeout.c"
|
||
grep -q 'SPDX-License-Identifier: Apache-2.0' "$SRC_T" \
|
||
&& pass "T10a 源码带 Apache-2.0 SPDX 头" || fail "T10a 源码缺 Apache-2.0 SPDX 头"
|
||
grep -q '独立实现,未拷贝' "$SRC_T" \
|
||
&& pass "T10b 源码声明独立实现、未拷贝 GPL 代码" || fail "T10b 缺独立实现声明"
|
||
if grep -nE 'clock_gettime|gettimeofday' "$SRC_T" | grep -qvE '^[0-9]+:[[:space:]]*(/\*|\*|//)'; then
|
||
fail "T10c 源码调用了 clock_gettime/gettimeofday(4.4 内核上 time64 ABI 错配)"
|
||
else
|
||
pass "T10c 源码不调用 clock_gettime/gettimeofday(只在注释里提及)"
|
||
fi
|
||
if grep -nE 'timerfd_settime|timerfd_create' "$SRC_T" | grep -qvE '^[0-9]+:[[:space:]]*(/\*|\*|//)'; then
|
||
fail "T10e 源码用了 timerfd(musl 的 timerfd_settime 在 4.4 上静默失效,必须用 setitimer)"
|
||
else
|
||
pass "T10e 源码不用 timerfd(超时走 setitimer+SIGALRM)"
|
||
fi
|
||
grep -q 'setitimer(ITIMER_REAL' "$SRC_T" && grep -q 'SIGALRM' "$SRC_T" \
|
||
&& pass "T10f 超时机制是 setitimer(ITIMER_REAL)+SIGALRM" \
|
||
|| fail "T10f 缺 setitimer/SIGALRM 超时机制"
|
||
grep -q 'setpgid' "$SRC_T" && grep -q 'kill(-' "$SRC_T" \
|
||
&& pass "T10d 进程组模型到位(setpgid + kill(-pgid))" \
|
||
|| fail "T10d 缺进程组模型(孙进程会漏杀)"
|
||
|
||
echo "== 11) 时长解析回归(宿主编译,真跑 parser,不做 sleep)"
|
||
if command -v cc >/dev/null 2>&1; then HOST_CC="cc"
|
||
elif command -v gcc >/dev/null 2>&1; then HOST_CC="gcc"
|
||
elif command -v clang >/dev/null 2>&1; then HOST_CC="clang"
|
||
elif command -v zig >/dev/null 2>&1; then HOST_CC="zig cc"
|
||
else HOST_CC=""; fi
|
||
|
||
if [ -z "$HOST_CC" ]; then
|
||
echo " SKIP: 没有宿主 C 编译器(cc/gcc/clang/zig),时长解析回归未运行"
|
||
elif $HOST_CC -O2 -Wno-unused-function -DTIMEOUT_UNIT_TEST -o "$TMP/dur-test" "$SRC_T" >"$TMP/hostcc.log" 2>&1; then
|
||
if "$TMP/dur-test" > "$TMP/dur.log" 2>&1; then
|
||
pass "T11a 时长解析 $(grep -c '^ok' "$TMP/dur.log") 例全过(含 0.01m/0.05m/1.5m/0.5h)"
|
||
else
|
||
fail "T11a 时长解析有用例失败"
|
||
grep '^FAIL' "$TMP/dur.log" | sed 's/^/ | /'
|
||
fi
|
||
# 单独点名 r1 评审抓到的三条(防止将来又被改回"只乘整数部分")
|
||
for c in "0.05m" "1.5m" "0.5h"; do
|
||
grep -q "DURATION '$c" "$TMP/dur.log" && ! grep -q "^FAIL.*'$c" "$TMP/dur.log" \
|
||
&& pass "T11b '$c' 用例存在且通过" || fail "T11b '$c' 用例缺失或失败"
|
||
done
|
||
else
|
||
fail "T11 宿主编译失败($HOST_CC)"; sed 's/^/ | /' "$TMP/hostcc.log" | tail -5
|
||
fi
|
||
|
||
echo "== 12) 信号失败告警路径(注入 setpgid 失败,验证不静默)"
|
||
if [ -n "$HOST_CC" ] && \
|
||
$HOST_CC -O2 -DTIMEOUT_TEST_SETPGID_FAIL -o "$TMP/timeout-pgfail" "$SRC_T" >"$TMP/hostcc2.log" 2>&1; then
|
||
"$TMP/timeout-pgfail" 1 sleep 30 > "$TMP/pgfail.out" 2>&1
|
||
PGRC=$?
|
||
[ "$PGRC" = 124 ] && pass "T12a 建组失败时仍杀掉直接子进程(rc=124)" \
|
||
|| fail "T12a rc=$PGRC(期望 124)"
|
||
grep -q '未建立独立进程组' "$TMP/pgfail.out" \
|
||
&& pass "T12b 建组失败有明确告警(不静默)" || fail "T12b 缺'未建立独立进程组'告警"
|
||
grep -q '退化为只给直接子进程' "$TMP/pgfail.out" \
|
||
&& pass "T12c 组信号失败后走 kill(pid) 兜底" || fail "T12c 缺兜底路径告警"
|
||
# 直接子进程必须真死:注入场景下它没有进程组保护,只能靠 kill(pid) 兜底
|
||
sleep 1
|
||
ps -A -o args= 2>/dev/null | grep -q '^sleep 30$' \
|
||
&& { fail "T12d 注入场景下 sleep 30 仍存活(兜底没生效)"
|
||
command -v pkill >/dev/null 2>&1 && pkill -f '^sleep 30$' 2>/dev/null; } \
|
||
|| pass "T12d 注入场景下没有残留 sleep 30"
|
||
else
|
||
echo " SKIP: 没有宿主 C 编译器,T12 未运行"
|
||
fi
|
||
|
||
echo
|
||
printf '== 结果: %d 通过, %d 失败\n' "$PASS" "$FAILN"
|
||
[ "$FAILN" = 0 ]
|