Files
NixMsg/test/report/generate.go
T

173 lines
5.7 KiB
Go
Raw 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.
// Package report 按 PRD 第 10 节 F01–F23 生成验收对照表。
package report
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"time"
)
// Status 验收结果。
type Status string
const (
StatusPass Status = "pass"
StatusFail Status = "fail"
StatusUntested Status = "untested"
)
// Item 单条验收结果。
type Item struct {
ID string `json:"id"`
Status Status `json:"status"`
Note string `json:"note,omitempty"`
}
// Results 结果文件格式。
type Results struct {
GeneratedAt string `json:"generated_at,omitempty"`
Items []Item `json:"items"`
}
// Feature 验收总表一行。
type Feature struct {
ID string
Summary string
}
// Features 是 PRD 第 10 节 F01–F23。
var Features = []Feature{
{ID: "F01", Summary: "批量开通整批校验、停用、删除群主转让、删除后同编号重开不串数据"},
{ID: "F02", Summary: "新设备登录后旧设备自动退出、换 IP 用令牌重连、两种密码锁定、重置密码后被踢、服务器故障不误报密码错误"},
{ID: "F03", Summary: "断开后状态及时变离线,全表可列出"},
{ID: "F04", Summary: "只通知订阅了的端"},
{ID: "F05", Summary: "崩溃不丢已提交消息,消息号去重和冲突,密码门生效,配额生效"},
{ID: "F06", Summary: "群成员收到同一份,入群前不补,发送者不收到自己的"},
{ID: "F07", Summary: "256 KiB 通过,超出拒绝,接收上限生效"},
{ID: "F08", Summary: "弱网最终送达且应用层不重复,重启后续传"},
{ID: "F09", Summary: "保留时间从发送时刻起算,超时过期"},
{ID: "F10", Summary: "短断线送到,长断线丢弃,服务器重启后宽限内重连送到"},
{ID: "F11", Summary: "发送方离线后到点仍发送"},
{ID: "F12", Summary: "延迟窗口内撤回对方收不到"},
{ID: "F13", Summary: "未推送必撤成功;群部分确认得到部分撤回"},
{ID: "F14", Summary: "回执能补送给当时离线的发送方"},
{ID: "F15", Summary: "输一次记住、改密失效、回复免密、进群仍要密码、防多账号轮流猜"},
{ID: "F16", Summary: "群主权限、退出后不再收到、解散后同编号新群不收旧消息"},
{ID: "F17", Summary: "后台管端、管注册、管群、查记录,响应里没有正文;API 令牌可用且不能越权"},
{ID: "F18", Summary: "送达后正文消失;记录天数 0 时连记录消失;防重仍在"},
{ID: "F19", Summary: "四种 SDK 通过同一清单"},
{ID: "F20", Summary: "裸 MQTT 能登录、收、确认、发"},
{ID: "F21", Summary: "默认一个端口提供后台、WebSocket、TCP、注册;后台可分到单独端口"},
{ID: "F22", Summary: "初始化后单文件或 Docker 启动、备份恢复、升级迁移、证书自动重载、指标可抓取"},
{ID: "F23", Summary: "注册开关、安全码校验、换码不影响已注册、输错锁定"},
}
// LoadResults 读取结果 JSON;缺项视为未测。
func LoadResults(path string) (Results, error) {
data, err := os.ReadFile(path)
if err != nil {
return Results{}, err
}
var r Results
if err := json.Unmarshal(data, &r); err != nil {
return Results{}, fmt.Errorf("parse results: %w", err)
}
return r, nil
}
// Normalize 把结果补全为 F01–F23;非法 status 报错。
func Normalize(r Results) ([]Item, error) {
byID := make(map[string]Item, len(r.Items))
for _, it := range r.Items {
id := strings.ToUpper(strings.TrimSpace(it.ID))
st, err := parseStatus(it.Status)
if err != nil {
return nil, fmt.Errorf("%s: %w", id, err)
}
it.ID = id
it.Status = st
byID[id] = it
}
out := make([]Item, 0, len(Features))
for _, f := range Features {
if it, ok := byID[f.ID]; ok {
out = append(out, it)
continue
}
out = append(out, Item{ID: f.ID, Status: StatusUntested, Note: "结果文件未提供"})
}
return out, nil
}
func parseStatus(s Status) (Status, error) {
switch Status(strings.ToLower(string(s))) {
case StatusPass, "通过", "ok", "passed":
return StatusPass, nil
case StatusFail, "失败", "failed":
return StatusFail, nil
case StatusUntested, "未测", "skip", "skipped", "":
return StatusUntested, nil
default:
return "", fmt.Errorf("unknown status %q", s)
}
}
func statusCN(s Status) string {
switch s {
case StatusPass:
return "通过"
case StatusFail:
return "失败"
default:
return "未测"
}
}
// WriteMarkdown 写出 F01–F23 对照表。
func WriteMarkdown(w io.Writer, items []Item) error {
summary := map[Status]int{}
byID := make(map[string]Item, len(items))
for _, it := range items {
byID[it.ID] = it
summary[it.Status]++
}
if _, err := fmt.Fprintf(w, "# NixMsg 验收对照表(PRD 第 10 节)\n\n"); err != nil {
return err
}
if _, err := fmt.Fprintf(w, "生成时间:%s\n\n", time.Now().UTC().Format(time.RFC3339)); err != nil {
return err
}
if _, err := fmt.Fprintf(w, "汇总:通过 %d,失败 %d,未测 %d\n\n", summary[StatusPass], summary[StatusFail], summary[StatusUntested]); err != nil {
return err
}
if _, err := fmt.Fprintf(w, "| 编号 | 一句话 | 结果 | 备注 |\n|---|---|---|---|\n"); err != nil {
return err
}
for _, f := range Features {
it := byID[f.ID]
note := it.Note
if note == "" {
note = "—"
}
if _, err := fmt.Fprintf(w, "| %s | %s | %s | %s |\n", f.ID, f.Summary, statusCN(it.Status), note); err != nil {
return err
}
}
return nil
}
// EmptyResults 返回全部未测的样例。
func EmptyResults() Results {
items := make([]Item, 0, len(Features))
for _, f := range Features {
items = append(items, Item{ID: f.ID, Status: StatusUntested, Note: ""})
}
return Results{
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
Items: items,
}
}