The shared asker (buildrun.Asker.Wait) answers an ended step whole: a key naming its answer as output-object has it read through the build seat's verb output on the step's node (Step.Whole) with the asker's Outputs, so the checker's judge, manifests judge, checking program and compose sandbox read the answer as before; a step whose object cannot be read is the wait's error. The checker reads its run asks' failed key's answer for a program that exited other than 0. The checker, the indexer and the assigner read a kept answer through process.Conn.CallFile (A-291), which receives the verb's chunks; the checker's manifest invokes the three build seats' output. Builds: design 47 G A-290, A-291, A-254 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
1611 lines
78 KiB
Go
1611 lines
78 KiB
Go
package check
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"mesh-delivery-checker/rules"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"testing/fstest"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliverycheckerclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdeliverychecker"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/composer"
|
|
gobuild "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/steprun"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// captured reads a result the build module's own code marshalled (testdata/captured/README).
|
|
func captured[T any](t testing.TB, name string) T {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join("testdata", "captured", name+".json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var v T
|
|
if err := json.Unmarshal(raw, &v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
type stepCall struct {
|
|
module, node, tool string
|
|
args map[string]any
|
|
}
|
|
|
|
// fakeSteps stands for the build modules: a handler for each tool answers what the module's code answers.
|
|
type fakeSteps struct {
|
|
mu sync.Mutex
|
|
calls []stepCall
|
|
handlers map[string]func(c stepCall) (any, error)
|
|
}
|
|
|
|
func (f *fakeSteps) Run(_ context.Context, module, node, tool string, args map[string]any) (json.RawMessage, error) {
|
|
c := stepCall{module, node, tool, args}
|
|
f.mu.Lock()
|
|
f.calls = append(f.calls, c)
|
|
h := f.handlers[tool]
|
|
f.mu.Unlock()
|
|
if h == nil {
|
|
return nil, fmt.Errorf("the fake build module has no tool %s", tool)
|
|
}
|
|
v, err := h(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
func (f *fakeSteps) tool(name string) []stepCall {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
var out []stepCall
|
|
for _, c := range f.calls {
|
|
if strings.HasSuffix(c.tool, "_"+name) {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type entries []buildstep.NodeToolchain
|
|
|
|
func (e entries) Entries(language string) []buildstep.NodeToolchain {
|
|
var out []buildstep.NodeToolchain
|
|
for _, x := range e {
|
|
if x.Language == language {
|
|
out = append(out, x)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type langs map[string][]string
|
|
|
|
func (l langs) BuildModules(repository string) []string {
|
|
if m, ok := l[repository]; ok {
|
|
return m
|
|
}
|
|
return []string{goBuildModule}
|
|
}
|
|
|
|
func goEntries(nodes ...string) entries {
|
|
var e entries
|
|
for _, n := range nodes {
|
|
e = append(e, buildstep.NodeToolchain{Node: n, Toolchain: buildstep.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "7"}})
|
|
e = append(e, buildstep.NodeToolchain{Node: n, Toolchain: buildstep.Toolchain{Language: "typescript", Version: "node24", Recipe: "3"}})
|
|
}
|
|
return e
|
|
}
|
|
|
|
const digC = "cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33cc33"
|
|
const digD = "dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44dd44"
|
|
|
|
type bench struct {
|
|
ex *Examiner
|
|
asks *buildrun.FakeAsks
|
|
steps *fakeSteps
|
|
kept *memKV
|
|
prev *memKV
|
|
fetch Fetched
|
|
digest map[string]Digested // by level
|
|
test func(c stepCall) (any, error)
|
|
// declines is how many asks naming needs a node lacking them declines before shanks, which has every need, takes
|
|
// one (issue 890); an ask naming none runs on shanks, which then has none.
|
|
declines int
|
|
// inputs are the checking program's kept inputs; heads the heads laid for it (none when nil); run, when set, answers
|
|
// a run ask in its place (a program that does not build or answer).
|
|
inputs *memKV
|
|
heads fs.FS
|
|
run func(a buildstep.StepAsk) (buildstep.Step, bool)
|
|
mu sync.Mutex
|
|
runs []buildstep.StepAsk // the run asks put
|
|
}
|
|
|
|
// benchAsks puts the bench's asks: a run ask is answered at once by the checking program's work (answerRun), written by
|
|
// write and kept apart (bench.runs); every other ask goes to inner (the fake bus by default), answered as it answers.
|
|
type benchAsks struct {
|
|
b *bench
|
|
inner buildrun.AskPublisher
|
|
write func(buildstep.Step)
|
|
}
|
|
|
|
func (b *bench) asker(inner buildrun.AskPublisher, write func(buildstep.Step)) benchAsks {
|
|
return benchAsks{b: b, inner: inner, write: write}
|
|
}
|
|
|
|
func (p benchAsks) PublishAsk(ctx context.Context, subject, id string, data []byte) error {
|
|
inner, write := p.inner, p.write
|
|
if inner == nil {
|
|
inner, write = p.b.asks, p.b.asks.Write
|
|
}
|
|
a, err := buildstep.DecodeStepAsk(data)
|
|
if err != nil || a.Run == nil {
|
|
return inner.PublishAsk(ctx, subject, id, data)
|
|
}
|
|
p.b.mu.Lock()
|
|
p.b.runs = append(p.b.runs, a)
|
|
p.b.mu.Unlock()
|
|
if step := p.b.answerRun(a); step.ID != "" {
|
|
write(step)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// trunkD is mesh-delivery's trunk the bench's checking program is built at.
|
|
var trunkD = strings.Repeat("d", 40)
|
|
|
|
// answerRun answers a run ask as the build module would: the checking program's work (rules.Program) over the input the
|
|
// ask names, read from the kept inputs and checked against its digest.
|
|
func (b *bench) answerRun(a buildstep.StepAsk) buildstep.Step {
|
|
began := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
|
|
step := buildstep.Step{ID: a.ID, Tool: a.Kind + " ask", Node: "shanks", State: buildstep.StepPassed, Started: began, Ended: began.Add(time.Second)}
|
|
if b.run != nil {
|
|
if s, ok := b.run(a); ok {
|
|
return s
|
|
}
|
|
}
|
|
answer := buildstep.RunAnswer{Built: true, Key: "program-0123456789abcdef"}
|
|
out, err := rules.Program(a.Run.Args, func() ([]byte, error) {
|
|
body, _, _, _ := b.inputs.Get(context.Background(), a.Run.Input.Digest)
|
|
return body, a.Run.Input.Verify(body)
|
|
}, b.heads)
|
|
if err != nil {
|
|
answer.Exit, answer.Said = 2, err.Error()
|
|
}
|
|
answer.Output = out
|
|
step.Result, _ = json.Marshal(answer)
|
|
return step
|
|
}
|
|
|
|
// refsOf reads the siblings of a fetch's arguments, as they arrive in an ask (JSON) or as the checker gave them.
|
|
func refsOf(v any) []BesideRef {
|
|
raw, _ := json.Marshal(v)
|
|
var refs []BesideRef
|
|
_ = json.Unmarshal(raw, &refs)
|
|
return refs
|
|
}
|
|
|
|
// toolPrefix is the start of each build module's tool names, as the fake build modules answer them.
|
|
var toolPrefix = map[string]string{goBuildModule: "build_go_", nodeBuildModule: "build_node_"}
|
|
|
|
// answer runs a check ask as the build module that pulls it does (gobuild.RunAsk), with the fake tools on shanks and the
|
|
// bench's kept verdicts, and answers the step it writes.
|
|
func (b *bench) answer(a buildstep.StepAsk) buildstep.Step {
|
|
bm, _ := testSeats().OfAskLanguage(a.Language)
|
|
module := bm.Module
|
|
const node = "shanks"
|
|
tool := func(name string) gobuild.StepFunc {
|
|
return func(ctx context.Context, args map[string]any) (any, error) {
|
|
raw, err := b.steps.Run(ctx, module, node, toolPrefix[module]+name, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return raw, nil
|
|
}
|
|
}
|
|
steps := gobuild.AskSteps{Fetch: tool("fetch"), Test: tool("test"),
|
|
Kept: func(ctx context.Context, level, digest string) (int, bool, error) {
|
|
k, found, err := (Kept{Store: b.kept}).Lookup(ctx, level, digest)
|
|
return int(k.Tests), found, err
|
|
}}
|
|
if module == goBuildModule {
|
|
steps.Digest = tool("digest")
|
|
}
|
|
began := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
|
|
if len(a.Needs) > 0 {
|
|
if b.declines > 0 {
|
|
b.declines--
|
|
result, _ := json.Marshal(buildstep.StepDeclined{Declined: true, Needs: a.Needs, Lacks: a.Needs})
|
|
return buildstep.Step{ID: a.ID, Tool: a.Kind + " ask", Node: "ace", State: buildstep.StepRefused, Started: began, Ended: began,
|
|
Reason: "declined", Result: result}
|
|
}
|
|
steps.Has = func(string) bool { return true }
|
|
}
|
|
out, err := gobuild.RunAsk(context.Background(), steps, a)
|
|
step := buildstep.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: buildstep.StepPassed, Started: began, Ended: began.Add(2 * time.Second)}
|
|
if out != nil {
|
|
step.Result, _ = json.Marshal(out) // a failed step keeps what it answered beside its error, as the step runner does
|
|
}
|
|
if err != nil {
|
|
step.State, step.Reason = buildstep.StepFailed, err.Error()
|
|
}
|
|
return step
|
|
}
|
|
|
|
// newBench is an examiner over fake build modules that merge cleanly, digest the captured fixture's packages and pass
|
|
// the captured slow fixture's result at both levels, on shanks.
|
|
func newBench(t testing.TB) *bench {
|
|
b := &bench{steps: &fakeSteps{}, kept: newMemKV(), prev: newMemKV(), inputs: newMemKV(), heads: fstest.MapFS{}}
|
|
b.fetch = captured[Fetched](t, "go-fetch-clean")
|
|
b.fetch.Modules, b.fetch.Changed, b.fetch.Trees = []string{"."}, []string{"README.md"}, nil
|
|
one := captured[Digested](t, "go-digest")
|
|
b.digest = map[string]Digested{
|
|
buildstep.LevelUnit: {Level: "unit", Packages: map[string]string{"fixture/slow": digA}},
|
|
buildstep.LevelIntegration: {Level: "integration", Packages: map[string]string{"fixture/slow": digB}},
|
|
}
|
|
_ = one
|
|
slow := captured[Tested](t, "go-test-slow")
|
|
b.test = func(c stepCall) (any, error) {
|
|
r := slow
|
|
r.Level, _ = c.args["level"].(string)
|
|
return r, nil
|
|
}
|
|
b.steps.handlers = map[string]func(stepCall) (any, error){
|
|
"build_go_fetch": func(stepCall) (any, error) { return b.fetch, nil },
|
|
"build_node_fetch": func(stepCall) (any, error) { return b.fetch, nil },
|
|
"build_go_digest": func(c stepCall) (any, error) { return echoBeside(b, b.digest[c.args["level"].(string)]), nil },
|
|
"build_go_test": func(c stepCall) (any, error) { return b.test(c) },
|
|
"build_node_test": func(c stepCall) (any, error) { return b.test(c) },
|
|
}
|
|
now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
|
|
b.asks = &buildrun.FakeAsks{Answer: b.answer}
|
|
b.ex = &Examiner{Asks: buildrun.Asker{Seats: testSeats(), Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 5 * time.Second}, Toolchains: goEntries("shanks"), Kept: Kept{Store: b.kept}, Previews: b.prev,
|
|
Modules: langs{}, Seats: testSeats(), Trunks: &Trunks{heads: map[string]string{DeliveryRepository: trunkD}}, Inputs: memInputs{b.inputs}, Now: func() time.Time { now = now.Add(50 * time.Millisecond); return now }}
|
|
return b
|
|
}
|
|
|
|
func caseOf(t testing.TB, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) *Case {
|
|
t.Helper()
|
|
raw := candidateOf(t, "feat/x", pulls...)
|
|
c, err := meshcatalog.DecodeCandidatesByGroupValue(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cs := CaseOf(c, map[string]string{repoA: trunkA, repoSDK: trunkS, "novox/mesh-controller": trunkA, "novox/mesh-catalog": trunkA,
|
|
"novox/mesh-host": trunkA, "novox/mesh-networking": trunkA})
|
|
if cs == nil {
|
|
t.Fatal("no case")
|
|
}
|
|
return cs
|
|
}
|
|
|
|
func (b *bench) judge(t testing.TB, cs *Case) Outcome {
|
|
t.Helper()
|
|
out, err := b.ex.Judge(context.Background(), cs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func testedWith(t testing.TB, name string, edit func(*Tested)) Tested {
|
|
r := captured[Tested](t, name)
|
|
if edit != nil {
|
|
edit(&r)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func packagesOf(c stepCall) []string {
|
|
var out []string
|
|
if l, ok := c.args["packages"].([]string); ok {
|
|
out = l
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestACleanMergeIsFetchedAtItsTrunkAndBothLevelsPass(t *testing.T) { // 1679/AC1
|
|
b := newBench(t)
|
|
cs := caseOf(t, pullOf(repoA, 7, head1))
|
|
out := b.judge(t, cs)
|
|
v := out.Verdict
|
|
if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Run != 2 || v.Levels[buildstep.LevelIntegration].Run != 2 {
|
|
t.Fatalf("verdict = %+v", v)
|
|
}
|
|
if v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun {
|
|
t.Errorf("composition = %+v, want not run", v.Composition)
|
|
}
|
|
if len(v.Machines) != 1 || v.Machines[0] != "shanks" {
|
|
t.Errorf("machines = %v", v.Machines)
|
|
}
|
|
fetch := b.steps.tool("fetch")
|
|
if len(fetch) != 1 || fetch[0].args["repository"] != repoA || fetch[0].args["commit"] != head1 || fetch[0].args["base"] != trunkA || fetch[0].args["ref"] != "refs/pull/7/head" {
|
|
t.Fatalf("fetch calls = %+v", fetch)
|
|
}
|
|
if v.Took.Clone <= 0 || v.Took.Unit <= 0 {
|
|
t.Errorf("took = %+v, want the clone and the unit level timed", v.Took)
|
|
}
|
|
// What passed is kept under each level's digest.
|
|
for _, k := range []string{"unit." + digA, "integration." + digB} {
|
|
if _, _, found, _ := b.kept.Get(context.Background(), k); !found {
|
|
t.Errorf("%s was not kept", k)
|
|
}
|
|
}
|
|
|
|
// ADR 0567 §12: every check ask carries every node's name for the names check; a check whose names cannot be read, or
|
|
// that reads none, is not asked and fails saying so.
|
|
for _, c := range []struct {
|
|
names []string
|
|
err error
|
|
fails string
|
|
}{{[]string{"anchor", "laptop"}, nil, ""}, {nil, nil, "no node record"}, {nil, errors.New("no bucket"), "no bucket"}} {
|
|
b := newBench(t)
|
|
b.ex.Nodes = namesOf{c.names, c.err}
|
|
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
asks := b.asks.Asks()
|
|
if c.fails != "" {
|
|
if out.Verdict.State != meshdeliverycheckerclient.VerdictFail || len(asks) != 0 || !strings.Contains(out.Verdict.Failed.Words, c.fails) {
|
|
t.Errorf("names %v, %v: verdict %+v, %d asks", c.names, c.err, out.Verdict, len(asks))
|
|
}
|
|
} else if len(asks) == 0 || strings.Join(asks[0].Check.NodeNames, ",") != "anchor,laptop" {
|
|
t.Errorf("the check ask carries the names %v", asks)
|
|
}
|
|
}
|
|
|
|
// Issue 1921 (B0, D4 to D6): the check's verdict is the checking program's, built at the group's mesh-delivery head,
|
|
// else at mesh-delivery's trunk, over the facts kept as its input; a program that is never begun, does not build or
|
|
// does not answer fails the check naming it, and a rule the change's program alters judges the check.
|
|
verdictOf := func(state, words string) json.RawMessage {
|
|
out, _ := json.Marshal(rules.Answer{V: rules.AnswerVersion, Verdict: meshdeliverychecker.VerdictsByGroupHeadsValue{State: state, Failed: &FailedFirst{Words: words}}})
|
|
return out
|
|
}
|
|
for _, row := range []struct {
|
|
name string
|
|
repo string // the member's repository: mesh-delivery unless named
|
|
run func(a buildstep.StepAsk, s buildstep.Step) (buildstep.Step, bool)
|
|
at string // the commit the program is built at; empty when it fails
|
|
fails string
|
|
runs int // the run asks put when it fails: each put twice when never begun, and none after the seat list's
|
|
}{
|
|
{name: "at the group's mesh-delivery head", at: head1},
|
|
{name: "at mesh-delivery's trunk", repo: "novox/mesh-catalog", at: trunkD},
|
|
{name: "never begun", run: func(buildstep.StepAsk, buildstep.Step) (buildstep.Step, bool) { return buildstep.Step{}, true },
|
|
fails: "no build node began the job run of the asks", runs: 2},
|
|
{name: "does not build", run: func(_ buildstep.StepAsk, s buildstep.Step) (buildstep.Step, bool) {
|
|
s.State, s.Reason = buildstep.StepFailed, "the program does not build: undefined: rules.Nothing"
|
|
return s, true
|
|
}, fails: "by the checking program: its run ask", runs: 1},
|
|
// Design 47 G A-290: a run whose program exited other than 0 ends its key failed, its answer kept; the checker reads it.
|
|
{name: "a program that exits other than 0", run: func(_ buildstep.StepAsk, s buildstep.Step) (buildstep.Step, bool) {
|
|
s.State, s.Reason = buildstep.StepFailed, "the program exited 2: bad flag"
|
|
s.Result, _ = json.Marshal(buildstep.RunAnswer{Key: "program-0123456789abcdef", Built: true, Exit: 2, Said: "bad flag"})
|
|
return s, true
|
|
}, fails: "it exited 2 on shanks: bad flag", runs: 1},
|
|
{name: "a rule the change alters", at: head1, run: func(a buildstep.StepAsk, s buildstep.Step) (buildstep.Step, bool) {
|
|
if a.Run.Args[0] != "-verdict" {
|
|
return s, false
|
|
}
|
|
s.Result, _ = json.Marshal(buildstep.RunAnswer{Key: "program-0123456789abcdef", Output: verdictOf(meshdeliverycheckerclient.VerdictFail, "the change's own rule refuses it")})
|
|
return s, true
|
|
}, fails: "the change's own rule refuses it"},
|
|
} {
|
|
b := newBench(t)
|
|
b.ex.BeginBound = 20 * time.Millisecond
|
|
if row.run != nil {
|
|
b.run = func(a buildstep.StepAsk) (buildstep.Step, bool) {
|
|
return row.run(a, buildstep.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: buildstep.StepPassed})
|
|
}
|
|
}
|
|
repo := cmp.Or(row.repo, repoA)
|
|
v := b.judge(t, caseOf(t, pullOf(repo, 7, head1))).Verdict
|
|
if row.fails != "" {
|
|
if v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) {
|
|
t.Errorf("%s: verdict %+v, want a fail saying %q", row.name, v, row.fails)
|
|
}
|
|
if row.at == "" && (len(b.runs) != row.runs || v.JudgedBy != nil || slices.ContainsFunc(b.runs, func(a buildstep.StepAsk) bool { return a.Run.Args[0] == "-verdict" })) {
|
|
t.Errorf("%s: %d run asks, judged by %+v: want %d and none judging", row.name, len(b.runs), v.JudgedBy, row.runs)
|
|
}
|
|
} else if v.State != meshdeliverycheckerclient.VerdictPass {
|
|
t.Errorf("%s: verdict %+v", row.name, v)
|
|
}
|
|
if row.at == "" {
|
|
continue
|
|
}
|
|
if by := v.JudgedBy; by == nil || by.Program.Commit != row.at || by.Program.Package != CheckingProgramPackage || by.Key == "" ||
|
|
len(by.Heads) != 1 || by.Heads[0].Repository != repo || by.Heads[0].Commit != head1 {
|
|
t.Errorf("%s: judged by %+v, want the program at %s over the member's head", row.name, v.JudgedBy, row.at)
|
|
}
|
|
if len(b.runs) != 2 || b.runs[0].Run.Args[0] != "-seats" || b.runs[1].Run.Args[0] != "-verdict" {
|
|
t.Fatalf("%s: run asks %+v, want the seat list and then the verdict", row.name, b.runs)
|
|
}
|
|
in := b.runs[1].Run.Input // the facts kept as their exact bytes under their digest
|
|
body, _, found, _ := b.inputs.Get(context.Background(), in.Digest)
|
|
if !found || in.Verify(body) != nil || in.Verb != InputVerb ||
|
|
!strings.Contains(string(body), `"group":"feat/x"`) {
|
|
t.Errorf("%s: the verdict's input %+v is not kept whole: %s", row.name, in, body)
|
|
}
|
|
// Review round 1 of milestone 47/B0: a level is carried as the rules read it, without its digest, its packages or
|
|
// its kept packages' names, so a group's facts stay within the input's bound.
|
|
for _, unread := range []string{`"digest":{`, `"packages":[`, `"unmet"`, `"package":"m/`} {
|
|
if strings.Contains(string(body), unread) {
|
|
t.Errorf("%s: the facts carry %s, which no rule reads: %s", row.name, unread, body)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// namesOf is the nodes' names as a test gives them.
|
|
type namesOf struct {
|
|
names []string
|
|
err error
|
|
}
|
|
|
|
func (n namesOf) Names(context.Context) ([]string, error) { return n.names, n.err }
|
|
|
|
func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
|
|
b := newBench(t)
|
|
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/slow": digA, "fixture/other": digC}}
|
|
if _, err := (Kept{Store: b.kept}).Keep(context.Background(), "unit", digC, keptValue("fixture/other", 40)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
tests := b.steps.tool("test")
|
|
if len(tests) != 2 || strings.Join(packagesOf(tests[0]), ",") != "fixture/slow" || tests[0].args["level"] != "unit" {
|
|
t.Fatalf("test calls = %+v, want the unit level of fixture/slow alone, then the integration level", tests)
|
|
}
|
|
if u := out.Verdict.Levels[buildstep.LevelUnit]; u.Run != 2 || u.Kept != 40 {
|
|
t.Errorf("unit = %+v, want 2 run and 40 kept", u)
|
|
}
|
|
|
|
// Issue 890/AC5: a package needing node-build-node is left unmet where the node lacks it; the check keeps the passes
|
|
// and asks the integration level again for a node with it, puts it again when a node declines it, and that node tests
|
|
// the package alone, its kept passes not counted twice.
|
|
// fixture/outside has no kept verdict after its pass (it reads outside itself: its digest moves every call), so a
|
|
// placed ask that ran more than its unmet packages would run and count it again.
|
|
b = newBench(t)
|
|
b.declines = 1
|
|
outside := 0
|
|
b.steps.handlers["build_go_digest"] = func(c stepCall) (any, error) {
|
|
d := echoBeside(b, b.digest[c.args["level"].(string)])
|
|
if d.Level == "integration" {
|
|
outside++
|
|
d.Packages = map[string]string{"fixture/slow": digB, "fixture/node": digC, "fixture/outside": fmt.Sprintf("%064x", outside)}
|
|
}
|
|
return d, nil
|
|
}
|
|
b.digest[buildstep.LevelIntegration] = Digested{Level: "integration",
|
|
Needs: []buildstep.PackageNeeds{{Package: "fixture/node", Needs: []string{buildstep.NeedNodeBuildNode}}}}
|
|
out = b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
asks := b.asks.Asks()
|
|
if out.Verdict.State != meshdeliverycheckerclient.VerdictPass || len(asks) != 3 || len(asks[0].Needs) != 0 {
|
|
t.Fatalf("verdict %+v, asks %+v: want a pass of three asks, the first naming no need", out.Verdict, asks)
|
|
}
|
|
for _, a := range asks { // ADR 0584: every check ask names the agreed toolchain, so a node off it declines
|
|
if a.Check.Toolchain == nil || a.Check.Toolchain.Version != "go1.27.1" || a.Check.Toolchain.Recipe != "7" {
|
|
t.Errorf("a check ask names toolchain %+v, want go1.27.1 recipe 7", a.Check.Toolchain)
|
|
}
|
|
}
|
|
for _, a := range asks[1:] {
|
|
if strings.Join(a.Needs, ",") != buildstep.NeedNodeBuildNode || strings.Join(a.Check.Levels, ",") != buildstep.LevelIntegration || strings.Join(a.Check.Packages, ",") != "fixture/node" {
|
|
t.Errorf("a placed ask %+v, want the integration level needing node-build-node", a)
|
|
}
|
|
}
|
|
tests = b.steps.tool("test")
|
|
if last := tests[len(tests)-1]; len(tests) != 3 || strings.Join(packagesOf(last), ",") != "fixture/node" || last.args["level"] != "integration" {
|
|
t.Errorf("test calls = %+v, want the unit level, the integration level of fixture/slow and fixture/outside, then fixture/node alone", tests)
|
|
}
|
|
slow := captured[Tested](t, "go-test-slow")
|
|
if i := out.Verdict.Levels[buildstep.LevelIntegration]; int(i.Run) != 2*slow.Tests || i.Kept != 0 {
|
|
t.Errorf("integration = %+v, want %d run and nothing counted kept", i, 2*slow.Tests)
|
|
}
|
|
// And where no node has the need, the check fails once the declines outlast the bound, naming the need.
|
|
b.kept, b.declines = newMemKV(), 1000
|
|
b.ex.Kept, b.ex.DeclineBound = Kept{Store: b.kept}, 200*time.Millisecond
|
|
if v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict; v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(failedOf(v).Words, "needs a node with node-build-node") {
|
|
t.Errorf("with no node having the need: %+v", v)
|
|
}
|
|
}
|
|
|
|
func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3
|
|
b := newBench(t)
|
|
failing := captured[Tested](t, "go-test-failing")
|
|
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digB}}
|
|
b.test = func(c stepCall) (any, error) {
|
|
if c.args["level"] == "unit" {
|
|
return failing, nil
|
|
}
|
|
return captured[Tested](t, "go-test-slow"), nil
|
|
}
|
|
cs := caseOf(t, pullOf(repoA, 7, head1))
|
|
first := b.judge(t, cs).Verdict
|
|
if first.State != meshdeliverycheckerclient.VerdictFail || first.Failed.Test != "unit: fixture/failing/bad.TestThreeIsFour" {
|
|
t.Fatalf("first = %+v", first)
|
|
}
|
|
if _, _, found, _ := b.kept.Get(context.Background(), "unit."+digB); !found {
|
|
t.Fatal("the package that passed in the failing run was not kept")
|
|
}
|
|
if _, _, found, _ := b.kept.Get(context.Background(), "unit."+digA); found {
|
|
t.Fatal("the package that failed was kept")
|
|
}
|
|
// Asked again with nothing changed: only the failed package runs; the good one is taken.
|
|
b.steps.calls = nil
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
tests := b.steps.tool("test")
|
|
if len(tests) == 0 || strings.Join(packagesOf(tests[0]), ",") != "fixture/failing/bad" {
|
|
t.Fatalf("the re-ask ran %+v, want only fixture/failing/bad", tests)
|
|
}
|
|
// The trunk moved and one digest changed: that package runs, the unchanged one is taken.
|
|
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}}
|
|
b.steps.calls = nil
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
tests = b.steps.tool("test")
|
|
if got := strings.Join(packagesOf(tests[0]), ","); got != "fixture/failing/bad,fixture/failing/good" && got != "fixture/failing/good,fixture/failing/bad" {
|
|
t.Errorf("after a digest changed the run was %q, want the failed package and the changed one", got)
|
|
}
|
|
}
|
|
|
|
func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1292/AC1
|
|
b := newBench(t)
|
|
b.ex.Modules = langs{repoA: {nodeBuildModule}}
|
|
b.fetch = captured[Fetched](t, "node-fetch-clean")
|
|
b.test = func(c stepCall) (any, error) {
|
|
return Tested{Level: c.args["level"].(string), Module: "modules/m", Passed: true, Elapsed: 0.4, Bound: 5}, nil
|
|
}
|
|
cs := caseOf(t, pullOf(repoA, 7, head1))
|
|
for i := 0; i < 2; i++ {
|
|
v := b.judge(t, cs).Verdict
|
|
if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Kept != 0 || v.Levels[buildstep.LevelUnit].Run != 1 {
|
|
t.Fatalf("round %d: verdict = %+v", i, v)
|
|
}
|
|
}
|
|
if n := len(b.steps.tool("test")); n != 4 {
|
|
t.Errorf("%d node test calls in two checks, want 4 (both levels each time)", n)
|
|
}
|
|
if n := len(b.steps.tool("digest")); n != 0 {
|
|
t.Errorf("the node build module was asked for a digest %d times", n)
|
|
}
|
|
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 {
|
|
t.Errorf("kept %v for a node package", keys)
|
|
}
|
|
}
|
|
|
|
// The operator, 2026-11-02 ("A failed check shows only its first fault"): a check runs every member, every level and the
|
|
// composition to its end and its verdict names every failure in the order judged, Failed keeping the first whole; the
|
|
// change's own tests still run first (issue 1289), and the rest of the level after them. A member that only failed its
|
|
// tests still gets its preview, so the composition is read and its failure named after the tests'. A member whose head
|
|
// does not merge has no preview of a merge, so the assigner's composition would only wait: the check does not wait for it
|
|
// and says why it was not run (review of 650a1d0). A member whose step failed after its fetch merged is previewed from the
|
|
// fetch the failed step answered, and the composition is read (design 47 G A-286).
|
|
func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
|
|
for _, kind := range []string{"tests", "conflicts", "errors"} {
|
|
conflicts, errs := kind == "conflicts", kind == "errors"
|
|
b := newBench(t)
|
|
comps := &fakeCompositions{v: refusedOn("x", "g14", "route-proxy is defined twice"), waits: conflicts}
|
|
b.ex.Compositions = comps
|
|
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"}
|
|
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
|
|
conflict := captured[Fetched](t, "go-fetch-conflict")
|
|
b.steps.handlers["build_go_fetch"] = func(c stepCall) (any, error) {
|
|
if conflicts && c.args["repository"] == repoSDK {
|
|
return conflict, nil
|
|
}
|
|
return b.fetch, nil
|
|
}
|
|
failing := captured[Tested](t, "go-test-failing")
|
|
b.test = func(c stepCall) (any, error) {
|
|
if errs && c.args["level"] == "unit" {
|
|
return nil, errors.New("the seat definitions in . do not generate")
|
|
}
|
|
if got := packagesOf(c); c.args["level"] == "integration" || (len(got) == 1 && got[0] == "m/internal/b") {
|
|
r := failing
|
|
r.Level = c.args["level"].(string)
|
|
return r, nil
|
|
}
|
|
return captured[Tested](t, "go-test-slow"), nil
|
|
}
|
|
cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))
|
|
done := make(chan meshdeliverychecker.VerdictsByGroupHeadsValue, 1)
|
|
go func() { done <- b.judge(t, cs).Verdict }()
|
|
var v meshdeliverychecker.VerdictsByGroupHeadsValue
|
|
select {
|
|
case v = <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatalf("conflicts %v: the check waits on a composition that only waits", conflicts)
|
|
}
|
|
if errs {
|
|
keys, _ := b.prev.Keys(context.Background())
|
|
if len(keys) != 2 || len(comps.asked) == 0 || v.Composition == nil || v.Composition.State == meshdeliverycheckerclient.CompositionNotRun {
|
|
t.Errorf("a step failed after its fetch merged: previews %v, asked %v, composition %+v, want both previews and the composition read",
|
|
keys, comps.asked, v.Composition)
|
|
}
|
|
continue
|
|
}
|
|
var got []string
|
|
for _, f := range v.Failures {
|
|
got = append(got, firstFailed(meshdeliverychecker.VerdictsByGroupHeadsValue{Failed: &FailedFirst{Test: f.Test, Machine: f.Machine, Words: f.Words}}))
|
|
}
|
|
tests := []string{"unit: fixture/failing/bad.TestThreeIsFour", "integration: fixture/failing/bad.TestThreeIsFour"}
|
|
want := append(append(slices.Clone(tests), tests...), "g14: not composed on g14: route-proxy is defined twice") // both members fail their tests
|
|
if conflicts {
|
|
want = append([]string{"shanks: novox/mesh-sdk#12 does not merge into the trunk: files in conflict: a.go"}, tests...)
|
|
}
|
|
if v.State != meshdeliverycheckerclient.VerdictFail || len(got) != len(want) {
|
|
t.Fatalf("conflicts %v: verdict %+v: failures\n%s\nwant\n%s", conflicts, v, strings.Join(got, "\n"), strings.Join(want, "\n"))
|
|
}
|
|
for i := range want {
|
|
if !strings.HasPrefix(got[i], want[i]) {
|
|
t.Errorf("conflicts %v: failure %d is %q, want %q", conflicts, i, got[i], want[i])
|
|
}
|
|
}
|
|
keys, _ := b.prev.Keys(context.Background())
|
|
switch {
|
|
case !conflicts && (len(keys) != 2 || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionFail):
|
|
t.Errorf("tests alone failed: previews %v, composition %+v, want both previews and the composition read", keys, v.Composition)
|
|
case conflicts && (len(comps.asked) != 0 || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun ||
|
|
!strings.Contains(v.Composition.Words, "novox/mesh-sdk#12 (its head does not merge)")):
|
|
t.Errorf("a member did not merge: asked %v, composition %+v, want not run naming novox/mesh-sdk#12", comps.asked, v.Composition)
|
|
}
|
|
var unit []string
|
|
for _, c := range b.steps.tool("test") {
|
|
if c.args["level"] == "unit" && c.args["workspace"] != nil {
|
|
unit = append(unit, strings.Join(packagesOf(c), ","))
|
|
}
|
|
}
|
|
if conflicts && (strings.Join(unit, " | ") != "m/internal/b | m/internal/a" || len(b.steps.tool("digest")) != 2) {
|
|
t.Errorf("the unit level ran %v and %d digests, want the changed test's package, then the rest, at both levels", unit, len(b.steps.tool("digest")))
|
|
}
|
|
v.Group, v.Heads = "feat/x", "0123456789ab"
|
|
if _, err := meshdeliverycheckerclient.EncodeVerdict(v); err != nil {
|
|
t.Errorf("conflicts %v: the verdict cannot be written: %v", conflicts, err)
|
|
}
|
|
if line := Line(v, cs, cs.Pulls[0], nil, nil, nil); !strings.HasPrefix(line, fmt.Sprintf("failed (%d): %s", len(want), want[0][:20])) ||
|
|
!strings.Contains(line, fmt.Sprintf("; and %d more | ", len(want)-1)) || utf8.RuneCountInString(line) > MaxLine {
|
|
t.Errorf("conflicts %v: line %q", conflicts, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheChangesOwnPassingTestRunsFirstAndTheRestFollow(t *testing.T) { // 1289/AC3
|
|
b := newBench(t)
|
|
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go", "modules/m/internal/a/a.go"}
|
|
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
|
|
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
if out.Verdict.State != meshdeliverycheckerclient.VerdictPass {
|
|
t.Fatalf("verdict = %+v", out.Verdict)
|
|
}
|
|
var unit []string
|
|
for _, c := range b.steps.tool("test") {
|
|
if c.args["level"] == "unit" {
|
|
unit = append(unit, strings.Join(packagesOf(c), ","))
|
|
}
|
|
}
|
|
if strings.Join(unit, " | ") != "m/internal/b | m/internal/a" {
|
|
t.Errorf("the unit level ran %v, want the changed test's package first, then the rest", unit)
|
|
}
|
|
}
|
|
|
|
func TestAHeadThatDoesNotMergeEndsFailedNamingTheFilesAndRunsNoTest(t *testing.T) { // 1289/AC4
|
|
b := newBench(t)
|
|
b.fetch = captured[Fetched](t, "go-fetch-conflict")
|
|
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
f := out.Verdict.Failed
|
|
if out.Verdict.State != meshdeliverycheckerclient.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: a.go, sub/b.go") {
|
|
t.Fatalf("verdict = %+v", out.Verdict)
|
|
}
|
|
if n := len(b.steps.tool("digest")) + len(b.steps.tool("test")); n != 0 {
|
|
t.Errorf("%d digest and test steps ran after a conflict", n)
|
|
}
|
|
// 1921/D1c: the node that ran the step is a fact, so the verdict judged from the facts names its machine.
|
|
if m := out.Verdict.Machines; len(m) != 1 || m[0] != "shanks" {
|
|
t.Errorf("machines = %v, want the node that ran the step", m)
|
|
}
|
|
}
|
|
|
|
func TestALevelWhoseDigestIsNotNamedRunsWholeAndKeepsNothing(t *testing.T) { // 1287/AC4
|
|
b := newBench(t)
|
|
b.steps.handlers["build_go_digest"] = func(stepCall) (any, error) { return nil, fmt.Errorf("go list failed") }
|
|
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
if out.Verdict.State != meshdeliverycheckerclient.VerdictPass {
|
|
t.Fatalf("verdict = %+v", out.Verdict)
|
|
}
|
|
tests := b.steps.tool("test")
|
|
if len(tests) != 2 || tests[0].args["packages"] != nil {
|
|
t.Errorf("test calls = %+v, want each level once, whole", tests)
|
|
}
|
|
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 {
|
|
t.Errorf("kept %v with no digest", keys)
|
|
}
|
|
}
|
|
|
|
func TestALevelOverItsBoundIsNamedWithItsSlowestTestsAndKeepsItsVerdict(t *testing.T) { // 1300/AC3
|
|
b := newBench(t)
|
|
b.test = func(c stepCall) (any, error) {
|
|
return testedWith(t, "go-test-slow", func(r *Tested) {
|
|
r.Level = c.args["level"].(string)
|
|
r.OverBound = true
|
|
r.OverWhich = "run"
|
|
r.Elapsed = 7
|
|
}), nil
|
|
}
|
|
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
v := out.Verdict
|
|
if v.State != meshdeliverycheckerclient.VerdictPass || strings.Join(v.OverBound, ",") != "unit,integration" {
|
|
t.Fatalf("verdict = %+v", v)
|
|
}
|
|
if len(out.Slowest) == 0 || !strings.Contains(out.Slowest[0], "TestTakesAWhile") {
|
|
t.Errorf("slowest = %v", out.Slowest)
|
|
}
|
|
}
|
|
|
|
func TestSkippedTestsAreCountedPerLevel(t *testing.T) { // 1300/AC6, 1251
|
|
b := newBench(t)
|
|
b.test = func(c stepCall) (any, error) {
|
|
return testedWith(t, "go-test-slow", func(r *Tested) { r.Level = c.args["level"].(string); r.Skipped = 3 }), nil
|
|
}
|
|
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
|
if v.Levels[buildstep.LevelUnit].Skipped != 3 || v.Levels[buildstep.LevelIntegration].Skipped != 3 {
|
|
t.Errorf("levels = %+v", v.Levels)
|
|
}
|
|
}
|
|
|
|
func TestAGroupIsOneVerdictAndEachMembersTestsReadTheOthersAtTheirHeads(t *testing.T) { // 1624/AC1
|
|
b := newBench(t)
|
|
cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))
|
|
v := b.judge(t, cs).Verdict
|
|
if v.State != meshdeliverycheckerclient.VerdictPass {
|
|
t.Fatalf("verdict = %+v", v)
|
|
}
|
|
fetch := b.steps.tool("fetch")
|
|
if len(fetch) != 2 {
|
|
t.Fatalf("%d fetches, want one for each member", len(fetch))
|
|
}
|
|
var forA []BesideRef
|
|
for _, f := range fetch {
|
|
if f.args["repository"] == repoA {
|
|
forA, _ = refsOf(f.args["beside"]), true
|
|
}
|
|
}
|
|
found := false
|
|
for _, r := range forA {
|
|
if r.Repository == repoSDK && r.Commit == head2 {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("mesh-delivery's tests were not given the SDK member at its head: %+v", forA)
|
|
}
|
|
}
|
|
|
|
func TestAnExaminerIsAJudgeOfTheChecker(t *testing.T) {
|
|
var _ Judge = (*Examiner)(nil)
|
|
}
|
|
|
|
const (
|
|
manA = "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"
|
|
)
|
|
|
|
func TestACleanMergeWritesItsPreviewWithTheTreesManifestsAndLocks(t *testing.T) { // 1445/AC1
|
|
b := newBench(t)
|
|
b.fetch.Trees = map[string]buildstep.ModuleTree{
|
|
"modules/avahi": {Tree: "7777777777777777777777777777777777777777", Manifest: manA, Locks: map[string]string{"go.sum": "ff00"}},
|
|
"modules/bare": {Tree: "8888888888888888888888888888888888888888"}, // no manifest digest: left out
|
|
}
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA)
|
|
raw, _, found, _ := b.prev.Get(context.Background(), key)
|
|
if !found {
|
|
t.Fatalf("no preview %s; the bucket holds %v", key, b.prev.vals)
|
|
}
|
|
pv, err := meshdeliverycheckerclient.DecodePreview(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := pv.Modules["modules/avahi"]
|
|
if pv.Merge != b.fetch.Merge || pv.Trunk != trunkA || pv.Head != head1 || len(pv.Modules) != 1 || m.Tree != "7777777777777777777777777777777777777777" || m.Manifest != manA || m.Locks["go.sum"] != "ff00" {
|
|
t.Errorf("preview = %+v", pv)
|
|
}
|
|
}
|
|
|
|
func TestAHeadThatDoesNotMergeWritesAPreviewOfItsConflictsAlone(t *testing.T) { // 1445/AC5
|
|
b := newBench(t)
|
|
b.fetch = captured[Fetched](t, "go-fetch-conflict")
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA)
|
|
raw, _, found, _ := b.prev.Get(context.Background(), key)
|
|
if !found {
|
|
t.Fatalf("no preview for a head that does not merge; the bucket holds %v", b.prev.vals)
|
|
}
|
|
pv, err := meshdeliverycheckerclient.DecodePreview(raw)
|
|
if err != nil || strings.Join(pv.Conflicts, ",") != "a.go,sub/b.go" || pv.Merge != "" || len(pv.Modules) != 0 {
|
|
t.Errorf("preview = %+v, %v, want the files in conflict and no merge", pv, err)
|
|
}
|
|
}
|
|
|
|
func TestAPreviewIsWrittenOnceAndAnExistingKeyStands(t *testing.T) {
|
|
b := newBench(t)
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
b.fetch.Merge = "5656565656565656565656565656565656565656"
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA)
|
|
if b.prev.creates[key] != 1 {
|
|
t.Errorf("the preview was created %d times, want once", b.prev.creates[key])
|
|
}
|
|
raw, _, _, _ := b.prev.Get(context.Background(), key)
|
|
if pv, _ := meshdeliverycheckerclient.DecodePreview(raw); pv.Merge == b.fetch.Merge {
|
|
t.Error("the second merge commit replaced the first")
|
|
}
|
|
}
|
|
|
|
func TestTheIndexerIsToldOfAPreviewOnceWhenItIsCreated(t *testing.T) { // 1445/AC4 (the checker's half)
|
|
b := newBench(t)
|
|
var told []string
|
|
b.ex.Announce = func(_ context.Context, event, key string) { told = append(told, event+" "+key) }
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA)
|
|
if len(told) != 1 || told[0] != EventPreviewWritten+" "+key {
|
|
t.Errorf("told %v, want once of %s", told, key)
|
|
}
|
|
}
|
|
|
|
type fakeBuilds map[string]builds.BuilderBuild
|
|
|
|
func (f fakeBuilds) Build(_ context.Context, module, fp2 string) (builds.BuilderBuild, bool, error) {
|
|
b, ok := f[module+" "+fp2]
|
|
return b, ok, nil
|
|
}
|
|
|
|
func TestTheOutcomeSaysIfEachCandidatesModuleIsBuiltFailedOrWaiting(t *testing.T) { // N7/AC1 (the checker reads the builder's keys)
|
|
b := newBench(t)
|
|
fpA := "src2:" + strings.Repeat("a", 64)
|
|
fpB := "src2:" + strings.Repeat("b", 64)
|
|
fpC := "src2:" + strings.Repeat("c", 64)
|
|
b.ex.Builds = fakeBuilds{
|
|
"avahi " + fpA: {State: builds.BuildDone},
|
|
"mesh-tools " + fpB: {State: builds.BuildFailed, Error: "boom"},
|
|
// netcheck has no key yet
|
|
}
|
|
p := pullOf(repoA, 7, head1)
|
|
p.Modules = []string{"avahi", "mesh-tools", "netcheck", "nofp"}
|
|
p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"avahi": {Fp2: fpA}, "mesh-tools": {Fp2: fpB}, "netcheck": {Fp2: fpC}, "nofp": {None2: "toolchain not yet reported"}}
|
|
out := b.judge(t, caseOf(t, p))
|
|
var got []string
|
|
for _, n := range out.Builds {
|
|
got = append(got, n.Module+" "+n.State)
|
|
}
|
|
if strings.Join(got, ", ") != "avahi built, mesh-tools failed, netcheck waiting" {
|
|
t.Errorf("builds = %v, want one note for each module that has a fingerprint, sorted", got)
|
|
}
|
|
}
|
|
|
|
// trunksOf are trunk heads of every repository, as the catalog half's trunk revision names them.
|
|
func trunksOf(t *testing.T, heads map[string]string) *Trunks {
|
|
t.Helper()
|
|
tr := &Trunks{}
|
|
tr.Set(heads)
|
|
return tr
|
|
}
|
|
|
|
func TestNothingIsKeptForAWorkspaceGivenSiblingsWhenTheDigestDoesNotSayItCoversThem(t *testing.T) { // review D1, ADR 0444, issue 1287
|
|
b := newBench(t)
|
|
// A build module whose digest does not echo the siblings (before DigestVersion 3).
|
|
b.steps.handlers["build_go_digest"] = func(c stepCall) (any, error) { return b.digest[c.args["level"].(string)], nil }
|
|
b.ex.Trunks = trunksOf(t, map[string]string{repoSDK: trunkS, "novox/mesh-catalog": trunkA})
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
if got := len(b.steps.tool("fetch")); got != 1 || b.steps.tool("fetch")[0].args["beside"] == nil {
|
|
t.Fatalf("the fetch was not given siblings: %+v", b.steps.tool("fetch"))
|
|
}
|
|
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 {
|
|
t.Errorf("kept %v for a package whose digest does not say it covers the siblings", keys)
|
|
}
|
|
// And a kept verdict of the same digest from elsewhere is not taken for it: everything runs.
|
|
if _, err := (Kept{Store: b.kept}).Keep(context.Background(), "unit", digA, keptValue("fixture/slow", 2)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.steps.calls = nil
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
if n := len(b.steps.tool("test")); n != 2 {
|
|
t.Errorf("%d test steps, want both levels run whole", n)
|
|
}
|
|
}
|
|
|
|
// besideDigest stands for the Go build module's digest over a workspace with siblings (DigestVersion 3, lead decision L5):
|
|
// each package's digest folds the siblings' commits in, and the answer echoes the lines it folded.
|
|
// echoBeside is d with the siblings the fetches placed, as the Go build module's digest echoes them (its DigestResult's
|
|
// Beside): every check now places the change's own repository at its trunk (issue 1887 A12).
|
|
func echoBeside(b *bench, d Digested) Digested {
|
|
var lines []string
|
|
if fetches := b.steps.tool("fetch"); len(fetches) > 0 { // the workspace digested is the last fetch's (issue 890: a placed ask fetches again)
|
|
for _, r := range refsOf(fetches[len(fetches)-1].args["beside"]) {
|
|
lines = append(lines, r.Repository+" "+r.Commit)
|
|
}
|
|
}
|
|
sort.Strings(lines)
|
|
d.Beside = lines
|
|
return d
|
|
}
|
|
|
|
func besideDigest(b *bench) func(c stepCall) (any, error) {
|
|
return func(c stepCall) (any, error) {
|
|
level := c.args["level"].(string)
|
|
var lines []string
|
|
for _, f := range b.steps.tool("fetch") {
|
|
refs, _ := refsOf(f.args["beside"]), true
|
|
for _, r := range refs {
|
|
lines = append(lines, r.Repository+" "+r.Commit)
|
|
}
|
|
}
|
|
sort.Strings(lines)
|
|
sum := sha256.Sum256([]byte(level + "\n" + strings.Join(lines, "\n")))
|
|
return Digested{Level: level, Packages: map[string]string{"fixture/slow": hex.EncodeToString(sum[:])}, Beside: lines}, nil
|
|
}
|
|
}
|
|
|
|
func TestACoreRepositorysPassingPackageIsKeptUnderADigestCoveringItsSiblings(t *testing.T) { // 1287 AC1-AC3, AC7 with L5
|
|
b := newBench(t)
|
|
b.steps.handlers["build_go_digest"] = besideDigest(b)
|
|
b.ex.Trunks = trunksOf(t, map[string]string{repoSDK: trunkS, "novox/mesh-catalog": trunkA})
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
keys, _ := b.kept.Keys(context.Background())
|
|
if len(keys) != 2 {
|
|
t.Fatalf("kept %v, want the passing package at both levels", keys)
|
|
}
|
|
// The same siblings again: everything is taken, nothing runs.
|
|
b.steps.calls = nil
|
|
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
|
if n := len(b.steps.tool("test")); n != 0 || v.Levels[buildstep.LevelUnit].Run != 0 || v.Levels[buildstep.LevelUnit].Kept == 0 {
|
|
t.Fatalf("%d test steps, unit %+v, want none run and the kept verdict taken", n, v.Levels[buildstep.LevelUnit])
|
|
}
|
|
// A sibling's commit moves: a new digest, so the package runs again and is kept under it.
|
|
b.ex.Trunks = trunksOf(t, map[string]string{repoSDK: trunkS2, "novox/mesh-catalog": trunkA})
|
|
b.steps.calls = nil
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
if n := len(b.steps.tool("test")); n != 2 {
|
|
t.Errorf("%d test steps after a sibling moved, want both levels run", n)
|
|
}
|
|
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 4 {
|
|
t.Errorf("kept %v, want a second key per level under the new digest", keys)
|
|
}
|
|
}
|
|
|
|
func TestADigestThatEchoesOtherSiblingsKeepsNothing(t *testing.T) { // 1287 with L5: an echo that differs is not trusted
|
|
b := newBench(t)
|
|
inner := besideDigest(b)
|
|
b.steps.handlers["build_go_digest"] = func(c stepCall) (any, error) {
|
|
v, err := inner(c)
|
|
d := v.(Digested)
|
|
d.Beside = d.Beside[:len(d.Beside)-1]
|
|
return d, err
|
|
}
|
|
b.ex.Trunks = trunksOf(t, map[string]string{repoSDK: trunkS, "novox/mesh-catalog": trunkA})
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 0 {
|
|
t.Errorf("kept %v under a digest that does not cover every sibling", keys)
|
|
}
|
|
}
|
|
|
|
type pkgPaths map[string][]TypeScriptPackage
|
|
|
|
func (p pkgPaths) TypeScriptPackages(repository string) []TypeScriptPackage { return p[repository] }
|
|
|
|
// contractsGit is a contracts package of mesh-delivery: one directory with its own package.json.
|
|
const contractsGit = "modules/mesh-delivery-contracts/ts/seats/git"
|
|
|
|
// groupPackageBench is a group of a TypeScript member (mesh-delivery, with a contracts package) and the mesh-sdk member,
|
|
// whose merges change changed of each repository.
|
|
func groupPackageBench(t *testing.T, changed map[string][]string) *bench {
|
|
b := newBench(t)
|
|
b.ex.Modules = langs{repoA: {goBuildModule, nodeBuildModule}, repoSDK: {goBuildModule, nodeBuildModule}}
|
|
b.ex.Packages = pkgPaths{
|
|
repoSDK: {{Dir: "", Paths: []string{"package-lock.json", "package.json", "src", "tsconfig.json"}}},
|
|
repoA: {{Dir: contractsGit, Paths: []string{contractsGit}}},
|
|
}
|
|
fetch := func(c stepCall) (any, error) {
|
|
f := b.fetch
|
|
f.Changed = changed[c.args["repository"].(string)]
|
|
return f, nil
|
|
}
|
|
b.steps.handlers["build_go_fetch"], b.steps.handlers["build_node_fetch"] = fetch, fetch
|
|
return b
|
|
}
|
|
|
|
func nodeFetchOf(b *bench, repo string) stepCall {
|
|
for _, c := range b.steps.tool("fetch") {
|
|
if c.module == nodeBuildModule && c.args["repository"] == repo {
|
|
return c
|
|
}
|
|
}
|
|
return stepCall{}
|
|
}
|
|
|
|
// design 47 G A-284, A-285 (ADR 0500 §5): every TypeScript fetch of a group's check is given every package any member's
|
|
// merge changes, each at its member's head with its directory there; the registry's only for a package none changes.
|
|
func TestATypeScriptMemberTakesEveryPackageTheGroupChanges(t *testing.T) {
|
|
sdk := map[string]any{"repository": repoSDK, "commit": head2, "ref": "refs/pull/12/head"}
|
|
git := map[string]any{"repository": repoA, "commit": head1, "ref": "refs/pull/7/head", "path": contractsGit}
|
|
for _, row := range []struct {
|
|
name string
|
|
changed map[string][]string
|
|
want []any
|
|
}{
|
|
{"the SDK's sources", map[string][]string{repoSDK: {"go/delivery/x.go", "src/state.ts"}}, []any{sdk}},
|
|
{"another member's contracts package", map[string][]string{repoA: {contractsGit + "/index.ts"}}, []any{git}},
|
|
{"both", map[string][]string{repoSDK: {"package.json"}, repoA: {contractsGit + "/package.json", "go.mod"}}, []any{sdk, git}},
|
|
{"Go alone", map[string][]string{repoSDK: {"go/delivery/x.go"}, repoA: {"modules/mesh-delivery-contracts/go/seats/git/contract.go"}}, nil},
|
|
{"the manifest alone", map[string][]string{repoSDK: {"module.json"}}, nil},
|
|
{"beside the named paths", map[string][]string{repoSDK: {"srcx/a.ts"}, repoA: {contractsGit + "x/a.ts"}}, nil},
|
|
} {
|
|
b := groupPackageBench(t, row.changed)
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)))
|
|
for _, repo := range []string{repoA, repoSDK} {
|
|
got, _ := nodeFetchOf(b, repo).args["group-packages"].([]any)
|
|
if !reflect.DeepEqual(got, row.want) {
|
|
t.Errorf("%s: the Node fetch of %s was given %v, want %v", row.name, repo, got, row.want)
|
|
}
|
|
if pkg, ok := nodeFetchOf(b, repo).args["package"]; ok {
|
|
t.Errorf("%s: the Node fetch of %s was given the retired package %v", row.name, repo, pkg)
|
|
}
|
|
}
|
|
}
|
|
// A lone pull request: no group package.
|
|
b := groupPackageBench(t, map[string][]string{repoA: {contractsGit + "/index.ts"}})
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
if got, ok := nodeFetchOf(b, repoA).args["group-packages"]; ok {
|
|
t.Errorf("a lone pull request's Node fetch was given %v", got)
|
|
}
|
|
}
|
|
|
|
// design 47 G A-284: the mesh-sdk member is judged first, and its TypeScript fetch still learns the package another
|
|
// member's merge changes: every member's other fetches come before any TypeScript one.
|
|
func TestEveryMembersMergeIsKnownBeforeATypeScriptFetch(t *testing.T) {
|
|
b := groupPackageBench(t, map[string][]string{repoA: {contractsGit + "/index.ts"}})
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)))
|
|
var order []string
|
|
for _, c := range b.steps.tool("fetch") {
|
|
order = append(order, c.module+" "+c.args["repository"].(string))
|
|
}
|
|
want := []string{goBuildModule + " " + repoSDK, goBuildModule + " " + repoA, nodeBuildModule + " " + repoSDK, nodeBuildModule + " " + repoA}
|
|
if !reflect.DeepEqual(order, want) {
|
|
t.Errorf("fetches in the order %v, want %v", order, want)
|
|
}
|
|
}
|
|
|
|
func TestNoStepOfACheckCarriesPublish(t *testing.T) { // ADR 0500 §4, 1309 (the checker's half): only the builder's trunk build publishes
|
|
b := groupPackageBench(t, map[string][]string{repoSDK: {"src/state.ts"}})
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)))
|
|
if len(b.steps.calls) == 0 {
|
|
t.Fatal("no step ran")
|
|
}
|
|
for _, c := range b.steps.calls {
|
|
if _, ok := c.args["publish"]; ok {
|
|
t.Errorf("%s was given publish: %v", c.tool, c.args)
|
|
}
|
|
if strings.HasSuffix(c.tool, "_build") || strings.HasSuffix(c.tool, "_pack") {
|
|
t.Errorf("the checker asked %s: a check builds no artifact of its own", c.tool)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1890/AC1, 1887/A5a: an ask whose step never ends (the build node stopped with it running, and nothing redelivered it in
|
|
// time) ends the check loudly at the asker's bound; the checker holds no place to free.
|
|
func TestAnAskWhoseStepNeverEndsEndsTheCheckLoudly(t *testing.T) {
|
|
b := newBench(t)
|
|
// A node took the ask and its step stays running: the bound runs from its start (review round 2 item 1).
|
|
b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
|
|
return buildstep.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: buildstep.StepRunning, Started: time.Now()}
|
|
}
|
|
b.ex.Asks = buildrun.Asker{Seats: testSeats(), Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 20 * time.Millisecond}
|
|
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
if f := out.Verdict.Failed; out.Verdict.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(f.Words, "on shanks did not end within") {
|
|
t.Fatalf("verdict = %+v", out.Verdict)
|
|
}
|
|
if n := len(b.asks.Asks()); n != 1 || b.asks.Asks()[0].Kind != buildstep.AskCheck || b.asks.Asks()[0].Check == nil {
|
|
t.Fatalf("asks = %+v, want one check ask", b.asks.Asks())
|
|
}
|
|
}
|
|
|
|
// 1887/A5a, ADR 0536 §4: a check is one ask per pull request and build module on the check stream, a re-judge after a
|
|
// trunk move one on the recheck stream, each carrying both levels with the kept verdicts taken by the build module.
|
|
func TestACheckIsOneAskAndAReJudgeGoesOnTheRecheckStream(t *testing.T) {
|
|
b := newBench(t)
|
|
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
|
|
asks := b.asks.Asks()
|
|
if len(asks) != 1 || asks[0].Kind != buildstep.AskCheck || asks[0].Language != buildstep.AskGo || asks[0].Asker != CheckerModule {
|
|
t.Fatalf("asks = %+v, want one Go check ask", asks)
|
|
}
|
|
if j := asks[0].Check; j == nil || !j.Kept || strings.Join(j.Levels, ",") != "unit,integration" || j.Fetch.Repository != repoA {
|
|
t.Fatalf("the job = %+v", asks[0].Check)
|
|
}
|
|
// B2g: the job carries the toolchain the checker agreed (ADR 0584), with its language, so a node on another declines it.
|
|
if tc := asks[0].Check.Toolchain; tc == nil || *tc != (buildstep.ToolchainUsed{Language: "go", Version: "go1.27.1", Recipe: "7"}) {
|
|
t.Errorf("the job's toolchain = %+v, want the agreed go toolchain", tc)
|
|
}
|
|
cs := caseOf(t, pullOf(repoA, 7, head1))
|
|
cs.Kind = KindTrunk
|
|
b.judge(t, cs)
|
|
if asks := b.asks.Asks(); len(asks) != 2 || asks[1].Kind != buildstep.AskRecheck {
|
|
t.Fatalf("asks = %+v, want the re-judge on recheck", asks)
|
|
}
|
|
}
|
|
|
|
// Review round 2 item 1: a check ask that waits in its stream longer than the step bound (no node has a free slot during a
|
|
// large rebuild) is not failed: the check goes on when a node takes it, and no second ask is put.
|
|
func TestACheckAskWaitingInItsStreamIsNotBounded(t *testing.T) {
|
|
b := newBench(t)
|
|
answer := b.asks.Answer
|
|
b.asks.Answer = nil
|
|
b.ex.Asks = buildrun.Asker{Seats: testSeats(), Publisher: b.asker(nil, nil), Waiter: b.asks, Bound: 20 * time.Millisecond}
|
|
done := make(chan Outcome, 1)
|
|
go func() { done <- b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) }()
|
|
for deadline := time.Now().Add(10 * time.Second); len(b.asks.Asks()) == 0; time.Sleep(time.Millisecond) {
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("no ask was put")
|
|
}
|
|
}
|
|
time.Sleep(100 * time.Millisecond) // five bounds in the stream
|
|
select {
|
|
case out := <-done:
|
|
t.Fatalf("the check ended while its ask waited: %+v", out.Verdict)
|
|
default:
|
|
}
|
|
b.asks.End(answer(b.asks.Asks()[0])) // a node took it and ran it
|
|
if out := <-done; out.Verdict.State != meshdeliverycheckerclient.VerdictPass || len(b.asks.Asks()) != 1 {
|
|
t.Fatalf("verdict %+v after %d asks, want a pass from the one ask", out.Verdict, len(b.asks.Asks()))
|
|
}
|
|
}
|
|
|
|
// 1917: the digest is taken inside the check ask, as the build module's own digest step answers it (no step key is asked
|
|
// of it), so a second check of the same heads keeps every package the first one passed: "N kept", nothing run again.
|
|
func TestASecondCheckOfTheSameHeadsKeepsWhatTheFirstPassed(t *testing.T) {
|
|
b := newBench(t)
|
|
first := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
|
u := first.Levels[buildstep.LevelUnit]
|
|
if first.State != meshdeliverycheckerclient.VerdictPass || u.Run == 0 || u.Kept != 0 {
|
|
t.Fatalf("the first check = %+v", first)
|
|
}
|
|
b.steps.calls = nil
|
|
second := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
|
if s := second.Levels[buildstep.LevelUnit]; second.State != meshdeliverycheckerclient.VerdictPass || s.Kept != u.Run || s.Run != 0 {
|
|
t.Fatalf("the second check's unit level = %+v, want %d kept and none run", s, u.Run)
|
|
}
|
|
if n := len(b.steps.tool("test")); n != 0 {
|
|
t.Errorf("%d test steps ran on the second check", n)
|
|
}
|
|
}
|
|
|
|
// keptList is the indexer's kept predefined list as a test gives it.
|
|
type keptList string
|
|
|
|
func (k keptList) PredefinedList(context.Context) (json.RawMessage, error) {
|
|
return json.RawMessage(k), nil
|
|
}
|
|
|
|
// fixedManifests are the indexer's kept manifests by digest as a test gives them.
|
|
type fixedManifests map[string]catalogue.Manifest
|
|
|
|
func (f fixedManifests) Manifest(_ context.Context, digest string) (catalogue.Manifest, error) {
|
|
m, ok := f[digest]
|
|
if !ok {
|
|
return catalogue.Manifest{}, errors.New("no manifest " + digest)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// fixedDigests are the indexer's manifest digests as a test gives them.
|
|
type fixedDigests map[string]string
|
|
|
|
func (f fixedDigests) ManifestDigests() map[string]string { return f }
|
|
|
|
// ADR 0578 §2: a change's manifests are judged by the indexer's judge, built at mesh-delivery's trunk and run as the
|
|
// composer is (a compose ask of its command); a refusal fails the check naming the manifest and each problem; a group
|
|
// whose mesh-delivery member changes the judge is judged by that member's head.
|
|
func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
|
web, trunk := strings.Repeat("e", 64), strings.Repeat("d", 40)
|
|
for _, row := range []struct {
|
|
name string
|
|
refused string
|
|
fails string
|
|
// untouched is the judge's refusal of mail, which the change leaves as it is, and trunkMail its refusal on the trunk:
|
|
// a problem the trunk had is a warning, one the change causes fails (ADR 0579 §2)
|
|
untouched, trunkMail string
|
|
// cycle reads the manifests, so the change's module graph is worked out (ADR 0603 §2): with "provisions" the change's
|
|
// web awaits mail's site while mail awaits web's database, and trunkCycle has the trunk's web await it too; with "bases"
|
|
// each is built on the other; with "verbs" each invokes the other's tool, and with "a verb and a provision" web invokes
|
|
// mail's tool while mail awaits web's database: both left out, so neither is refused (ADR 0614 §3, §6).
|
|
cycle string
|
|
trunkCycle bool
|
|
// series is the series of web's package the change raises it to, and the words its status line names (DLV-C4).
|
|
series, seriesWords string
|
|
// renames is what the change does to web's renamed-from ("bring", "drop") and what the seat mesh-config's verb
|
|
// renames answers ("served", "open" on g14, "not served", "unasked" with no seat to ask, "error"); DLV-C5.
|
|
renames, seat string
|
|
// unparsed is the indexer's words of why the pull request's head's seat definition of web-api does not parse.
|
|
unparsed string
|
|
// known are the judge's known packages the request hands it (DLV-I14, design 32): the packages state's names and,
|
|
// of web's package new in the group, the name its go.mod says at the member's head.
|
|
known []string
|
|
// emptyState is a packages state answered and empty, no member declaring a package: the judge is handed an empty
|
|
// list, never nil, so every name a manifest's packages names is refused (review round 1, DLV-I14).
|
|
emptyState bool
|
|
// indexer makes web the holder of the seat mesh-catalog, and the trunk's manifest validator answers for it: "held" with no verdict,
|
|
// "aside" with a field it does not know; ownJudge has the group change the judge, so the manifest is also judged by the
|
|
// trunk's, in a second ask (ADR 0633 clause 6, 1585/T22, T23)
|
|
indexer string
|
|
ownJudge bool
|
|
// seats is the seat list the checker holds: "read" gives the judge its two definitions as the request's Seats, "unread"
|
|
// is a reader the seat has not answered, and the manifests are then judged by nothing (ADR 0636 §1).
|
|
seats string
|
|
// try judges the same change as a whole group try, whose manifests come from the indexer's candidate of the try's
|
|
// group (issue 1921): the same judge, the same verdicts.
|
|
try bool
|
|
// heads has the change touch a contracts module too, whose head defines the Delivery seat mesh-catalog with a verb of
|
|
// a part it includes: "domain" is the domain's contracts module, so the judges get that definition over the list's
|
|
// (ADR 0636 §4, ADR 0633 §6); "outside" is mesh-contracts, whose definition of another domain's seat is not taken
|
|
// (ADR 0636 §5); "misnamed" is the domain's module whose seats/mesh-catalog/seat.json names another seat.
|
|
heads string
|
|
// headSeats lays heads that add a seat: the checking program's seat list is carried by every judge ask's request (ADR
|
|
// 0636 §4, issue 1921 D4).
|
|
headSeats bool
|
|
}{
|
|
{name: "the seat list read is the request's Seats", seats: "read"},
|
|
{name: "the members' heads' definitions over the list's, for the trunk's manifest validator too", seats: "read", indexer: "ok", heads: "domain"},
|
|
{name: "a head's definition of a seat outside its module's domain is not taken", seats: "read", indexer: "ok", heads: "outside"},
|
|
{name: "a head's definition naming another seat than its directory's fails the judging", seats: "read", heads: "misnamed",
|
|
fails: `names the seat "other", not "mesh-catalog"`},
|
|
{name: "the seat list not read judges nothing", seats: "unread",
|
|
fails: "the change's manifests cannot be judged: the seat list is not read (the seat list is held from the bootstrap"},
|
|
{name: "a seat definition that does not parse", unparsed: "seats/web-api/seat.json: unexpected end of JSON input",
|
|
fails: "novox/mesh-delivery#7: the seat definition of web-api (module web-contracts, go) does not parse: seats/web-api/seat.json: unexpected end of JSON input"},
|
|
{name: "a rename brought, the verb not served", renames: "bring", seat: "not served",
|
|
fails: "web (novox/mesh-delivery#7) renames new-key from old-key: refused, the holder of the seat mesh-config does not serve its verb renames"},
|
|
{name: "a rename brought, the verb served", renames: "bring", seat: "served"},
|
|
{name: "a renamed-from dropped while its window is open", renames: "drop", seat: "open",
|
|
fails: "web (novox/mesh-delivery#7) drops renamed-from old-key of new-key while its rename window is open on g14"},
|
|
{name: "a renamed-from dropped once its window closed", renames: "drop", seat: "served"},
|
|
{name: "a rename the check cannot ask of", renames: "bring", seat: "error", fails: "the check cannot ask seat:mesh-config.renames: the bus said no"},
|
|
{name: "a rename with no seat to ask", renames: "drop", seat: "unasked", fails: "this checker cannot ask seat:mesh-config.renames"},
|
|
{name: "accepted"},
|
|
{name: "a package series raised", series: "0.2", seriesWords: "web/pkg series 0.1 → 0.2"},
|
|
{name: "a package series kept", series: "0.1"},
|
|
{name: "a package new in the group", series: "0.1", known: []string{"git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/web/go"}},
|
|
{name: "an answered packages state holding no name", emptyState: true, known: []string{}},
|
|
{name: "the heads' seat list is carried by the head's judge and the trunk's", headSeats: true, indexer: "ok", ownJudge: true},
|
|
{name: "accepted in a group try", try: true},
|
|
{name: "refused in a group try", try: true, refused: "web has no id", fails: "the indexer's judge at mesh-delivery's trunk " + strings.Repeat("d", 40) + ` refuses the manifest of web: web has no id`},
|
|
{name: "a cycle the group try makes", try: true, cycle: "provisions", fails: "the change's module graph has the cycle mail -> web -> mail"},
|
|
{name: "the indexer accepted by the trunk's manifest validator", indexer: "ok"},
|
|
{name: "the indexer held by the trunk's manifest validator", indexer: "held", fails: "the indexer web, the holder of the seat mesh-catalog, would never run: the trunk's manifest validator at mesh-delivery's trunk " + strings.Repeat("d", 40) + " does not accept its manifest: the manifest validator holds it, with no verdict yet"},
|
|
{name: "the indexer with a field the trunk's manifest validator does not know", indexer: "aside", fails: "the manifest validator does not know its fields new_field, so it waits"},
|
|
{name: "the indexer refused by the trunk's manifest validator", indexer: "refused", refused: "web has no id", fails: "does not accept its manifest: web has no id"},
|
|
{name: "the indexer under a changed judge is judged by the trunk's as well", indexer: "aside", ownJudge: true, fails: "the manifest validator does not know its fields new_field, so it waits"},
|
|
{name: "a touched manifest the judge holds", indexer: "heldplain", fails: "holds the manifest of web: it gave no verdict"},
|
|
{name: "an untouched manifest's problem the trunk had", untouched: "mail says no credential form", trunkMail: "mail says no credential form"},
|
|
{name: "a problem the change causes in an untouched manifest", untouched: "mail reads a state web no longer keeps",
|
|
fails: "refuses the manifest of mail: mail reads a state web no longer keeps"},
|
|
{name: "a cycle the change makes", cycle: "provisions", fails: "the change's module graph has the cycle mail -> web -> mail"},
|
|
{name: "a cycle the trunk has", cycle: "provisions", trunkCycle: true},
|
|
{name: "a cycle of bases", cycle: "bases", fails: "the change's module graph has the cycle mail -> web -> mail"},
|
|
{name: "a cycle of verbs", cycle: "verbs"},
|
|
{name: "a cycle of a verb and a provision", cycle: "a verb and a provision"},
|
|
{name: "refused", refused: "web requires \"amqp\"\nweb has no id", fails: "the indexer's judge at mesh-delivery's trunk " + strings.Repeat("d", 40) + ` refuses the manifest of web: web requires "amqp"; web has no id`},
|
|
} {
|
|
b := newBench(t)
|
|
if row.headSeats {
|
|
b.heads = fstest.MapFS{
|
|
repoA + "/modules/x-contracts/module.json": {Data: []byte(`{"module": "x-contracts", "build": {"artifacts": [{"name": "go", "kind": "package", "language": "go", "generated-from": "."}]}}`)},
|
|
repoA + "/modules/x-contracts/seats/node-x/seat.json": {Data: []byte(`{"format":1,"seat":"node-x","scope":"node","grain":"whole","domain":"Delivery","decision":"ADR 1"}`)},
|
|
}
|
|
}
|
|
b.ex.Manifests = fixedDigests{"mail": strings.Repeat("a", 64), "web": strings.Repeat("b", 64)}
|
|
b.ex.Trunks = fixedTrunks{heads: map[string]string{DeliveryRepository: trunk}}
|
|
b.ex.Predefined = keptList(`{"font-size": {"type": "number", "description": "the size"}}`)
|
|
if row.cycle != "" {
|
|
offer := func(name string) []catalogue.Offer {
|
|
return []catalogue.Offer{{Name: name, Scope: catalogue.ScopeMesh, AnsweredBy: catalogue.AnsweredByHelper}}
|
|
}
|
|
on := func(module string) *catalogue.Build {
|
|
return &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "BASE", Module: module, Artifact: "img"}}}
|
|
}
|
|
trunkWeb := catalogue.Manifest{Module: "web", Provides: offer("database")}
|
|
if row.trunkCycle {
|
|
trunkWeb.Requires = []string{"site"}
|
|
}
|
|
mail := catalogue.Manifest{Module: "mail", Provides: offer("site"), Requires: []string{"database"}}
|
|
changed := catalogue.Manifest{Module: "web", Provides: offer("database"), Requires: []string{"site"}}
|
|
switch row.cycle {
|
|
case "bases":
|
|
mail = catalogue.Manifest{Module: "mail", Build: on("web")}
|
|
changed = catalogue.Manifest{Module: "web", Build: on("mail")}
|
|
case "verbs":
|
|
mail = catalogue.Manifest{Module: "mail", Tools: []string{"mail_send"}, Invokes: []string{"web.web_page"}}
|
|
changed = catalogue.Manifest{Module: "web", Tools: []string{"web_page"}, Invokes: []string{"mail.mail_send"}}
|
|
case "a verb and a provision":
|
|
mail = catalogue.Manifest{Module: "mail", Tools: []string{"mail_send"}, Requires: []string{"database"}}
|
|
changed = catalogue.Manifest{Module: "web", Provides: offer("database"), Invokes: []string{"mail.mail_send"}}
|
|
}
|
|
b.ex.Read = fixedManifests{strings.Repeat("a", 64): mail, strings.Repeat("b", 64): trunkWeb, web: changed}
|
|
}
|
|
if row.series != "" {
|
|
pkg := func(series string) *catalogue.Build {
|
|
return &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "pkg", Kind: catalogue.ArtifactPackage, Language: "go", Series: series}}}
|
|
}
|
|
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web", Build: pkg("")},
|
|
web: {Module: "web", Build: pkg(row.series)}}
|
|
}
|
|
if row.known != nil {
|
|
b.ex.KnownPackages = knownNames{"git.novox.be/novox/mesh-sdk/go": "mesh-sdk/go"}
|
|
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web"}, web: {Module: "web",
|
|
Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "pkg", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}}}}}}
|
|
b.ex.Forge = headFiles{repoA + " modules/web/go/go.mod " + head1: "module git.novox.be/novox/web/go\n\ngo 1.26\n"}
|
|
}
|
|
if row.emptyState {
|
|
b.ex.KnownPackages = knownNames{}
|
|
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web"}, web: {Module: "web"}}
|
|
}
|
|
if row.renames != "" {
|
|
renamed := map[string]catalogue.SettingDeclaration{"new-key": {Type: catalogue.ConfigString, RenamedFrom: "old-key"}}
|
|
plain := map[string]catalogue.SettingDeclaration{"new-key": {Type: catalogue.ConfigString}}
|
|
trunkWeb, headWeb := catalogue.Manifest{Module: "web", Settings: plain}, catalogue.Manifest{Module: "web", Settings: renamed}
|
|
if row.renames == "drop" {
|
|
trunkWeb, headWeb = headWeb, trunkWeb
|
|
}
|
|
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): trunkWeb, web: headWeb}
|
|
// The verb faked from its definition: the answer of the seat's fixture (mesh-contracts seats/mesh-config).
|
|
fake := fakeRenames{}
|
|
switch row.seat {
|
|
case "not served":
|
|
fake.err = fmt.Errorf("%w (nothing answers it)", ErrRenamesNotServed)
|
|
case "error":
|
|
fake.err = errors.New("the bus said no")
|
|
case "open":
|
|
fake.answer = meshconfig.RenamesAnswer{Renames: []meshconfig.RenamesAnswerRenamesItem{{Module: "web", From: "old-key", To: "new-key", At: "2026-11-04T12:00:00Z", Open: []string{"g14"}}}}
|
|
}
|
|
if row.seat != "unasked" {
|
|
b.ex.Renames = fake
|
|
}
|
|
}
|
|
if row.unparsed != "" {
|
|
b.ex.SeatDefinitions = fakeSeatDefinitions{"novox/mesh-delivery#7": {{Seat: "web-api", Module: "web-contracts", Directory: "go", Why: row.unparsed}}}
|
|
}
|
|
if row.indexer != "" && row.indexer != "heldplain" {
|
|
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web"},
|
|
web: {Module: "web", Claims: []catalogue.Claim{{Name: CatalogSeat}}}}
|
|
}
|
|
if row.seats != "" {
|
|
b.ex.SeatList = fakeSeatList{read: row.seats == "read"}
|
|
}
|
|
var judged []buildstep.ComposeJob
|
|
answer := b.asks.Answer
|
|
b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
|
|
if a.Compose == nil {
|
|
return answer(a)
|
|
}
|
|
judged = append(judged, *a.Compose)
|
|
read := composer.ManifestRead{Accepted: row.refused == "", Refused: row.refused}
|
|
switch row.indexer {
|
|
case "held", "heldplain":
|
|
read = composer.ManifestRead{}
|
|
case "aside":
|
|
read.SetAside = "new_field"
|
|
}
|
|
verdict, _ := json.Marshal(composer.Verdict{V: composer.Version, SDK: "exe:1", Cases: []composer.CaseVerdict{{Name: JudgeCase,
|
|
Manifests: map[string]composer.ManifestRead{"web": read, "mail": {Accepted: row.untouched == "", Refused: row.untouched}}},
|
|
{Name: JudgeTrunk, Manifests: map[string]composer.ManifestRead{"web": {Accepted: true}, "mail": {Accepted: row.trunkMail == "", Refused: row.trunkMail}}}}})
|
|
result, _ := json.Marshal(buildstep.ComposeAnswer{Key: "k", Verdict: verdict})
|
|
return buildstep.Step{ID: a.ID, State: buildstep.StepPassed, Result: result}
|
|
}
|
|
p := pullOf(repoA, 7, head1)
|
|
if row.ownJudge {
|
|
p = pullOf(DeliveryRepository, 7, head1)
|
|
b.fetch.Changed = []string{JudgeDir + "/judge/judge.go"}
|
|
}
|
|
p.Modules = []string{"web"}
|
|
if row.heads != "" {
|
|
contracts, seat := "mesh-delivery-contracts", "mesh-catalog"
|
|
switch row.heads {
|
|
case "outside":
|
|
contracts = "mesh-contracts"
|
|
case "misnamed":
|
|
seat = "other"
|
|
}
|
|
dir := "modules/" + contracts
|
|
p.Modules = append(p.Modules, contracts)
|
|
// The checking program reads the heads the build node lays for its run (D6): the running checker reads none.
|
|
b.heads = fstest.MapFS{
|
|
repoA + "/" + dir + "/module.json": {Data: []byte(`{"module": "` + contracts + `", "build": {"artifacts": [{"name": "go", "kind": "package", "language": "go", "generated-from": "."}]}}`)},
|
|
repoA + "/" + dir + "/seats/mesh-catalog/seat.json": {Data: []byte(`{"format":1,"seat":"` + seat + `","scope":"mesh","grain":"whole","domain":"Delivery","decision":"ADR 1","includes":["lists"],` +
|
|
`"verbs":[{"name":"modules","input":{"type":"object"},"answer":{"type":"object"}}]}`)},
|
|
repoA + "/" + dir + "/seats/mesh-catalog/fixtures/a.json": {Data: []byte(`{}`)},
|
|
repoA + "/" + dir + "/parts/lists.json": {Data: []byte(`{"format":1,"part":"lists","verbs":[{"name":"seat-definitions","input":{"type":"object"},"answer":{"type":"object"}}]}`)},
|
|
}
|
|
}
|
|
p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"web": {Fp2: "src2:" + strings.Repeat("f", 64), Manifest: web}}
|
|
// Issue 1921: the judge is built at the group's heads too, also a member that publishes no package yet.
|
|
cs := caseOf(t, p, pullOf("novox/mesh-networking", 9, head2))
|
|
if row.try {
|
|
cs = TryCase("try-1", Try{Members: []TryMember{{Repository: repoA, Head: head1, Trunk: trunkA}}}, nil)
|
|
b.ex.Candidates = &fakeTryCandidates{cand: meshcatalog.CandidatesByGroupValue{Fpset: "fps", Pulls: []meshcatalog.CandidatesByGroupValuePullsItem{p}}}
|
|
}
|
|
v := b.judge(t, cs).Verdict
|
|
if line := Line(v, cs, cs.Pulls[0], nil, nil, nil); row.seriesWords != "" && !strings.Contains(line, row.seriesWords) || row.seriesWords == "" && strings.Contains(line, "series") {
|
|
t.Errorf("%s: the line %q, want it to name %q", row.name, line, row.seriesWords)
|
|
}
|
|
// The join (A2j finding 2): the verdict names each series change as {module, artifact, from, to}.
|
|
if want := []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem(nil); row.series == "0.2" {
|
|
if want = []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}}; !reflect.DeepEqual(v.Series, want) {
|
|
t.Errorf("%s: the verdict's series %+v, want %+v", row.name, v.Series, want)
|
|
}
|
|
} else if len(v.Series) != 0 {
|
|
t.Errorf("%s: the verdict's series %+v, want none", row.name, v.Series)
|
|
}
|
|
if row.indexer == "ok" && !slices.ContainsFunc(cs.notes, func(n string) bool {
|
|
return strings.Contains(n, "the new indexer's manifest of web is accepted by the trunk's manifest validator")
|
|
}) {
|
|
t.Errorf("%s: the case says %v, want the new indexer's manifest said accepted by the trunk's manifest validator", row.name, cs.notes)
|
|
}
|
|
if row.seats == "unread" || row.heads == "misnamed" {
|
|
if v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) || len(judged) != 0 {
|
|
t.Errorf("%s: %+v, judged %d, want a fail saying %q before any judge", row.name, v.Failed, len(judged), row.fails)
|
|
}
|
|
continue
|
|
}
|
|
// Issue 1921: the judge of the change is built at the group's heads (also a member that publishes no package yet), but
|
|
// the trunk's manifest validator that rules on a new indexer is built at the latest published versions alone, never
|
|
// at a group's head (ADR 0633 clause 6): a group with a member of its own then judges the indexer a second time.
|
|
trunkRuns := 0
|
|
if row.indexer != "" && row.indexer != "heldplain" && (row.ownJudge || !row.try) {
|
|
trunkRuns = 1
|
|
}
|
|
if want := 1 + trunkRuns; len(judged) != want {
|
|
t.Fatalf("%s: %d judge asks %+v, want %d", row.name, len(judged), judged, want)
|
|
}
|
|
for i, j := range judged {
|
|
pkgs := j.Fetch.Packages
|
|
atGroup := pkgs["git.novox.be/novox/mesh-networking"] == head2
|
|
switch trunkRun := trunkRuns == 1 && i == len(judged)-1; {
|
|
case row.try:
|
|
case trunkRun && atGroup:
|
|
t.Errorf("%s: the trunk's manifest validator is built with the group's head: %v", row.name, pkgs)
|
|
case !trunkRun && !atGroup:
|
|
t.Errorf("%s: the judge at %v is fetched with the packages %v, want the group's mesh-networking member at its head %s", row.name, j.Fetch.Commit, pkgs, head2)
|
|
}
|
|
}
|
|
if row.ownJudge && (len(judged) != 2 || judged[0].Fetch.Commit != head1 || judged[1].Fetch.Commit != trunk) {
|
|
t.Fatalf("%s: judged %+v, want the head's judge and then the trunk's", row.name, judged)
|
|
}
|
|
if row.ownJudge {
|
|
judged = judged[1:]
|
|
} else if trunkRuns == 1 {
|
|
judged = judged[1:] // the group's run; the trunk's is the one read below
|
|
}
|
|
if len(judged) != 1 || judged[0].Fetch.Repository != DeliveryRepository || judged[0].Fetch.Commit != trunk ||
|
|
judged[0].Package != JudgePackage || judged[0].Dir != JudgeDir {
|
|
t.Fatalf("%s: judged %+v (verdict %+v)", row.name, judged, v)
|
|
}
|
|
for _, j := range judged {
|
|
var seated composer.Request
|
|
if err := json.Unmarshal(j.Request, &seated); err != nil || (len(seated.Seats) == 1) != row.headSeats ||
|
|
(row.headSeats && !strings.Contains(string(seated.Seats[0]), `"node-x"`)) {
|
|
t.Errorf("%s: the judge at %v is handed the seats %s, want the heads' list: %v", row.name, j.Fetch.Commit, seated.Seats, err)
|
|
}
|
|
}
|
|
var req composer.Request
|
|
if err := json.Unmarshal(judged[0].Request, &req); err != nil || req.Cases[0].Manifests["web"].Digest != web ||
|
|
req.Cases[1].Name != JudgeTrunk || req.Cases[1].Manifests["web"].Digest != strings.Repeat("b", 64) ||
|
|
req.Predefined["font-size"].Type != "number" ||
|
|
req.Cases[0].Manifests["mail"].Digest != strings.Repeat("a", 64) {
|
|
t.Fatalf("%s: the request %+v: %v", row.name, req, err)
|
|
}
|
|
if row.heads != "" {
|
|
// Review round 2 of G (programs.go's digest side): the checking program's -seats input carries the digest of
|
|
// every definition the reader holds, never the list, and its answer is laid over the list (below).
|
|
var digests map[string]string
|
|
b.inputs.mu.Lock()
|
|
for _, body := range b.inputs.vals {
|
|
var f rules.Facts
|
|
if json.Unmarshal(body, &f) == nil && f.SeatDigests != nil {
|
|
digests = f.SeatDigests
|
|
}
|
|
}
|
|
b.inputs.mu.Unlock()
|
|
if want := map[string]string{"a": rules.SeatDigest(json.RawMessage(`{"seat":"a"}`)), "b": rules.SeatDigest(json.RawMessage(`{"seat": "b"}`))}; !reflect.DeepEqual(digests, want) {
|
|
t.Errorf("%s: the -seats input carries the digests %v, want the reader's list's %v", row.name, digests, want)
|
|
}
|
|
var got []string
|
|
for _, raw := range req.Seats {
|
|
var d struct {
|
|
Seat string `json:"seat"`
|
|
Verbs []struct {
|
|
Name string `json:"name"`
|
|
} `json:"verbs"`
|
|
}
|
|
_ = json.Unmarshal(raw, &d)
|
|
for _, v := range d.Verbs {
|
|
d.Seat += "." + v.Name
|
|
}
|
|
got = append(got, d.Seat)
|
|
}
|
|
want := []string{"a", "b", "mesh-catalog.seat-definitions.modules"}
|
|
if row.heads == "outside" {
|
|
want = want[:2]
|
|
}
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("%s: the trunk's manifest validator's seats %v, want %v: the list's with the head's definition, its part resolved", row.name, got, want)
|
|
}
|
|
} else if row.seats == "read" && (len(req.Seats) != 2 || string(req.Seats[0]) != `{"seat":"a"}` || string(req.Seats[1]) != `{"seat":"b"}`) {
|
|
t.Errorf("%s: the request's seats %s, want the two definitions the reader holds", row.name, req.Seats)
|
|
}
|
|
if row.seats == "" && !row.headSeats && len(req.Seats) != 0 {
|
|
t.Errorf("%s: the request's seats %s, want none from a checker that follows no list", row.name, req.Seats)
|
|
}
|
|
if !reflect.DeepEqual(req.Packages, row.known) || len(req.Input) != 0 && string(req.Input) != "null" {
|
|
t.Errorf("%s: the judge's known packages %v and input %s, want the known packages %v and no input", row.name,
|
|
req.Packages, req.Input, row.known)
|
|
}
|
|
switch {
|
|
case row.fails == "" && v.State != meshdeliverycheckerclient.VerdictPass:
|
|
t.Errorf("%s: %+v", row.name, v)
|
|
case row.fails != "" && (v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)):
|
|
t.Errorf("%s: %+v, want a fail saying %q", row.name, v.Failed, row.fails)
|
|
}
|
|
}
|
|
if !changesJudge(DeliveryRepository, []Fetched{{Changed: []string{JudgeDir + "/judge/judge.go"}}}) ||
|
|
changesJudge(DeliveryRepository, []Fetched{{Changed: []string{JudgeDir + "/internal/index/x.go"}}}) ||
|
|
changesJudge("novox/mesh-catalog", []Fetched{{Changed: []string{JudgeDir + "/judge/judge.go"}}}) {
|
|
t.Error("which merge changes the judge")
|
|
}
|
|
}
|
|
|
|
// fakeSeatList is the seat list reader as a test gives it: two definitions once read, else the reader's words.
|
|
type fakeSeatList struct{ read bool }
|
|
|
|
func (f fakeSeatList) Definitions() ([]json.RawMessage, error) {
|
|
if !f.read {
|
|
return nil, errors.New("the seat list is held from the bootstrap (2 seats) and nothing new is acted on")
|
|
}
|
|
return []json.RawMessage{json.RawMessage(`{"seat":"a"}`), json.RawMessage(`{"seat":"b"}`)}, nil
|
|
}
|
|
|
|
// knownNames are the packages state's names as a test gives them, each with the <module>/<artifact> it is built from.
|
|
type knownNames map[string]string
|
|
|
|
func (k knownNames) PackageNames() map[string]string { return k }
|
|
|
|
// fakeRenames is the seat mesh-config's verb renames as its definition answers it.
|
|
type fakeRenames struct {
|
|
answer meshconfig.RenamesAnswer
|
|
err error
|
|
}
|
|
|
|
func (f fakeRenames) Renames(context.Context, string) (meshconfig.RenamesAnswer, error) {
|
|
return f.answer, f.err
|
|
}
|
|
|
|
// fakeSeatDefinitions are the indexer's unparsed seat definitions by pull request (pullKey).
|
|
type fakeSeatDefinitions map[string][]UnparsedSeat
|
|
|
|
func (f fakeSeatDefinitions) Unparsed(_ context.Context, repository string, pull int) ([]UnparsedSeat, error) {
|
|
return f[fmt.Sprintf("%s#%d", repository, pull)], nil
|
|
}
|