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:
@@ -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}")
|
||||
Reference in New Issue
Block a user