|
|
@@ -2,7 +2,8 @@
|
|
|
# =============================================================================
|
|
|
# CFC 接口测试闭环(定时任务主体)
|
|
|
# 流程:git pull 最新 -> 检测新增/变更需求 -> 跑冒烟(含回归对比) ->
|
|
|
-# 为新增接口补全用例并跑新测试 -> 提交测试结果 -> push 到远端(与开发团队闭环)
|
|
|
+# 为新增接口补全用例并跑新测试 -> 输出测试用例完成情况(总数/已完成/未开始原因) ->
|
|
|
+# 提交测试结果 -> push 到远端(与开发团队闭环)
|
|
|
#
|
|
|
# 网络说明:原默认目标 192.168.16.251 已不可用(上游失效)。
|
|
|
# 当前默认测试目标为 cfc.iwintrue.com(公网可达、后端在线)。
|
|
|
@@ -252,6 +253,155 @@ if INVALID:
|
|
|
print(f"[守卫] INVALID 触发:{';'.join(reasons)} --> 跳过提交/推送/基线更新")
|
|
|
PY
|
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 4.5) 测试用例完成情况检查(总数 / 已完成 / 未完成原因)
|
|
|
+# 统计自动化用例(test()) 与文档化用例(TC-xxxx),并标注未开始执行的原因,
|
|
|
+# 追加到报告与趋势日志,供人工与闭环跟踪。
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+python - "$CUR_TSV" "$RUN_DIR" "$REPO_DIR" "$TREND" <<'PY'
|
|
|
+import sys, os, re, glob, json
|
|
|
+cur_tsv, run_dir, repo_dir, trend = sys.argv[1:5]
|
|
|
+
|
|
|
+# —— 读取 step4 导出的指标(接口冒烟)——
|
|
|
+metrics = {}
|
|
|
+_mp = os.path.join(repo_dir, 'interface_test_metrics.env')
|
|
|
+try:
|
|
|
+ for line in open(_mp, encoding='utf-8', errors='ignore'):
|
|
|
+ line = line.strip()
|
|
|
+ if '=' in line and not line.startswith('#'):
|
|
|
+ k, v = line.split('=', 1)
|
|
|
+ metrics[k.strip()] = v.strip()
|
|
|
+except FileNotFoundError:
|
|
|
+ pass
|
|
|
+INVALID = metrics.get('INVALID', '0') == '1'
|
|
|
+OK_COUNT = int(metrics.get('OK_COUNT', '0') or 0)
|
|
|
+
|
|
|
+tests_dir = os.path.join(repo_dir, 'tests')
|
|
|
+
|
|
|
+# 1) 文档化用例 TC-xxxx
|
|
|
+doc_tcs = set()
|
|
|
+for f in glob.glob(os.path.join(tests_dir, 'test-cases', '*.md')):
|
|
|
+ try:
|
|
|
+ t = open(f, encoding='utf-8', errors='ignore').read()
|
|
|
+ except Exception:
|
|
|
+ continue
|
|
|
+ for m in re.findall(r'TC-[A-Z]?-?\d+', t):
|
|
|
+ doc_tcs.add(m)
|
|
|
+TOTAL_DOC = len(doc_tcs)
|
|
|
+
|
|
|
+# 2) 自动化用例 test()(按层级分类)
|
|
|
+specs = glob.glob(os.path.join(tests_dir, 'e2e', '**', '*.spec.js'), recursive=True)
|
|
|
+e2e_total = dom = roles = api = skip = 0
|
|
|
+for f in specs:
|
|
|
+ try:
|
|
|
+ src = open(f, encoding='utf-8', errors='ignore').read()
|
|
|
+ except Exception:
|
|
|
+ continue
|
|
|
+ n = len(re.findall(r'^\s*test\s*\(', src, re.M))
|
|
|
+ e2e_total += n
|
|
|
+ s = (len(re.findall(r'test\s*\.\s*(?:skip|fixme)\s*\(', src))
|
|
|
+ + len(re.findall(r'test\s*\.\s*describe\s*\.\s*skip\s*\(', src)))
|
|
|
+ skip += s
|
|
|
+ p = f.replace('\\', '/')
|
|
|
+ if '/e2e/api/' in p:
|
|
|
+ api += n
|
|
|
+ elif '/e2e/roles/' in p:
|
|
|
+ roles += n
|
|
|
+ else:
|
|
|
+ dom += n
|
|
|
+completed_runnable = e2e_total - skip
|
|
|
+
|
|
|
+# 3) API e2e 末次实跑(.last-run.json)
|
|
|
+api_pass = api_fixme = 0
|
|
|
+lr = os.path.join(tests_dir, 'test-results', '.last-run.json')
|
|
|
+if os.path.exists(lr):
|
|
|
+ try:
|
|
|
+ j = json.load(open(lr, encoding='utf-8'))
|
|
|
+ if isinstance(j, dict):
|
|
|
+ api_pass = int(j.get('passed', j.get('stats', {}).get('expected', 0) or 0))
|
|
|
+ api_fixme = int(j.get('fixme', j.get('stats', {}).get('skipped', 0) or 0))
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
+
|
|
|
+# 4) NOT_FOUND 端点数(从本轮 CUR_TSV 直接统计,避免依赖 env 未导出字段)
|
|
|
+nf = 0
|
|
|
+try:
|
|
|
+ for line in open(cur_tsv, encoding='utf-8', errors='ignore'):
|
|
|
+ c = line.rstrip('\n').split('\t')
|
|
|
+ if len(c) >= 7 and c[6].strip() == 'NOT_FOUND':
|
|
|
+ nf += 1
|
|
|
+except Exception:
|
|
|
+ pass
|
|
|
+
|
|
|
+def pct(a, b):
|
|
|
+ return f"{a * 100 // b}%" if b else "0%"
|
|
|
+
|
|
|
+reasons = []
|
|
|
+if dom > 0:
|
|
|
+ reasons.append((dom, f"DOM 型 E2E 无法执行(CFC-ENV-001):{dom} 个用例已编写,但远程 cfc.iwintrue.com 为纯 Spring Boot API 后端、未部署 uni-app H5 / cfc-web 前端,浏览器型用例无法运行,未开始执行。"))
|
|
|
+if skip > 0:
|
|
|
+ reasons.append((skip, f"已知缺陷/待上线导致跳过:{skip} 个用例被 test.skip/test.fixme 标记(能量发放 500-CFC-EN-001、昵称未持久化-CFC-FM-001 等),待后端修复后启用。"))
|
|
|
+if nf > 0:
|
|
|
+ reasons.append((nf, f"新功能待部署后验证:接口冒烟发现 {nf} 个 NOT_FOUND 端点,对应前端页面尚未部署到测试环境,相关 E2E 暂挂起。"))
|
|
|
+if TOTAL_DOC > 0:
|
|
|
+ reasons.append((TOTAL_DOC, f"文档化用例未全量自动化:{TOTAL_DOC} 条设计用例(TC-xxxx),核心主链路已落地 e2e,边缘/异常流仍以文档形式待补自动化。"))
|
|
|
+
|
|
|
+section = []
|
|
|
+section.append("\n## 测试用例完成情况")
|
|
|
+section.append(f"- 自动化用例总数(可执行 `test()`):**{e2e_total}**(api 层 {api} / 角色分包 {roles} / DOM 层 {dom})")
|
|
|
+section.append(f"- 文档化用例(设计层 TC-xxxx):**{TOTAL_DOC}**")
|
|
|
+section.append(f"- 已完成(自动化且未被跳过):**{completed_runnable}/{e2e_total} = {pct(completed_runnable, e2e_total)}**")
|
|
|
+section.append(f"- 已实跑通过:接口冒烟 BIZ_OK **{OK_COUNT}** 端点 + API e2e 末次 **{api_pass}** 通过 / {api_fixme} fixme")
|
|
|
+section.append("")
|
|
|
+section.append(f"### 未完成 / 未开始的原因(共 {len(reasons)} 类)")
|
|
|
+if reasons:
|
|
|
+ for i, (c, t) in enumerate(reasons, 1):
|
|
|
+ section.append(f"{i}. [{c} 项] {t}")
|
|
|
+else:
|
|
|
+ section.append("- 无(全部用例均已落地并可执行)")
|
|
|
+section.append("")
|
|
|
+section.append("> 说明:自动化完成率按 `test()` 未被 skip/fixme 计;DOM 层受 CFC-ENV-001 限制需前端部署后执行,属环境阻塞而非用例缺失。")
|
|
|
+report_txt = "\n".join(section)
|
|
|
+
|
|
|
+# 追加到本轮运行目录报告(始终)
|
|
|
+_run_rpt = os.path.join(run_dir, 'INTERFACE_TEST_REPORT.md')
|
|
|
+try:
|
|
|
+ _b = open(_run_rpt, encoding='utf-8', errors='ignore').read()
|
|
|
+ open(_run_rpt, 'w', encoding='utf-8').write(_b.rstrip() + "\n\n" + report_txt)
|
|
|
+except FileNotFoundError:
|
|
|
+ open(_run_rpt, 'w', encoding='utf-8').write(report_txt)
|
|
|
+
|
|
|
+# 仅有效轮才写回仓库根报告
|
|
|
+if not INVALID:
|
|
|
+ _repo_rpt = os.path.join(repo_dir, 'INTERFACE_TEST_REPORT.md')
|
|
|
+ try:
|
|
|
+ _b = open(_repo_rpt, encoding='utf-8', errors='ignore').read()
|
|
|
+ open(_repo_rpt, 'w', encoding='utf-8').write(_b.rstrip() + "\n\n" + report_txt)
|
|
|
+ except FileNotFoundError:
|
|
|
+ pass
|
|
|
+
|
|
|
+# JSON 供工具读取
|
|
|
+js = dict(e2e_total=e2e_total, api=api, roles=roles, dom=dom, skip=skip,
|
|
|
+ completed_runnable=completed_runnable, doc_total=TOTAL_DOC,
|
|
|
+ smoke_ok=OK_COUNT, api_pass=api_pass, api_fixme=api_fixme,
|
|
|
+ reasons=[{"count": c, "reason": t} for c, t in reasons], invalid=INVALID)
|
|
|
+json.dump(js, open(os.path.join(run_dir, 'test_case_completion.json'), 'w', encoding='utf-8'),
|
|
|
+ ensure_ascii=False, indent=2)
|
|
|
+if not INVALID:
|
|
|
+ json.dump(js, open(os.path.join(repo_dir, 'test_case_completion.json'), 'w', encoding='utf-8'),
|
|
|
+ ensure_ascii=False, indent=2)
|
|
|
+
|
|
|
+# 趋势日志
|
|
|
+with open(trend, 'a', encoding='utf-8') as f:
|
|
|
+ f.write(f"\tTC_E2E={e2e_total}\tTC_DONE={completed_runnable}\tTC_DOC={TOTAL_DOC}\tTC_SKIP={skip}\tTC_DOM={dom}\n")
|
|
|
+
|
|
|
+# 导出指标给 bash(追加,供 commit 信息使用)
|
|
|
+with open(os.path.join(repo_dir, 'interface_test_metrics.env'), 'a', encoding='utf-8') as f:
|
|
|
+ f.write(f"TC_E2E_TOTAL={e2e_total}\nTC_DONE={completed_runnable}\nTC_DOC={TOTAL_DOC}\nTC_SKIP={skip}\n")
|
|
|
+
|
|
|
+print(f"[测试用例] 总数(自动化)={e2e_total} 已完成(可运行)={completed_runnable} 文档化={TOTAL_DOC} 跳过={skip} | 原因 {len(reasons)} 类")
|
|
|
+PY
|
|
|
+
|
|
|
# ---------------------------------------------------------------------------
|
|
|
# 5) 守卫判定:无效轮(宕机/NON_JSON 风暴/502 抖动/登录失败/部分轮)
|
|
|
# 不提交、不推送、不更新回归基线,避免污染趋势与开发侧闭环
|
|
|
@@ -280,8 +430,8 @@ git add -f INTERFACE_TEST_REPORT.md interface_test_report.tsv cfc-frontend/INTER
|
|
|
if git diff --cached --quiet; then
|
|
|
log "无测试结果变更,跳过提交"
|
|
|
else
|
|
|
- git commit -m "test(interface): 接口冒烟闭环 $(date '+%Y-%m-%d %H:%M') — 通过率 ${RATE}% (fix=${FIXED_COUNT} reg=${REGRESSED_COUNT})" \
|
|
|
- >>"$LOG_DIR/interface_cron.log" 2>&1 && log "已提交测试结果"
|
|
|
+ git commit -m "test(interface): 接口冒烟闭环 $(date '+%Y-%m-%d %H:%M') — 通过率 ${RATE}% (fix=${FIXED_COUNT} reg=${REGRESSED_COUNT}); 测试用例 ${TC_DONE:-?}/${TC_E2E_TOTAL:-?} 完成(文档化${TC_DOC:-?})" \
|
|
|
+ >>"$LOG_DIR/interface_cron.log" 2>&1 && log "已提交测试结果(测试用例完成 ${TC_DONE:-?}/${TC_E2E_TOTAL:-?})"
|
|
|
fi
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|