Files
xiaotutongxue/scripts/verify-all.py
T
lovife 4e68d0e5da test(e2e): 新增填报流程端到端脚本与测试计划
既有 37 个脚本是按模块切片的(门禁/草稿/提交/审核各一个),有三处固定盲区:
跨步骤的状态轨迹没人断言、切片之间那条缝(驳回后改了字段再交)、以及脚本
docstring 自述与实际断言不符(e2e-m3-draft.py:11 自述的那条断言全文没有)。

docs/11 给出 A~J 十组覆盖矩阵与判据口径(含用 grep 核对实际断言、不读自述的
规程),scripts/e2e-m3-flow.py 只补这三处、其余交给既有脚本,并钉住 F-125 /
F-128 的现状(XFAIL)。verify-all 的脚本数口径随之变成 38。

配套 scripts/dev-reveal-roster-idcard.py:L3-B 走查要在门禁界面手输身份证,
而明文只在建夹具那一刻存在过(库里是 AES 密文 + 脱敏 + HMAC 索引),
此脚本只读解密打印,不入库;用完连同夹具任务一起 --clean。
2026-10-14 15:40:18 +08:00

214 lines
8.9 KiB
Python
Raw 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.
# -*- coding: utf-8 -*-
"""交付自检:一条命令把"这个仓库现在到底对不对"跑一遍。
## 为什么需要它
这个项目的验证手段有六类,散在六个地方:
| 类别 | 命令 | 需要什么 |
|---|---|---|
| 契约 ↔ 路由 | `check-api-parity.py` | 无 |
| 实体 ↔ 建表 | `check-entity-columns.py` | 无 |
| 文档自身一致 | `verify-docs.py` | 无 |
| 手册 ↔ 代码文案 | `check-manual.py` | 无 |
| 接线(定义 ↔ 使用) | `check-wiring.py` | 无 |
| 接口行为(断言总数见下方"用法"里的实测值) | `scripts/e2e-*.py` | **后端在跑 + PG/Redis** |
| 生产配置拦截 | `e2e-prodguard.py` | **能起 jar** |
| CI 那个 profile 能不能起 | `check-test-profile.py` | 后端 jar + PG/Redis |
**要记住"该跑哪几个、哪些需要前置"本身就是一种负担**,而漏跑一个的后果是
"以为过了"。所以这里合成一个入口,并且**如实说明哪些因为前置不足而跳过**
(不是静默跳过 —— 那与 F-85 是同一类问题)。
## 用法
```bash
python scripts/verify-all.py # 全部(跳过需要外部前置的,并说明)
python scripts/verify-all.py --with-e2e # 连 38 个脚本的接口断言一起跑(需要后端在跑)
python scripts/verify-all.py --quick # 只跑不需要任何前置的五项
```
> 脚本数与断言总数会随需求变化(2026-10 需求收敛后是 **37 个脚本**,加上
> `e2e-m3-flow.py` 后是 **38 个**;断言总数以本命令
> 实际输出为准 —— 别再往文档里抄一个会过期的固定数字)。
退出码:0 = 全过;1 = 有失败(**跳过不算失败**,但会明确列出来)。
"""
import argparse
import glob
import os
import re
import shutil
import subprocess
import sys
import time
import urllib.request
sys.stdout.reconfigure(encoding="utf-8")
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(ROOT)
GREEN, RED, YELLOW, DIM, RESET = "\033[32m", "\033[31m", "\033[33m", "\033[2m", "\033[0m"
if not sys.stdout.isatty():
GREEN = RED = YELLOW = DIM = RESET = ""
def run(cmd, timeout=3600):
"""跑一条命令,返回 (退出码, 输出)。"""
try:
p = subprocess.run(cmd, shell=True, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=timeout)
return p.returncode, (p.stdout or "") + (p.stderr or "")
except subprocess.TimeoutExpired:
return -9, "超时"
def backend_up():
try:
with urllib.request.urlopen("http://localhost:8080/api/v1/auth/captcha", timeout=5):
return True
except Exception:
return False
def main():
ap = argparse.ArgumentParser(description="交付自检")
ap.add_argument("--with-e2e", action="store_true", help="连 e2e 一起跑(需要后端在跑)")
ap.add_argument("--quick", action="store_true", help="只跑不需要前置的五项")
args = ap.parse_args()
results = [] # (名称, 结果, 摘要)
print("=" * 92)
print(" 交付自检 仓库:%s" % ROOT)
print("=" * 92)
# ── 一、不需要任何前置的机械校验 ──
print("\n【一】机械校验(不需要后端/数据库)")
checks = [
("契约 ↔ 路由", "python scripts/check-api-parity.py --quiet",
r"合计:契约 (\d+) / 已实现 (\d+) / 缺口 (\d+)"),
("实体 ↔ 建表", "python scripts/check-entity-columns.py --quiet",
r"(\d+) 个实体全部与迁移一致((\d+) 张表)"),
("文档自身一致", "python scripts/verify-docs.py", r"结果: (\d+) 项全部通过"),
("手册 ↔ 代码文案", "python scripts/check-manual.py", r"结果:.*"),
("接线(定义↔使用)", "python scripts/check-wiring.py --quiet",
r"未接线的声明 (\d+) 条"),
]
for name, cmd, pat in checks:
code, out = run(cmd, timeout=300)
line = ""
m = re.search(pat, out)
if m:
line = m.group(0).strip()
else:
tail = [x for x in out.strip().split("\n") if x.strip()]
line = tail[-1][:80] if tail else "(无输出)"
ok = code == 0
results.append((name, ok, line))
print(" %s%-14s%s %s" % (GREEN if ok else RED, name, RESET, line))
# 脚本语法(改脚本时最容易踩:中文串里嵌半角引号)
bad = []
for fn in sorted(glob.glob("scripts/*.py")):
try:
compile(open(fn, encoding="utf-8").read(), fn, "exec")
except SyntaxError as e:
bad.append("%s:%s" % (os.path.basename(fn), e.lineno))
ok = not bad
results.append(("脚本语法", ok, "全部可编译" if ok else "有语法错:%s" % ", ".join(bad)))
print(" %s%-14s%s %s" % (GREEN if ok else RED, "脚本语法", RESET,
"全部可编译" if ok else "有语法错:%s" % ", ".join(bad)))
if args.quick:
return summary(results)
# ── 二、需要后端在跑 / 能起 jar 的 ──
print("\n【二】带前置的检查")
up = backend_up()
if not up:
msg = "**跳过**:后端 8080 没在跑(先 `cd backend && java -jar ... --spring.profiles.active=dev`)"
results.append(("接口断言(E2E)", None, msg))
print(" %s%-14s%s %s" % (YELLOW, "接口断言", RESET, msg))
elif not args.with_e2e:
msg = "**跳过**:要跑就加 `--with-e2e`(38 个脚本、约 10 分钟)"
results.append(("接口断言(E2E)", None, msg))
print(" %s%-14s%s %s" % (YELLOW, "接口断言", RESET, msg))
else:
t0 = time.time()
files = sorted(glob.glob("scripts/e2e-*.py"))
total = fails = 0
failed_names = []
for f in files:
code, out = run("python %s" % f, timeout=900)
m = re.search(r"通过 (\d+) 失败 (\d+) 共 (\d+)", out)
if m:
total += int(m.group(1))
if int(m.group(2)) > 0:
fails += int(m.group(2))
failed_names.append(os.path.basename(f))
else:
m2 = re.search(r"结果: (\d+)/(\d+)", out)
if m2:
total += int(m2.group(2))
if m2.group(1) != m2.group(2):
fails += 1
failed_names.append(os.path.basename(f))
else:
fails += 1
failed_names.append(os.path.basename(f) + "(无汇总)")
ok = fails == 0
msg = "%d 个脚本 / %d 条断言 / 失败 %d%s(%.0fs)" % (
len(files), total, fails,
"" if ok else ":" + ", ".join(failed_names[:5]), time.time() - t0)
results.append(("接口断言(E2E)", ok, msg))
print(" %s%-14s%s %s" % (GREEN if ok else RED, "接口断言", RESET, msg))
# 生产自检:需要 jar
jar = glob.glob("backend/xt-server/target/xt-server-*.jar")
if not jar:
msg = "**跳过**:没有 jar(先 `cd backend && mvn -DskipTests package`)"
results.append(("生产配置拦截", None, msg))
print(" %s%-14s%s %s" % (YELLOW, "生产配置拦截", RESET, msg))
else:
# 起 jar 的检查会撞端口,所以只在没别的 java 在跑时做 —— 如实说明
code, out = run("python scripts/e2e-prodguard.py", timeout=900)
m = re.search(r"通过 (\d+) 失败 (\d+) 共 (\d+)", out)
ok = code == 0
msg = m.group(0) if m else ("通过" if ok else "失败")
results.append(("生产配置拦截", ok, msg))
print(" %s%-14s%s %s" % (GREEN if ok else RED, "生产配置拦截", RESET, msg))
if up:
code, out = run("python scripts/check-test-profile.py", timeout=600)
m = re.search(r"结果:(.*)", out)
ok = code == 0
msg = (m.group(1).strip() if m else ("通过" if ok else "失败"))[:70]
results.append(("test profile 能起", ok, msg))
print(" %s%-14s%s %s" % (GREEN if ok else RED, "test profile 能起", RESET, msg))
else:
results.append(("test profile 能起", None, "**跳过**:后端没在跑"))
print(" %s%-14s%s **跳过**:后端没在跑" % (YELLOW, "test profile 能起", RESET))
return summary(results)
def summary(results):
print("\n" + "=" * 92)
passed = [r for r in results if r[1] is True]
failed = [r for r in results if r[1] is False]
skipped = [r for r in results if r[1] is None]
print(" 通过 %d 失败 %d 跳过 %d" % (len(passed), len(failed), len(skipped)))
for name, ok, msg in failed:
print(" %s✗ %s%s:%s" % (RED, name, RESET, msg[:110]))
for name, ok, msg in skipped:
# ⚠️ 跳过要**明确列出来**,不能静默 —— 否则"以为过了"(与 F-85 同类)
print(" %s— %s 跳过:%s%s" % (YELLOW, name, msg[:100], RESET))
print("=" * 92)
sys.stdout.flush()
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())