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 } } }