mesh/delivery merged: source.main moved to novox/mesh-delivery@848c6fdc5648
mesh/delivery-group group feat/548-one-check merged: every member is merged
mesh/check failed: ace: the check of novox/mesh-delivery#65 refused: the installed go version could not be read: `go version` with GOTOOLCHAIN=local f…
356 lines
17 KiB
Go
356 lines
17 KiB
Go
package check
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// One check per delivery (novox/hq ADR 0548): the checker's part. A merge turn holds new checks off its repository (§3);
|
|
// a trunk or sibling move never ends a running check and carries its verdict once it ended (§1, §5, §6); only a head git
|
|
// refused gets a whole check after a move (§7); the line of a carried verdict says so (§8).
|
|
|
|
// turnOf is a turn's value on a repository for a delivery of the heads hash.
|
|
func turnOf(t testing.TB, repo, group, heads string) []byte {
|
|
t.Helper()
|
|
at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC)
|
|
data, err := delivery.EncodeTurn(delivery.Turn{Repository: repo, Waiting: []delivery.TurnWait{{Group: group, Heads: heads, Approved: at, Since: at}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func turnKey(t testing.TB, repo string) string {
|
|
t.Helper()
|
|
k, err := delivery.TurnsKey(repo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return k
|
|
}
|
|
|
|
// waitFor polls a condition the checker's goroutines make true, failing after two seconds.
|
|
func waitFor(t testing.TB, what string, ok func() bool) {
|
|
t.Helper()
|
|
for deadline := time.Now().Add(2 * time.Second); !ok(); time.Sleep(time.Millisecond) {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("%s did not happen", what)
|
|
}
|
|
}
|
|
}
|
|
|
|
// pullWithFP is a pull request touching the module m at the fingerprint `src2:` and 64 times the hex digit d.
|
|
func pullWithFP(repo string, number int, head string, d string) delivery.CandidatePull {
|
|
p := pullOf(repo, number, head)
|
|
p.Modules = []string{"m"}
|
|
p.Fps = map[string]delivery.CandidateModule{"m": {FP2: "src2:" + strings.Repeat(d, 64)}}
|
|
return p
|
|
}
|
|
|
|
func TestOneCheckPerDelivery(t *testing.T) {
|
|
ctx := context.Background()
|
|
for name, run := range map[string]func(t *testing.T){
|
|
// §3: no claim while a turn names one of its repositories; a claim on the turn's delete event.
|
|
"a turn on its repository holds a new check off until the turn goes": func(t *testing.T) {
|
|
j := passJudge()
|
|
r := newRig(t, j)
|
|
turns := &Turns{}
|
|
r.ch.Turns = turns
|
|
turns.Put(turnKey(t, repoA), turnOf(t, repoA, "fix/other", "0123456789ab"), false)
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
r.ch.Wait()
|
|
if j.count() != 0 || len(r.verdicts.vals) != 0 {
|
|
t.Fatalf("judged %d times with a turn on %s, verdicts %v", j.count(), repoA, r.verdicts.vals)
|
|
}
|
|
if held := r.ch.Held()[keyOf(t, "feat/x", cand)]; !strings.Contains(held, repoA) || !strings.Contains(held, "fix/other") {
|
|
t.Errorf("the hold is said as %q, want the turn's repository and group", held)
|
|
}
|
|
if !turns.Put(turnKey(t, repoA), nil, true) {
|
|
t.Fatal("the turn's delete changed nothing")
|
|
}
|
|
r.ch.Reconsider(ctx) // what the watch of turns does on the delete
|
|
r.ch.Wait()
|
|
if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != delivery.VerdictPass {
|
|
t.Errorf("judged %d times after the turn went, want once", j.count())
|
|
}
|
|
},
|
|
"a turn on another repository, or of its own heads, holds nothing": func(t *testing.T) {
|
|
j := passJudge()
|
|
r := newRig(t, j)
|
|
turns := &Turns{}
|
|
r.ch.Turns = turns
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
c, _ := delivery.DecodeCandidate(cand)
|
|
turns.Put(turnKey(t, repoSDK), turnOf(t, repoSDK, "fix/other", "0123456789ab"), false)
|
|
turns.Put(turnKey(t, repoA), turnOf(t, repoA, "feat/x", GateHeads(c)), false)
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
r.ch.Wait()
|
|
if j.count() != 1 {
|
|
t.Errorf("judged %d times, want once: the turns hold another repository and its own merge", j.count())
|
|
}
|
|
},
|
|
// §3, the race: a check claimed before the gate's turn is judged on the commit it read, never restarted.
|
|
"a check claimed before a turn was written runs to its verdict": func(t *testing.T) {
|
|
release, started := make(chan struct{}), make(chan struct{})
|
|
j := &judgeFunc{f: func(context.Context, *Case, int) (Outcome, error) {
|
|
close(started)
|
|
<-release
|
|
return Outcome{Verdict: passing()}, nil
|
|
}}
|
|
r := newRig(t, j)
|
|
turns := &Turns{}
|
|
r.ch.Turns = turns
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
<-started
|
|
turns.Put(turnKey(t, repoA), turnOf(t, repoA, "fix/other", "0123456789ab"), false)
|
|
r.ch.Reconsider(ctx)
|
|
close(release)
|
|
r.ch.Wait()
|
|
if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != delivery.VerdictPass {
|
|
t.Errorf("judged %d times; the running check is not ended by the turn", j.count())
|
|
}
|
|
},
|
|
// §1, §5: a trunk move during a running check calls the judge once, writes its verdict, keeps the status pending (a pass
|
|
// on an old trunk is never shown), then asks one carry, on the recheck stream, and no check.
|
|
"a trunk move during a check ends nothing and asks one carry after it": func(t *testing.T) {
|
|
var r *rig
|
|
var kinds []string
|
|
var from []map[string]string
|
|
judged := make(chan struct{})
|
|
j := &judgeFunc{f: func(ctx context.Context, c *Case, call int) (Outcome, error) {
|
|
kinds, from = append(kinds, c.Kind), append(from, c.CarriedFrom)
|
|
if call == 1 {
|
|
r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) // the trunk moves while it runs
|
|
r.ch.TrunkMoved(ctx) // as the watch of the source says it
|
|
close(judged)
|
|
} else if s := owedState(t, r, repoA, 7, head1); s != "pending" {
|
|
t.Errorf("the status during the carry is %q, want pending: the pass on the old trunk is never shown", s)
|
|
}
|
|
return Outcome{Verdict: passing()}, nil
|
|
}}
|
|
r = newRig(t, j)
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
<-judged
|
|
r.ch.Wait()
|
|
v := r.verdict(t, keyOf(t, "feat/x", cand))
|
|
if j.count() != 2 || kinds[0] != KindNew || kinds[1] != KindTrunk || from[0] != nil || from[1][repoA] != trunkA {
|
|
t.Fatalf("judged %d times as %v carried from %v; want a whole check, then one carry from %s", j.count(), kinds, from, trunkA[:12])
|
|
}
|
|
if v.State != delivery.VerdictPass || v.Trunk[repoA] != trunkA2 || v.CarriedFrom[repoA] != trunkA {
|
|
t.Errorf("the verdict is %s on %v carried from %v; want a pass on the new trunk carried from the old", v.State, v.Trunk, v.CarriedFrom)
|
|
}
|
|
if s := owedState(t, r, repoA, 7, head1); s != "success" {
|
|
t.Errorf("the status after the carry is %q, want success", s)
|
|
}
|
|
if r.verdicts.creates[keyOf(t, "feat/x", cand)] != 1 {
|
|
t.Error("the verdict key was made again: a carry rewrites it")
|
|
}
|
|
},
|
|
// §5: the composition is kept when no fingerprint of a module it composed changed, and runs again otherwise.
|
|
"a carry keeps a passed composition while the fingerprints stay the same": func(t *testing.T) {
|
|
var kept []bool
|
|
j := &judgeFunc{f: func(_ context.Context, c *Case, _ int) (Outcome, error) {
|
|
kept = append(kept, c.Composition != nil)
|
|
v := passing()
|
|
v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "v0.1.14-x", FPSet: c.FPSet}
|
|
return Outcome{Verdict: v}, nil
|
|
}}
|
|
r := newRig(t, j)
|
|
cand := candidateOf(t, "feat/x", pullWithFP(repoA, 7, head1, "a"))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
r.ch.Wait()
|
|
r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS})
|
|
r.ch.TrunkMoved(ctx) // the indexer's fingerprints of the candidate stay the same
|
|
r.ch.Wait()
|
|
r.ch.Candidate(ctx, "feat%2Fx", candidateOf(t, "feat/x", pullWithFP(repoA, 7, head1, "b"))) // a fingerprint changed with the move
|
|
r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS})
|
|
r.ch.TrunkMoved(ctx)
|
|
r.ch.Wait()
|
|
if len(kept) != 3 || kept[0] || !kept[1] || kept[2] {
|
|
t.Errorf("the composition was kept in the checks %v; want composed, kept on the carry, composed again once a fingerprint changed", kept)
|
|
}
|
|
},
|
|
// §6: a sibling's move puts the verdict out of date, its status pending saying it is carried, and the carry's verdict
|
|
// names the sibling's old commit; the status follows the carry's verdict. Which packages run is the digest's (only
|
|
// those that name MESH_CHECK_BESIDE change with a sibling), the build module's part.
|
|
"a sibling's move is carried and the status follows the carry": func(t *testing.T) {
|
|
var r *rig
|
|
var from map[string]string
|
|
j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) {
|
|
if call == 2 {
|
|
from = c.CarriedFrom
|
|
if s := owedState(t, r, repoA, 7, head1); s != "pending" {
|
|
t.Errorf("the status during the sibling's carry is %q, want pending", s)
|
|
}
|
|
}
|
|
return Outcome{Verdict: passing(), Beside: map[string]string{catalogueRepository: r.trunks.Heads()[catalogueRepository]}}, nil
|
|
}}
|
|
r = newRig(t, j)
|
|
r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS, catalogueRepository: trunkS})
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
r.ch.Wait()
|
|
if b := r.verdict(t, keyOf(t, "feat/x", cand)).Beside; b[catalogueRepository] != trunkS {
|
|
t.Fatalf("the verdict names the siblings %v, want the catalogue at %s", b, trunkS[:12])
|
|
}
|
|
r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS, catalogueRepository: trunkS2})
|
|
r.ch.TrunkMoved(ctx)
|
|
r.ch.Wait()
|
|
v := r.verdict(t, keyOf(t, "feat/x", cand))
|
|
if j.count() != 2 || from[catalogueRepository] != trunkS || v.CarriedFrom[catalogueRepository] != trunkS || v.Beside[catalogueRepository] != trunkS2 {
|
|
t.Errorf("judged %d times, carried from %v, verdict beside %v carried from %v; want one carry over the catalogue's move", j.count(), from, v.Beside, v.CarriedFrom)
|
|
}
|
|
if s := owedState(t, r, repoA, 7, head1); s != "success" {
|
|
t.Errorf("the status after the carry is %q, want success", s)
|
|
}
|
|
},
|
|
// §7: a carry whose merge conflicts fails naming the files and asks no check; a later move onto which the head merges
|
|
// cleanly asks a whole check, not a carry.
|
|
"a carry git refuses fails naming the files, and the next move asks a whole check": func(t *testing.T) {
|
|
var kinds []string
|
|
var from []map[string]string
|
|
j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) {
|
|
kinds, from = append(kinds, c.Kind), append(from, c.CarriedFrom)
|
|
if call == 2 {
|
|
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "ace",
|
|
Words: repoA + "#7 " + ConflictWords + "go.mod"}}}, nil
|
|
}
|
|
return Outcome{Verdict: passing()}, nil
|
|
}}
|
|
r := newRig(t, j)
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
r.ch.Wait()
|
|
r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS})
|
|
r.ch.TrunkMoved(ctx)
|
|
r.ch.Wait()
|
|
r.ch.Reconsider(ctx) // anything else that looks again asks nothing of a conflict
|
|
r.ch.Wait()
|
|
if v := r.verdict(t, keyOf(t, "feat/x", cand)); j.count() != 2 || !IsConflictFailure(v) || !strings.Contains(v.Failed.Words, "go.mod") {
|
|
t.Fatalf("judged %d times, verdict %+v; want the carry failed naming go.mod and nothing asked after it", j.count(), v.Failed)
|
|
}
|
|
r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS})
|
|
r.ch.TrunkMoved(ctx)
|
|
r.ch.Wait()
|
|
if j.count() != 3 || kinds[1] != KindTrunk || from[1] == nil || kinds[2] != KindNew || from[2] != nil {
|
|
t.Errorf("checks %v carried from %v; want a whole check, a carry, then a whole check after the conflict", kinds, from)
|
|
}
|
|
},
|
|
// §8, §6: the line of a carried verdict names the head on the new trunk commit first, as the gate and the merge tool
|
|
// read a line, then where it was carried from and what the carry ran and kept; while it is carried it is pending and says so.
|
|
"the line of a carry says where it was carried from": func(t *testing.T) {
|
|
cs, p := lineCase(t, pullOf(repoA, 7, head1))
|
|
v := passVerdict()
|
|
v.Trunk = map[string]string{repoA: trunkA}
|
|
v.Beside = map[string]string{catalogueRepository: trunkS2}
|
|
v.CarriedFrom = map[string]string{repoA: trunkA2, catalogueRepository: trunkS}
|
|
line := Line(v, cs, p, nil, nil, nil)
|
|
if !strings.HasPrefix(line, "pass 111111111111 on aaaaaaaaaaaa | carried from abababababab, mesh-catalog 555555555555 | unit 12 run") {
|
|
t.Errorf("the carried line = %q", line)
|
|
}
|
|
if head, trunk := checkWordsOf(line); head != head1[:12] || trunk != trunkA[:12] {
|
|
t.Errorf("the line names %s on %s first, want the head on the new trunk", head, trunk)
|
|
}
|
|
cs.CarriedFrom = map[string]string{repoA: trunkA2}
|
|
pending := Line(delivery.Verdict{State: delivery.VerdictRunning}, cs, p, nil, nil, nil)
|
|
if !strings.HasPrefix(pending, "being carried to 111111111111 on aaaaaaaaaaaa | from abababababab") {
|
|
t.Errorf("the pending line of a carry = %q", pending)
|
|
}
|
|
},
|
|
// §3 (review round 1): two approved merges in one turn whose trunk moved each get their carry; neither holds the other's.
|
|
"two merges waiting in one turn each get their carry": func(t *testing.T) {
|
|
var r *rig
|
|
j := &judgeFunc{f: func(context.Context, *Case, int) (Outcome, error) { return Outcome{Verdict: passing()}, nil }}
|
|
r = newRig(t, j)
|
|
candA, candC := candidateOf(t, "feat/a", pullOf(repoA, 7, head1)), candidateOf(t, "feat/c", pullOf(repoA, 8, head2))
|
|
r.ch.Candidate(ctx, "feat%2Fa", candA)
|
|
r.ch.Candidate(ctx, "feat%2Fc", candC)
|
|
r.ch.Wait()
|
|
a, _ := delivery.DecodeCandidate(candA)
|
|
c, _ := delivery.DecodeCandidate(candC)
|
|
at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC)
|
|
turn, err := delivery.EncodeTurn(delivery.Turn{Repository: repoA, Waiting: []delivery.TurnWait{
|
|
{Group: "feat/a", Heads: GateHeads(a), Approved: at, Since: at}, {Group: "feat/c", Heads: GateHeads(c), Approved: at.Add(time.Minute), Since: at}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
turns := &Turns{}
|
|
turns.Put(turnKey(t, repoA), turn, false)
|
|
r.ch.Turns = turns
|
|
r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) // a third merge moves the trunk under both
|
|
r.ch.TrunkMoved(ctx)
|
|
r.ch.Wait()
|
|
for _, cand := range [][]byte{candA, candC} {
|
|
group := map[bool]string{true: "feat/a", false: "feat/c"}[string(cand) == string(candA)]
|
|
if v := r.verdict(t, keyOf(t, group, cand)); v.Trunk[repoA] != trunkA2 || v.CarriedFrom[repoA] != trunkA {
|
|
t.Errorf("%s: verdict on %v carried from %v; want its carry claimed and done", group, v.Trunk, v.CarriedFrom)
|
|
}
|
|
}
|
|
},
|
|
// §6 (review round 1): the node-engine read at the commit the mesh runs is named, and a new running commit is carried.
|
|
"a new running node-engine is carried": func(t *testing.T) {
|
|
runs := runsAt{nodeEngineModule: trunkS}
|
|
j := &judgeFunc{f: func(_ context.Context, c *Case, _ int) (Outcome, error) {
|
|
return Outcome{Verdict: passing(), Beside: map[string]string{nodeEngineRepo: runs[nodeEngineModule]}}, nil
|
|
}}
|
|
r := newRig(t, j)
|
|
r.ch.Runs = runs
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
r.ch.Wait()
|
|
r.ch.TrunkMoved(ctx) // nothing moved: nothing is carried
|
|
r.ch.Wait()
|
|
runs[nodeEngineModule] = trunkS2
|
|
r.ch.TrunkMoved(ctx) // what the indexer's watch does when mesh-host's running commit changes
|
|
r.ch.Wait()
|
|
v := r.verdict(t, keyOf(t, "feat/x", cand))
|
|
if j.count() != 2 || v.CarriedFrom[nodeEngineRepo] != trunkS || v.Beside[nodeEngineRepo] != trunkS2 {
|
|
t.Errorf("judged %d times, verdict beside %v carried from %v; want one carry over the node-engine's new running commit", j.count(), v.Beside, v.CarriedFrom)
|
|
}
|
|
},
|
|
// §5: a carry that fails names the move it failed on.
|
|
"a carry that fails names the move": func(t *testing.T) {
|
|
j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) {
|
|
if call == 1 {
|
|
return Outcome{Verdict: passing()}, nil
|
|
}
|
|
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}}, nil
|
|
}}
|
|
r := newRig(t, j)
|
|
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
|
|
r.ch.Candidate(ctx, "feat%2Fx", cand)
|
|
r.ch.Wait()
|
|
r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS})
|
|
r.ch.TrunkMoved(ctx)
|
|
r.ch.Wait()
|
|
v := r.verdict(t, keyOf(t, "feat/x", cand))
|
|
if v.State != delivery.VerdictFail || v.CarriedFrom != nil || !strings.HasPrefix(v.Failed.Words, "want 4") ||
|
|
!strings.Contains(v.Failed.Words, repoA+" "+trunkA[:12]+" to "+trunkA2[:12]) {
|
|
t.Errorf("the failed carry is %s carried from %v with words %q; want the test's words, then the move", v.State, v.CarriedFrom, v.Failed.Words)
|
|
}
|
|
},
|
|
} {
|
|
t.Run(name, run)
|
|
}
|
|
}
|
|
|
|
// checkWordsOf is the head and trunk a line names first, as the gate reads it (mesh-delivery-gate onestatus.go checkLine).
|
|
func checkWordsOf(line string) (string, string) {
|
|
for _, part := range strings.Split(line, " | ") {
|
|
f := strings.Fields(part)
|
|
for i := 0; i+2 < len(f); i++ {
|
|
if f[i+1] == "on" && len(f[i]) == 12 && len(f[i+2]) == 12 {
|
|
return f[i], f[i+2]
|
|
}
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|