368 lines
13 KiB
Go
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)
|
|
}
|
|
}
|