222 lines
9.1 KiB
Go
222 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// **A group is merged in one pass on its one Approve, and delivered by one plan** (novox/hq ADR 0418, ADR 0419, ADR
|
|
// 0421, issues 978 and 1047): the members are not ordered, none waits for another's merge or delivery, and a trunk
|
|
// move does not ask the operator a second time.
|
|
|
|
const (
|
|
gCtl, gCat = "novox/mesh-controller", "novox/mesh-catalog"
|
|
gHc, gHk = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
|
|
gTrunk0, gT1 = "a2c2c35000000000000000000000000000000000", "d28ca95000000000000000000000000000000000"
|
|
)
|
|
|
|
// approvedGroup is a group of two pull requests on one branch, checks green, composed green and the operator's Approve
|
|
// asked and not yet answered (w.op.auto is off).
|
|
func approvedGroup(t *testing.T) *world {
|
|
w := newWorld(t)
|
|
w.op.auto = false
|
|
w.h.PullUpdated(pr(gCtl, 1, gHc, "feat/g", "internal/a.go"))
|
|
w.h.PullUpdated(pr(gCat, 2, gHk, "feat/g", "modules/app/module.json"))
|
|
w.h.Checked(judged(gCtl, 1, gHc, "pass", gTrunk0))
|
|
w.h.Checked(judged(gCat, 2, gHk, "pass", gTrunk0))
|
|
w.settleAll()
|
|
g := w.h.groups["feat/g"]
|
|
if g == nil || g.Check == nil {
|
|
t.Fatalf("no composed check was asked: %+v", g)
|
|
}
|
|
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed",
|
|
Members: []struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Commit string `json:"commit"`
|
|
Trunk string `json:"trunk,omitempty"`
|
|
}{{Owner: "novox", Repo: "mesh-controller", Commit: gHc, Trunk: gTrunk0[:12]},
|
|
{Owner: "novox", Repo: "mesh-catalog", Commit: gHk, Trunk: gTrunk0[:12]}}})
|
|
w.settleAll()
|
|
if w.op.count() != 1 || len(w.op.open()) != 1 {
|
|
t.Fatalf("the green group is asked once: %d asks, open %v", w.op.count(), w.op.open())
|
|
}
|
|
return w
|
|
}
|
|
|
|
// Issue 1047 (the rule the lead settled, ADR 0421 §3): an approved group whose trunk moved through a merge that is not
|
|
// its member's has each out-of-date member's check asked at once, then the composed check, and merges on the same
|
|
// Approve — with no second one asked.
|
|
func TestAnApprovedGroupWhoseTrunkMovedIsCheckedAgainAndMergedOnTheSameApprove(t *testing.T) {
|
|
w := approvedGroup(t)
|
|
asked := len(w.ctl.checks)
|
|
w.h.PullMerged(merged(gCtl, 5, "5555555555555555", gT1))
|
|
w.settleAll()
|
|
// The member whose trunk moved has its own check asked, in the same look; the other member's trunk did not move.
|
|
if got := w.ctl.checks[asked:]; len(got) != 1 || got[0] != ":"+IDOf(gCtl, gHc) {
|
|
t.Fatalf("the trunk's move asked %v; wanted the moved member's own check once", got)
|
|
}
|
|
if w.ctl.baseSHAs[len(w.ctl.baseSHAs)-1] != gT1[:12] && w.ctl.baseSHAs[len(w.ctl.baseSHAs)-1] != gT1 {
|
|
t.Fatalf("its check was not asked against the trunk's head: %v", w.ctl.baseSHAs)
|
|
}
|
|
// The Approve is given while the checks run: nothing merges on the verdict out of date.
|
|
w.op.answer(w.op.open()[0], OptionApprove)
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 0 {
|
|
t.Fatalf("merged while the group's checks were out of date: %v", w.forge.merges)
|
|
}
|
|
// The member is judged again: the composed check is asked on the new verdicts.
|
|
w.later(time.Minute)
|
|
w.h.Checked(judged(gCtl, 1, gHc, "pass", gT1))
|
|
w.settleAll()
|
|
g := w.h.groups["feat/g"]
|
|
last := w.ctl.checks[len(w.ctl.checks)-1]
|
|
if last != "feat/g:"+IDOf(gCat, gHk)+","+IDOf(gCtl, gHc) {
|
|
t.Fatalf("the composed check was not asked again: %v", w.ctl.checks)
|
|
}
|
|
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed again"})
|
|
w.settleAll()
|
|
sort.Strings(w.forge.merges)
|
|
if len(w.forge.merges) != 2 || w.forge.merges[0] != "novox/mesh-catalog#2@"+gHk || w.forge.merges[1] != "novox/mesh-controller#1@"+gHc {
|
|
t.Fatalf("both members were not merged on the new composed verdict: %v", w.forge.merges)
|
|
}
|
|
if w.op.count() != 1 {
|
|
t.Fatalf("a second Approve was asked: %d", w.op.count())
|
|
}
|
|
}
|
|
|
|
// ADR 0418 §4: a member the forge refuses is merged again on its fresh check, while the other merged in the same pass.
|
|
func TestAMemberTheForgeRefusesIsMergedAgainWhileTheOtherMergedInTheSamePass(t *testing.T) {
|
|
w := approvedGroup(t)
|
|
w.forge.refuse = errAskedAgain
|
|
w.op.answer(w.op.open()[0], OptionApprove)
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 2 {
|
|
t.Fatalf("both members are asked in the same pass: %v", w.forge.merges)
|
|
}
|
|
w.forge.refuse = nil
|
|
w.later(2 * time.Minute)
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 4 {
|
|
t.Fatalf("a refused member is asked again a minute on: %v", w.forge.merges)
|
|
}
|
|
if d := w.delivery(IDOf(gCat, gHk)); d.Ready.Merged == nil {
|
|
t.Fatalf("not merged: %+v", d.Ready)
|
|
}
|
|
}
|
|
|
|
// Issue 978 (the pin): a feature's single pull request in one repository is a lone delivery, asked alone.
|
|
func TestAFeaturesSinglePullRequestIsALoneDeliveryAskedAlone(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.op.auto = false
|
|
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
|
|
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
|
|
w.settleAll()
|
|
d := w.delivery(IDOf("novox/app", readyHead))
|
|
if d.Group != "" || len(w.h.groups) != 0 {
|
|
t.Fatalf("a single pull request made a group: %q %v", d.Group, w.h.groups)
|
|
}
|
|
if w.op.count() != 1 {
|
|
t.Fatalf("a lone delivery is asked once, alone: %d", w.op.count())
|
|
}
|
|
}
|
|
|
|
// A group answer says its members' gate is the composed check's (agreed with the controller, issue 977).
|
|
func TestTheGroupsAnswerSaysItsGateIsTheComposedChecks(t *testing.T) {
|
|
w := approvedGroup(t)
|
|
lines := w.h.Groups(false)
|
|
if len(lines) != 1 || !lines[0].ComposedGate {
|
|
t.Fatalf("groups: %+v", lines)
|
|
}
|
|
raw, _ := json.Marshal(lines[0])
|
|
var got map[string]any
|
|
if err := json.Unmarshal(raw, &got); err != nil || got["composed_gate"] != true {
|
|
t.Fatalf("the group line says %s", raw)
|
|
}
|
|
if _, ordered := got["order"]; ordered {
|
|
t.Fatalf("a group line still says an order: %s", raw)
|
|
}
|
|
}
|
|
|
|
// ADR 0403 §1: a member whose own gate the group's composed check judges instead (the controller says its gate layer
|
|
// "composed") counts as passed here once its repository's check did not fail, so the group's composed check is asked.
|
|
func TestAMemberWhoseGateIsTheComposedChecksCountsAsPassed(t *testing.T) {
|
|
w := newWorld(t)
|
|
w.op.auto = false
|
|
w.h.PullUpdated(pr(gCtl, 1, gHc, "feat/g", "internal/a.go"))
|
|
w.h.PullUpdated(pr(gCat, 2, gHk, "feat/g", "modules/app/module.json"))
|
|
for _, c := range []struct {
|
|
repo string
|
|
n int
|
|
head string
|
|
}{{gCtl, 1, gHc}, {gCat, 2, gHk}} {
|
|
v := judged(c.repo, c.n, c.head, "pass", gTrunk0)
|
|
v.Gate = &struct {
|
|
Verdict string `json:"verdict"`
|
|
Summary string `json:"summary"`
|
|
Modules []string `json:"modules,omitempty"`
|
|
Dependents []string `json:"dependents,omitempty"`
|
|
}{Verdict: GateComposed, Summary: "judged by its group's composed check"}
|
|
v.RepoCheck = &struct {
|
|
Verdict string `json:"verdict"`
|
|
Summary string `json:"summary"`
|
|
}{Verdict: "pass", Summary: "the repository's own check passed"}
|
|
w.h.Checked(v)
|
|
}
|
|
w.settleAll()
|
|
if len(w.ctl.checks) != 1 || w.ctl.checks[0] != "feat/g:"+IDOf(gCat, gHk)+","+IDOf(gCtl, gHc) {
|
|
t.Fatalf("the composed check was not asked for members judged by it: %v", w.ctl.checks)
|
|
}
|
|
// A member whose repository check failed is not passed, whatever the composed check will say.
|
|
w2 := newWorld(t)
|
|
w2.h.PullUpdated(pr(gCtl, 1, gHc, "feat/g", "internal/a.go"))
|
|
v := judged(gCtl, 1, gHc, "pass", gTrunk0)
|
|
v.Gate = &struct {
|
|
Verdict string `json:"verdict"`
|
|
Summary string `json:"summary"`
|
|
Modules []string `json:"modules,omitempty"`
|
|
Dependents []string `json:"dependents,omitempty"`
|
|
}{Verdict: GateComposed}
|
|
v.RepoCheck = &struct {
|
|
Verdict string `json:"verdict"`
|
|
Summary string `json:"summary"`
|
|
}{Verdict: "fail", Summary: "a test failed"}
|
|
w2.h.Checked(v)
|
|
if w2.state(IDOf(gCtl, gHc)) == Ready {
|
|
t.Fatalf("a member whose repository check failed is ready")
|
|
}
|
|
}
|
|
|
|
// Issue 1049 (survey question 1, confirmed here): a composed check asked after one member has already merged is asked
|
|
// with ALL the members, the merged one at the head it merged, never with the unmerged one alone. The controller needs two
|
|
// or more members to compose (mesh-controller delivery.go askGroupCheck), and a merge of a contained head is a no-op
|
|
// there.
|
|
func TestAComposedCheckAskedAfterOneMemberMergedIsAskedWithAllMembers(t *testing.T) {
|
|
w := approvedGroup(t)
|
|
w.forge.refuseRepo = map[string]error{gCtl: errAskedAgain}
|
|
w.op.answer(w.op.open()[0], OptionApprove)
|
|
w.settleAll()
|
|
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
|
|
w.settleAll()
|
|
if g := w.h.groups["feat/g"]; g == nil || !g.Closed {
|
|
t.Fatalf("the group did not close when a member merged: %+v", g)
|
|
}
|
|
// The unmerged member gets a new head and its own verdict: the group's composed check is due again.
|
|
const gHc2 = "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3"
|
|
asked := len(w.ctl.checks)
|
|
w.h.PullUpdated(pr(gCtl, 1, gHc2, "feat/g", "internal/a.go"))
|
|
w.h.Checked(judged(gCtl, 1, gHc2, "pass", gT1))
|
|
w.settleAll()
|
|
var composed []string
|
|
for _, c := range w.ctl.checks[asked:] {
|
|
if len(c) > 7 && c[:7] == "feat/g:" {
|
|
composed = append(composed, c)
|
|
}
|
|
}
|
|
if len(composed) != 1 || composed[0] != "feat/g:"+IDOf(gCat, gHk)+","+IDOf(gCtl, gHc2) {
|
|
t.Fatalf("the composed check was asked with %v; wanted once, with both members (the merged one at its head)", composed)
|
|
}
|
|
}
|