上一批把显示区改成了「一组圆」,这一批让前台真正认多个圆。 - clip-path 从 circle() 换成 path():circle() 只表达得了一个圆。每个圆一段 圆弧子路径、**同绕向**(sweep=0),nonzero 填充规则下多段自然取并集, 重叠处不会互相挖洞。 - 拼路径的活单独放 fog-clip.js(rond-map.js 是个大 IIFE,函数取不出来), 这样能直接跑 node 单测;layout 里必须排在 rond-map.js 之前加载。 - 顺带清掉已经死掉的 fitCircle:多圆之后「圆心 + 半径」表达不了,留着就是 两套并行的贴合口径。fitParams 现在只给 fitBBox(单圆时外接矩形正是那个 圆的外接正方形,视野等价)。 - 守卫改名 TestTopicFogIsClippedToAreas,钉住两端(模板发 fogAreas ↔ 脚本认 fogAreas)与两处顺序(裁剪注册在 applyFit 之后、fog-clip.js 在 rond-map.js 之前);fogAreas 必须**无条件**下发(空数组 = 不限区域),不要包在条件里。 - verify.sh 挂进前端单测:以前只靠人记得手动跑 = 等于没有。找不到 node 直接 失败而不是跳过(跳过会让验收看着全绿、实际一行前端脚本都没验)。 验证(临时两圆专题,SQL 造的,已删): - fog-clip.test.js 15 条:把生成的 path 解析回圆弧再验几何,含绕向一致性与 半径 0/负数的夹取。 - 端到端像素验证:自校准出「CSS 容器坐标 → 图像像素」(scale≈2、offset≈19.2, 正是 site_cdp 的 --clip 外扩 10px×scale,别再手算);三张掩码图证明并集语义 —— A 独有点只有 A 亮、B 独有点只有 B 亮、**重叠中心两圆都亮且并集也亮 (无洞)**,整体集合差异 0.013%。 - 迷雾掩码:86,776 个像素,按两圆并集的解析式判 **圆外 0 个**;两个圆内分别 有 52,609 / 46,757 个迷雾像素 ⇒ 第二个圆真的被认了,不是只看第一个。
233 lines
10 KiB
Bash
233 lines
10 KiB
Bash
#!/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
|
||
|
||
# --- 第 1.5 段:前端脚本单测(node 直跑,不依赖数据库)--------------------------
|
||
# internal/web/static/js/*.test.js 是纯函数用例(日期解析、迷雾裁剪路径拼装)。
|
||
# 以前只靠人记得手动跑 = 等于没有:改了脚本没人拦。这里挂进验收。
|
||
# 找不到 node 时**直接失败**而不是「跳过」—— 跳过会让这次验收看着全绿,
|
||
# 而实际上前端脚本一行都没验(这正是「0 skip」那条纪律要防的事)。
|
||
NODE_BIN="${NODE_BIN:-}"
|
||
if [ -z "$NODE_BIN" ]; then
|
||
for cand in "$(command -v node 2>/dev/null)" \
|
||
"$HOME/.workbuddy/binaries/node/versions/22.22.2-6/node.exe" \
|
||
"/c/Program Files/nodejs/node.exe"; do
|
||
if [ -n "$cand" ] && [ -x "$cand" ]; then NODE_BIN="$cand"; break; fi
|
||
done
|
||
fi
|
||
[ -n "$NODE_BIN" ] || fail "找不到 node(前端脚本单测跑不了)。用 NODE_BIN=/path/to/node 指定后再跑"
|
||
JSPASS=0; JSFAIL=0
|
||
for t in "$ROOT"/internal/web/static/js/*.test.js; do
|
||
[ -f "$t" ] || continue
|
||
if jsout=$("$NODE_BIN" "$t" 2>&1); then
|
||
n=$(printf '%s\n' "$jsout" | sed -n 's/.*pass=\([0-9]\{1,\}\).*/\1/p' | tail -1)
|
||
JSPASS=$((JSPASS + ${n:-0}))
|
||
ok "前端单测 $(basename "$t"):pass=${n:-0}"
|
||
else
|
||
printf '%s\n' "$jsout" | tail -25
|
||
JSFAIL=$((JSFAIL + 1))
|
||
echo "✗ 前端单测失败:$(basename "$t")"
|
||
fi
|
||
done
|
||
[ "$JSFAIL" = 0 ] || fail "前端单测有 $JSFAIL 个文件失败"
|
||
echo "== 前端单测合计:pass=$JSPASS,失败文件 $JSFAIL"
|
||
|
||
# --- 第二段:全量(或定点)------------------------------------------------------
|
||
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
|