130 lines
5.2 KiB
Python
130 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
r"""extract_capability_list.py — 能力清单义务层提取器(B 覆盖总览面板·义务层)
|
||
|
||
把 game/tests/README.md「能力清单」的四列表(能力|系统|真实入口|代表用例)按旅程段
|
||
解析成机器可读 JSONP .js,供 tools/coverage_report.html 经 <script src> 直载
|
||
(file:// 下 fetch 本地 json 被 CORS 拦——JSONP 是 B 面板全数据源的统一形态)。
|
||
|
||
用法:
|
||
python tools/extract_capability_list.py [--readme <路径>] [--out <路径>]
|
||
|
||
判据口径:
|
||
- 只认表头恰为「能力|系统|真实入口|代表用例」的四列表(覆盖域表等五列表不取);
|
||
- 旅程段标题形态=「**旅程段N:名称**」,段名进每行的 segment 字段;
|
||
- 代表用例列按「;」切分成用例指针数组(`文件:函数` 形态逐字保真);
|
||
- 本工具是义务层的静态投影,产出随 README 变更重跑即得——面板只读本产物,禁在面板里改义务。
|
||
|
||
产出(JSONP):
|
||
window.__QA_COVERAGE__.capability_list = {
|
||
"meta": {"generated_at", "source", "rows_total", "segments_total"},
|
||
"segments": [{"segment": 名称, "rows": [{"ability", "system", "entry", "cases": []}]}],
|
||
}
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import datetime
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
DEFAULT_README = ROOT / "game" / "tests" / "README.md"
|
||
DEFAULT_OUT = ROOT / "game" / "tests" / ".gates" / "coverage" / "capability_list.js"
|
||
|
||
SEGMENT_RE = re.compile(r"^\*\*旅程段[一二三四五六七八九十]+[::](.+?)\*\*\s*$")
|
||
|
||
|
||
|
||
def split_row(line: str) -> list[str]:
|
||
body = line.strip()
|
||
if body.startswith("|"):
|
||
body = body[1:]
|
||
if body.endswith("|"):
|
||
body = body[:-1]
|
||
return [c.strip() for c in body.split("|")]
|
||
|
||
|
||
def parse(readme: Path) -> dict:
|
||
lines = readme.read_text(encoding="utf-8").splitlines()
|
||
segments: list[dict] = []
|
||
cur_seg: str | None = None
|
||
cur_rows: list[dict] | None = None
|
||
in_capability_table = False
|
||
for line in lines:
|
||
seg = SEGMENT_RE.match(line.strip())
|
||
if seg:
|
||
if cur_seg is not None:
|
||
segments.append({"segment": cur_seg, "rows": cur_rows or []})
|
||
cur_seg = seg.group(1).strip()
|
||
cur_rows = []
|
||
in_capability_table = False
|
||
continue
|
||
if cur_seg is None:
|
||
continue
|
||
if not line.strip().startswith("|"):
|
||
in_capability_table = False
|
||
continue
|
||
cells = split_row(line)
|
||
if all(not set(c) - {"-", " ", ":"} for c in cells) and cells:
|
||
continue # 表头分隔行 |---|---|
|
||
if (len(cells) == 4 and cells[0] == "能力" and cells[1] == "系统"
|
||
and cells[2] == "真实入口" and cells[3].startswith("代表用例")):
|
||
in_capability_table = True
|
||
continue
|
||
if in_capability_table and len(cells) == 4:
|
||
ability, system, entry, cases = cells
|
||
cur_rows.append({
|
||
"ability": ability.strip("`"),
|
||
"system": system,
|
||
"entry": entry,
|
||
"cases": [c.strip().strip("`") for c in cases.split(";") if c.strip()],
|
||
})
|
||
if cur_seg is not None:
|
||
segments.append({"segment": cur_seg, "rows": cur_rows or []})
|
||
rows_total = sum(len(s["rows"]) for s in segments)
|
||
return {
|
||
"meta": {
|
||
"generated_at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||
"source": str(readme),
|
||
"rows_total": rows_total,
|
||
"segments_total": len(segments),
|
||
},
|
||
"segments": segments,
|
||
}
|
||
|
||
|
||
def dump_js(payload: dict, out: Path) -> None:
|
||
out.parent.mkdir(parents=True, exist_ok=True)
|
||
js = "/* 能力清单义务层投影——由 extract_capability_list.py 生成,随 README 变更重跑;人工修订无效(义务源=README 能力清单)。 */\n"
|
||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||
js += "window.__QA_COVERAGE__.capability_list = %s;\n" % json.dumps(payload, ensure_ascii=False, indent=2)
|
||
out.write_text(js, encoding="utf-8", newline="\n")
|
||
|
||
|
||
def main() -> int:
|
||
if hasattr(sys.stdout, "reconfigure"):
|
||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||
ap = argparse.ArgumentParser(description="能力清单义务层提取(B 面板·义务层)")
|
||
ap.add_argument("--readme", default=str(DEFAULT_README))
|
||
ap.add_argument("--out", default=str(DEFAULT_OUT))
|
||
args = ap.parse_args()
|
||
readme = Path(args.readme)
|
||
if not readme.is_file():
|
||
print("[capability] README 不存在:%s" % readme)
|
||
return 1
|
||
payload = parse(readme)
|
||
dump_js(payload, Path(args.out))
|
||
print("[capability] 义务层投影已落盘 %s(旅程段 %d / 能力行 %d)" % (
|
||
args.out, payload["meta"]["segments_total"], payload["meta"]["rows_total"]))
|
||
if payload["meta"]["rows_total"] == 0:
|
||
print("[capability] 红:能力行数为 0——表头或旅程段标题形态不匹配,禁当空义务面用")
|
||
return 1
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|