78 lines
3.0 KiB
Python
78 lines
3.0 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""统一测试报告:PASS/FAIL/SKIP/SENT 四态 + Markdown 输出 + 退出码."""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
from dataclasses import dataclass, field
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
STATUSES = ("PASS", "FAIL", "SKIP", "SENT")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class CaseResult:
|
|||
|
|
suite: str
|
|||
|
|
name: str
|
|||
|
|
status: str
|
|||
|
|
detail: str = ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
@dataclass
|
|||
|
|
class HarnessReport:
|
|||
|
|
title: str = "K1 HIL 自动化测试报告"
|
|||
|
|
cases: list[CaseResult] = field(default_factory=list)
|
|||
|
|
notes: list[str] = field(default_factory=list)
|
|||
|
|
started: datetime = field(default_factory=datetime.now)
|
|||
|
|
|
|||
|
|
def add(self, suite: str, name: str, status: str, detail: str = "") -> None:
|
|||
|
|
status = status.upper()
|
|||
|
|
assert status in STATUSES, f"bad status {status}"
|
|||
|
|
self.cases.append(CaseResult(suite, name, status, detail))
|
|||
|
|
print(f"[{status:>4}] {suite}/{name}" + (f" | {detail}" if detail else ""), flush=True)
|
|||
|
|
|
|||
|
|
def extend_rows(self, suite: str, rows) -> None:
|
|||
|
|
"""吸收 test_protocol_app_sim.Results 风格的 (name, status, detail) 行."""
|
|||
|
|
for name, status, detail in rows:
|
|||
|
|
self.add(suite, name, status, detail)
|
|||
|
|
|
|||
|
|
def counts(self) -> dict:
|
|||
|
|
return {s: sum(1 for c in self.cases if c.status == s) for s in STATUSES}
|
|||
|
|
|
|||
|
|
def exit_code(self) -> int:
|
|||
|
|
return 1 if self.counts()["FAIL"] else 0
|
|||
|
|
|
|||
|
|
def summary_text(self) -> str:
|
|||
|
|
n = self.counts()
|
|||
|
|
return f"PASS={n['PASS']} FAIL={n['FAIL']} SKIP={n['SKIP']} SENT={n['SENT']}"
|
|||
|
|
|
|||
|
|
def print_summary(self) -> None:
|
|||
|
|
print("\n===== 汇总 =====", flush=True)
|
|||
|
|
for suite in dict.fromkeys(c.suite for c in self.cases):
|
|||
|
|
sub = [c for c in self.cases if c.suite == suite]
|
|||
|
|
n = {s: sum(1 for c in sub if c.status == s) for s in STATUSES}
|
|||
|
|
print(f" {suite:<12} PASS={n['PASS']} FAIL={n['FAIL']} SKIP={n['SKIP']} SENT={n['SENT']}", flush=True)
|
|||
|
|
print(f" {'TOTAL':<12} {self.summary_text()}", flush=True)
|
|||
|
|
for c in self.cases:
|
|||
|
|
if c.status == "FAIL":
|
|||
|
|
print(f" FAIL: {c.suite}/{c.name} | {c.detail}", flush=True)
|
|||
|
|
|
|||
|
|
def write_markdown(self, path: str) -> str:
|
|||
|
|
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
|||
|
|
lines = [
|
|||
|
|
f"# {self.title}",
|
|||
|
|
"",
|
|||
|
|
f"- 时间: {self.started.isoformat(timespec='seconds')}",
|
|||
|
|
f"- 结果: {self.summary_text()}",
|
|||
|
|
]
|
|||
|
|
for note in self.notes:
|
|||
|
|
lines.append(f"- {note}")
|
|||
|
|
lines += ["", "| 套件 | 用例 | 结果 | 详情 |", "|------|------|------|------|"]
|
|||
|
|
for c in self.cases:
|
|||
|
|
detail = c.detail.replace("|", "\\|").replace("\n", " ")
|
|||
|
|
lines.append(f"| {c.suite} | {c.name} | {c.status} | {detail} |")
|
|||
|
|
lines.append("")
|
|||
|
|
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
|||
|
|
f.write("\n".join(lines))
|
|||
|
|
print(f"报告已保存: {os.path.abspath(path)}", flush=True)
|
|||
|
|
return path
|