#!/bin/sh # # run_tests.sh — meow_ntpsync 宿主沙箱单测(M22) # # 不碰真机、不联网:用假的 ntpd / hwclock / 网络探测注入, # 验证「成功 / 超时 / 失败 / 网络未就绪 / 短路 / 返回码」这些分支。 # # 用法: sh patch_v2/ntpsync/tests/run_tests.sh [meow_ntpsync 路径] # set -u HERE=$(cd "$(dirname "$0")" && pwd) SUT=${1:-"$HERE/../meow_ntpsync"} W=$(mktemp -d) trap 'rm -rf "$W"' EXIT PASS=0 FAIL=0 ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$1"; } bad() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n' "$1"; } check(){ if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (want=$3 got=$2)"; fi; } # ---- 假 ntpd:行为由 MODE 决定;第一行记自己的 pid,供「trap 收尸」断言用 ---- cat >"$W/ntpd" <<'EOF' #!/bin/sh echo "$$" >> "$FAKE_NTPD_CALLS" # 断言调用方必须给 -c <临时配置> case " $* " in *" -c "*) : ;; *) echo "no -c" >&2; exit 9 ;; esac case "$FAKE_NTPD_MODE" in ok) echo "ntpd: time set +1.000000 s"; exit 0 ;; fail) echo "ntpd: no server suitable" >&2; exit 3 ;; hang) exec sleep 3600 ;; # 不 exec:留着 cmdline 供「孤儿自证」;sleep 只挂 120s,万一漏杀也会自己退 hangkeep) sleep 120; exit 0 ;; slew) echo "ntpd: time slew +0.000042 s"; exit 0 ;; esac exit 1 EOF chmod +x "$W/ntpd" # ---- 假 hwclock ---- cat >"$W/hwclock" <<'EOF' #!/bin/sh echo "$*" >> "$FAKE_HWCLOCK_CALLS" [ "$FAKE_HWCLOCK_MODE" = fail ] && exit 1 exit 0 EOF chmod +x "$W/hwclock" # 每种模式都跑在一个干净的沙箱里 run_case() { # $1=名字 其余=meow_ntpsync 参数;通过环境变量控制 name=$1 shift CALLS="$W/calls.$name" HW="$W/hw.$name" : >"$CALLS" : >"$HW" export FAKE_NTPD_CALLS="$CALLS" FAKE_HWCLOCK_CALLS="$HW" export MEOW_NTPD="$W/ntpd" MEOW_HWCLOCK="$W/hwclock" export MEOW_NTPSYNC_STATE="$W/state.$name" MEOW_NTPSYNC_LOCK="$W/lock.$name" export MEOW_NTPSYNC_LOG="$W/log.$name" export MEOW_NTPSYNC_ATTEMPT_TIMEOUT="${CASE_TIMEOUT:-2}" } echo "== meow_ntpsync 沙箱单测 ($SUT)" # 1) 正常同步 rm -f "$W/state.1" run_case 1 FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT"; rc=$? check "同步成功返回 0" "$rc" 0 check "hwclock 被调用一次" "$(wc -l <"$W/hw.1" | tr -d ' ')" 1 check "hwclock 参数为 -w -u" "$(cat "$W/hw.1")" "-w -u" check "写了 state(epoch)" "$(grep -c '^epoch=' "$W/state.1" 2>/dev/null || echo 0)" 1 # 1b) 单独验证 gen_conf 的内容(截获 -c 指向的文件) cat >"$W/ntpd2" <<'EOF' #!/bin/sh while [ $# -gt 0 ]; do [ "$1" = "-c" ] && cp "$2" "$FAKE_CONF_DUMP"; shift; done exit 0 EOF chmod +x "$W/ntpd2" rm -f "$W/state.1b" run_case 1b FAKE_CONF_DUMP="$W/conf.1b" MEOW_NTPD="$W/ntpd2" FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT" check "配置每个 server 都带 iburst" \ "$(grep -c '^server .* iburst$' "$W/conf.1b" 2>/dev/null)" \ "$(grep -c '^server ' "$W/conf.1b" 2>/dev/null)" # 2) ntpd 挂死 → 看门狗必须杀掉并返回 1(假 ntpd 挂 3600s,看门狗 2s 必须掐断) run_case 2 t0=$(date +%s) FAKE_NTPD_MODE=hang MEOW_NETCHECK=true sh "$SUT"; rc=$? t1=$(date +%s); el=$((t1 - t0)) check "挂死返回 1" "$rc" 1 # 上界放宽到 90s:宿主是多 agent 共享机,实测负载高时 sleep 5 能跑成 85s; # 假 ntpd 自己会挂 3600s,所以 90s 仍能证明是看门狗掐断而不是自然退出。 if [ "$el" -ge 2 ] && [ "$el" -le 90 ]; then ok "看门狗掐断挂死的 ntpd (实测 ${el}s)" else bad "看门狗时长异常 (实测 ${el}s, 期望 2~90s)"; fi check "挂死时 ntpd 子进程已死" "$(kill -0 "$(head -n 1 "$W/calls.2" 2>/dev/null)" 2>/dev/null && echo alive || echo dead)" dead check "挂死不写 RTC" "$(wc -l <"$W/hw.2" | tr -d ' ')" 0 # 3) ntpd 明确失败 → 返回 1,不写 RTC run_case 3 FAKE_NTPD_MODE=fail MEOW_NETCHECK=true sh "$SUT"; rc=$? check "失败返回 1" "$rc" 1 check "失败不写 RTC" "$(wc -l <"$W/hw.3" | tr -d ' ')" 0 # 4) 单次模式网络未就绪 → 立刻返回 1,且不去打 ntpd run_case 4 t0=$(date +%s) FAKE_NTPD_MODE=ok MEOW_NETCHECK=false sh "$SUT"; rc=$? t1=$(date +%s); el=$((t1 - t0)) check "网络未就绪返回 1" "$rc" 1 check "网络未就绪不调 ntpd" "$(wc -l <"$W/calls.4" | tr -d ' ')" 0 if [ "$el" -le 10 ]; then ok "单次模式不做无谓等待 (${el}s)"; else bad "单次模式等了 ${el}s"; fi # 5) 开机模式:网络一直不来,-t 3 → 按时退出 run_case 5 t0=$(date +%s) FAKE_NTPD_MODE=ok MEOW_NETCHECK=false sh "$SUT" -b -t 3; rc=$? t1=$(date +%s); el=$((t1 - t0)) check "开机模式超时返回 1" "$rc" 1 if [ "$el" -ge 3 ] && [ "$el" -le 90 ]; then ok "开机模式在 -t 内退出 (${el}s)" else bad "开机模式退出时长异常 (${el}s)"; fi # 6) 开机模式:网络晚到 → 仍能同步成功 # 只在第 2 次探测才就绪(证明「先等、后来到了」这条路径),-t 给足 cat >"$W/latecheck" <<'EOF' #!/bin/sh n=$(cat "$LATE_FILE" 2>/dev/null || echo 0) n=$((n + 1)); echo "$n" >"$LATE_FILE" [ "$n" -gt 1 ] EOF chmod +x "$W/latecheck" rm -f "$W/late.n" "$W/state.6" run_case 6 LATE_FILE="$W/late.n" FAKE_NTPD_MODE=ok MEOW_NETCHECK="$W/latecheck" sh "$SUT" -b -t 90; rc=$? check "网络晚到仍返回 0" "$rc" 0 # 7) 非法参数 sh "$SUT" --bogus >/dev/null 2>&1; check "未知参数返回 2" "$?" 2 sh "$SUT" -t abc >/dev/null 2>&1; check "非数字 -t 返回 2" "$?" 2 # 8) 1h 内已同步 → 短路,不调 ntpd run_case 8 echo "epoch=$(date +%s)" >"$W/state.8" FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT"; rc=$? check "已同步短路返回 0" "$rc" 0 check "短路不调 ntpd" "$(wc -l <"$W/calls.8" | tr -d ' ')" 0 # 9) RTC 写失败:时间已对,返回 0,但日志要留痕 rm -f "$W/state.9" run_case 9 FAKE_NTPD_MODE=slew FAKE_HWCLOCK_MODE=fail MEOW_NETCHECK=true sh "$SUT"; rc=$? check "RTC 写失败仍返回 0" "$rc" 0 check "日志记录 RTC 写失败" "$(grep -c 'rtc write FAILED' "$W/log.9" 2>/dev/null)" 1 # 10) -S 打印状态 run_case 10 echo "epoch=12345" >"$W/state.10" out=$(sh "$SUT" -S) check "-S 打印上次记录" "$(echo "$out" | grep -c '^epoch=12345$')" 1 # ---------------------------------------------------------------- 回归(r1 P2) # 11) P2-1:SIGTERM 打断时 trap 必须把 attempt() 拉起的 ntpd 子进程一起收尸。 # 不修的话 ntpd 会孤儿化(后面被 pidof 当成「已有 ntpd 在跑」→ 永久静默 skip)。 run_case 11 rm -rf "$W/lock.11" # ATTEMPT_TIMEOUT 设 180s:远大于下面的观察窗口,保证「看门狗顺手杀子进程」 # 不会掩盖孤儿问题(假 ntpd 本身挂 3600s)。 FAKE_NTPD_MODE=hang MEOW_NETCHECK=true MEOW_NTPSYNC_ATTEMPT_TIMEOUT=180 \ sh "$SUT" -f >/dev/null 2>&1 & sut=$! i=0 while [ "$i" -lt 40 ] && [ ! -s "$W/calls.11" ]; do sleep 0.2; i=$((i + 1)); done child=$(head -n 1 "$W/calls.11" 2>/dev/null) kill -TERM "$sut" 2>/dev/null # 60s 观察窗口:远早于 ATTEMPT_TIMEOUT(180s);上界放宽是因为宿主负载高 i=0 while [ "$i" -lt 60 ]; do kill -0 "$sut" 2>/dev/null || break sleep 1 i=$((i + 1)) done if kill -0 "$sut" 2>/dev/null; then bad "SIGTERM 后主进程 60s 内未退出" kill -9 "$sut" 2>/dev/null else ok "SIGTERM 后主进程已退出" fi if [ -n "$child" ] && kill -0 "$child" 2>/dev/null; then bad "SIGTERM 后 ntpd 子进程成孤儿 (pid=$child)" kill -9 "$child" 2>/dev/null else ok "SIGTERM 后 ntpd 子进程已被收尸" fi if [ -d "$W/lock.11" ]; then bad "SIGTERM 后锁未释放"; else ok "SIGTERM 后锁已释放"; fi # 12) P2-1:上次遗留的孤儿 ntpd 要被收尸,不能因 pidof 命中就静默 skip + rc=0。 # 不修的话 hwclock 根本不会被调用(旧代码直接 "skip: an ntpd is already running")。 run_case 12 rm -rf "$W/lock.12" "$W/state.12" mkdir -p "$W/orphan" cp "$W/ntpd" "$W/orphan/ntpd" cat >"$W/fakepidof" <<'EOF' #!/bin/sh cat "$FAKE_PIDOF_OUT" 2>/dev/null EOF chmod +x "$W/fakepidof" # 造一个「上次运行遗留」的 ntpd:cmdline 里带本次锁目录。 # 用 hangkeep(不 exec)——真 ntpd 是单一 ELF,cmdline 保持不变; # 若用 exec 版,cmdline 会变成 "sleep 120",就认不出「是我们的孤儿」了。 # stdio 全丢 /dev/null:否则它(及其 sleep 子进程)会占住调用方的管道, # 让 `run_tests.sh | tail` 永远等不到 EOF。 FAKE_NTPD_CALLS="$W/orphan.calls" FAKE_NTPD_MODE=hangkeep \ "$W/orphan/ntpd" -n -q -g -c "$W/lock.12/ntp.conf" >/dev/null 2>&1 & orphan=$! sleep 1 echo "$orphan" >"$W/pidof.out" FAKE_PIDOF_OUT="$W/pidof.out" MEOW_NTPSYNC_PIDOF="$W/fakepidof" \ FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT" -f; rc=$? check "遗留孤儿不阻断本次同步" "$rc" 0 check "遗留孤儿被收尸" "$(kill -0 "$orphan" 2>/dev/null && echo alive || echo dead)" dead check "孤儿被收尸后仍写了 RTC" "$(wc -l <"$W/hw.12" | tr -d ' ')" 1 kill -9 "$orphan" 2>/dev/null # 13) P2-2:日志路径不可写时 hwclock 仍必须被执行(重定向失败不得吞掉命令)。 # 不修的话 hwclock 的 marker 为空,而整轮仍返回 0 → RTC 一小时不更新。 run_case 13 rm -f "$W/state.13" FAKE_NTPD_MODE=ok MEOW_NETCHECK=true MEOW_NTPSYNC_LOG="/nonexistent_dir_$$/x.log" \ sh "$SUT" -f; rc=$? check "日志不可写仍返回 0" "$rc" 0 check "日志不可写 hwclock 仍被执行" "$(wc -l <"$W/hw.13" | tr -d ' ')" 1 # 14) P2-3:锁里有存活持有者( 子目录)时不得抢锁。 # 不修的话(旧代码只读 pid 文件,读不到当 other=0)会 rm -rf 抢锁并并发打 ntpd。 run_case 14 rm -rf "$W/lock.14" mkdir -p "$W/lock.14/$$" FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT" -f; rc=$? check "锁被占用时返回 0" "$rc" 0 check "锁被占用时不并发调 ntpd" "$(wc -l <"$W/calls.14" | tr -d ' ')" 0 check "锁被占用时不写 RTC" "$(wc -l <"$W/hw.14" | tr -d ' ')" 0 check "未抢走别人的锁" "$([ -d "$W/lock.14/$$" ] && echo kept || echo stolen)" kept rm -rf "$W/lock.14" # 15) P2-3 反向:持有者已死的锁必须能被接管(否则崩溃一次就把校时永久卡死) run_case 15 rm -rf "$W/lock.15" mkdir -p "$W/lock.15/999999" FAKE_NTPD_MODE=ok MEOW_NETCHECK=true sh "$SUT" -f; rc=$? check "死锁可被接管并同步" "$rc" 0 check "接管后 ntpd 被调用" "$(wc -l <"$W/calls.15" | tr -d ' ')" 1 rm -rf "$W/lock.15" # 16) P2-1 补充:崩溃记录里的 pid 已被无关进程复用时,不能把它当成「别人的 ntpd」 # 而永久跳过。cmdline 不匹配 = 不是我们的孤儿 → 忽略它,照常校时。 # 不修的话 remaining 会收下这个 pid → "skip: a foreign ntpd" → hwclock 不执行。 run_case 16 rm -rf "$W/lock.16" "$W/state.16" mkdir -p "$W/lock.16/999999" # 持有者已死 sleep 300 & # 无关进程,占住一个将被「复用」的 pid unrelated=$! echo "$unrelated" >"$W/lock.16/ntpd.pid" FAKE_NTPD_MODE=ok MEOW_NETCHECK=true MEOW_NTPSYNC_PIDOF=false sh "$SUT" -f; rc=$? check "pid 复用不阻断同步" "$rc" 0 check "pid 复用后仍写了 RTC" "$(wc -l <"$W/hw.16" | tr -d ' ')" 1 check "无关进程未被误杀" "$(kill -0 "$unrelated" 2>/dev/null && echo alive || echo killed)" alive kill -9 "$unrelated" 2>/dev/null rm -rf "$W/lock.16" echo echo "== 结果: PASS=$PASS FAIL=$FAIL" [ "$FAIL" -eq 0 ] || exit 1