- 需求 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 脚本骨架、覆盖率审计脚本与指标、评审修复脚本
152 lines
5.9 KiB
Python
152 lines
5.9 KiB
Python
# -*- 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}")
|