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 脚本骨架、覆盖率审计脚本与指标、评审修复脚本
This commit is contained in:
xst
2026-09-29 10:03:35 +08:00
parent 1a53a1f537
commit 8c61209dd4
47 changed files with 14470 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
"""确定性覆盖率追溯计算:需求 REQ -> 测试点 TP -> 测试用例。
用法: python output/compute_coverage.py
输出: 控制台统计 + output/logs/coverage_metrics.json
"""
from __future__ import annotations
import json
import re
import sys
from collections import defaultdict
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"
POINTS = REPO / "output" / "test_points" / f"{BASE}_测试点.md"
ANALYSIS = REPO / "output" / "analysis" / f"{BASE}_分析.md"
TP_RE = re.compile(r"TP-[A-Z]-\d{3}")
REQ_RE = re.compile(r"REQ-\d{3}")
RISK_RE = re.compile(r"RISK-[A-Z]+")
# ---------- 1. 用例 ----------
headers, rows = load_markdown_table(CASES)
idx = {name: headers.index(name) for name in headers}
cases = []
for row in rows:
cases.append({
"id": row[idx["用例编号"]].strip(),
"module": row[idx["模块"]].strip(),
"priority": row[idx["优先级"]].strip(),
"type": row[idx["类型"]].strip(),
"note": row[idx["备注"]],
})
tp_to_cases: dict[str, list[str]] = defaultdict(list)
case_tps: dict[str, list[str]] = {}
no_tp_cases: list[str] = []
risks_in_cases: dict[str, list[str]] = defaultdict(list)
for case in cases:
tps = sorted(set(TP_RE.findall(case["note"])))
case_tps[case["id"]] = tps
if not tps:
no_tp_cases.append(case["id"])
for tp in tps:
tp_to_cases[tp].append(case["id"])
for risk in sorted(set(RISK_RE.findall(case["note"]))):
risks_in_cases[risk].append(case["id"])
# ---------- 2. 测试点 ----------
point_text = POINTS.read_text(encoding="utf-8")
blocks: dict[str, dict] = {}
current: str | None = None
for line in point_text.splitlines():
m = re.match(r"^### (TP-[A-Z]-\d{3}):\s*(.*)$", line)
if m:
current = m.group(1)
blocks[current] = {"title": m.group(2).strip(), "priority": "", "reqs": []}
continue
if current is None:
continue
pm = re.match(r"^- 优先级:\s*(P\d)", line)
if pm:
blocks[current]["priority"] = pm.group(1)
blocks[current]["reqs"].extend(REQ_RE.findall(line))
tp_priority = {tp: blk["priority"] for tp, blk in blocks.items()}
tp_reqs = {tp: sorted(set(blk["reqs"])) for tp, blk in blocks.items()}
# ---------- 3. 需求条目 ----------
analysis_text = ANALYSIS.read_text(encoding="utf-8")
req_ids: list[str] = []
for line in analysis_text.splitlines():
m = re.match(r"^\|\s*(REQ-\d{3})\s*\|", line)
if m and m.group(1) not in req_ids:
req_ids.append(m.group(1))
# ---------- 4. 统计 ----------
total_tp = len(blocks)
covered_tp = [tp for tp in blocks if tp_to_cases.get(tp)]
p0_tp = [tp for tp, p in tp_priority.items() if p == "P0"]
p1_tp = [tp for tp, p in tp_priority.items() if p == "P1"]
p0_covered = [tp for tp in p0_tp if tp_to_cases.get(tp)]
p1_covered = [tp for tp in p1_tp if tp_to_cases.get(tp)]
req_to_tp: dict[str, list[str]] = defaultdict(list)
for tp, reqs in tp_reqs.items():
for req in reqs:
req_to_tp[req].append(tp)
req_without_tp = [r for r in req_ids if not req_to_tp.get(r)]
# 反向:用例 -> 需求
req_to_cases: dict[str, list[str]] = defaultdict(list)
for tp, cids in tp_to_cases.items():
for req in tp_reqs.get(tp, []):
req_to_cases[req].extend(cids)
for req in req_to_cases:
req_to_cases[req] = sorted(set(req_to_cases[req]))
metrics = {
"case_total": len(cases),
"tp_total": total_tp,
"tp_covered": len(covered_tp),
"tp_coverage": round(len(covered_tp) / total_tp * 100, 2) if total_tp else 0,
"p0_tp_total": len(p0_tp),
"p0_tp_covered": len(p0_covered),
"p0_tp_coverage": round(len(p0_covered) / len(p0_tp) * 100, 2) if p0_tp else 100,
"p1_tp_total": len(p1_tp),
"p1_tp_covered": len(p1_covered),
"p1_tp_coverage": round(len(p1_covered) / len(p1_tp) * 100, 2) if p1_tp else 100,
"req_total": len(req_ids),
"req_with_tp": len(req_ids) - len(req_without_tp),
"req_coverage": round((len(req_ids) - len(req_without_tp)) / len(req_ids) * 100, 2) if req_ids else 0,
"req_without_tp": req_without_tp,
"uncovered_tps": [tp for tp in blocks if not tp_to_cases.get(tp)],
"no_tp_cases": no_tp_cases,
"risk_coverage": {k: sorted(set(v)) for k, v in risks_in_cases.items()},
"tp_to_cases": {tp: sorted(set(v)) for tp, v in tp_to_cases.items()},
"req_to_tp": {k: sorted(set(v)) for k, v in req_to_tp.items()},
"req_to_cases": req_to_cases,
"tp_priority": tp_priority,
"module_case_count": {},
}
mod_count: dict[str, int] = defaultdict(int)
for case in cases:
mod_count[case["module"]] += 1
metrics["module_case_count"] = dict(sorted(mod_count.items()))
out = REPO / "output" / "logs" / "coverage_metrics.json"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(metrics, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"用例总数: {metrics['case_total']}")
print(f"测试点总数: {total_tp}(P0={len(p0_tp)} P1={len(p1_tp)})")
print(f"测试点->用例覆盖率: {metrics['tp_coverage']}% ({len(covered_tp)}/{total_tp})")
print(f"P0 测试点->用例覆盖率: {metrics['p0_tp_coverage']}% ({len(p0_covered)}/{len(p0_tp)})")
print(f"P1 测试点->用例覆盖率: {metrics['p1_tp_coverage']}% ({len(p1_covered)}/{len(p1_tp)})")
print(f"需求->测试点覆盖率: {metrics['req_coverage']}% ({metrics['req_with_tp']}/{len(req_ids)})")
print(f"无测试点覆盖的需求条目: {req_without_tp or '无'}")
print(f"无用例覆盖的测试点: {metrics['uncovered_tps'] or '无'}")
print(f"无 TP 追溯号的用例: {no_tp_cases or '无'}")
print(f"风险覆盖: " + "; ".join(f"{k}={len(set(v))}条用例" for k, v in sorted(risks_in_cases.items())))
print(f"已写出: {out}")