Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/checker_test.go
T

368 lines
13 KiB
Go

package check
import (
"context"
"encoding/json"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
const (
repoA = "novox/mesh-delivery"
repoSDK = "novox/mesh-sdk"
head1 = "1111111111111111111111111111111111111111"
head2 = "2222222222222222222222222222222222222222"
trunkA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
trunkA2 = "abababababababababababababababababababab"
trunkS = "5555555555555555555555555555555555555555"
)
// candidateOf is a candidate of a group with the given pull requests, as the indexer writes one.
func candidateOf(t testing.TB, group string, pulls ...delivery.CandidatePull) []byte {
t.Helper()
heads := make([]delivery.Head, len(pulls))
for i, p := range pulls {
heads[i] = p.Head
}
hash, err := delivery.HeadsHash(heads)
if err != nil {
t.Fatal(err)
}
c := delivery.Candidate{V: delivery.Version, Group: group, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}
data, err := delivery.EncodeCandidate(c)
if err != nil {
t.Fatal(err)
}
return data
}
func pullOf(repo string, number int, head string) delivery.CandidatePull {
return delivery.CandidatePull{Head: delivery.Head{Repository: repo, Number: number, Head: head}, Modules: []string{}}
}
// judgeFunc is a Judge written as a function; it counts its calls.
type judgeFunc struct {
mu sync.Mutex
calls int
f func(ctx context.Context, c *Case, call int) (Outcome, error)
}
func (j *judgeFunc) Judge(ctx context.Context, c *Case) (Outcome, error) {
j.mu.Lock()
j.calls++
n := j.calls
j.mu.Unlock()
return j.f(ctx, c, n)
}
func (j *judgeFunc) count() int { j.mu.Lock(); defer j.mu.Unlock(); return j.calls }
func passing() delivery.Verdict {
return delivery.Verdict{State: delivery.VerdictPass, Levels: map[string]delivery.LevelResult{
delivery.LevelUnit: {Run: 2, Kept: 3}, delivery.LevelIntegration: {Run: 1}}}
}
func passJudge() *judgeFunc {
return &judgeFunc{f: func(context.Context, *Case, int) (Outcome, error) { return Outcome{Verdict: passing()}, nil }}
}
type rig struct {
ch *Checker
verdicts *memKV
owed *memKV
trunks *Trunks
}
func newRig(t testing.TB, j Judge) *rig {
t.Helper()
r := &rig{verdicts: newMemKV(), owed: newMemKV(), trunks: &Trunks{}}
r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS})
r.ch = &Checker{Verdicts: r.verdicts, Owed: r.owed, Judge: j, Trunks: r.trunks, Link: "https://view.invalid/checks", Now: time.Now}
return r
}
func (r *rig) setTrunks(t testing.TB, heads map[string]string) bool {
t.Helper()
data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads})
if err != nil {
t.Fatal(err)
}
return r.trunks.Put(data, false)
}
func (r *rig) verdict(t testing.TB, key string) delivery.Verdict {
t.Helper()
raw, _, found, _ := r.verdicts.Get(context.Background(), key)
if !found {
t.Fatalf("no verdict %s; the bucket holds %v", key, r.verdicts.vals)
}
v, err := delivery.DecodeVerdict(raw)
if err != nil {
t.Fatal(err)
}
return v
}
func keyOf(t testing.TB, group string, value []byte) string {
t.Helper()
c, err := delivery.DecodeCandidate(value)
if err != nil {
t.Fatal(err)
}
k, err := delivery.CheckerVerdictKey(group, c.HeadsHash)
if err != nil {
t.Fatal(err)
}
return k
}
func TestACandidateNotYetCheckedGetsItsVerdictKeyCreatedOnceAndIsJudged(t *testing.T) { // N6/AC1, 1678
ctx, j := context.Background(), passJudge()
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
key := keyOf(t, "feat/x", cand)
if v := r.verdict(t, key); v.State != delivery.VerdictPass || v.Group != "feat/x" || v.Trunk[repoA] != trunkA {
t.Fatalf("verdict = %+v", v)
}
if r.verdicts.creates[key] != 1 || j.count() != 1 {
t.Errorf("creates = %d, judged = %d, want one each", r.verdicts.creates[key], j.count())
}
// The same candidate heard again, with nothing changed, is not judged again.
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
if j.count() != 1 {
t.Errorf("judged %d times after the same candidate again, want once", j.count())
}
}
// racyKV lets another instance create the key just before this one's create.
type racyKV struct {
*memKV
other []byte
}
func (r racyKV) Create(ctx context.Context, key string, value []byte) (uint64, error) {
if r.other != nil {
_, _ = r.memKV.Create(ctx, key, r.other)
}
return r.memKV.Create(ctx, key, value)
}
func TestASecondInstancesCreateIsRefusedAndItDoesNothing(t *testing.T) { // N6/AC1
ctx, j := context.Background(), passJudge()
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
key := keyOf(t, "feat/x", cand)
other, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], State: delivery.VerdictRunning})
r.ch.Verdicts = racyKV{r.verdicts, other}
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
if j.count() != 0 {
t.Errorf("the instance that lost the create judged %d times, want none", j.count())
}
if v := r.verdict(t, key); v.State != delivery.VerdictRunning {
t.Errorf("the other instance's key was changed: %+v", v)
}
}
func TestARestartWithARunningKeyJudgesItAgain(t *testing.T) { // N6/AC2
ctx, j := context.Background(), passJudge()
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
key := keyOf(t, "feat/x", cand)
left, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], Trunk: map[string]string{repoA: trunkA}, State: delivery.VerdictRunning})
if _, err := r.verdicts.Create(ctx, key, left); err != nil {
t.Fatal(err)
}
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
if j.count() != 1 || r.verdict(t, key).State != delivery.VerdictPass {
t.Fatalf("judged %d times, verdict %+v, want the left check judged once and passed", j.count(), r.verdict(t, key))
}
}
func TestACheckPastItsBoundFailsNamingTheStuckStep(t *testing.T) { // N6/AC3
ctx := context.Background()
j := &judgeFunc{f: func(ctx context.Context, c *Case, _ int) (Outcome, error) {
c.Progress.Set("test the unit level of mesh-delivery", "shanks")
<-ctx.Done()
return Outcome{}, ctx.Err()
}}
r := newRig(t, j)
r.ch.Bound = 30 * time.Millisecond
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
v := r.verdict(t, keyOf(t, "feat/x", cand))
if v.State != delivery.VerdictFail || v.Failed.Machine != "shanks" || !strings.Contains(v.Failed.Words, "stuck at: test the unit level of mesh-delivery") {
t.Fatalf("verdict = %+v, want a failure naming shanks and the stuck step", v)
}
}
func TestNoQualifyingNodeFailsWithTheReasonAndIsJudgedAgainWhenTheNodesChange(t *testing.T) { // N6/AC4
ctx := context.Background()
j := &judgeFunc{f: func(_ context.Context, _ *Case, n int) (Outcome, error) {
if n == 1 {
return Outcome{Verdict: failure("shanks", "no go "+NodesWords+": none is online")}, nil
}
return Outcome{Verdict: passing()}, nil
}}
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
key := keyOf(t, "feat/x", cand)
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
if v := r.verdict(t, key); !IsNodesFailure(v) {
t.Fatalf("verdict = %+v, want a failure of the nodes", v)
}
r.ch.Reconsider(ctx) // nothing changed: not judged again
r.ch.Wait()
if j.count() != 1 {
t.Fatalf("judged %d times with the nodes unchanged, want once", j.count())
}
r.ch.NodesChanged(ctx)
r.ch.Wait()
if j.count() != 2 || r.verdict(t, key).State != delivery.VerdictPass {
t.Fatalf("after the nodes changed: judged %d times, verdict %+v", j.count(), r.verdict(t, key))
}
}
func TestHqAndARepositoryWithNoTrunkGetNoVerdict(t *testing.T) { // N6/AC6, ADR 0484 clauses 8 and 11
ctx, j := context.Background(), passJudge()
r := newRig(t, j)
r.ch.Candidate(ctx, "feat/hq", candidateOf(t, "feat/hq", pullOf(HQRepository, 3, head1)))
r.ch.Candidate(ctx, "feat/y", candidateOf(t, "feat/y", pullOf("novox/unknown", 4, head1)))
r.ch.Wait()
if j.count() != 0 || len(r.verdicts.vals) != 0 || len(r.owed.vals) != 0 {
t.Errorf("judged %d times, %d verdicts, %d statuses, want none", j.count(), len(r.verdicts.vals), len(r.owed.vals))
}
}
func TestAGroupWithHqJudgesItsOtherMembersAndWritesNoStatusForHq(t *testing.T) { // ADR 0484 clause 8
ctx, j := context.Background(), passJudge()
r := newRig(t, j)
r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(HQRepository, 3, head1), pullOf(repoA, 7, head2)))
r.ch.Wait()
if j.count() != 1 {
t.Fatalf("judged %d times, want once", j.count())
}
keys, _ := r.owed.Keys(ctx)
if len(keys) != 1 || !strings.HasPrefix(keys[0], "novox/mesh-delivery.7.") && !strings.Contains(keys[0], "mesh-delivery") {
t.Errorf("statuses %v, want one for mesh-delivery#7 only", keys)
}
}
func TestATrunkMoveRewritesTheSameVerdictKeyAndTheHeadsStatus(t *testing.T) { // 1300/AC7
ctx, j := context.Background(), passJudge()
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
key := keyOf(t, "feat/x", cand)
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
_, rev1, _, _ := r.verdicts.Get(ctx, key)
owedKey, _ := delivery.CheckerOwedKey(repoA, 7, head1)
_, orev1, _, _ := r.owed.Get(ctx, owedKey)
if !r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) {
t.Fatal("the trunks did not change")
}
r.ch.Reconsider(ctx)
r.ch.Wait()
_, rev2, _, _ := r.verdicts.Get(ctx, key)
if j.count() != 2 || rev2 <= rev1 || r.verdict(t, key).Trunk[repoA] != trunkA2 {
t.Fatalf("judged %d times, revisions %d then %d, trunk %v", j.count(), rev1, rev2, r.verdict(t, key).Trunk)
}
if r.verdicts.creates[key] != 1 {
t.Errorf("the key was created %d times, want the same key rewritten", r.verdicts.creates[key])
}
if _, orev2, _, _ := r.owed.Get(ctx, owedKey); orev2 <= orev1 {
// the line is the same when nothing differs; the status is rewritten when the words change, here the trunk is in them
t.Logf("the status keeps revision %d (its words did not change)", orev2)
}
// A new head is another key, and the older head's status is not touched.
older, _, _, _ := r.owed.Get(ctx, owedKey)
cand2 := candidateOf(t, "feat/x", pullOf(repoA, 7, head2))
r.ch.Candidate(ctx, "feat%2Fx", cand2)
r.ch.Wait()
if again, _, _, _ := r.owed.Get(ctx, owedKey); string(again) != string(older) {
t.Errorf("the older head's status was rewritten")
}
if n := len(r.owed.vals); n != 2 {
t.Errorf("%d statuses, want one for each head", n)
}
}
func TestNoCheckStartsWhileADeliveryBuildsAndOneStartsWhenItEnds(t *testing.T) { // N8/AC1
ctx, j := context.Background(), passJudge()
r := newRig(t, j)
held := true
r.ch.Hold = func() bool { return held }
r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1)))
r.ch.Wait()
if j.count() != 0 {
t.Fatalf("judged %d times while held", j.count())
}
held = false
r.ch.Reconsider(ctx)
r.ch.Wait()
if j.count() != 1 {
t.Fatalf("judged %d times after the hold ended, want once", j.count())
}
}
func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T) { // 1300/AC5 (unit part)
ctx := context.Background()
var seen []string
var r *rig
j := &judgeFunc{f: func(ctx context.Context, c *Case, _ int) (Outcome, error) {
key, _ := delivery.CheckerOwedKey(repoA, 7, head1)
raw, _, _, _ := r.owed.Get(ctx, key)
o, _ := delivery.DecodeCheckerOwed(raw)
seen = append(seen, o.State)
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "m/a.TestX"}}}, nil
}}
r = newRig(t, j)
r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1)))
r.ch.Wait()
key, _ := delivery.CheckerOwedKey(repoA, 7, head1)
raw, _, _, _ := r.owed.Get(ctx, key)
o, err := delivery.DecodeCheckerOwed(raw)
if err != nil {
t.Fatal(err)
}
if len(seen) != 1 || seen[0] != "pending" || o.State != "failure" || o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Target, "https://view.invalid/checks/") {
t.Errorf("while running %v, at the end %+v", seen, o)
}
}
func TestSweepClosesARunningKeyNoCandidateHoldsAndLeavesOneItDoes(t *testing.T) {
ctx, j := context.Background(), passJudge()
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
live := keyOf(t, "feat/x", cand)
dead, _ := delivery.CheckerVerdictKey("feat/gone", "0123456789ab")
for _, k := range []string{live, dead} {
group, heads, _ := delivery.ParseCheckerVerdictKey(k)
raw, _ := delivery.EncodeVerdict(delivery.Verdict{Group: group, Heads: heads, State: delivery.VerdictRunning})
_, _ = r.verdicts.Create(ctx, k, raw)
}
r.ch.init()
var c delivery.Candidate
_ = json.Unmarshal(cand, &c)
r.ch.known["feat%2Fx"] = c
if err := r.ch.Sweep(ctx); err != nil {
t.Fatal(err)
}
if v := r.verdict(t, dead); v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "closed at start") {
t.Errorf("the dead key = %+v", v)
}
if v := r.verdict(t, live); v.State != delivery.VerdictRunning {
t.Errorf("the live key = %+v, want it left for the check to take over", v)
}
}