216 lines
10 KiB
Go
216 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
sdk "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// **The gate asks through its bucket asks and merges through the bridge** (novox/hq ADR 0437 §6, §8, ADR 0441 §1 to §5,
|
|
// ADR 0450 §3, issue 1348). Fixture sources: the key and the id of an ask and the layout of a merge are mesh-sdk go/delivery's
|
|
// (MergeAskKey, AskID, Merge); the act an ask binds is spelled as mesh-controller cmd/mesh-controller/moved.go:98-106 verifies
|
|
// it and as the teller checks it (mesh-delivery modules/mesh-delivery-teller teller.go mergeAct, headsOf); the bridge's
|
|
// messages are the ones mesh-delivery modules/mesh-delivery-forge-bridge internal/bridge acts.go writes (RefusedMessage) and
|
|
// bridge.go merged writes, and the fake bridge here (bridge_test.go) writes them in that layout.
|
|
|
|
// AC1: a ready group writes one ask key, with today's words, and the gate calls no router.
|
|
func TestAReadyGroupWritesOneAskKeyWithTodaysWordsAndCallsNoRouter(t *testing.T) {
|
|
w := approvedGroup(t)
|
|
allGreen(w)
|
|
if w.op.count() != 1 {
|
|
t.Fatalf("%d asks written, wanted one", w.op.count())
|
|
}
|
|
key := w.op.open()[0]
|
|
kind, group, hash, err := sdk.ParseAskKey(key)
|
|
if err != nil || kind != "merge" || group != "feat/g" {
|
|
t.Fatalf("the key %q is not merge.<group>.<heads hash>: %q %q %v", key, kind, group, err)
|
|
}
|
|
ask := w.op.byKey[key]
|
|
if ask.Group != "feat/g" || len(ask.Heads) != 2 {
|
|
t.Fatalf("the ask value %+v does not name the group and both heads", ask)
|
|
}
|
|
if want, err := sdk.MergeAskKey("feat/g", ask.Heads); err != nil || want != key || !strings.HasSuffix(key, hash) {
|
|
t.Fatalf("the heads %v give the key %q, the ask is under %q (%v)", ask.Heads, want, key, err)
|
|
}
|
|
if ask.Ask.ID != sdk.AskID(key) {
|
|
t.Fatalf("the router's id of the ask is %q, wanted %q", ask.Ask.ID, sdk.AskID(key))
|
|
}
|
|
// Today's words (approval.go composeMergeAsk): the headline names the group, the details its members.
|
|
if !strings.Contains(ask.Ask.Headline, "group feat") || !strings.Contains(ask.Ask.Details, "mesh-controller#1") ||
|
|
!strings.Contains(ask.Ask.Details, "mesh-catalog#2") || len(ask.Ask.Options) != 2 || ask.Ask.Who != asks.Operator {
|
|
t.Fatalf("the ask is not today's merge ask: %+v", ask.Ask)
|
|
}
|
|
// The act the Approve binds is the one the teller checks and the controller verifies: this ask's router id, the option, and
|
|
// the heads as `owner/repo@<first 12 characters>` to the whole commit.
|
|
approve, _ := ask.Ask.Option(OptionApprove)
|
|
heads := map[string]string{IDOf(gCtl, gHc): gHc, IDOf(gCat, gHk): gHk}
|
|
want, _ := asks.ActDigest(asks.Act{"verb": "mesh-delivery.merge", "ask": sdk.AskID(key), "option": "approve",
|
|
"heads": "novox/mesh-catalog@d2d2d2d2d2d2=" + gHk + ",novox/mesh-controller@c1c1c1c1c1c1=" + gHc})
|
|
if approve.Binds != want {
|
|
t.Fatalf("the Approve binds %s, wanted the act %v of %v", approve.Binds, mergeAct(sdk.AskID(key), "approve", heads), heads)
|
|
}
|
|
if len(w.forge.calls) != 0 || len(w.merges.puts) != 0 || len(w.forge.merges) != 0 {
|
|
t.Fatalf("something was written beside the ask: forge %v, merges %d", w.forge.calls, len(w.merges.puts))
|
|
}
|
|
}
|
|
|
|
// answeredFirst is a group approved by the teller's answer, with the bridge saying nothing yet.
|
|
func answeredFirst(t *testing.T) *world {
|
|
w := approvedGroup(t)
|
|
allGreen(w)
|
|
w.merges.manual = true
|
|
w.asked = w.op.open()[0]
|
|
w.op.answer(w.asked, OptionApprove)
|
|
w.settleAll()
|
|
return w
|
|
}
|
|
|
|
// AC2: an answer naming the heads asked now hands the first member only; the bridge's merged hands the next; its refused
|
|
// hands none.
|
|
func TestAnAnswerHandsTheFirstMemberOnlyAndTheBridgesMergedHandsTheNext(t *testing.T) {
|
|
w := answeredFirst(t)
|
|
if len(w.merges.puts) != 1 {
|
|
t.Fatalf("the gate wrote %d merges, wanted the first member handed once", len(w.merges.puts))
|
|
}
|
|
first := w.merges.last("feat/g")
|
|
if len(first.Pulls) != 1 || first.Pulls[0].Repository != gCat || first.Pulls[0].State != sdk.MergeMerging || first.Pulls[0].Head.Head != gHk {
|
|
t.Fatalf("the first member is not handed alone, merging, at its head: %+v", first.Pulls)
|
|
}
|
|
if first.Approved[sdk.ApprovedKey(gCat, 2)] != gHk || first.Approved[sdk.ApprovedKey(gCtl, 1)] != gHc || len(first.Approved) != 2 {
|
|
t.Fatalf("the merge does not carry the Approve's heads of both members: %v", first.Approved)
|
|
}
|
|
if first.Ask != w.asked {
|
|
t.Fatalf("the merge names no ask: %+v", first)
|
|
}
|
|
w.settleAll()
|
|
if len(w.merges.puts) != 1 {
|
|
t.Fatalf("the next member was handed before the bridge said the first merged: %d merges written", len(w.merges.puts))
|
|
}
|
|
w.merges.say(sdk.Forge{Kind: sdk.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
|
w.settleAll()
|
|
if len(w.merges.puts) != 2 {
|
|
t.Fatalf("the second member was not handed after the first merged: %d merges written", len(w.merges.puts))
|
|
}
|
|
second := w.merges.last("feat/g")
|
|
if len(second.Pulls) != 2 || second.Pulls[0].State != sdk.MergeMerged || second.Pulls[0].Commit != strings.Repeat("a", 40) ||
|
|
second.Pulls[1].Repository != gCtl || second.Pulls[1].State != sdk.MergeMerging {
|
|
t.Fatalf("the next merge is not the first merged and the second merging: %+v", second.Pulls)
|
|
}
|
|
}
|
|
|
|
// AC2: the bridge's refused hands no member, and the group is said urgent, naming the member and the forge's words.
|
|
func TestTheBridgesRefusalHandsNoMemberAndIsSaidUrgent(t *testing.T) {
|
|
w := answeredFirst(t)
|
|
w.merges.say(sdk.Forge{Kind: sdk.ForgeRefused, Repository: gCat, Number: 2, Head: gHk, Act: "merge",
|
|
Words: "the forge refused gitea_merge_pull_request: mesh/repo-check (missing)"})
|
|
w.later(time30s)
|
|
w.settleAll()
|
|
if len(w.merges.puts) != 2 {
|
|
t.Fatalf("%d merges written; wanted the first handed and then written refused, and none handed after", len(w.merges.puts))
|
|
}
|
|
refused := w.merges.last("feat/g")
|
|
if len(refused.Pulls) != 1 || refused.Pulls[0].State != sdk.MergeRefused || !strings.Contains(refused.Pulls[0].Words, "mesh/repo-check (missing)") {
|
|
t.Fatalf("the refusal is not written into the merge: %+v", refused.Pulls)
|
|
}
|
|
lines := urgentLines(w)
|
|
if len(lines) != 1 || lines[0].ID != IDOf(gCat, gHk) || !strings.Contains(lines[0].Says, "feat/g") ||
|
|
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") {
|
|
t.Fatalf("the refusal is not said as one urgent line: %+v", lines)
|
|
}
|
|
}
|
|
|
|
// ADR 0441 §2: before a member is handed, every member's required statuses are read again; one that is not a success holds
|
|
// the group, even after the first member merged.
|
|
func TestTheNextMemberIsNotHandedWhileARequiredStatusOfTheGroupIsNotASuccess(t *testing.T) {
|
|
w := answeredFirst(t)
|
|
w.merges.say(sdk.Forge{Kind: sdk.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
|
|
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing meanwhile
|
|
w.settleAll()
|
|
if len(w.merges.puts) != 1 {
|
|
t.Fatalf("the second member was handed with a required status missing: %d merges written", len(w.merges.puts))
|
|
}
|
|
if lines := urgentLines(w); len(lines) != 1 || !strings.Contains(lines[0].Says, "mesh/repo-check (missing)") {
|
|
t.Fatalf("the held group is not said urgent: %+v", lines)
|
|
}
|
|
}
|
|
|
|
// AC4: once every member is merged the gate writes source.main once, and each merge is announced with the ask, its asker the
|
|
// teller, and the heads.
|
|
func TestAGroupMergedWholeMovesSourceMainOnceAndAnnouncesWithTheTeller(t *testing.T) {
|
|
w := approvedGroup(t)
|
|
allGreen(w)
|
|
w.op.answer(w.op.open()[0], OptionApprove)
|
|
w.settleAll()
|
|
w.settleAll()
|
|
if len(w.forge.merges) != 2 {
|
|
t.Fatalf("merges %v", w.forge.merges)
|
|
}
|
|
if len(w.source.moves) != 1 {
|
|
t.Fatalf("source.main moved %d times, wanted once", len(w.source.moves))
|
|
}
|
|
main := w.source.moves[0]
|
|
commit := strings.Repeat("c", 40)
|
|
if main.Group != "feat/g" || main.Heads[gCat] != commit || main.Heads[gCtl] != commit || len(main.Heads) != 2 {
|
|
t.Fatalf("source.main is %+v; wanted both repositories at their merge commits, moved by the group", main)
|
|
}
|
|
final := w.merges.last("feat/g")
|
|
if len(final.Pulls) != 2 || final.Pulls[0].State != sdk.MergeMerged || final.Pulls[1].State != sdk.MergeMerged {
|
|
t.Fatalf("the merge is not written as all merged: %+v", final.Pulls)
|
|
}
|
|
say := mergedSaid(w)
|
|
if len(say) != 2 {
|
|
t.Fatalf("announced %d merges, wanted each member's once: %v", len(say), say)
|
|
}
|
|
for _, m := range say {
|
|
if m["asker"] != sdk.TellerModule || !strings.HasPrefix(m["ask_id"].(string), "merge-feat_g-") || m["option"] != OptionApprove {
|
|
t.Fatalf("the announcement does not name the ask and its asker the teller: %v", m)
|
|
}
|
|
}
|
|
w.settleAll()
|
|
if len(w.source.moves) != 1 {
|
|
t.Fatalf("source.main moved again: %d", len(w.source.moves))
|
|
}
|
|
}
|
|
|
|
// 1106: the same refusal again and again with the trunk not moved is not asked on without end. Three refusals in a row with
|
|
// the same cause end the asking by a verdict, a status or a composed check, and say it urgent; a trunk's move, or the
|
|
// operator's Approve, starts it again.
|
|
func TestARefusalThatRepeatsWithNoTrunkMoveIsNotAskedAgainWithoutEnd(t *testing.T) {
|
|
w := answeredFirst(t)
|
|
d := w.h.deliveries[IDOf(gCat, gHk)]
|
|
refuse := func(took string) {
|
|
w.h.ForgeRefused(sdk.Forge{Kind: sdk.ForgeRefused, Repository: gCat, Number: 2, Head: gHk, Act: "merge",
|
|
Words: "novox/mesh-catalog#2 is not merged: its verdict names no trunk commit it was judged against, and main is now f70a99dbf485. " +
|
|
"The check of head d2d2d2d2d2d2 merged into trunk f70a99dbf485 was asked; the last check took " + took + ". Ask again once both statuses pass"})
|
|
}
|
|
tried := w.now
|
|
for i, took := range []string{"4m1s", "4m9s"} {
|
|
refuse(took)
|
|
d.Ready.LastTry = tried
|
|
w.h.mergeAgainNow(d)
|
|
if !d.Ready.LastTry.IsZero() {
|
|
t.Fatalf("refusal %d: the merge was not asked again at once on a verdict, as after a first refusal", i+1)
|
|
}
|
|
}
|
|
refuse("4m5s") // the third in a row, with only the check's duration changed
|
|
if d.Ready.Repeats != 3 {
|
|
t.Fatalf("%d refusals counted for the same cause, wanted 3", d.Ready.Repeats)
|
|
}
|
|
d.Ready.LastTry = tried
|
|
w.h.mergeAgainNow(d) // the composed check or a verdict answers: nothing is asked
|
|
if d.Ready.LastTry.IsZero() {
|
|
t.Fatal("the merge was asked again after the same refusal three times with the trunk not moved")
|
|
}
|
|
if lines := urgentLines(w); len(lines) != 1 || !strings.Contains(lines[0].Says, "names no trunk commit") {
|
|
t.Fatalf("the repeated refusal is not said urgent: %+v", lines)
|
|
}
|
|
// The trunk moves: the merge is asked again.
|
|
d.TrunkNow = "a1a1a1a1a1a1a1a1"
|
|
w.h.mergeAgainNow(d)
|
|
if !d.Ready.LastTry.IsZero() {
|
|
t.Fatal("a trunk's move did not let the merge be asked again")
|
|
}
|
|
}
|