package check import ( "context" "encoding/json" "fmt" "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 } // writtenTries is a memKV that keeps every running try it was given to write, in order. type writtenTries struct { *memKV running []Try } func (w *writtenTries) Update(ctx context.Context, key string, value []byte, rev uint64) (uint64, error) { if t, err := DecodeTry(value); err == nil && !t.Over() { w.running = append(w.running, t) } return w.memKV.Update(ctx, key, value, rev) } var tryTrunks = map[string]string{repoA: trunkA, repoSDK: trunkS, hqRepo: 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) } // Issue 1890: tries on a build node (a step of theirs running, or none named yet) and tries waiting for one (queued, or // their asks not taken) are bounded apart, MaxTries and MaxTriesWaiting; the refusal names both counts. onNode := func(i int) Try { return Try{ID: fmt.Sprintf("try-%d", i), Repository: repoA, Head: fmt.Sprintf("%040x", i), Trunk: trunkA, State: TryRunning, Steps: []TryStep{{Ask: "a", State: delivery.StepRunning}}} } waitingFor := func(i int) Try { return Try{ID: fmt.Sprintf("try-%d", i), Repository: repoA, Head: fmt.Sprintf("%040x", i), Trunk: trunkA, State: TryRunning, Steps: []TryStep{{Ask: "a", State: TryAsked}}} } for _, c := range []struct { what string holding, wait int refused string }{ {"four queued behind the checks, none on a node: a fifth is taken", 0, 4, ""}, {"four on a node", MaxTries, 0, "now 4 run and 0 wait"}, {"three on a node, eight waiting", 3, MaxTriesWaiting, "a new try is refused while 4 tries run on a build node or 8 wait for one; now 3 run and 8 wait"}, } { kv := newMemKV() for i := range c.holding + c.wait { tr := waitingFor(i + 1) if i < c.holding { tr = onNode(i + 1) } data, _ := encodeTry(tr) _, _ = kv.Create(context.Background(), tr.ID, data) } _, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoSDK, Head: head1}, at.Add(time.Minute)) if (c.refused == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.refused)) { t.Errorf("%s: answered %v, want refused %q", c.what, err, c.refused) } } } func TestATryOfARepositoryWithNoCheckedLevelIsRefused(t *testing.T) { // the agreed error of the try store := memTries{newMemKV()} for _, repo := range []string{"novox/no-trunk", "novox/mesh-lab"} { // hq is checked since R4g (ADR 0527): see tryTrunks _, 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) written := &writtenTries{memKV: kv} tr := &Tryer{Tries: written, 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) } // Issue 1890: while it runs, the try names its ask as soon as it is put, and then where its step stands; the end // keeps each ask's node and state. if len(written.running) < 3 || len(written.running[1].Asks) != 1 || written.running[1].Steps[0].State != TryAsked || written.running[2].Steps[0].Node != "shanks" { t.Errorf("the running try's writes = %+v, want its ask named while it ran, then its node", written.running) } if len(got.Steps) != 1 || got.Steps[0].Node != "shanks" || got.Steps[0].State != delivery.StepPassed { t.Errorf("the ended try's steps = %+v", got.Steps) } // The try names the asks it put, so the delivery view counts their steps as a try's (build_usage). if len(got.Asks) != 1 { t.Errorf("asks = %v, want the one check ask the try put", got.Asks) } else if _, err := delivery.AskStepKey("shanks", got.Asks[0]); err != nil { t.Errorf("the try names %q, which is no ask id: %v", got.Asks[0], err) } 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 is no error since R4g: the try's ask waits in the check stream until a node pulls it (ADR 0525 §4). } func TestATryLeftRunningIsTakenOver(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) tr := &Tryer{Tries: kv, Judge: b.ex} 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) } }