Files
tamakyi 44763dfc3f 专题的多圆显示区:前台按并集裁剪迷雾
上一批把显示区改成了「一组圆」,这一批让前台真正认多个圆。

- 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 个迷雾像素 ⇒ 第二个圆真的被认了,不是只看第一个。
2026-10-12 16:44:55 +08:00

233 lines
10 KiB
Bash
Raw Permalink 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
# --- 第 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