Files
rond-with-you/scripts/verify.sh
T
tamakyi 881f2933ff 收尾:验收脚本自动清死元组、城市圈首末到访按可见到访算、补使用说明
verify.sh:测试库 ≥ VACUUM_MIN_MB(默认 200MB)时先 VACUUM (ANALYZE)——一轮全量会留几百 MB
死元组,级联删除随之变慢(同一夹具用例实测 13.6s → 10.2s)。只用普通 VACUUM:FULL 要
ACCESS EXCLUSIVE 锁,有别的服务连着测试库时会把验收整段卡住。

/api/cities 的首末到访改成按可见到访的到达时间算(内层 min/max(v.arrival)),不再取
places.first/last_visit_at——那两列是全量的,会被已并入 / 子段的到访推早推晚。

README 补两节:「数据编辑页上的重复到访」(判定口径与清理范围)、「为什么有些到访在页面上看不到」
(已并入 / 子段不计入显示与统计,但导出必须原样带着),并在开发提示里记下验收环境的死元组与
is_active 两个坑。

回归:city_agg_test.go 补了一段能区分的断言——把某地点最晚那次到访标成已并入后,城市最近到访
必须从 09-13 退到 09-08(原先只比对同口径时,两种算法在夹具数据上结果一样,是条假绿的断言)。
2026-09-29 23:49:39 +08:00

203 lines
8.6 KiB
Bash
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.
#!/usr/bin/env bash
#
# 一键验收:把「必须带六个环境变量、且必须 0 skip」这条纪律固化下来。
#
# ./scripts/verify.sh # 全量验收(默认,约 8 分钟)
# ./scripts/verify.sh --fast # 只做 gofmt / vet / build(约 20 秒)
# ./scripts/verify.sh --race # 全量 + -race(更慢,需 mingw 在 PATH)
# ./scripts/verify.sh --cover # 全量 + 覆盖率(跨包口径,见下)
# ./scripts/verify.sh --smoke # 只跑关键页冒烟(需站点已在 --base 上运行)
# ./scripts/verify.sh --all # 全量 + 冒烟
# ./scripts/verify.sh --pkg ./internal/web/ --run 'TestMapPreview' # 定点跑
#
# 退出码:0 = 全绿且 0 skip;非 0 = 有失败或有跳过。
set -u -o pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
ABS="$ROOT"
command -v pwd >/dev/null && ABS="$(pwd -W 2>/dev/null || pwd)"
CONF="${ROND_TEST_CONF:-conf/app-test.ini}"
ROND_SRC="${ROND_SRC:-$ABS/data/20260914_151649.rondbackup}"
FOG_SRC="${FOG_SRC:-$ABS/data/Snapshot-20260914T154839+0800.fwss}"
OUT="$ABS/storage/.verify"
CLEAN_OUT="$OUT/acceptance-clean.rondbackup"
LOG="$OUT/verify.log"
COVOUT="$OUT/cover.out"
FAST=0; RACE=0; COVER=0; SMOKE=0; SMOKE_ONLY=0; PKGS="./..."; RUN=""
while [ $# -gt 0 ]; do
case "$1" in
--fast) FAST=1 ;;
--race) RACE=1 ;;
--cover) COVER=1 ;;
--smoke) SMOKE=1; SMOKE_ONLY=1 ;;
--all) SMOKE=1 ;;
--pkg) PKGS="${2:?--pkg 后面要给包路径}"; shift ;;
--run) RUN="${2:?--run 后面要给正则}"; shift ;;
*) echo "未知参数:$1" >&2; exit 2 ;;
esac
shift
done
fail() { echo "✗ $*" >&2; exit 1; }
ok() { echo "✓ $*"; }
echo "== 仓库:$ROOT"
# 冒烟单独跑:它要一个真在跑的站点(浏览器口径),跟单元测试是两回事
run_smoke() {
local py="${ROND_PY:-python}"
command -v "$py" >/dev/null 2>&1 || py="${ROND_PY:-python3}"
command -v "$py" >/dev/null 2>&1 || fail "找不到 python(可用 ROND_PY 指定)"
echo "== 冒烟:关键页渲染(站长浏览器 + 访客 HTTP/浏览器)"
# 路径必须给 Windows 形式:这里传的是 Git Bash 的 $ROOT(/c/...),
# Windows 版 python 会把它拼成 C:\c\... 然后报 No such file
"$py" "$ABS/scripts/smoke.py" --base "${ROND_BASE:-http://localhost:8080}" \
--shot-dir "$OUT/smoke" || fail "关键页冒烟"
ok "关键页冒烟"
}
if [ "$FAST" = 1 ]; then
unformatted="$(gofmt -l internal/ main.go 2>&1)"
[ -n "$unformatted" ] && fail "gofmt 不干净(注意 CRLF 会假警):$(echo "$unformatted" | tr '\n' ' ')"
ok "gofmt"
go vet ./... || fail "go vet"
ok "go vet"
go build ./... || fail "go build"
ok "go build"
exit 0
fi
if [ "$SMOKE_ONLY" = 1 ]; then
run_smoke
exit 0
fi
# --- 六个环境变量 ---------------------------------------------------------------
[ -f "$CONF" ] || fail "找不到 $CONF"
[ -f "$ROND_SRC" ] || fail "找不到备份包 ROND_SRC=$ROND_SRC"
[ -f "$FOG_SRC" ] || fail "找不到迷雾快照 FOG_SRC=$FOG_SRC"
if [ -n "${ROND_DSN:-}" ]; then
DSN="$ROND_DSN"
else
DSN="$(grep -m1 -iE '^[[:space:]]*dsn[[:space:]]*=' "$CONF" | sed 's/^[^=]*=[[:space:]]*//' | tr -d '\r\n')"
fi
[ -n "$DSN" ] || fail "没能从 $CONF 取到 dsn(也不在环境里),别把口令写进脚本"
# 指到维护库 postgres 会多出「users 不存在」+ 一堆 stats 假 skip,直接拦住
DBNAME="$(printf '%s' "$DSN" | sed -E 's#^[^/]*//[^/]*/([^/?]+).*#\1#')"
case "$DBNAME" in
test|*test*) : ;;
*) fail "ROND_DSN 指向的库名是 '$DBNAME',不含 test —— 验收必须用应用测试库(rond_fog_test)" ;;
esac
export ROND_TEST_CONF="$CONF" ROND_SRC FOG_SRC ROND_DSN="$DSN"
export ROND_CLEAN_OUT="$CLEAN_OUT" ROND_EXPORT="$CLEAN_OUT"
mkdir -p "$OUT"
echo "== 库:$DBNAME 备份:$(basename "$ROND_SRC") / $(basename "$FOG_SRC")"
# --- 死元组回收:库一胖,级联删除就变 IO 爬虫,整套验收到点跑不完 ----------------
#
# 每个夹具都导入一整包(约千条到访 + 每行一整份 Core Data 原始行)再删掉,
# 一轮全量下来能留 ~370MB 死元组;实测 16MB 时 5m49s 跑完、385MB 时 30 分钟还在爬
# (看着像 hang:测试进程 CPU 长时间不动)。这里在库超过阈值时先收一次。
#
# 用普通 VACUUM 而不是 VACUUM FULL:FULL 要 ACCESS EXCLUSIVE 锁,万一有别的服务连着
# 这个库就会把验收卡住;普通 VACUUM 是并发的,清掉死元组即可恢复扫描速度。
# 没有 psql 就跳过(不拦人),真慢了手动来一次 VACUUM FULL 把文件也缩回去。
VACUUM_MIN_MB="${VACUUM_MIN_MB:-200}"
if command -v psql >/dev/null 2>&1; then
DB_MB="$(psql "$DSN" -A -t -c "SELECT pg_database_size(current_database())/1024/1024" 2>/dev/null || echo 0)"
if [ "${DB_MB:-0}" -ge "$VACUUM_MIN_MB" ]; then
echo "== 测试库 ${DB_MB}MB ≥ ${VACUUM_MIN_MB}MB,先清死元组(否则级联删除会慢到跑不完)"
psql "$DSN" -q -c "VACUUM (ANALYZE)" || echo "-- VACUUM 失败,照跑(不影响正确性,只是会慢)"
fi
else
echo "-- 本机没有 psql,跳过死元组回收;若验收跑得很慢,先手动 VACUUM (FULL, ANALYZE) 一次"
fi
GOFLAGS_EXTRA=""
[ "$RACE" = 1 ] && GOFLAGS_EXTRA="$GOFLAGS_EXTRA -race"
# 必须 -coverpkg:-cover 只统计「测试文件所在包」自己的执行,凡是主要由别的包驱动的
# 代码(rond 的导出被 internal/web 驱动、geo/gpx/notes/votes 被 web/stats 驱动)
# 一律假报 0%,看着像没测,其实是量错了。
[ "$COVER" = 1 ] && GOFLAGS_EXTRA="$GOFLAGS_EXTRA -coverpkg=./internal/... -coverprofile=$COVOUT"
if [ "$RACE" = 1 ] && command -v gcc >/dev/null 2>&1; then :; else
for d in "$HOME/scoop/apps/mingw/current/bin"; do
[ -d "$d" ] && export PATH="$d:$PATH"
done
fi
if [ "$RACE" = 1 ] && ! command -v gcc >/dev/null 2>&1; then
fail "-race 需要 C 编译器:scoop install mingw,或把 mingw 的 bin 加进 PATH"
fi
# --- 第一段:先产出一份「纯净往返包」,第二段要读它 ------------------------------
if [ -z "$RUN" ] && [ "$PKGS" = "./..." ]; then
echo "== 第一段:导出纯净往返包"
if ! go test -count=1 -run '^TestMovementCleanExport$' -v ./internal/web/ 2>&1 | tee "$OUT/stage1.log" | grep -E '^(--- (PASS|FAIL|SKIP)|ok|FAIL)'; then
fail "第一段导出失败,见 $OUT/stage1.log"
fi
grep -q '^--- PASS' "$OUT/stage1.log" || fail "TestMovementCleanExport 没跑起来(被 skip 了?)"
ok "纯净往返包已生成:$(basename "$CLEAN_OUT")"
fi
# --- 第二段:全量(或定点)------------------------------------------------------
RUNARG=""
[ -n "$RUN" ] && RUNARG="-run $RUN"
echo "== 第二段:go test -count=1 -v $GOFLAGS_EXTRA $RUNARG $PKGS"
# shellcheck disable=SC2086
go test -count=1 -v $GOFLAGS_EXTRA -timeout 2400s $RUNARG $PKGS >"$LOG" 2>&1
RC=$?
PASS=$(grep -cE '^[[:space:]]*--- PASS' "$LOG" || true)
FAILN=$(grep -cE '^[[:space:]]*--- FAIL' "$LOG" || true)
SKIP=$(grep -cE '^[[:space:]]*--- SKIP' "$LOG" || true)
BADPKG=$(grep -c '^FAIL' "$LOG" || true)
echo "== 结果:PASS=$PASS FAIL=$FAILN SKIP=$SKIP 非ok包=$BADPKG"
echo "== 日志:$LOG"
if [ "$SKIP" != 0 ]; then
echo "-- 以下用例被跳过(=没跑。门控测试缺环境变量时 go test 依然回 ok,别当通过看)--"
grep -n -B1 -E '^[[:space:]]*--- SKIP' "$LOG" | head -40
fi
if [ "$FAILN" != 0 ] || [ "$BADPKG" != 0 ]; then
echo "-- 失败明细(前 40 行)--"
grep -n -B2 -E '^[[:space:]]*--- FAIL' "$LOG" | head -40
fi
if [ "$RC" != 0 ] || [ "$FAILN" != 0 ] || [ "$BADPKG" != 0 ]; then
fail "有失败"
fi
if [ "$SKIP" != 0 ]; then
fail "有 $SKIP 个用例被跳过 ⇒ 这次不算验收过"
fi
ok "全绿且 0 skip(PASS=$PASS)"
if [ "$COVER" = 1 ] && [ -s "$COVOUT" ]; then
echo "== 覆盖率(按包,跨包执行已计入)"
# 必须按 (文件,行段) 取 max 而不是求和:-coverpkg 下每个测试二进制都会给同一段代码
# 写一条记录,求和等于把语句数乘上二进制份数,每包都会看着只有 1/N。
awk 'NR>1 { key=$1; n=$(NF-1); c=$NF+0
if (!(key in seen)) seen[key]=c; else if (c>seen[key]) seen[key]=c
stmt[key]=n; split(key,a,":"); pkg=a[1]; sub(/\/[^/]+$/,"",pkg); pk[key]=pkg }
END { for (k in stmt) { p=pk[k]; tot[p]+=stmt[k]; if (seen[k]>0) cov[p]+=stmt[k] }
for (p in tot) printf " %-36s %5.1f%% %d/%d\n", p, 100*cov[p]/tot[p], cov[p], tot[p] }' \
"$COVOUT" | sort -k2 -n
echo -n " == 项目合计 "
go tool cover -func="$COVOUT" | tail -1
echo " 明细:go tool cover -html=$COVOUT"
fi
if [ "$SMOKE" = 1 ]; then
run_smoke
fi