feat: 测试交付 Q1 混沌压测骨架与 Q4 镜像骨架
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/test/report"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: genreport <results.json> [out.md]")
|
||||
os.Exit(2)
|
||||
}
|
||||
r, err := report.LoadResults(os.Args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
items, err := report.Normalize(r)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
out := os.Stdout
|
||||
if len(os.Args) >= 3 {
|
||||
f, err := os.Create(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
out = f
|
||||
}
|
||||
if err := report.WriteMarkdown(out, items); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeFillsMissingAsUntested(t *testing.T) {
|
||||
t.Parallel()
|
||||
items, err := Normalize(Results{Items: []Item{
|
||||
{ID: "F01", Status: StatusPass, Note: "ok"},
|
||||
{ID: "f08", Status: "失败", Note: "flaky"},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 23 {
|
||||
t.Fatalf("len=%d", len(items))
|
||||
}
|
||||
if items[0].Status != StatusPass || items[0].Note != "ok" {
|
||||
t.Fatalf("F01=%+v", items[0])
|
||||
}
|
||||
if items[7].Status != StatusFail {
|
||||
t.Fatalf("F08=%+v", items[7])
|
||||
}
|
||||
if items[22].Status != StatusUntested {
|
||||
t.Fatalf("F23=%+v", items[22])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteMarkdownSample(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join("testdata", "sample_results.json")
|
||||
r, err := LoadResults(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
items, err := Normalize(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := WriteMarkdown(&buf, items); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := buf.String()
|
||||
for _, id := range []string{"F01", "F12", "F23"} {
|
||||
if !strings.Contains(out, id) {
|
||||
t.Fatalf("missing %s in:\n%s", id, out)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(out, "未测") {
|
||||
t.Fatalf("expected 未测:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "通过 0,失败 0,未测 23") {
|
||||
t.Fatalf("summary mismatch:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyResultsRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "empty.json")
|
||||
raw, err := json.MarshalIndent(EmptyResults(), "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if writeErr := os.WriteFile(path, raw, 0o644); writeErr != nil {
|
||||
t.Fatal(writeErr)
|
||||
}
|
||||
r, err := LoadResults(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(r.Items) != 23 {
|
||||
t.Fatalf("items=%d", len(r.Items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStatusRejectsUnknown(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := Normalize(Results{Items: []Item{{ID: "F01", Status: "maybe"}}}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"generated_at": "2026-09-30T00:00:00Z",
|
||||
"items": [
|
||||
{"id": "F01", "status": "untested", "note": ""},
|
||||
{"id": "F02", "status": "untested", "note": ""},
|
||||
{"id": "F03", "status": "untested", "note": ""},
|
||||
{"id": "F04", "status": "untested", "note": ""},
|
||||
{"id": "F05", "status": "untested", "note": ""},
|
||||
{"id": "F06", "status": "untested", "note": ""},
|
||||
{"id": "F07", "status": "untested", "note": ""},
|
||||
{"id": "F08", "status": "untested", "note": ""},
|
||||
{"id": "F09", "status": "untested", "note": ""},
|
||||
{"id": "F10", "status": "untested", "note": ""},
|
||||
{"id": "F11", "status": "untested", "note": ""},
|
||||
{"id": "F12", "status": "untested", "note": ""},
|
||||
{"id": "F13", "status": "untested", "note": ""},
|
||||
{"id": "F14", "status": "untested", "note": ""},
|
||||
{"id": "F15", "status": "untested", "note": ""},
|
||||
{"id": "F16", "status": "untested", "note": ""},
|
||||
{"id": "F17", "status": "untested", "note": ""},
|
||||
{"id": "F18", "status": "untested", "note": ""},
|
||||
{"id": "F19", "status": "untested", "note": ""},
|
||||
{"id": "F20", "status": "untested", "note": ""},
|
||||
{"id": "F21", "status": "untested", "note": ""},
|
||||
{"id": "F22", "status": "untested", "note": ""},
|
||||
{"id": "F23", "status": "untested", "note": ""}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user