Просмотр исходного кода

fix(cron): push 遇远端领先自动 rebase 后重试,避免测试结果不落远端

E2E Test Bot 1 неделя назад
Родитель
Сommit
0d5d16cfe1
1 измененных файлов с 111 добавлено и 16 удалено
  1. 111 16
      run_interface_tests_cron.sh

+ 111 - 16
run_interface_tests_cron.sh

@@ -177,38 +177,106 @@ for m,p,b,c in top('SERVER_ERR'):
 lines.append("")
 lines.append("> 说明:冒烟为空体请求,部分 500/400 属预期(缺必填参数)。NOT_FOUND 与回归劣化才需优先处理。")
 
+# ───────────────────────── 守卫:识别无效轮 ─────────────────────────
+# 上游宕机 / 网关抖动 / 登录失败 / 部分轮 不应被当作真实回归提交,亦不可污染
+# 回归基线(PREV_TSV)。命中则标 INVALID,由 bash 侧跳过 commit/push/基线更新。
+non_json   = cats.get('NON_JSON', 0)
+server_err = cats.get('SERVER_ERR', 0)
+conn_err   = cats.get('CONN_ERR', 0)
+http_502   = sum(1 for v in cur.values() if v[2] == '502')   # v[2]=http 列
+nz_rate    = (non_json * 100 // total) if total else 0
+
+NON_JSON_THRESHOLD = 20    # NON_JSON 占比 > 20% 视为上游风暴
+HTTP502_THRESHOLD  = 10    # HTTP 502 数 > 10 视为网关抖动
+MIN_TOTAL          = 400   # 用例数 < 400 视为部分轮(如登录失败跳过 auth 用例)
+
+login_failed = False
+_smoke_out = os.path.join(os.path.dirname(cur_tsv), 'smoke_stdout.txt')
+try:
+    with open(_smoke_out, encoding='utf-8', errors='ignore') as _f:
+        _so = _f.read()
+    if ('父/师 token 均未取得' in _so) or ('未取到 token' in _so) or ('登录态接口将被跳过' in _so):
+        login_failed = True
+except FileNotFoundError:
+    pass
+
+reasons = []
+if login_failed:
+    reasons.append('登录失败(auto-login 未取得 token,auth 用例被跳过)')
+if nz_rate > NON_JSON_THRESHOLD:
+    reasons.append(f'NON_JSON 风暴({nz_rate}%)')
+if http_502 > HTTP502_THRESHOLD:
+    reasons.append(f'HTTP502 网关抖动({http_502})')
+if total < MIN_TOTAL:
+    reasons.append(f'用例数异常({total}<{MIN_TOTAL},疑似部分轮/中断)')
+
+INVALID = len(reasons) > 0
+
+if INVALID:
+    banner = ("\n> ⚠️ **本轮判定 INVALID:结果不提交、不推送、不更新回归基线。**\n"
+              f"> 原因:{';'.join(reasons)}\n"
+              "> 处置:保留上次有效基线不变,待上游稳定后重跑即可;勿将此轮数值当作真实回归。\n")
+    lines.append(banner)
+
 report = "\n".join(lines) + "\n"
-with open(rpt, 'w', encoding='utf-8') as f:
+
+# 报告恒写入本轮运行目录(供排查);仅当有效时同步到仓库根(避免污染已提交产物)
+_run_report = os.path.join(os.path.dirname(cur_tsv), 'INTERFACE_TEST_REPORT.md')
+with open(_run_report, 'w', encoding='utf-8') as f:
     f.write(report)
+if not INVALID:
+    with open(rpt, 'w', encoding='utf-8') as f:
+        f.write(report)
 
-# 趋势日志
+# 趋势日志(本地,不随提交推送;INVALID 也记录以留痕)
 with open(trend, 'a', encoding='utf-8') as f:
     f.write(f"{ts}\ttotal={total}\tBIZ_OK={ok}\trate={rate}%\t"
             f"fixed={len(fixed)}\tregressed={len(regressed)}\t"
-            f"NOT_FOUND={cats.get('NOT_FOUND',0)}\tSERVER_ERR={cats.get('SERVER_ERR',0)}\n")
+            f"NOT_FOUND={cats.get('NOT_FOUND',0)}\tSERVER_ERR={cats.get('SERVER_ERR',0)}\t"
+            f"NON_JSON={non_json}\tHTTP502={http_502}\tINVALID={1 if INVALID else 0}"
+            + (f"({ '|'.join(reasons) })" if INVALID else "") + "\n")
 
-# 导出真实指标供 bash 提交信息使用(修复 rate/fixed/regressed 在子进程不可见导致
+# 导出真实指标供 bash 判定(修复 rate/fixed/regressed 在子进程不可见导致
 # commit 失败 + 假通过率 ?%;bash 侧 set -u 下 unbound variable 亦会导致整条命令展开失败)
 _metrics_path = os.path.join(os.path.dirname(trend), 'interface_test_metrics.env')
 with open(_metrics_path, 'w', encoding='utf-8') as _mf:
-    _mf.write(f"RATE={rate}\nFIXED_COUNT={len(fixed)}\nREGRESSED_COUNT={len(regressed)}\n")
+    _mf.write(f"RATE={rate}\nFIXED_COUNT={len(fixed)}\nREGRESSED_COUNT={len(regressed)}\n"
+              f"TOTAL={total}\nOK_COUNT={ok}\nNON_JSON_COUNT={non_json}\n"
+              f"SERVER_ERR_COUNT={server_err}\nCONN_ERR_COUNT={conn_err}\n"
+              f"HTTP_502_COUNT={http_502}\nINVALID={1 if INVALID else 0}\n"
+              f"INVALID_REASON={'|'.join(reasons)}\n")
 
 print(report)
 print(f"\n[趋势] 已写入 {trend}")
+if INVALID:
+    print(f"[守卫] INVALID 触发:{';'.join(reasons)} --> 跳过提交/推送/基线更新")
 PY
 
 # ---------------------------------------------------------------------------
-# 5) 提交测试结果 + 补全用例
+# 5) 守卫判定:无效轮(宕机/NON_JSON 风暴/502 抖动/登录失败/部分轮)
+#    不提交、不推送、不更新回归基线,避免污染趋势与开发侧闭环
+# ---------------------------------------------------------------------------
+INVALID=0; INVALID_REASON=""
+RATE="?"; FIXED_COUNT=0; REGRESSED_COUNT=0
+METRICS_ENV="$REPO_DIR/interface_test_metrics.env"
+[ -f "$METRICS_ENV" ] && . "$METRICS_ENV"
+if [ "${INVALID:-0}" -eq 1 ]; then
+  log "⚠️ 守卫触发 INVALID:本轮结果不提交 / 不推送 / 不更新回归基线"
+  log "   原因: ${INVALID_REASON}"
+  log "   处置: 保留 PREV_TSV 基线不变,待上游稳定后重跑即可"
+  log "   排查: 本轮明细见 $RUN_DIR/results_$TS.tsv 与 $RUN_DIR/smoke_stdout.txt"
+  log "===== 本轮结束(INVALID,已跳过推送)====="
+  exit 0
+fi
+
+# ---------------------------------------------------------------------------
+# 6) 提交测试结果 + 补全用例(仅有效轮)
 # ---------------------------------------------------------------------------
 git add -f ops_interface_smoke.py
 # 强制纳入报告产物(防止未来 .gitignore 规则或路径规范导致漏提);
 # EXTRA_CASES 仅在存在且有内容时才加入,避免缺失路径使整个 git add 失败(旧逻辑缺陷)
 git add -f INTERFACE_TEST_REPORT.md interface_test_report.tsv cfc-frontend/INTERFACE_TEST_MATRIX.md
 [ -s "$EXTRA_CASES" ] && git add -f "$EXTRA_CASES"
-# 读取 Python 计算出的真实指标(修复 set -u 下 unbound variable 致 commit 失败 + 假通过率 ?%)
-RATE="?"; FIXED_COUNT=0; REGRESSED_COUNT=0
-METRICS_ENV="$REPO_DIR/interface_test_metrics.env"
-[ -f "$METRICS_ENV" ] && . "$METRICS_ENV"
 if git diff --cached --quiet; then
   log "无测试结果变更,跳过提交"
 else
@@ -217,15 +285,42 @@ else
 fi
 
 # ---------------------------------------------------------------------------
-# 6) 推送远端,与开发团队形成闭环
+# 7) 推送远端,与开发团队形成闭环
+#    增强:远端领先(非快进)时自动 rebase 后重试,避免测试结果不落远端。
+#         凭证/网络类错误不盲目重试,直接失败并留待下次运行。
 # ---------------------------------------------------------------------------
-if git push origin "$BRANCH" >>"$LOG_DIR/interface_cron.log" 2>&1; then
-  log "git push 成功 — 测试闭环已同步远端"
-else
-  log "git push 失败(凭证/网络),下次运行将重试"
+PUSH_LOG="$RUN_DIR/push.log"
+MAX_PUSH_RETRY=2
+push_ok=0
+attempt=0
+while [ "$attempt" -lt $((MAX_PUSH_RETRY + 1)) ]; do
+  attempt=$((attempt + 1))
+  if git push origin "$BRANCH" >"$PUSH_LOG" 2>&1; then
+    push_ok=1
+    log "git push 成功 — 测试闭环已同步远端(第 ${attempt} 次尝试)"
+    break
+  fi
+  # 仅在「远端领先/非快进」时自动 rebase 后重试
+  if grep -qiE "non-fast-forward|fetch first|Updates were rejected|rejected" "$PUSH_LOG"; then
+    log "git push 被拒(远端领先/非快进)。rebase 后重试(${attempt}/${MAX_PUSH_RETRY})"
+    # rebase 期间用 --autostash 保护未提交改动(如事后刷新的 interface_test_report.tsv)
+    if git pull --rebase --autostash origin "$BRANCH" >>"$LOG_DIR/interface_cron.log" 2>&1; then
+      log "rebase 完成,重新尝试 push"
+    else
+      log "rebase 失败(可能存在冲突),中止 push 重试"
+      break
+    fi
+  else
+    # 凭证/网络等其他错误:不盲目重试,避免空耗与误报
+    log "git push 失败(疑似凭证/网络,非非快进类): $(grep -iE 'error|fatal|timed out|could not|permission denied' "$PUSH_LOG" | head -3 | tr '\n' ' ')"
+    break
+  fi
+done
+if [ "$push_ok" -ne 1 ]; then
+  log "git push 最终失败,本轮结果未同步远端(下次运行将重试)"
 fi
 
-# 更新回归基线
+# 更新回归基线(仅有效轮才覆盖,避免无效轮污染下一轮回归对比)
 cp "$CUR_TSV" "$PREV_TSV"
 # 同步仓库根目录的全量结果 TSV(供人工/工具读取,避免与报告一起变陈旧)
 cp "$CUR_TSV" "interface_test_report.tsv"