fix: 验收对照表按子项记结果不再虚报通过

This commit is contained in:
Nixevol
2026-09-30 16:24:56 +08:00
parent c72d1f0427
commit cb9512fe11
11 changed files with 1138 additions and 93 deletions
+160 -11
View File
@@ -17,13 +17,26 @@ const (
StatusPass Status = "pass"
StatusFail Status = "fail"
StatusUntested Status = "untested"
StatusPartial Status = "partial"
)
// SubItem 是 PRD 一句话里的一个子项。
type SubItem struct {
ID string `json:"id"`
Summary string `json:"summary,omitempty"`
Status Status `json:"status"`
Evidence string `json:"evidence,omitempty"`
Note string `json:"note,omitempty"`
}
// Item 单条验收结果。
type Item struct {
ID string `json:"id"`
Status Status `json:"status"`
Note string `json:"note,omitempty"`
ID string `json:"id"`
Status Status `json:"status"`
Note string `json:"note,omitempty"`
Subs []SubItem `json:"subs,omitempty"`
Passed int `json:"passed,omitempty"`
Total int `json:"total,omitempty"`
}
// Results 结果文件格式。
@@ -89,6 +102,16 @@ func Normalize(r Results) ([]Item, error) {
}
it.ID = id
it.Status = st
for i, sub := range it.Subs {
subSt, subErr := parseStatus(sub.Status)
if subErr != nil {
return nil, fmt.Errorf("%s/%s: %w", id, sub.ID, subErr)
}
it.Subs[i].Status = subSt
}
if len(it.Subs) > 0 {
it = Rollup(it)
}
byID[id] = it
}
out := make([]Item, 0, len(Features))
@@ -102,12 +125,73 @@ func Normalize(r Results) ([]Item, error) {
return out, nil
}
// Rollup 根据子项计算整项状态:全部通过才算通过,否则部分通过或失败或未测。
func Rollup(it Item) Item {
if len(it.Subs) == 0 {
return it
}
pass, fail, untested := 0, 0, 0
for _, s := range it.Subs {
switch s.Status {
case StatusPass:
pass++
case StatusFail:
fail++
default:
untested++
}
}
it.Passed = pass
it.Total = len(it.Subs)
switch {
case fail > 0:
it.Status = StatusFail
case untested == 0 && pass == len(it.Subs):
it.Status = StatusPass
case pass == 0 && fail == 0:
it.Status = StatusUntested
default:
it.Status = StatusPartial
}
it.Note = rollupNote(it)
return it
}
func rollupNote(it Item) string {
var parts []string
for _, s := range it.Subs {
switch s.Status {
case StatusPass:
ev := s.Evidence
if ev == "" {
ev = "已测"
}
parts = append(parts, s.ID+" 通过("+ev+")")
case StatusFail:
msg := s.Note
if msg == "" {
msg = s.Evidence
}
parts = append(parts, s.ID+" 失败("+msg+")")
default:
msg := s.Note
if msg == "" {
msg = "未测"
}
parts = append(parts, s.ID+" 未测("+msg+")")
}
}
return strings.Join(parts, ";")
}
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 StatusPartial, "部分通过", "partial_pass":
return StatusPartial, nil
case StatusUntested, "未测", "skip", "skipped", "":
return StatusUntested, nil
default:
@@ -115,12 +199,17 @@ func parseStatus(s Status) (Status, error) {
}
}
func statusCN(s Status) string {
switch s {
func statusCN(it Item) string {
switch it.Status {
case StatusPass:
return "通过"
case StatusFail:
return "失败"
case StatusPartial:
if it.Total > 0 {
return fmt.Sprintf("部分通过 (%d/%d)", it.Passed, it.Total)
}
return "部分通过"
default:
return "未测"
}
@@ -140,7 +229,11 @@ func WriteMarkdown(w io.Writer, items []Item) error {
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 {
if _, err := fmt.Fprintf(w, "汇总:通过 %d,部分通过 %d,失败 %d,未测 %d\n\n",
summary[StatusPass], summary[StatusPartial], summary[StatusFail], summary[StatusUntested]); err != nil {
return err
}
if _, err := fmt.Fprintf(w, "整项「通过」表示该行 PRD 一句话里的子项均有测试证据。部分覆盖不得标通过。\n\n"); err != nil {
return err
}
if _, err := fmt.Fprintf(w, "| 编号 | 一句话 | 结果 | 备注 |\n|---|---|---|---|\n"); err != nil {
@@ -148,17 +241,73 @@ func WriteMarkdown(w io.Writer, items []Item) error {
}
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 {
note := compactNote(it)
if _, err := fmt.Fprintf(w, "| %s | %s | %s | %s |\n", f.ID, f.Summary, statusCN(it), note); err != nil {
return err
}
}
if _, err := fmt.Fprintf(w, "\n## 子项\n\n"); err != nil {
return err
}
if _, err := fmt.Fprintf(w, "| 子项 | 摘要 | 结果 | 证据或原因 |\n|---|---|---|---|\n"); err != nil {
return err
}
for _, f := range Features {
it := byID[f.ID]
for _, s := range it.Subs {
detail := s.Evidence
if s.Status != StatusPass {
if s.Note != "" {
detail = s.Note
}
}
if detail == "" {
detail = "—"
}
subStatus := statusCN(Item{Status: s.Status})
if _, err := fmt.Fprintf(w, "| %s | %s | %s | %s |\n", s.ID, s.Summary, subStatus, detail); err != nil {
return err
}
}
}
return nil
}
func compactNote(it Item) string {
if it.Status == StatusPass {
if it.Total > 0 {
return fmt.Sprintf("子项 %d/%d 通过", it.Passed, it.Total)
}
return "通过"
}
var bits []string
for _, s := range it.Subs {
if s.Status == StatusPass {
continue
}
label := "未测"
if s.Status == StatusFail {
label = "失败"
}
msg := s.Note
if msg == "" {
msg = s.Evidence
}
if msg == "" {
bits = append(bits, s.ID+" "+label)
continue
}
bits = append(bits, s.ID+" "+label+":"+msg)
}
if len(bits) == 0 {
if it.Note != "" {
return it.Note
}
return "—"
}
return strings.Join(bits, ";")
}
// EmptyResults 返回全部未测的样例。
func EmptyResults() Results {
items := make([]Item, 0, len(Features))