Files
xst 8c61209dd4 test(物流公司管理需求): 需求文档与 QE Fleet 全流程设计产物入库
- 需求 v1.1 + 2026-09-29 需求方确认的 Q7~Q19 决议(含 BR-07/BR-09/6.7/6.9 矛盾表述修订)
- 测试点 159 个、测试用例 164 条(P0 55/P1 81/P2 23/P3 5),需求→测试点→用例三级追溯覆盖率 100%
- 质量裁决 PASS(首轮 PASS_WITH_FIX,13 项待确认确认后复裁决升级);评审原始 2 阻断项+9 建议项+3 优化项已全部修复
- 确认单:与 4 个历史需求均为并行关系、0 冲突、0 回写
- 附导出 v1/v2 版本快照、Playwright/Appium 脚本骨架、覆盖率审计脚本与指标、评审修复脚本
2026-09-29 10:03:35 +08:00

230 lines
12 KiB
Python
Raw Permalink 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 -*-
"""1) 为评审修复过的用例补 [AI修正] 标注;2) 生成覆盖率审计报告。"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "scripts"))
sys.stdout.reconfigure(encoding="utf-8")
from export_excel import load_markdown_table # noqa: E402
BASE = "物流公司管理需求"
CASES = REPO / "output" / "test_cases" / f"{BASE}_测试用例.md"
METRICS = REPO / "output" / "logs" / "coverage_metrics.json"
AUDIT = REPO / "output" / "analysis" / f"{BASE}_覆盖率审计.md"
FIX_MAP = {
"PLT_BSC_LC_028": "B-001", "PLT_BSC_LC_032": "B-001",
"PLT_BSC_LC_073": "B-002", "PLT_BSC_LC_074": "B-002",
"PLT_BSC_LC_017": "S-001", "PLT_BSC_LC_049": "S-002", "PLT_BSC_LC_054": "S-002",
"PLT_BSC_LC_061": "S-003", "PLT_BSC_LC_034": "S-003", "PLT_BSC_LC_035": "S-003",
"PLT_BSC_LC_036": "S-003", "PLT_BSC_LC_052": "S-005", "PLT_BSC_LC_067": "S-006",
"PLT_TMS_LC_037": "S-007", "PLT_BSC_LC_069": "S-008", "PLT_BSC_LC_020": "S-009",
"PLT_BSC_LC_010": "O-001",
}
# ---------- 1) AI修正 标注 ----------
lines = CASES.read_text(encoding="utf-8").splitlines(keepends=True)
line_of: dict[str, int] = {}
for i, line in enumerate(lines):
if line.startswith("| PLT_"):
line_of[line.split("|")[1].strip()] = i
marked = 0
for case_id, fix_id in FIX_MAP.items():
li = line_of[case_id]
line = lines[li]
tag = f";[AI修正: 评审修复 {fix_id}]"
if tag in line:
continue
if line.rstrip().endswith("|") and line.rstrip().endswith(" |"):
idx = line.rstrip().rfind(" |")
line = line[:idx] + tag + line[idx:]
else:
raise SystemExit(f"❌ {case_id} 备注列尾部结构异常")
lines[li] = line
marked += 1
CASES.write_text("".join(lines), encoding="utf-8")
print(f"✅ 已为 {marked} 条用例补 [AI修正: 评审修复] 标注")
# ---------- 2) 覆盖率审计报告 ----------
m = json.loads(METRICS.read_text(encoding="utf-8"))
headers, rows = load_markdown_table(CASES)
idx = {n: headers.index(n) for n in headers}
cases = [{"id": r[idx["用例编号"]], "module": r[idx["模块"]], "priority": r[idx["优先级"]],
"type": r[idx["类型"]], "note": r[idx["备注"]]} for r in rows]
header_of = {
"menu": "平台端-基础信息-物流公司管理-列表",
"new": "平台端-基础信息-物流公司管理-新增",
"edit": "平台端-基础信息-物流公司管理-编辑",
"del": "平台端-基础信息-物流公司管理-删除启停",
"contract": "平台端-基础信息-物流公司管理-合同管理",
"account": "平台端-整车运输-运费核算",
"source": "平台端-整车运输-货源管理",
"pay": "平台端-整车运输-承运结算-打款",
"list": "平台端-整车运输-列表展示",
"auth": "平台端-权限与机构隔离",
"x": "平台端-跨需求回归",
}
tp_ids = sorted(m["tp_to_cases"].keys(), key=lambda t: (t.split("-")[1], int(t.split("-")[2])))
tp_groups: dict[str, list[str]] = {}
for tp in tp_ids:
tp_groups.setdefault(tp.split("-")[1], []).append(tp)
def pct(x: float) -> str:
return f"{x:.0f}%"
risk_cases = m["risk_coverage"]
risk_priority = {"RISK-DATA": "P1", "RISK-AVAILABILITY": "P2", "RISK-FINANCIAL": "P2",
"RISK-COMPLIANCE": "P2", "RISK-COMPATIBILITY": "P2"}
L: list[str] = []
L.append(f"# {BASE} 覆盖率审计")
L.append("")
L.append("> 生成时间: 2026-09-29")
L.append(f"> 审计对象: output/test_points/{BASE}_测试点.md(159 个测试点)→ output/test_cases/{BASE}_测试用例.md(164 条用例)")
L.append(f"> 追溯骨干: output/analysis/{BASE}_分析.md 第 12 章(38 条 REQ)")
L.append(f"> 风险基线: output/analysis/{BASE}_风险评估.md(P0=0;P1=1 项 RISK-DATA;P2=4 项)")
L.append("> 质量门限: fleet_config quality_gate.min_coverage = 0.95;P0 测试点→用例 100%;P1 测试点→用例 ≥90%;P0 风险 100%")
L.append("> 审计方法: 以用例「备注列」的 TP 追溯号与测试点「描述」中的 REQ 标注建立三级追溯链,**全量脚本化计算**(脚本 output/compute_coverage.py,指标留存 output/logs/coverage_metrics.json),非抽样估算;跨用例引用在矩阵中按互引关系折算")
L.append("> 时效说明: 本报告在本轮评审修复(2 个阻断项 + 9 条建议项 + 3 条优化项)与**需求方 2026-09-29 确认 13 项待确认口径**之后执行,反映**最终产物状态**;被修复用例均在备注中标注 `[AI修正: 评审修复 <编号>]`,被确认项标注 `[已确认 2026-09-29: <口径>]` + `[AI修正: 待确认已确认 2026-09-29]`")
L.append("")
L.append("## 覆盖概览")
L.append("")
L.append("| 指标 | 值 | 目标 | 状态 |")
L.append("| :--- | :---: | :---: | :---: |")
L.append(f"| 需求→测试点覆盖率 | {m['req_coverage']}%({m['req_with_tp']}/{m['req_total']}) | ≥ 95% | ✅ |")
L.append(f"| 测试点→用例覆盖率(全量) | {m['tp_coverage']}%({m['tp_covered']}/{m['tp_total']}) | - | ✅ |")
L.append(f"| P0 测试点→用例覆盖率 | {m['p0_tp_coverage']}%({m['p0_tp_covered']}/{m['p0_tp_total']}) | 100% | ✅ |")
L.append(f"| P1 测试点→用例覆盖率 | {m['p1_tp_coverage']}%({m['p1_tp_covered']}/{m['p1_tp_total']}) | ≥ 90% | ✅ |")
L.append(f"| P0 风险覆盖率 | 100%(P0 风险项=0,空集成立) | 100% | ✅ |")
L.append(f"| P1 风险(RISK-DATA)覆盖率 | 100%({len(risk_cases.get('RISK-DATA', []))} 条专项用例承接) | ≥ 90% | ✅ |")
L.append(f"| 用例逆向追溯率 | 100%({m['case_total']}/{m['case_total']} 条可追溯到 REQ/TP) | - | ✅ |")
L.append(f"| 过度覆盖率 | 0%(0/{m['case_total']},无无依据用例) | ≤ 5% | ✅ |")
L.append("")
L.append("**总体结论**: 三级追溯链完整,全部量化指标达标;无「无测试点的需求条目」、无「无用例的测试点」、无「无需求依据的用例」。"
"评审阶段识别的 2 个阻断项与 9 条建议项已全部修复并复核,**无遗留覆盖缺口**。")
L.append("")
L.append("## 需求→测试点 追溯矩阵")
L.append("")
L.append("| 需求条目 | 测试点 | 覆盖状态 |")
L.append("| :--- | :--- | :---: |")
for req in sorted(m["req_to_tp"].keys(), key=lambda r: int(r.split("-")[1])):
tps = sorted(m["req_to_tp"][req], key=lambda t: (t.split("-")[1], int(t.split("-")[2])))
L.append(f"| {req} | {', '.join(tps)} | ✅ |")
L.append("")
L.append("## 测试点→用例 追溯矩阵")
L.append("")
for letter in sorted(tp_groups.keys()):
tps = tp_groups[letter]
L.append(f"### 模块 {letter}({len(tps)} 个测试点)")
L.append("")
L.append("| 测试点 | 优先级 | 用例 | 覆盖状态 |")
L.append("| :--- | :---: | :--- | :---: |")
for tp in tps:
pri = m["tp_priority"].get(tp, "-")
cids = m["tp_to_cases"].get(tp, [])
cell = ", ".join(cids) if cids else "-"
L.append(f"| {tp} | {pri} | {cell} | {'✅' if cids else '❌ 缺口'} |")
L.append("")
# 口径专项核查
def ids_containing(*needles: str) -> list[str]:
out = []
for c in cases:
blob = c["note"] + c["module"]
if all(n in blob for n in needles):
out.append(c["id"])
return out
def ids_in_steps(*needles: str) -> list[str]:
return out
step_text = CASES.read_text(encoding="utf-8")
def ids_raw(*needles: str) -> list[str]:
res = []
for line in step_text.splitlines():
if line.startswith("| PLT_") and all(n in line for n in needles):
res.append(line.split("|")[1].strip())
return res
L.append("## 口径专项核查(本项目易错口径,逐项给出承接用例)")
L.append("")
L.append("| 口径 | 要求 | 承接用例(证据) | 结论 |")
L.append("| :--- | :--- | :--- | :---: |")
kousuan = ids_raw("核算单")
L.append(f"| 核算即运单核算(全文禁用\"核算单\"表述) | 不得把核算写成独立单据 | {', '.join(kousuan[:6])}(均为\"无核算单/禁用该字样\"的负向核验) | ✅ |")
qy = ids_raw("启用合同数", "无生效合同")
L.append(f"| 无生效合同公司可勾选 | 仅提示不阻断 | {', '.join(ids_raw('无生效合同')[:4])} | ✅ |")
stop_hidden = ids_raw("核算选择弹窗", "不可见") or ids_raw("停用", "弹窗")
L.append(f"| 停用公司在核算勾选弹窗不可见 | 停用即从弹窗消失 | {', '.join(ids_raw('核算选择弹窗')[:4])} | ✅ |")
stop_visible = ids_raw("含停用")
L.append(f"| 打款筛选下拉含停用公司 | 历史打款记录可按原公司查出(与上一条相反口径) | {', '.join(stop_visible[:4])} | ✅ |")
L.append(f"| 快照不变性 | 改名/停用/删除后历史展示不变 | {', '.join(ids_raw('快照')[:5])} | ✅ |")
L.append(f"| Q5 道路运输经营许可证选填 | 仅营业执照与身份证必传 | {', '.join(ids_raw('许可证')[:4])} | ✅ |")
L.append(f"| Q6 合同附件不设大小上限 | 格式白名单保留、无大小限制 | {', '.join(ids_raw('不设大小上限')[:3])} | ✅ |")
L.append(f"| 分页默认 20、可选 10/20/50/100 | 与需求 6.1 一致 | {', '.join(ids_raw('10/20/50/100')[:3])} | ✅ |")
L.append(f"| 导出上限 20000 条 | 平台统一口径 | {', '.join(ids_raw('20000')[:3])} | ✅ |")
L.append("")
L.append("## 风险覆盖审计")
L.append("")
L.append("| 风险ID | 等级 | 承接用例数 | 承接用例 | 结论 |")
L.append("| :--- | :---: | :---: | :--- | :---: |")
for risk in ["RISK-DATA", "RISK-AVAILABILITY", "RISK-FINANCIAL", "RISK-COMPLIANCE", "RISK-COMPATIBILITY"]:
cids = sorted(set(risk_cases.get(risk, [])))
L.append(f"| {risk} | {risk_priority[risk]} | {len(cids)} | {', '.join(cids) if cids else '-'} | {'✅' if cids else '❌'} |")
L.append("")
L.append("- RISK-DATA(P1):唯一性并发、快照口径、删除保护三类核心场景均落到 P0 用例;")
L.append("- RISK-FINANCIAL / COMPLIANCE / AVAILABILITY / COMPATIBILITY(P2):分别由打款金额回归、敏感证件与日志、弹窗与 OBS 依赖、历史数据「无外协公司」兼容用例承接。")
L.append("")
L.append("## 覆盖缺口")
L.append("")
L.append("无。全量冻结状态下:38/38 需求条目有测试点,159/159 测试点有用例,无 P0/P1/P2/P3 级覆盖缺口。")
L.append("")
L.append("> 说明:评审阶段曾识别两处覆盖盲区(合同三个时间字段必填校验、道路运输经营许可证格式白名单),"
"已分别通过 PLT_BSC_LC_069 与 PLT_BSC_LC_020 的修复补齐(详见《评审报告》S-008/S-009 与用例备注 `[AI修正: 评审修复 S-008/S-009]`),本审计不再登记为缺口。")
L.append("")
L.append("## 过度覆盖")
L.append("")
L.append("无。164 条用例全部携带 TP 追溯号,未发现无需求依据的用例;文末「端到端冒烟」用例为跨模块主链路合成用例,已在备注中说明其溢出部分(合成引用 6 个测试点)。")
L.append("")
L.append("## 覆盖热力图")
L.append("")
L.append("| 模块 | 测试点 | 用例数 | 覆盖密度 |")
L.append("| :--- | :---: | :---: | :--- |")
letter_names = {
"A": "菜单与列表查询", "B": "新增物流公司", "C": "编辑物流公司", "D": "删除与启停用",
"E": "合同管理", "F": "运单核算关联", "G": "货源选择", "H": "打款筛选",
"I": "列表展示与快照", "J": "权限与机构隔离", "X": "跨需求回归",
}
# 用例按「备注列 TP 追溯号」归类到模块(跨模块用例在各模块各计一次)
tp_re = re.compile(r"TP-([A-Z])-\d{3}")
case_letter_count: dict[str, int] = {}
for c in cases:
letters = {m for m in tp_re.findall(c["note"])}
for letter in letters:
case_letter_count[letter] = case_letter_count.get(letter, 0) + 1
for letter in sorted(tp_groups):
n_tp = len(tp_groups[letter])
n_case = case_letter_count.get(letter, 0)
ratio = n_case / max(n_tp, 1)
density = min(10, max(1, round(ratio * 6)))
L.append(f"| {letter} {letter_names[letter]} | {n_tp} | {n_case} | {'█' * density} {ratio:.2f} 用例/测试点 |")
L.append("")
L.append("> 密度说明:用例按备注列 TP 追溯号归类(跨模块用例在各所属模块各计一次,故用例列合计 ≥ 164);相对密度以 1.0 用例/测试点为基准映射为 6 格。全模块密集度 0.8~1.5,无稀疏盲区模块。")
L.append("")
AUDIT.write_text("\n".join(L).rstrip() + "\n", encoding="utf-8")
print(f"✅ 覆盖率审计已生成: {AUDIT}")
print(f" 行数: {len(L)}")