run_interface_tests_cron.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. #!/usr/bin/env bash
  2. # =============================================================================
  3. # CFC 接口测试闭环(定时任务主体)
  4. # 流程:git pull 最新 -> 检测新增/变更需求 -> 跑冒烟(含回归对比) ->
  5. # 为新增接口补全用例并跑新测试 -> 提交测试结果 -> push 到远端(与开发团队闭环)
  6. #
  7. # 网络说明:原默认目标 192.168.16.251 已不可用(上游失效)。
  8. # 当前默认测试目标为 cfc.iwintrue.com(公网可达、后端在线)。
  9. # 本脚本设计为在宿主机用 Windows 任务计划程序每 30 分钟调用,
  10. # 不要在受限沙箱后台运行(后台网络被拦截)。
  11. # =============================================================================
  12. set -uo pipefail
  13. REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  14. cd "$REPO_DIR" || exit 1
  15. LOG_DIR="$REPO_DIR/logs"
  16. mkdir -p "$LOG_DIR"
  17. # 防重入:若上轮仍在 25 分钟内,跳过本轮(避免 30 分钟间隔下长耗时测试重叠)
  18. LOCK="$LOG_DIR/.loop.lock"
  19. NOW=$(date +%s)
  20. if [ -f "$LOCK" ]; then
  21. LAST=$(cat "$LOCK" 2>/dev/null || echo 0)
  22. if [ $((NOW - LAST)) -lt 1500 ]; then
  23. echo "[$(date +%H:%M:%S)] 上轮仍在运行(锁 $(( (NOW - LAST) / 60 )) 分钟前),跳过本轮"
  24. exit 0
  25. fi
  26. fi
  27. echo "$NOW" > "$LOCK"
  28. trap 'rm -f "$LOCK"' EXIT
  29. BRANCH="${CFC_BRANCH:-cfclub}"
  30. BASE_URL="${CFC_BASE:-http://cfc.iwintrue.com}"
  31. HOST_HDR="${CFC_HOST:-cfc.iwintrue.com}"
  32. TIMEOUT="${CFC_TIMEOUT:-8}"
  33. TEST_SCRIPT="ops_interface_smoke.py"
  34. SCANNER="scan_frontend_endpoints.py"
  35. TS="$(date +%Y%m%d_%H%M%S)"
  36. RUN_DIR="$LOG_DIR/run_$TS"
  37. mkdir -p "$RUN_DIR"
  38. CUR_TSV="$RUN_DIR/results_$TS.tsv"
  39. ENDPOINTS_CUR="$RUN_DIR/endpoints_$TS.txt"
  40. EXTRA_CASES="$REPO_DIR/interface_test_extra_cases.tsv"
  41. BASELINE_EP="$REPO_DIR/interface_test_baseline_endpoints.txt"
  42. PREV_TSV="$REPO_DIR/interface_test_prev.tsv"
  43. TREND="$REPO_DIR/interface_test_trend.log"
  44. MANAGED="INTERFACE_TEST_REPORT.md interface_test_report.tsv cfc-frontend/INTERFACE_TEST_MATRIX.md ops_interface_smoke.py"
  45. log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_DIR/interface_cron.log"; }
  46. log "===== CFC 接口测试闭环 开始 $TS ====="
  47. log "REPO=$REPO_DIR BRANCH=$BRANCH BASE=$BASE_URL HOST=$HOST_HDR"
  48. # ---------------------------------------------------------------------------
  49. # 1) 拉取远端最新(含开发团队的修复/新需求)
  50. # 仅丢弃我们生成的报告类文件本地改动(每次都会重新生成),其余未提交改动用
  51. # --autostash 处理,避免破坏用户其他在途工作。
  52. # ---------------------------------------------------------------------------
  53. git checkout -- INTERFACE_TEST_REPORT.md interface_test_report.tsv cfc-frontend/INTERFACE_TEST_MATRIX.md 2>/dev/null
  54. if git pull --rebase --autostash origin "$BRANCH" >>"$LOG_DIR/interface_cron.log" 2>&1; then
  55. log "git pull 成功"
  56. else
  57. log "git pull 失败(可能本地有冲突或凭证问题),用当前代码继续"
  58. fi
  59. # ---------------------------------------------------------------------------
  60. # 2) 检测新增/变更需求(前端接口面变化)
  61. # ---------------------------------------------------------------------------
  62. python "$SCANNER" --out "$ENDPOINTS_CUR" 2>/dev/null
  63. NEWEP=""; RMVEP=""
  64. if [ -f "$BASELINE_EP" ]; then
  65. NEWEP=$(comm -13 <(sort "$BASELINE_EP") <(sort "$ENDPOINTS_CUR"))
  66. RMVEP=$(comm -23 <(sort "$BASELINE_EP") <(sort "$ENDPOINTS_CUR"))
  67. n_new=$(echo "$NEWEP" | grep -c . || true)
  68. n_rmv=$(echo "$RMVEP" | grep -c . || true)
  69. log "接口面变化:新增 $n_new 个,移除 $n_rmv 个"
  70. if [ "$n_new" -gt 0 ]; then
  71. : > "$EXTRA_CASES"
  72. while IFS= read -r line; do
  73. [ -z "$line" ] && continue
  74. m=$(echo "$line" | awk '{print $1}')
  75. p=$(echo "$line" | awk '{print $2}')
  76. printf 'auto\tauto\t%s\t%s\tfalse\tfalse\n' "$m" "$p" >> "$EXTRA_CASES"
  77. done <<< "$NEWEP"
  78. log "已为新增接口生成补全用例 -> $EXTRA_CASES"
  79. fi
  80. else
  81. log "首次运行:建立接口基线(不触发补全)"
  82. fi
  83. cp "$ENDPOINTS_CUR" "$BASELINE_EP"
  84. # ---------------------------------------------------------------------------
  85. # 3) 跑冒烟测试(前台调用,含登录态;新增接口通过 --extra 一并测试)
  86. # ---------------------------------------------------------------------------
  87. EXTRA_FLAG=""
  88. [ -s "$EXTRA_CASES" ] && EXTRA_FLAG="--extra $EXTRA_CASES"
  89. log "开始执行冒烟测试 $EXTRA_FLAG"
  90. python "$TEST_SCRIPT" --base "$BASE_URL" --host "$HOST_HDR" \
  91. --timeout "$TIMEOUT" --auto-login --authed $EXTRA_FLAG \
  92. --report "$CUR_TSV" > "$RUN_DIR/smoke_stdout.txt" 2>&1
  93. log "冒烟测试完成,结果 -> $CUR_TSV"
  94. # ---------------------------------------------------------------------------
  95. # 4) 汇总 + 回归对比 + 生成报告
  96. # ---------------------------------------------------------------------------
  97. python - "$CUR_TSV" "$PREV_TSV" "INTERFACE_TEST_REPORT.md" "$TS" "$TREND" <<'PY'
  98. import sys, collections, os
  99. cur_tsv, prev_tsv, rpt, ts, trend = sys.argv[1:6]
  100. def load(path):
  101. d = {}
  102. try:
  103. with open(path, encoding='utf-8') as f:
  104. next(f, None)
  105. for line in f:
  106. c = line.rstrip('\n').split('\t')
  107. if len(c) < 7: continue
  108. page, fn, method, path, http, biz, cat = c
  109. d[(method, path)] = (page, fn, http, biz, cat)
  110. except FileNotFoundError:
  111. pass
  112. return d
  113. cur = load(cur_tsv)
  114. prev = load(prev_tsv) if prev_tsv and os.path.exists(prev_tsv) else {}
  115. cats = collections.Counter(v[4] for v in cur.values())
  116. total = len(cur)
  117. ok = cats.get('BIZ_OK', 0)
  118. rate = ok * 100 // total if total else 0
  119. # 回归对比
  120. fixed = [] # 之前非 OK -> 现在 OK
  121. regressed = [] # 之前 OK -> 现在非 OK
  122. for k, v in cur.items():
  123. cat = v[4]
  124. if k in prev:
  125. pcat = prev[k][4]
  126. if pcat != 'BIZ_OK' and cat == 'BIZ_OK':
  127. fixed.append((k, pcat, cat))
  128. elif pcat == 'BIZ_OK' and cat != 'BIZ_OK':
  129. regressed.append((k, pcat, cat))
  130. def top(cat, n=20):
  131. return [(k[0], k[1], v[3], v[4]) for k, v in cur.items() if v[4] == cat][:n]
  132. lines = []
  133. lines.append(f"# CFC 接口冒烟测试报告 ({ts})\n")
  134. lines.append(f"- 目标: {os.environ.get('CFC_HOST', 'cfc.iwintrue.com')} 用例总数: **{total}**")
  135. lines.append(f"- 业务成功率(BIZ_OK): **{ok}/{total} = {rate}%**\n")
  136. lines.append("## 分类汇总")
  137. for k in ('BIZ_OK','BIZ_FAIL','AUTH_FAIL','NOT_FOUND','SERVER_ERR','NON_JSON','CONN_ERR'):
  138. if cats.get(k): lines.append(f"- {k}: {cats[k]}")
  139. lines.append("")
  140. if fixed:
  141. lines.append(f"## ✅ 回归修复({len(fixed)} 个,之前失败现已通过)")
  142. for (m,p),pc,_ in fixed[:30]:
  143. lines.append(f"- `{m} {p}` (原 {pc})")
  144. lines.append("")
  145. if regressed:
  146. lines.append(f"## ⚠️ 回归劣化({len(regressed)} 个,之前通过现失败)")
  147. for (m,p),_,c in regressed[:30]:
  148. lines.append(f"- `{m} {p}` (现 {c})")
  149. lines.append("")
  150. lines.append(f"## 需开发团队关注的契约缺口 (NOT_FOUND={cats.get('NOT_FOUND',0)})")
  151. for m,p,b,c in top('NOT_FOUND'):
  152. lines.append(f"- `{m} {p}` (biz={b}, {c})")
  153. lines.append("")
  154. lines.append(f"## 健壮性风险 (SERVER_ERR={cats.get('SERVER_ERR',0)})")
  155. for m,p,b,c in top('SERVER_ERR'):
  156. lines.append(f"- `{m} {p}` (http={b}, {c})")
  157. lines.append("")
  158. lines.append("> 说明:冒烟为空体请求,部分 500/400 属预期(缺必填参数)。NOT_FOUND 与回归劣化才需优先处理。")
  159. report = "\n".join(lines) + "\n"
  160. with open(rpt, 'w', encoding='utf-8') as f:
  161. f.write(report)
  162. # 趋势日志
  163. with open(trend, 'a', encoding='utf-8') as f:
  164. f.write(f"{ts}\ttotal={total}\tBIZ_OK={ok}\trate={rate}%\t"
  165. f"fixed={len(fixed)}\tregressed={len(regressed)}\t"
  166. f"NOT_FOUND={cats.get('NOT_FOUND',0)}\tSERVER_ERR={cats.get('SERVER_ERR',0)}\n")
  167. # 导出真实指标供 bash 提交信息使用(修复 rate/fixed/regressed 在子进程不可见导致
  168. # commit 失败 + 假通过率 ?%;bash 侧 set -u 下 unbound variable 亦会导致整条命令展开失败)
  169. _metrics_path = os.path.join(os.path.dirname(trend), 'interface_test_metrics.env')
  170. with open(_metrics_path, 'w', encoding='utf-8') as _mf:
  171. _mf.write(f"RATE={rate}\nFIXED_COUNT={len(fixed)}\nREGRESSED_COUNT={len(regressed)}\n")
  172. print(report)
  173. print(f"\n[趋势] 已写入 {trend}")
  174. PY
  175. # ---------------------------------------------------------------------------
  176. # 5) 提交测试结果 + 补全用例
  177. # ---------------------------------------------------------------------------
  178. git add -f ops_interface_smoke.py
  179. # 强制纳入报告产物(防止未来 .gitignore 规则或路径规范导致漏提);
  180. # EXTRA_CASES 仅在存在且有内容时才加入,避免缺失路径使整个 git add 失败(旧逻辑缺陷)
  181. git add -f INTERFACE_TEST_REPORT.md interface_test_report.tsv cfc-frontend/INTERFACE_TEST_MATRIX.md
  182. [ -s "$EXTRA_CASES" ] && git add -f "$EXTRA_CASES"
  183. # 读取 Python 计算出的真实指标(修复 set -u 下 unbound variable 致 commit 失败 + 假通过率 ?%)
  184. RATE="?"; FIXED_COUNT=0; REGRESSED_COUNT=0
  185. METRICS_ENV="$REPO_DIR/interface_test_metrics.env"
  186. [ -f "$METRICS_ENV" ] && . "$METRICS_ENV"
  187. if git diff --cached --quiet; then
  188. log "无测试结果变更,跳过提交"
  189. else
  190. git commit -m "test(interface): 接口冒烟闭环 $(date '+%Y-%m-%d %H:%M') — 通过率 ${RATE}% (fix=${FIXED_COUNT} reg=${REGRESSED_COUNT})" \
  191. >>"$LOG_DIR/interface_cron.log" 2>&1 && log "已提交测试结果"
  192. fi
  193. # ---------------------------------------------------------------------------
  194. # 6) 推送远端,与开发团队形成闭环
  195. # ---------------------------------------------------------------------------
  196. if git push origin "$BRANCH" >>"$LOG_DIR/interface_cron.log" 2>&1; then
  197. log "git push 成功 — 测试闭环已同步远端"
  198. else
  199. log "git push 失败(凭证/网络),下次运行将重试"
  200. fi
  201. # 更新回归基线
  202. cp "$CUR_TSV" "$PREV_TSV"
  203. # 同步仓库根目录的全量结果 TSV(供人工/工具读取,避免与报告一起变陈旧)
  204. cp "$CUR_TSV" "interface_test_report.tsv"
  205. log "已同步全量结果 -> interface_test_report.tsv"
  206. log "===== 本轮结束 ====="