Files
MeowOS-For-R3P2/scripts/build/test_build_devkit.sh
T
AskaEth c93814a78b fix(m53): r1 评审 P2 — 小数后缀时长、文档数字、信号失败告警
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 失败。
2026-11-11 21:23:52 +08:00

220 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 ]