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

152 lines
5.9 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 -*-
"""确定性覆盖率追溯计算:需求 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}")