//go:build integration package main import ( "encoding/json" "fmt" "strings" "sync" "testing" ) // Issue 1781 (ADR 0493 §1, §2, §4), the module whole over a real bus: a milestone made by its verb is kept in the // state the manifest declares, and two instances linking one issue to two milestones at the same moment leave it in // exactly one; the other is refused naming the first, and the milestones and the list say the same. func TestAnIssueRaceIntoTwoMilestonesOverTheBusEndsInOne(t *testing.T) { b := newBus(t) var events []event var evMu sync.Mutex settings := writeSettings(t, 365) one, two := b.start(t, settings, &events, &evMu), b.start(t, settings, &events, &evMu) must := func(raw json.RawMessage, err error) json.RawMessage { t.Helper() if err != nil { t.Fatal(err) } return raw } must(one.call("open", map[string]any{"title": "a milestone's issue", "report": "seen"})) must(one.call("milestone-set", map[string]any{"design": 47, "id": "R6", "outcome": "nodes apply their wants", "order": 1})) must(two.call("milestone-set", map[string]any{"design": 47, "id": "3 fix", "outcome": "a group merges in one pass", "order": 2})) errs := make([]error, 2) var wg sync.WaitGroup for k, c := range []struct { i *instance target string }{{one, "47/R6"}, {two, "47/3-fix"}} { wg.Add(1) go func() { defer wg.Done() _, errs[k] = c.i.call("link", map[string]any{"number": 365, "kind": "milestone", "target": c.target}) }() } wg.Wait() if (errs[0] == nil) == (errs[1] == nil) { t.Fatalf("both or neither linked: %v", errs) } refused := errs[0] if refused == nil { refused = errs[1] } if !strings.Contains(refused.Error(), "is in milestone") { t.Fatalf("refused with %v", refused) } var read MilestonesRead _ = json.Unmarshal(must(two.call("milestones", map[string]any{"design": 47})), &read) if len(read.Milestones) != 2 || read.Milestones[0].ID != "R6" || read.Milestones[1].ID != "3-fix" || len(read.Milestones[0].Issues)+len(read.Milestones[1].Issues) != 1 { t.Fatalf("%+v", read.Milestones) } in := "47/R6" if len(read.Milestones[1].Issues) == 1 { in = "47/3-fix" } var l Listed _ = json.Unmarshal(must(one.call("list", map[string]any{"linked": "milestone:" + in})), &l) if len(l.Rows) != 1 || l.Rows[0].Number != 365 { t.Fatalf("listed %+v", l) } var hist MilestonesRead _ = json.Unmarshal(must(one.call("milestones", map[string]any{"one": "47/R6"})), &hist) if len(hist.History) != 1 { t.Fatalf("history %d values", len(hist.History)) } // The moves history over the real log: the open, from nothing to open, read by the other instance. var ch ChangesRead _ = json.Unmarshal(must(two.call("changes", map[string]any{"since": "60m"})), &ch) if len(ch.Changes) != 1 || ch.Changes[0].Number != 365 || ch.Changes[0].Kind != OpOpened || ch.Changes[0].To != StatusOpen || ch.Changes[0].Title != "a milestone's issue" { t.Fatalf("changes %+v", ch) } evMu.Lock() defer evMu.Unlock() var kinds []string for _, e := range events { kinds = append(kinds, e.Key) } if fmt.Sprint(kinds) != "[opened milestone-set milestone-set linked]" { t.Fatalf("events %v", kinds) } }