mesh-issues keeps each design's milestone in the state milestones (history 5), says milestone-set and milestone-moved, and serves milestones, milestone-set and milestone-move. An issue's milestone is its own link of kind milestone, so at most one milestone per issue is checked inside the issue's own write; kind commit links a fixing commit. The triage links no milestone, pull request or commit. import with milestones=N seeds a design's table once. mesh-knowledge's progress reads issue-tracker.milestones and its table parse is gone. Squashed from four commits so the branch history does not carry a compiled bundle that was committed by mistake (6 MB); the path is now in .gitignore.
188 lines
8.2 KiB
Go
188 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Issue 1781 (ADR 0493 §1 to §4): a milestone is a record made by its verb; an issue is linked to at most one, only to
|
|
// an open one, and only by the verb; the triage links no milestone, pull request or commit.
|
|
func TestAMilestoneIsMadeByItsVerbAndAnIssueBelongsToAtMostOne(t *testing.T) {
|
|
h := newHarness(t)
|
|
a, b := h.open(t, "a"), h.open(t, "b")
|
|
for _, c := range []struct {
|
|
ask MilestoneSetAsk
|
|
says string
|
|
}{
|
|
{MilestoneSetAsk{Design: 0, ID: "R6", Outcome: "x"}, "design's number"},
|
|
{MilestoneSetAsk{Design: 47, ID: "R6/x", Outcome: "x"}, "short name"},
|
|
{MilestoneSetAsk{Design: 47, ID: "R6"}, "needs its outcome"},
|
|
{MilestoneSetAsk{Design: 47, ID: "R6", Outcome: "nodes apply their wants", Order: 2}, ""},
|
|
{MilestoneSetAsk{Design: 47, ID: "3 fix", Outcome: "a group merges in one pass", Order: 1}, ""},
|
|
{MilestoneSetAsk{Design: 47, ID: "R6", Outcome: "nodes apply their wants"}, "nothing changed"},
|
|
} {
|
|
out, err := h.milestoneSet("lead@shanks", c.ask)
|
|
if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) &&
|
|
(out == nil || !strings.Contains(out.Said, c.says)) {
|
|
t.Fatalf("%+v: want %q, got %+v %v", c.ask, c.says, out, err)
|
|
}
|
|
}
|
|
links := []struct {
|
|
l LinkAsk
|
|
says string
|
|
}{
|
|
{LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6"}, ""},
|
|
{LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/3 fix"}, "in milestone 47/R6 already"},
|
|
{LinkAsk{Number: b, Kind: LinkMilestone, Target: "47/R9"}, "there is no milestone 47/R9"},
|
|
{LinkAsk{Number: b, Kind: LinkMilestone, Target: "R6"}, "design/id"},
|
|
{LinkAsk{Number: b, Kind: LinkCommit, Target: "mesh-catalog@" + strings.Repeat("a", 40)}, ""},
|
|
{LinkAsk{Number: b, Kind: LinkCommit, Target: "mesh-catalog@abc123"}, "full 40-character"},
|
|
{LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6", Remove: true}, ""},
|
|
{LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/3-fix"}, ""},
|
|
{LinkAsk{Number: b, Kind: LinkMilestone, Target: "47/R6"}, ""},
|
|
}
|
|
for _, c := range links {
|
|
_, err := h.link("lead@shanks", c.l)
|
|
if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) {
|
|
t.Fatalf("%+v: want %q, got %v", c.l, c.says, err)
|
|
}
|
|
}
|
|
for _, kind := range madeByPeople {
|
|
target := map[string]string{LinkPullRequest: "mesh-catalog#1", LinkMilestone: "47/R6", LinkCommit: "mesh-catalog@" + strings.Repeat("b", 40)}[kind]
|
|
if _, err := h.link(GroomBy, LinkAsk{Number: a, Kind: kind, Target: target}); err == nil || !strings.Contains(err.Error(), "triage links no") {
|
|
t.Fatalf("the triage linked %s: %v", kind, err)
|
|
}
|
|
}
|
|
read, err := h.milestones(MilestonesAsk{Design: 47})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(read.Milestones) != 2 || read.Milestones[0].Ref != "47/3-fix" || read.Milestones[1].Ref != "47/R6" ||
|
|
fmt.Sprint(read.Milestones[0].Issues) != fmt.Sprint([]uint64{a}) || fmt.Sprint(read.Milestones[1].Issues) != fmt.Sprint([]uint64{b}) {
|
|
t.Fatalf("%+v", read.Milestones)
|
|
}
|
|
one, err := h.milestones(MilestonesAsk{One: "47/R6"})
|
|
if err != nil || len(one.Milestones) != 1 || len(one.History) == 0 || one.Milestones[0].Made.By != "lead@shanks" {
|
|
t.Fatalf("%+v %v", one, err)
|
|
}
|
|
// Closing waits for its issues; a closed milestone takes no new one; each change is said.
|
|
if _, err := h.milestoneMove("lead@shanks", "47/R6", MilestoneClosed, "delivered"); err == nil || !strings.Contains(err.Error(), fmt.Sprint(b)) {
|
|
t.Fatalf("closed over an open issue: %v", err)
|
|
}
|
|
if _, err := h.Move(MoveAsk{Number: b, Status: StatusWontfix, Reason: "not needed"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.milestoneMove("lead@shanks", "47/R6", MilestoneClosed, "delivered"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.link("lead@shanks", LinkAsk{Number: a, Kind: LinkMilestone, Target: "47/R6", Remove: true}); err == nil {
|
|
t.Fatal("a removed a link it does not have")
|
|
}
|
|
c := h.open(t, "c")
|
|
if _, err := h.link("lead@shanks", LinkAsk{Number: c, Kind: LinkMilestone, Target: "47/R6"}); err == nil || !strings.Contains(err.Error(), "is closed") {
|
|
t.Fatalf("linked to a closed milestone: %v", err)
|
|
}
|
|
if out, err := h.milestoneSet("lead@shanks", MilestoneSetAsk{Design: 47, ID: "R6", Announce: true}); err != nil || !out.Announced {
|
|
t.Fatalf("announce: %+v %v", out, err)
|
|
}
|
|
var kinds []string
|
|
for _, e := range h.said() {
|
|
if strings.HasPrefix(e.Event, "milestone") {
|
|
kinds = append(kinds, e.Event+":"+fmt.Sprint(e.Body["milestone"]))
|
|
}
|
|
}
|
|
if strings.Join(kinds, " ") != "milestone-set:47/R6 milestone-set:47/3-fix milestone-moved:47/R6 milestone-set:47/R6" {
|
|
t.Fatalf("said %v", kinds)
|
|
}
|
|
}
|
|
|
|
// listRecords is a record holding one design document.
|
|
type listRecords struct {
|
|
path, content string
|
|
}
|
|
|
|
func (r listRecords) Standing() (string, error) {
|
|
return "0123456789abcdef0123456789abcdef01234567", nil
|
|
}
|
|
func (r listRecords) List(string) ([]string, []string, error) {
|
|
return nil, []string{"12-other.md", filepath.Base(r.path)}, nil
|
|
}
|
|
func (r listRecords) Read(string) (string, string, error) { return r.content, "", nil }
|
|
|
|
// Issue 1781: the seed makes each row's milestone, links what one row names, refuses what two rows name or another
|
|
// milestone holds, closes a milestone whose issues are done, and run again changes nothing.
|
|
func TestTheSeedTakesADesignsTableOnce(t *testing.T) {
|
|
h := newHarness(t)
|
|
a, b, c, d := h.open(t, "a"), h.open(t, "b"), h.open(t, "c"), h.open(t, "d")
|
|
if _, err := h.Move(MoveAsk{Number: d, Status: StatusWontfix, Reason: "folded"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
table := fmt.Sprintf("# 47 — Delivery\n\n## Milestones\n\n| # | Outcome | Issues |\n|---|---|---|\n"+
|
|
"| 1 | the first | %d, %d (ADR 0493 §2) |\n| 3 fix | a fix | %d, %d, 999 |\n| R4d | none yet | opened when it is prepared |\n"+
|
|
"| 6 | done | %d |\n\n## Next\n\n| a | Issues |\n|---|---|\n| x | %d |\n", a, b, c, b, d, a)
|
|
rec := listRecords{path: "03-DESIGN/01-to-be/47-delivery.md", content: table}
|
|
dry, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47, DryRun: true}, rec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dry.Made != 4 || dry.Linked != 3 || dry.Refused != 2 || dry.Closed != 1 || len(dry.Rows[1].Missing) != 1 {
|
|
t.Fatalf("dry run %+v", dry)
|
|
}
|
|
if read, _ := h.milestones(MilestonesAsk{}); len(read.Milestones) != 0 {
|
|
t.Fatalf("a dry run wrote %+v", read.Milestones)
|
|
}
|
|
got, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47}, rec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fmt.Sprintf("%d %d %d %d", got.Made, got.Linked, got.Refused, got.Closed) != "4 3 2 1" {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
read, _ := h.milestones(MilestonesAsk{Design: 47})
|
|
var say []string
|
|
for _, m := range read.Milestones {
|
|
say = append(say, fmt.Sprintf("%s:%s:%v", m.ID, m.Status, m.Issues))
|
|
}
|
|
if want := fmt.Sprintf("1:open:[%d] 3-fix:open:[%d] R4d:open:[] 6:closed:[%d]", a, c, d); strings.Join(say, " ") != want {
|
|
t.Fatalf("got %s, want %s", strings.Join(say, " "), want)
|
|
}
|
|
if read.Milestones[3].Imported == nil || read.Milestones[3].Imported.Path != rec.path {
|
|
t.Fatalf("%+v", read.Milestones[3])
|
|
}
|
|
again, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 47}, rec)
|
|
if err != nil || again.Made+again.Linked+again.Closed != 0 || !strings.HasPrefix(again.Said, "Changed nothing") {
|
|
t.Fatalf("again %+v %v", again, err)
|
|
}
|
|
if _, err := h.importMilestones("lead@shanks", MilestoneImportAsk{Design: 48}, rec); err == nil {
|
|
t.Fatal("seeded a design the record does not hold")
|
|
}
|
|
}
|
|
|
|
// Issue 1781 S1: design 47's table, as hq's branch writes it, names every issue in one row and nothing in words the
|
|
// seed would guess.
|
|
func TestDesign47sTableNamesEachIssueOnce(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join("testdata", "milestones", "47-delivery-from-commit-to-delivered.md"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rows, warn := parseTable(string(raw))
|
|
if len(rows) != 33 || len(warn) != 0 {
|
|
t.Fatalf("%d rows, warnings %v", len(rows), warn)
|
|
}
|
|
seen := map[uint64]string{}
|
|
for _, r := range rows {
|
|
if _, err := canonicalID(r.ID); err != nil {
|
|
t.Error(err)
|
|
}
|
|
for _, n := range r.Issues {
|
|
if other, ok := seen[n]; ok {
|
|
t.Errorf("issue %d is in rows %s and %s", n, other, r.ID)
|
|
}
|
|
seen[n] = r.ID
|
|
}
|
|
}
|
|
}
|