mesh-controller's try asked build-agent, which leaves the checking at R4e. A try is now a key in the checker's state tries: the checker judges the head merged onto its trunk as it judges a pull request's head, through the same judge, places and holds, writes its end in the key and owes the forge nothing (no status, no preview, no verdict). One try per head, at most four at once; hq and a repository whose trunk the gate does not name are refused with "no level of <repo> is checked by the checker".
197 lines
7.0 KiB
Go
197 lines
7.0 KiB
Go
package check
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// memTries is the tries state as the tools see it, over a memKV.
|
|
type memTries struct{ kv *memKV }
|
|
|
|
func (m memTries) Keys() ([]string, error) { return m.kv.Keys(context.Background()) }
|
|
func (m memTries) Get(key string) (json.RawMessage, error) {
|
|
v, _, _, err := m.kv.Get(context.Background(), key)
|
|
return v, err
|
|
}
|
|
func (m memTries) Create(key string, value []byte) error {
|
|
_, err := m.kv.Create(context.Background(), key, value)
|
|
return err
|
|
}
|
|
|
|
var tryTrunks = map[string]string{repoA: trunkA, repoSDK: trunkS, HQRepository: trunkA}
|
|
|
|
func readTry(t testing.TB, kv *memKV, id string) Try {
|
|
t.Helper()
|
|
raw, _, found, _ := kv.Get(context.Background(), id)
|
|
if !found {
|
|
t.Fatalf("no try %s", id)
|
|
}
|
|
tr, err := DecodeTry(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return tr
|
|
}
|
|
|
|
func TestATryIsAskedQueuedOncePerHeadAndAtMostFourAtOnce(t *testing.T) { // ADR 0501 §5
|
|
kv := newMemKV()
|
|
store := memTries{kv}
|
|
at := time.Date(2026, 10, 22, 18, 0, 0, 0, time.UTC)
|
|
id, err := AskTry(store, tryTrunks, TryAsk{Repository: repoA, Head: head1, Trunk: "main"}, at)
|
|
if err != nil || !strings.HasPrefix(id, "try-") {
|
|
t.Fatalf("id %q, %v", id, err)
|
|
}
|
|
tr := readTry(t, kv, id)
|
|
if tr.State != TryQueued || tr.Trunk != trunkA || tr.Branch != "main" || tr.Head != head1 || tr.Repository != repoA {
|
|
t.Errorf("try = %+v, want queued on the trunk's head", tr)
|
|
}
|
|
again, err := AskTry(store, tryTrunks, TryAsk{Repository: repoA, Head: head1, Trunk: "main"}, at.Add(time.Second))
|
|
if err != nil || again != id {
|
|
t.Errorf("a second ask of the same head answered %q, %v, want %q", again, err, id)
|
|
}
|
|
heads := []string{head2, trunkA2, trunkS2}
|
|
for i, h := range heads {
|
|
if _, err := AskTry(store, tryTrunks, TryAsk{Repository: repoA, Head: h, Trunk: trunkA}, at.Add(time.Duration(i+2)*time.Second)); err != nil {
|
|
t.Fatalf("try %d: %v", i+2, err)
|
|
}
|
|
}
|
|
if _, err := AskTry(store, tryTrunks, TryAsk{Repository: repoSDK, Head: head1}, at.Add(time.Minute)); err == nil || !strings.Contains(err.Error(), "4 tries wait or run") {
|
|
t.Errorf("a fifth try answered %v, want refused naming the four", err)
|
|
}
|
|
}
|
|
|
|
func TestATryOfARepositoryWithNoCheckedLevelIsRefused(t *testing.T) { // the agreed error of the try
|
|
store := memTries{newMemKV()}
|
|
for _, repo := range []string{HQRepository, "novox/mesh-lab"} {
|
|
_, err := AskTry(store, tryTrunks, TryAsk{Repository: repo, Head: head1, Trunk: "main"}, time.Now())
|
|
if err == nil || err.Error() != "no level of "+repo+" is checked by the checker" {
|
|
t.Errorf("%s: %v", repo, err)
|
|
}
|
|
}
|
|
for _, bad := range []TryAsk{{Repository: repoA, Head: "abc"}, {Repository: repoA, Head: head1, Level: "e2e"},
|
|
{Repository: repoA, Head: head1, Tests: "a b"}, {Repository: "x", Head: head1}, {Repository: repoA, Head: head1, Trunk: "-x"}} {
|
|
if _, err := AskTry(store, tryTrunks, bad, time.Now()); err == nil {
|
|
t.Errorf("%+v was taken", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { // ADR 0501 §5
|
|
ctx := context.Background()
|
|
kv := newMemKV()
|
|
id, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1, Trunk: trunkA}, time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := newBench(t)
|
|
tr := &Tryer{Tries: kv, Judge: b.ex}
|
|
raw, _, _, _ := kv.Get(ctx, id)
|
|
tr.Put(ctx, id, raw)
|
|
tr.Wait()
|
|
got := readTry(t, kv, id)
|
|
if got.State != TryPass || got.Took == "" || got.Head != head1 || got.Trunk != trunkA || got.Said == "" || got.Levels[delivery.LevelUnit].Run != 2 {
|
|
t.Fatalf("try = %+v", got)
|
|
}
|
|
fetch := b.steps.tool("fetch")
|
|
if len(fetch) != 1 || fetch[0].args["commit"] != head1 || fetch[0].args["base"] != trunkA || fetch[0].args["ref"] != "" {
|
|
t.Errorf("fetch = %+v, want the head merged onto the trunk, with no pull request's ref", fetch)
|
|
}
|
|
if keys, _ := b.prev.Keys(ctx); len(keys) != 0 {
|
|
t.Errorf("a try wrote the previews %v", keys)
|
|
}
|
|
// An ended try is not run again.
|
|
raw, _, _, _ = kv.Get(ctx, id)
|
|
tr.Put(ctx, id, raw)
|
|
tr.Wait()
|
|
if n := len(b.steps.tool("fetch")); n != 1 {
|
|
t.Errorf("an ended try was fetched again: %d fetches", n)
|
|
}
|
|
}
|
|
|
|
func TestATryOfOneLevelAndSomeTestsRunsThemAloneAndKeepsNothing(t *testing.T) {
|
|
ctx := context.Background()
|
|
kv := newMemKV()
|
|
id, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1, Level: delivery.LevelIntegration, Tests: "TestBus|TestStore"}, time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := newBench(t)
|
|
tr := &Tryer{Tries: kv, Judge: b.ex}
|
|
raw, _, _, _ := kv.Get(ctx, id)
|
|
tr.Put(ctx, id, raw)
|
|
tr.Wait()
|
|
tests := b.steps.tool("test")
|
|
if len(tests) != 1 || tests[0].args["level"] != delivery.LevelIntegration || tests[0].args["run"] != "TestBus|TestStore" {
|
|
t.Fatalf("tests = %+v, want the integration level alone with the run pattern", tests)
|
|
}
|
|
if n := len(b.steps.tool("digest")); n != 0 {
|
|
t.Errorf("%d digests for a try of some tests", n)
|
|
}
|
|
if keys, _ := b.kept.Keys(ctx); len(keys) != 0 {
|
|
t.Errorf("kept %v from a try of some tests", keys)
|
|
}
|
|
if got := readTry(t, kv, id); got.State != TryPass || !strings.Contains(got.Said, "only the tests TestBus|TestStore") {
|
|
t.Errorf("try = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestAFailingTryNamesWhatFailedAndAStepThatCannotRunIsAnError(t *testing.T) {
|
|
ctx := context.Background()
|
|
kv := newMemKV()
|
|
b := newBench(t)
|
|
b.test = func(c stepCall) (any, error) { return captured[Tested](t, "go-test-failing"), nil }
|
|
id, _ := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1}, time.Now())
|
|
tr := &Tryer{Tries: kv, Judge: b.ex}
|
|
raw, _, _, _ := kv.Get(ctx, id)
|
|
tr.Put(ctx, id, raw)
|
|
tr.Wait()
|
|
if got := readTry(t, kv, id); got.State != TryFail || len(got.Failed) != 1 || !strings.Contains(got.Failed[0], "TestThreeIsFour") {
|
|
t.Errorf("try = %+v", got)
|
|
}
|
|
// No build node: the try could not run, which is an error, not a failure of the change.
|
|
kv2 := newMemKV()
|
|
b2 := newBench(t)
|
|
b2.ex.Toolchains = entries{}
|
|
id2, _ := AskTry(memTries{kv2}, tryTrunks, TryAsk{Repository: repoA, Head: head1}, time.Now())
|
|
tr2 := &Tryer{Tries: kv2, Judge: b2.ex}
|
|
raw, _, _, _ = kv2.Get(ctx, id2)
|
|
tr2.Put(ctx, id2, raw)
|
|
tr2.Wait()
|
|
if got := readTry(t, kv2, id2); got.State != TryError {
|
|
t.Errorf("try with no node = %+v, want error", got)
|
|
}
|
|
}
|
|
|
|
func TestATryWaitsWhileADeliveryBuildsAndATryLeftRunningIsTakenOver(t *testing.T) {
|
|
ctx := context.Background()
|
|
kv := newMemKV()
|
|
id, _ := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1}, time.Now())
|
|
// Left running by a checker that stopped.
|
|
raw, rev, _, _ := kv.Get(ctx, id)
|
|
cur, _ := DecodeTry(raw)
|
|
cur.State = TryRunning
|
|
data, _ := encodeTry(cur)
|
|
if _, err := kv.Update(ctx, id, data, rev); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := newBench(t)
|
|
held := true
|
|
tr := &Tryer{Tries: kv, Judge: b.ex, Hold: func() bool { return held }}
|
|
tr.Reconsider(ctx)
|
|
tr.Wait()
|
|
if n := len(b.steps.tool("fetch")); n != 0 {
|
|
t.Fatalf("a try ran while a delivery builds")
|
|
}
|
|
held = false
|
|
tr.Reconsider(ctx)
|
|
tr.Wait()
|
|
if got := readTry(t, kv, id); got.State != TryPass {
|
|
t.Errorf("the try left running = %+v, want taken over and passed", got)
|
|
}
|
|
}
|