A pull request's head announced again from the bus's history at mesh-delivery's first start was proposed with no verdict and nothing ever asked its check: the controller had taken that announcement long before, and stalled raised it after an hour for the operator. A proposed delivery with no verdict and no check asked is now asked through delivery-check once it has waited past a grace longer than a check takes; an announcement carrying its head's decided gate status takes it. The proposed bound runs from the ask, and H2's close may re-ask once (a table row) before the delivery is the operator's.
171 lines
6.7 KiB
Go
171 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A delivery that waits for its check gets one (novox/hq issue 290): a head adopted while it already waited —
|
|
// its announcement heard again from the bus's history, which the controller took long ago — has its check asked
|
|
// by this owner; so does one whose announcement the controller missed. Once asked, the proposed bound means
|
|
// "asked and not answered", and healer H2 may ask it once more before it is the operator's.
|
|
|
|
// The live case: a pull request opened before merge checks existed, announced again at this owner's first
|
|
// start, read back proposed after a restart. Its check is asked at the first tick, once, for that head alone.
|
|
func TestAProposedDeliveryAdoptedWithNoVerdictHasItsCheckAsked(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/hq", head)
|
|
w.h.PullUpdated(pr("novox/hq", 94, head, "issues/245-246", "04-ISSUES/245/00-report.md"))
|
|
w.settleAll()
|
|
if len(w.ctl.checks) != 0 {
|
|
t.Fatalf("a head just announced was asked at once, before the controller's own check had time: %v", w.ctl.checks)
|
|
}
|
|
|
|
// A restart, hours later: read back proposed, no verdict, nothing asked.
|
|
again := newWorldOver(t, w.store, w.ctl, w.forge, w.now.Add(10*time.Hour))
|
|
if s := again.state(id); s != Proposed {
|
|
t.Fatalf("read back, it is %s", s)
|
|
}
|
|
again.settleAll()
|
|
if len(w.ctl.checks) != 1 || w.ctl.checks[0] != ":"+id {
|
|
t.Fatalf("its check was asked %v, wanted once for its head alone", w.ctl.checks)
|
|
}
|
|
d := again.delivery(id)
|
|
if d.CheckAsk == nil || d.CheckAsk.ID != "check-1" || d.CheckAsk.By != byOwner || d.State != Proposed {
|
|
t.Fatalf("the ask is not kept: %+v (%s)", d.CheckAsk, d.State)
|
|
}
|
|
if notes := w.forge.notesOn(head); len(notes) == 0 || !strings.Contains(notes[len(notes)-1], "its check asked") {
|
|
t.Fatalf("the ask is not noted on the head: %q", notes)
|
|
}
|
|
// Asked, it is not stalled: the bound runs from the ask.
|
|
if st := again.h.Stalled(); len(st) != 0 {
|
|
t.Fatalf("a check just asked is stalled: %+v", st)
|
|
}
|
|
// Asked once: the ticks after do not ask again.
|
|
again.later(30 * time.Minute)
|
|
again.settleAll()
|
|
if len(w.ctl.checks) != 1 {
|
|
t.Fatalf("a check asked and in flight was asked again: %v", w.ctl.checks)
|
|
}
|
|
// The verdict arrives: checked, then ready, by itself.
|
|
again.h.Checked(verdict("novox/hq", 94, head, "pass"))
|
|
if s := again.state(id); s != Ready {
|
|
t.Fatalf("its verdict left it %s", s)
|
|
}
|
|
}
|
|
|
|
// The same head adopted at a first start with no restart: the grace passes, and its check is asked.
|
|
func TestAMissedAnnouncementHasItsCheckAskedAfterTheGrace(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.later(checkGrace - time.Minute)
|
|
w.settleAll()
|
|
if len(w.ctl.checks) != 0 {
|
|
t.Fatalf("asked within the grace: %v", w.ctl.checks)
|
|
}
|
|
// The controller does not answer: nothing is kept as asked, and it is tried again a minute on.
|
|
w.later(2 * time.Minute)
|
|
w.ctl.down = true
|
|
w.settleAll()
|
|
if d := w.delivery(id); d.CheckAsk != nil {
|
|
t.Fatalf("an ask the controller did not take is kept as asked: %+v", d.CheckAsk)
|
|
}
|
|
w.ctl.down = false
|
|
w.settleAll()
|
|
if len(w.ctl.checks) != 0 {
|
|
t.Fatalf("asked again within the minute: %v", w.ctl.checks)
|
|
}
|
|
w.later(time.Minute)
|
|
w.settleAll()
|
|
if len(w.ctl.checks) != 1 || w.delivery(id).CheckAsk == nil {
|
|
t.Fatalf("a missed announcement was not asked once the grace passed: %v", w.ctl.checks)
|
|
}
|
|
// A verdict that arrived in the meantime, or a delivery past its proposal, is never asked.
|
|
other := IDOf("novox/app", "bbbbbbbbbbbb2222")
|
|
w.h.PullUpdated(pr("novox/app", 8, "bbbbbbbbbbbb2222", "feat/y"))
|
|
w.h.Checked(verdict("novox/app", 8, "bbbbbbbbbbbb2222", "fail"))
|
|
w.later(time.Hour)
|
|
w.settleAll()
|
|
if w.state(other) != Rejected || len(w.ctl.checks) != 1 {
|
|
t.Fatalf("a head with a verdict was asked: %s %v", w.state(other), w.ctl.checks)
|
|
}
|
|
}
|
|
|
|
// An announcement heard again carrying its head's decided verdict takes it; a pending one does not.
|
|
func TestAnAnnouncementCarryingItsHeadsVerdictTakesIt(t *testing.T) {
|
|
w := newWorld(t)
|
|
p := pr("novox/app", 7, head, "feat/x", "src/a.go")
|
|
p.HeadChecks = map[string]string{"mesh/merge-gate": "success", "mesh/repo-check": "success"}
|
|
w.h.PullUpdated(p)
|
|
if s := w.state(IDOf("novox/app", head)); s != Ready {
|
|
t.Fatalf("a head whose gate passed on the forge is %s", s)
|
|
}
|
|
q := pr("novox/app", 8, "bbbbbbbbbbbb2222", "feat/y")
|
|
q.HeadChecks = map[string]string{"mesh/merge-gate": "pending"}
|
|
w.h.PullUpdated(q)
|
|
if s := w.state(IDOf("novox/app", "bbbbbbbbbbbb2222")); s != Proposed {
|
|
t.Fatalf("a pending gate on the forge left it %s", s)
|
|
}
|
|
}
|
|
|
|
// Asked and not answered past the bound: stalled, and H2's close re-asks once — the second close is refused
|
|
// by the table, and the delivery is the operator's.
|
|
func TestH2ReasksAStalledCheckOnceThenItIsTheOperators(t *testing.T) {
|
|
w := newWorld(t)
|
|
id := IDOf("novox/app", head)
|
|
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
|
|
w.later(checkGrace + time.Minute)
|
|
w.settleAll()
|
|
if len(w.ctl.checks) != 1 {
|
|
t.Fatalf("asked %v", w.ctl.checks)
|
|
}
|
|
if _, err := w.h.Close(id, "too early"); err == nil {
|
|
t.Fatal("re-asked within the bound")
|
|
}
|
|
w.later(Bounds[Proposed].For + time.Minute)
|
|
st := w.h.Stalled()
|
|
if len(st) != 1 || st[0].ID != id || !strings.Contains(st[0].H2, "re-ask") {
|
|
t.Fatalf("stalled %+v", st)
|
|
}
|
|
// The controller does not take the re-ask: nothing is spent.
|
|
w.ctl.down = true
|
|
if _, err := w.h.Close(id, "delivery stalled"); err == nil {
|
|
t.Fatal("a re-ask the controller did not take was said done")
|
|
}
|
|
if d := w.delivery(id); d.CheckAsk.Reasked != 0 {
|
|
t.Fatal("a re-ask that was not made was spent")
|
|
}
|
|
w.ctl.down = false
|
|
said, err := w.h.Close(id, "delivery stalled")
|
|
if err != nil || !strings.Contains(said, "re-asked") {
|
|
t.Fatalf("H2's close: %q %v", said, err)
|
|
}
|
|
d := w.delivery(id)
|
|
if len(w.ctl.checks) != 2 || d.State != Proposed || d.CheckAsk.Reasked != 1 || d.CheckAsk.By != byH2 ||
|
|
d.Transitions[len(d.Transitions)-1].Event != EvReask {
|
|
t.Fatalf("after the re-ask: %v %s %+v", w.ctl.checks, d.State, d.CheckAsk)
|
|
}
|
|
if len(w.h.Stalled()) != 0 {
|
|
t.Fatal("a check just re-asked is stalled")
|
|
}
|
|
// Still not answered: stalled again, and the operator's.
|
|
w.later(Bounds[Proposed].For + time.Minute)
|
|
st = w.h.Stalled()
|
|
if len(st) != 1 || !strings.Contains(st[0].H2, "operator's") {
|
|
t.Fatalf("stalled after the re-ask %+v", st)
|
|
}
|
|
if _, err := w.h.Close(id, "delivery stalled"); err == nil || !strings.Contains(err.Error(), "once already") {
|
|
t.Fatalf("a second re-ask: %v", err)
|
|
}
|
|
if len(w.ctl.checks) != 2 {
|
|
t.Fatalf("a second re-ask reached the controller: %v", w.ctl.checks)
|
|
}
|
|
// The verdict, late, still settles it.
|
|
w.h.Checked(verdict("novox/app", 7, head, "pass"))
|
|
if s := w.state(id); s != Ready {
|
|
t.Fatalf("a late verdict left it %s", s)
|
|
}
|
|
}
|