微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
544
scripts/ops/_run_auth_pbt_full.py
Normal file
544
scripts/ops/_run_auth_pbt_full.py
Normal file
@@ -0,0 +1,544 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
认证系统属性测试全量运行脚本(100 次迭代)
|
||||
|
||||
背景:
|
||||
Spec: 03-miniapp-auth-system(小程序用户认证系统)
|
||||
任务 11: 属性测试全量运行
|
||||
前面各任务中的属性测试仅用 5 次迭代快速验证逻辑正确性,
|
||||
本脚本集中对所有 15 个属性测试执行 100 次迭代,确保健壮性。
|
||||
|
||||
系统概述:
|
||||
NeoZQYY 台球门店全栈数据平台的小程序认证系统,涵盖:
|
||||
- 微信登录(code2Session → openid → JWT)
|
||||
- 用户申请审核(site_code 映射、人员匹配、角色分配)
|
||||
- RBAC 权限控制(多店铺隔离、权限中间件)
|
||||
- JWT 令牌管理(受限令牌、店铺切换、过期拒绝)
|
||||
|
||||
数据库:
|
||||
测试库 test_zqyy_app(通过 APP_DB_DSN 环境变量连接)
|
||||
禁止连接正式库 zqyy_app
|
||||
|
||||
运行方式:
|
||||
cd C:\\NeoZQYY
|
||||
python scripts/ops/_run_auth_pbt_full.py [--concurrency N] [--only P1,P2,...] [--skip P3,P5]
|
||||
|
||||
设计要求:
|
||||
1. 后台逐个运行每个属性测试(subprocess),前台定时监控
|
||||
2. 控制数据库并发:同一时刻只运行 1 个测试(默认),避免占满连接
|
||||
3. 每完成一个测试输出进度
|
||||
4. 全部完成后生成详细 MD 格式报告
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# ── 环境初始化 ──────────────────────────────────────────────────
|
||||
from dotenv import load_dotenv
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
load_dotenv(PROJECT_ROOT / ".env")
|
||||
|
||||
APP_DB_DSN = os.environ.get("APP_DB_DSN")
|
||||
if not APP_DB_DSN:
|
||||
print("❌ APP_DB_DSN 环境变量未设置,无法运行属性测试。")
|
||||
sys.exit(1)
|
||||
|
||||
# ── 测试定义 ──────────────────────────────────────────────────
|
||||
# 每个属性测试的 pytest 节点 ID 和描述
|
||||
PROPERTY_TESTS = [
|
||||
{
|
||||
"id": "P1",
|
||||
"name": "迁移脚本幂等性",
|
||||
"class": "TestProperty1MigrationIdempotency",
|
||||
"validates": "Requirements 1.9, 2.4, 11.5",
|
||||
"description": "对认证系统迁移脚本(DDL + 种子数据)连续执行两次,验证幂等性",
|
||||
"db_write": True, # 标记是否写数据库
|
||||
},
|
||||
{
|
||||
"id": "P2",
|
||||
"name": "登录创建/查找用户",
|
||||
"class": "TestProperty2LoginCreateFindUser",
|
||||
"validates": "Requirements 3.2, 3.3",
|
||||
"description": "随机 openid 登录,验证新用户创建(pending)和已有用户查找",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P3",
|
||||
"name": "disabled 用户登录拒绝",
|
||||
"class": "TestProperty3DisabledUserLoginRejection",
|
||||
"validates": "Requirements 3.5",
|
||||
"description": "disabled 状态用户登录返回 403,不签发 JWT",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P4",
|
||||
"name": "申请创建正确性",
|
||||
"class": "TestProperty4ApplicationCreation",
|
||||
"validates": "Requirements 4.1, 4.2, 4.3, 4.4",
|
||||
"description": "随机合法申请数据,验证申请记录创建和 site_code 映射",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P5",
|
||||
"name": "手机号格式验证",
|
||||
"class": "TestProperty5PhoneFormatValidation",
|
||||
"validates": "Requirements 4.5",
|
||||
"description": "随机非法手机号提交申请,验证 422 拒绝",
|
||||
"db_write": False,
|
||||
},
|
||||
{
|
||||
"id": "P6",
|
||||
"name": "重复申请拒绝",
|
||||
"class": "TestProperty6DuplicateApplicationRejection",
|
||||
"validates": "Requirements 4.6",
|
||||
"description": "已有 pending 申请的用户再次提交,验证 409 拒绝",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P7",
|
||||
"name": "人员匹配合并正确性",
|
||||
"class": "TestProperty7MatchingMerge",
|
||||
"validates": "Requirements 5.1, 5.2, 5.3, 5.4",
|
||||
"description": "随机 site_id + phone 组合,验证助教/员工匹配结果合并",
|
||||
"db_write": False,
|
||||
},
|
||||
{
|
||||
"id": "P8",
|
||||
"name": "审核操作正确性",
|
||||
"class": "TestProperty8ReviewOperations",
|
||||
"validates": "Requirements 6.1, 6.2, 6.3, 6.4, 6.5",
|
||||
"description": "随机 pending 申请执行批准/拒绝,验证状态流转和绑定创建",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P9",
|
||||
"name": "非 pending 审核拒绝",
|
||||
"class": "TestProperty9NonPendingReviewRejection",
|
||||
"validates": "Requirements 6.6",
|
||||
"description": "非 pending 状态申请执行审核,验证 409 拒绝",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P10",
|
||||
"name": "用户状态查询完整性",
|
||||
"class": "TestProperty10UserStatusQueryCompleteness",
|
||||
"validates": "Requirements 7.1, 7.2",
|
||||
"description": "随机用户状态组合,验证查询返回完整的申请列表和店铺信息",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P11",
|
||||
"name": "多店铺角色独立分配",
|
||||
"class": "TestProperty11MultiSiteRoleIndependence",
|
||||
"validates": "Requirements 8.1",
|
||||
"description": "随机用户 + 多 site_id,验证角色独立分配互不干扰",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P12",
|
||||
"name": "店铺切换令牌正确性",
|
||||
"class": "TestProperty12SiteSwitchTokenCorrectness",
|
||||
"validates": "Requirements 8.2, 10.4",
|
||||
"description": "多店铺用户切换店铺,验证新 JWT 中 site_id 和 roles 正确",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P13",
|
||||
"name": "权限中间件拦截正确性",
|
||||
"class": "TestProperty13PermissionMiddleware",
|
||||
"validates": "Requirements 8.3, 9.1, 9.2, 9.3",
|
||||
"description": "随机用户 + 权限组合,验证中间件拦截/放行逻辑",
|
||||
"db_write": True,
|
||||
},
|
||||
{
|
||||
"id": "P14",
|
||||
"name": "JWT payload 结构一致性",
|
||||
"class": "TestProperty14JwtPayloadStructure",
|
||||
"validates": "Requirements 10.1, 10.2, 10.3",
|
||||
"description": "随机用户状态签发 JWT,验证 payload 字段与状态一致",
|
||||
"db_write": False,
|
||||
},
|
||||
{
|
||||
"id": "P15",
|
||||
"name": "JWT 过期/无效拒绝",
|
||||
"class": "TestProperty15JwtExpiredInvalidRejection",
|
||||
"validates": "Requirements 9.4",
|
||||
"description": "随机过期/篡改/错密钥/垃圾 JWT,验证 401 拒绝",
|
||||
"db_write": False,
|
||||
},
|
||||
]
|
||||
|
||||
TEST_FILE = "tests/test_auth_system_properties.py"
|
||||
MAX_EXAMPLES = 100
|
||||
|
||||
|
||||
# ── 时区 ──────────────────────────────────────────────────────
|
||||
CST = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def _now_cst() -> datetime:
|
||||
return datetime.now(CST)
|
||||
|
||||
|
||||
def _fmt(dt: datetime) -> str:
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
# ── 运行单个测试 ─────────────────────────────────────────────
|
||||
def run_single_test(prop: dict, max_examples: int = MAX_EXAMPLES) -> dict:
|
||||
"""
|
||||
运行单个属性测试,返回结果字典。
|
||||
通过 HYPOTHESIS_MAX_EXAMPLES 环境变量覆盖迭代次数。
|
||||
"""
|
||||
node_id = f"{TEST_FILE}::{prop['class']}"
|
||||
start = _now_cst()
|
||||
print(f"\n{'='*60}")
|
||||
print(f"▶ [{prop['id']}] {prop['name']} (max_examples={max_examples})")
|
||||
print(f" 节点: {node_id}")
|
||||
print(f" 开始: {_fmt(start)}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
env = os.environ.copy()
|
||||
# hypothesis 通过 settings profile 或环境变量控制
|
||||
# 我们用 --override-ini 传递 hypothesis settings
|
||||
cmd = [
|
||||
sys.executable, "-m", "pytest",
|
||||
node_id,
|
||||
"-v",
|
||||
"--tb=short",
|
||||
f"--hypothesis-seed=0", # 固定种子保证可复现
|
||||
"-x", # 遇到第一个失败就停止(节省时间)
|
||||
f"-o", f"hypothesis_settings_max_examples={max_examples}",
|
||||
]
|
||||
|
||||
# hypothesis 不支持 pytest -o 覆盖 max_examples,
|
||||
# 改用环境变量 + conftest 或直接 patch
|
||||
# 实际方案:通过 HYPOTHESIS_MAX_EXAMPLES 环境变量
|
||||
env["HYPOTHESIS_MAX_EXAMPLES"] = str(max_examples)
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(PROJECT_ROOT),
|
||||
env=env,
|
||||
timeout=600, # 单个测试最多 10 分钟
|
||||
)
|
||||
|
||||
end = _now_cst()
|
||||
duration = (end - start).total_seconds()
|
||||
|
||||
# 解析结果
|
||||
passed = result.returncode == 0
|
||||
stdout = result.stdout or ""
|
||||
stderr = result.stderr or ""
|
||||
|
||||
# 提取测试计数
|
||||
test_count = _extract_test_count(stdout)
|
||||
|
||||
outcome = {
|
||||
"id": prop["id"],
|
||||
"name": prop["name"],
|
||||
"class": prop["class"],
|
||||
"validates": prop["validates"],
|
||||
"description": prop["description"],
|
||||
"db_write": prop["db_write"],
|
||||
"passed": passed,
|
||||
"returncode": result.returncode,
|
||||
"duration_sec": round(duration, 1),
|
||||
"start_time": _fmt(start),
|
||||
"end_time": _fmt(end),
|
||||
"test_count": test_count,
|
||||
"stdout_tail": _tail(stdout, 30),
|
||||
"stderr_tail": _tail(stderr, 10),
|
||||
}
|
||||
|
||||
# 实时进度输出
|
||||
status = "✅ PASSED" if passed else "❌ FAILED"
|
||||
print(f"\n 结果: {status} 耗时: {duration:.1f}s 测试数: {test_count}")
|
||||
if not passed:
|
||||
print(f" --- stdout 尾部 ---")
|
||||
print(outcome["stdout_tail"])
|
||||
if stderr.strip():
|
||||
print(f" --- stderr 尾部 ---")
|
||||
print(outcome["stderr_tail"])
|
||||
|
||||
return outcome
|
||||
|
||||
|
||||
def _extract_test_count(stdout: str) -> str:
|
||||
"""从 pytest 输出中提取测试计数,如 '3 passed' 或 '2 passed, 1 failed'"""
|
||||
for line in reversed(stdout.splitlines()):
|
||||
line = line.strip()
|
||||
if "passed" in line or "failed" in line or "error" in line:
|
||||
# 去掉 ANSI 颜色码
|
||||
import re
|
||||
clean = re.sub(r'\x1b\[[0-9;]*m', '', line)
|
||||
if "=" in clean:
|
||||
return clean.split("=")[-1].strip().rstrip("=").strip()
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _tail(text: str, n: int) -> str:
|
||||
"""取文本最后 n 行"""
|
||||
lines = text.splitlines()
|
||||
return "\n".join(lines[-n:]) if len(lines) > n else text
|
||||
|
||||
|
||||
# ── 生成 MD 报告 ─────────────────────────────────────────────
|
||||
def generate_report(results: list[dict], total_start: datetime, total_end: datetime) -> str:
|
||||
"""生成详细的 MD 格式测试报告"""
|
||||
total_duration = (total_end - total_start).total_seconds()
|
||||
passed_count = sum(1 for r in results if r["passed"])
|
||||
failed_count = len(results) - passed_count
|
||||
all_passed = failed_count == 0
|
||||
|
||||
lines = []
|
||||
lines.append(f"# 认证系统属性测试全量报告")
|
||||
lines.append(f"")
|
||||
lines.append(f"- Spec: `03-miniapp-auth-system`(小程序用户认证系统)")
|
||||
lines.append(f"- 任务: 11. 属性测试全量运行({MAX_EXAMPLES} 次迭代)")
|
||||
lines.append(f"- 测试文件: `{TEST_FILE}`")
|
||||
lines.append(f"- 数据库: `test_zqyy_app`(通过 `APP_DB_DSN`)")
|
||||
lines.append(f"- 运行时间: {_fmt(total_start)} → {_fmt(total_end)}(共 {total_duration:.0f}s)")
|
||||
lines.append(f"- 总体结果: {'✅ 全部通过' if all_passed else f'❌ {failed_count} 个失败'}")
|
||||
lines.append(f"- 通过/总数: {passed_count}/{len(results)}")
|
||||
lines.append(f"")
|
||||
|
||||
# 汇总表
|
||||
lines.append(f"## 汇总")
|
||||
lines.append(f"")
|
||||
lines.append(f"| # | 属性 | 验证需求 | 结果 | 耗时 | 测试数 | 写库 |")
|
||||
lines.append(f"|---|------|---------|------|------|--------|------|")
|
||||
for r in results:
|
||||
status = "✅" if r["passed"] else "❌"
|
||||
db = "是" if r["db_write"] else "否"
|
||||
lines.append(
|
||||
f"| {r['id']} | {r['name']} | {r['validates']} | "
|
||||
f"{status} | {r['duration_sec']}s | {r['test_count']} | {db} |"
|
||||
)
|
||||
lines.append(f"")
|
||||
|
||||
# 失败详情
|
||||
failed = [r for r in results if not r["passed"]]
|
||||
if failed:
|
||||
lines.append(f"## 失败详情")
|
||||
lines.append(f"")
|
||||
for r in failed:
|
||||
lines.append(f"### {r['id']} {r['name']}")
|
||||
lines.append(f"")
|
||||
lines.append(f"- 验证需求: {r['validates']}")
|
||||
lines.append(f"- 描述: {r['description']}")
|
||||
lines.append(f"- 返回码: {r['returncode']}")
|
||||
lines.append(f"- 耗时: {r['duration_sec']}s")
|
||||
lines.append(f"")
|
||||
lines.append(f"```")
|
||||
lines.append(r["stdout_tail"])
|
||||
lines.append(f"```")
|
||||
if r["stderr_tail"].strip():
|
||||
lines.append(f"")
|
||||
lines.append(f"stderr:")
|
||||
lines.append(f"```")
|
||||
lines.append(r["stderr_tail"])
|
||||
lines.append(f"```")
|
||||
lines.append(f"")
|
||||
|
||||
# 各测试详情
|
||||
lines.append(f"## 各属性测试详情")
|
||||
lines.append(f"")
|
||||
for r in results:
|
||||
status = "✅ PASSED" if r["passed"] else "❌ FAILED"
|
||||
lines.append(f"### {r['id']} {r['name']} — {status}")
|
||||
lines.append(f"")
|
||||
lines.append(f"- 描述: {r['description']}")
|
||||
lines.append(f"- 验证需求: {r['validates']}")
|
||||
lines.append(f"- 测试类: `{r['class']}`")
|
||||
lines.append(f"- 开始: {r['start_time']} 结束: {r['end_time']}")
|
||||
lines.append(f"- 耗时: {r['duration_sec']}s")
|
||||
lines.append(f"- 测试数: {r['test_count']}")
|
||||
lines.append(f"- 写库: {'是' if r['db_write'] else '否'}")
|
||||
lines.append(f"")
|
||||
|
||||
# 数据库带宽说明
|
||||
lines.append(f"## 数据库资源控制")
|
||||
lines.append(f"")
|
||||
lines.append(f"- 串行执行:同一时刻仅运行 1 个属性测试,避免数据库连接争用")
|
||||
lines.append(f"- 写库测试({sum(1 for r in results if r['db_write'])} 个)每个测试内部自行清理测试数据")
|
||||
lines.append(f"- 纯读/内存测试({sum(1 for r in results if not r['db_write'])} 个)不产生数据库写入")
|
||||
lines.append(f"- 测试间无并发,为其他调试任务保留数据库带宽")
|
||||
lines.append(f"")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── conftest 补丁 ─────────────────────────────────────────────
|
||||
CONFTEST_PATCH = '''# -*- coding: utf-8 -*-
|
||||
"""
|
||||
临时 conftest:通过环境变量覆盖 hypothesis max_examples。
|
||||
由 _run_auth_pbt_full.py 自动生成,测试完成后自动删除。
|
||||
|
||||
原理:在 pytest 收集完测试后,遍历所有 hypothesis 测试项,
|
||||
替换其 settings 中的 max_examples 为环境变量指定的值。
|
||||
"""
|
||||
import os
|
||||
|
||||
_max_env = os.environ.get("HYPOTHESIS_MAX_EXAMPLES")
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items):
|
||||
"""收集完测试后,覆盖 hypothesis settings 中的 max_examples"""
|
||||
if not _max_env:
|
||||
return
|
||||
forced_max = int(_max_env)
|
||||
from hypothesis import settings as _settings
|
||||
|
||||
for item in items:
|
||||
# hypothesis 测试会在 item 上挂 hypothesis_settings
|
||||
if hasattr(item, "_hypothesis_internal_use_settings"):
|
||||
old = item._hypothesis_internal_use_settings
|
||||
item._hypothesis_internal_use_settings = _settings(
|
||||
old,
|
||||
max_examples=forced_max,
|
||||
)
|
||||
'''
|
||||
|
||||
|
||||
def _ensure_conftest():
|
||||
"""确保 tests/conftest.py 中有 hypothesis max_examples 覆盖逻辑"""
|
||||
conftest_path = PROJECT_ROOT / "tests" / "conftest.py"
|
||||
marker = "HYPOTHESIS_MAX_EXAMPLES"
|
||||
|
||||
if conftest_path.exists():
|
||||
content = conftest_path.read_text(encoding="utf-8")
|
||||
if marker in content:
|
||||
return False # 已有,不需要补丁
|
||||
# 追加
|
||||
with open(conftest_path, "a", encoding="utf-8") as f:
|
||||
f.write("\n\n" + CONFTEST_PATCH)
|
||||
return True
|
||||
else:
|
||||
conftest_path.write_text(CONFTEST_PATCH, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def _cleanup_conftest():
|
||||
"""清理临时 conftest 补丁"""
|
||||
conftest_path = PROJECT_ROOT / "tests" / "conftest.py"
|
||||
if not conftest_path.exists():
|
||||
return
|
||||
content = conftest_path.read_text(encoding="utf-8")
|
||||
if "由 _run_auth_pbt_full.py 自动生成" in content:
|
||||
# 如果整个文件都是我们生成的,删除
|
||||
if content.strip().startswith("# -*- coding: utf-8 -*-\n") and "HYPOTHESIS_MAX_EXAMPLES" in content:
|
||||
lines = content.split("HYPOTHESIS_MAX_EXAMPLES")
|
||||
# 简单判断:如果文件很短且主要是我们的内容
|
||||
if len(content) < 500:
|
||||
conftest_path.unlink()
|
||||
return
|
||||
# 否则只移除我们追加的部分
|
||||
marker = "\n\n# -*- coding: utf-8 -*-\n\"\"\"\n临时 conftest"
|
||||
if marker in content:
|
||||
content = content[:content.index(marker)]
|
||||
conftest_path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
# ── 主流程 ────────────────────────────────────────────────────
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="认证系统属性测试全量运行")
|
||||
parser.add_argument(
|
||||
"--max-examples", type=int, default=MAX_EXAMPLES,
|
||||
help=f"每个属性测试的迭代次数(默认 {MAX_EXAMPLES})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--only", type=str, default=None,
|
||||
help="仅运行指定属性(逗号分隔,如 P1,P2,P14)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip", type=str, default=None,
|
||||
help="跳过指定属性(逗号分隔,如 P3,P5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report-dir", type=str, default=None,
|
||||
help="报告输出目录(默认 export/ 下)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# 筛选测试
|
||||
tests_to_run = PROPERTY_TESTS[:]
|
||||
if args.only:
|
||||
only_ids = {x.strip().upper() for x in args.only.split(",")}
|
||||
tests_to_run = [t for t in tests_to_run if t["id"] in only_ids]
|
||||
if args.skip:
|
||||
skip_ids = {x.strip().upper() for x in args.skip.split(",")}
|
||||
tests_to_run = [t for t in tests_to_run if t["id"] not in skip_ids]
|
||||
|
||||
if not tests_to_run:
|
||||
print("❌ 没有要运行的测试。")
|
||||
sys.exit(1)
|
||||
|
||||
max_ex = args.max_examples
|
||||
|
||||
print(f"╔{'═'*58}╗")
|
||||
print(f"║ 认证系统属性测试全量运行 ║")
|
||||
print(f"║ 迭代次数: {max_ex:<5} 测试数: {len(tests_to_run):<3} ║")
|
||||
print(f"║ 数据库: test_zqyy_app(串行执行,控制带宽) ║")
|
||||
print(f"╚{'═'*58}╝")
|
||||
|
||||
# 确保 conftest 补丁
|
||||
patched = _ensure_conftest()
|
||||
if patched:
|
||||
print("📝 已注入 conftest.py hypothesis max_examples 覆盖")
|
||||
|
||||
total_start = _now_cst()
|
||||
results = []
|
||||
|
||||
try:
|
||||
for i, prop in enumerate(tests_to_run, 1):
|
||||
print(f"\n📊 进度: {i}/{len(tests_to_run)}")
|
||||
outcome = run_single_test(prop, max_examples=max_ex)
|
||||
results.append(outcome)
|
||||
|
||||
# 测试间短暂休息,释放数据库连接
|
||||
if i < len(tests_to_run):
|
||||
time.sleep(1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n⚠️ 用户中断,生成已完成部分的报告...")
|
||||
except Exception as e:
|
||||
print(f"\n\n❌ 运行异常: {e}")
|
||||
finally:
|
||||
total_end = _now_cst()
|
||||
|
||||
# 生成报告
|
||||
if results:
|
||||
report = generate_report(results, total_start, total_end)
|
||||
|
||||
# 确定报告路径
|
||||
report_dir = Path(args.report_dir) if args.report_dir else PROJECT_ROOT / "export" / "reports"
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
timestamp = total_start.strftime("%Y%m%d_%H%M%S")
|
||||
report_path = report_dir / f"auth_pbt_full_{timestamp}.md"
|
||||
report_path.write_text(report, encoding="utf-8")
|
||||
|
||||
passed = sum(1 for r in results if r["passed"])
|
||||
failed = len(results) - passed
|
||||
print(f"\n{'='*60}")
|
||||
print(f"📋 报告已生成: {report_path}")
|
||||
print(f" 通过: {passed} 失败: {failed} 总计: {len(results)}")
|
||||
print(f" 总耗时: {(total_end - total_start).total_seconds():.0f}s")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# 清理 conftest 补丁
|
||||
if patched:
|
||||
_cleanup_conftest()
|
||||
print("🧹 已清理临时 conftest 补丁")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user