Files
mesh-catalog/modules/mesh-build-go/buildstep/askrun_test.go
T

471 lines
24 KiB
Go

package buildstep
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"testing"
"time"
nodebuildgo "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
nodebuildnode "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildnode"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
const runCommit = "0123456789abcdef0123456789abcdef01234567"
// recorder is a build module's step tools that record each call.
type recorder struct {
calls []string
fetch map[string]any
tests map[string]bool // package or module directory -> passed
digests map[string]string
needs []sdkstep.PackageNeeds
}
func (r *recorder) steps() AskSteps {
call := func(name string, answer func(args map[string]any) any) StepFunc {
return func(ctx context.Context, args map[string]any) (any, error) {
b, _ := json.Marshal(args)
r.calls = append(r.calls, name+" "+string(b))
return answer(args), nil
}
}
return AskSteps{
Fetch: call("fetch", func(map[string]any) any { return r.fetch }),
Build: call("build", func(args map[string]any) any {
return map[string]any{"digest": "sha256:" + args["artifact"].(string), "reference": "artifact-store://" + args["artifact"].(string)}
}),
Copy: call("copy", func(args map[string]any) any { return map[string]any{"digest": "sha256:copied"} }),
Digest: call("digest", func(map[string]any) any {
return map[string]any{"level": "unit", "packages": r.digests, "needs": r.needs}
}),
Test: call("test", func(args map[string]any) any {
passed := true
if pkgs, ok := args["packages"].([]string); ok {
for _, p := range pkgs {
passed = passed && r.tests[p]
}
}
if dir, ok := args["module"].(string); ok {
passed = r.tests[dir]
}
return map[string]any{"passed": passed, "tests": 1}
}),
Kept: func(ctx context.Context, level, digest string) (int, bool, error) { return 7, digest == "d-kept", nil },
}
}
func ask(job any) sdkstep.StepAsk {
a := sdkstep.StepAsk{ID: sdkstep.NewID(), Kind: sdkstep.AskCheck, Language: sdkstep.AskGo, Asker: "t", At: time.Now()}
switch j := job.(type) {
case sdkstep.BuildJob:
a.Build = &j
case sdkstep.CheckJob:
a.Check = &j
}
return a
}
// 1887/A3: a build ask fetches once, builds each artifact from the workspace with publish, copies a pinned one without a
// fetch, and answers each artifact's digest and reference; an artifact's seat and the version the builder gave it reach
// its build (CAT-BG2).
func TestRunAskBuilds(t *testing.T) {
r := &recorder{fetch: map[string]any{"workspace": "/w/1"}}
steps := r.steps()
var released []string
steps.Release = func(ws string) error { released = append(released, ws); return nil }
// ADR 0593 §2: a Go module's build ends by saying which mesh packages its programs import.
steps.Imports = func(ctx context.Context, args map[string]any) (any, error) {
b, _ := json.Marshal(args)
r.calls = append(r.calls, "imports "+string(b))
return map[string]any{"imports": []string{"git.novox.be/novox/mesh-sdk/go/announce", "git.novox.be/novox/mesh-sdk/go/health"}}, nil
}
// Issue 1585: the ask's package versions reach the fetch, and the ask's own fetch arguments stay as they were.
a := ask(sdkstep.BuildJob{Module: "m", Path: "modules/m", Publish: true,
Fetch: &sdkstep.Fetch{Repository: "novox/x", Commit: runCommit},
Artifacts: []sdkstep.AskArtifact{{Artifact: "base", From: "registry/b@sha256:00"}, {Artifact: "bundle"}, {Artifact: "lib", Seat: "go-package-registry", Version: "v0.1.15"}}})
a.Packages = sdkstep.PackageVersions{"git.novox.be/novox/mesh-sdk/go": runCommit}
out, err := RunAsk(context.Background(), steps, a)
if err != nil {
t.Fatal(err)
}
if len(a.Build.Fetch.Packages) != 0 {
t.Error("the ask's fetch arguments were changed")
}
got := strings.Join(r.calls, "\n")
want := `copy {"artifact":"base","from":"registry/b@sha256:00","module":"m"}
fetch {"commit":"` + runCommit + `","packages":{"git.novox.be/novox/mesh-sdk/go":"` + runCommit + `"},"repository":"novox/x"}
build {"artifact":"bundle","module":"modules/m","publish":true,"workspace":"/w/1"}
build {"artifact":"lib","module":"modules/m","publish":true,"seat":"go-package-registry","version":"v0.1.15","workspace":"/w/1"}
imports {"module":"modules/m","workspace":"/w/1"}`
if got != want {
t.Fatalf("calls:\n%s\nwant:\n%s", got, want)
}
if len(released) != 1 || released[0] != "/w/1" {
t.Errorf("released %v, want the one workspace", released)
}
answer := out.(sdkstep.BuildAnswer)
if len(answer.Artifacts) != 3 || answer.Artifacts[1].Reference != "artifact-store://bundle" {
t.Fatalf("answer %+v", answer)
}
if !slices.Equal(answer.Imports, []string{"git.novox.be/novox/mesh-sdk/go/announce", "git.novox.be/novox/mesh-sdk/go/health"}) {
t.Errorf("the answer's imports are %v", answer.Imports)
}
}
// A build ask for another toolchain is declined (ADR 0584 clause 2), so the node sits out and the asker puts it again.
func TestRunAskRefusesAnotherToolchain(t *testing.T) {
r := &recorder{fetch: map[string]any{"workspace": "/w"}}
s := r.steps()
s.Toolchain = func(context.Context) (sdkstep.ToolchainUsed, error) {
return sdkstep.ToolchainUsed{Version: "go1.27", Recipe: "3"}, nil
}
_, err := RunAsk(context.Background(), s, ask(sdkstep.BuildJob{Module: "m", Path: ".", Fetch: &sdkstep.Fetch{Repository: "novox/x", Commit: runCommit},
Artifacts: []sdkstep.AskArtifact{{Artifact: "bundle"}}, Toolchain: &sdkstep.ToolchainUsed{Version: "go1.26", Recipe: "3"}}))
var dec *Declining
if !errors.As(err, &dec) || len(dec.Lacks) != 1 || len(r.calls) != 0 {
t.Fatalf("err %v, calls %v", err, r.calls)
}
// A check ask names the toolchain too (ADR 0584 clause 2), and a node on another declines it before it fetches.
check := ask(sdkstep.CheckJob{Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}, Levels: []string{sdkstep.LevelUnit},
Toolchain: &sdkstep.ToolchainUsed{Version: "go1.26", Recipe: "3"}})
_, err = RunAsk(context.Background(), s, check)
if !errors.As(err, &dec) || len(r.calls) != 0 {
t.Fatalf("a check ask: err %v, calls %v", err, r.calls)
}
}
// 1887/A3, A5a: a check ask runs the levels of one check after one fetch: it digests the unit level, takes the kept
// verdicts, tests the packages whose tests changed first and then the others, also after one of them failed, and runs
// every level, also after one that did not pass, so one check names every failure (the operator, 2026-11-02).
func TestRunAskChecksTheLevelsOfOneCheck(t *testing.T) {
r := &recorder{
fetch: map[string]any{"workspace": "/w", "changed": []string{"pkg/b/b_test.go"}},
digests: map[string]string{"x/pkg/a": "d-a", "x/pkg/b": "d-b", "x/pkg/k": "d-kept"},
tests: map[string]bool{"x/pkg/a": true, "x/pkg/b": false},
}
out, err := RunAsk(context.Background(), r.steps(), ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit, sdkstep.LevelIntegration}, Kept: true,
Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit, Base: runCommit}}))
if err != nil {
t.Fatal(err)
}
got := strings.Join(r.calls, "\n")
want := `fetch {"base":"` + runCommit + `","commit":"` + runCommit + `","repository":"novox/x"}
digest {"level":"unit","workspace":"/w"}
test {"level":"unit","packages":["x/pkg/b"],"workspace":"/w"}
test {"level":"unit","packages":["x/pkg/a"],"workspace":"/w"}
digest {"level":"integration","workspace":"/w"}
test {"level":"integration","packages":["x/pkg/b"],"workspace":"/w"}
test {"level":"integration","packages":["x/pkg/a"],"workspace":"/w"}`
if got != want {
t.Fatalf("calls:\n%s\nwant:\n%s", got, want)
}
a := out.(sdkstep.CheckAnswer)
if a.Fetch.Workspace != "/w" || !slices.Equal(a.Fetch.Changed, []string{"pkg/b/b_test.go"}) {
t.Fatalf("the answer's fetch is the tool's whole answer, typed: %+v", a.Fetch)
}
if len(a.Levels) != 2 {
t.Fatalf("levels %+v, want both levels", a.Levels)
}
u := a.Levels[0]
if u.Level != sdkstep.LevelUnit || len(u.Kept) != 1 || u.Kept[0].Package != "x/pkg/k" || u.Kept[0].Tests != 7 || len(u.Tests) != 2 || u.Digest == nil {
t.Fatalf("the unit level %+v", u)
}
// ADR 0567 §12: a check's node names reach the digest and test tools, for the names check (a kept verdict misses when
// they change).
r.calls = nil
if _, err := RunAsk(context.Background(), r.steps(), ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit}, NodeNames: []string{"anchor"},
Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}})); err != nil || !strings.Contains(strings.Join(r.calls, "\n"), `digest {"level":"unit","node-names":["anchor"]`) ||
!strings.Contains(strings.Join(r.calls, "\n"), `test {"level":"unit","node-names":["anchor"]`) {
t.Fatalf("the node names are not handed to the digest and test tools: %v, %v", r.calls, err)
}
// Issue 890/AC2: a package declaring node-build-node is not tested on a node that lacks it but named unmet, and is
// tested on a node that has it.
for _, has := range []bool{false, true} {
r.calls, r.tests["x/pkg/b"] = nil, true
r.needs = []sdkstep.PackageNeeds{{Package: "x/pkg/a", Needs: []string{sdkstep.NeedNodeBuildNode}}}
steps := r.steps()
if has {
steps.Has = func(n string) bool { return n == sdkstep.NeedNodeBuildNode }
}
out, err := RunAsk(context.Background(), steps, ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit}, Kept: true,
Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}}))
if err != nil {
t.Fatal(err)
}
u := out.(sdkstep.CheckAnswer).Levels[0]
tested := strings.Contains(strings.Join(r.calls, "\n"), `"x/pkg/a"`)
if tested != has || (len(u.Unmet) == 0) == !has || (!has && (u.Unmet[0].Package != "x/pkg/a" || sdkstep.UnmetNeeds([]sdkstep.LevelAnswer{u})[0] != sdkstep.NeedNodeBuildNode)) {
t.Fatalf("has %v: tested x/pkg/a %v, unmet %+v, calls %v", has, tested, u.Unmet, r.calls)
}
}
// A placed ask naming its packages tests those alone and takes no other's kept verdict, also when the level would
// run whole (ADR 0552 §4).
for _, digests := range []map[string]string{r.digests, nil} {
r.calls, r.digests = nil, digests
steps := r.steps()
steps.Has = func(string) bool { return true }
out, err := RunAsk(context.Background(), steps, ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit}, Kept: true, Packages: []string{"x/pkg/a"},
Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}}))
if err != nil {
t.Fatal(err)
}
u := out.(sdkstep.CheckAnswer).Levels[0]
if last := r.calls[len(r.calls)-1]; !strings.HasSuffix(last, `"packages":["x/pkg/a"],"workspace":"/w"}`) || len(u.Kept) != 0 || len(u.Tests) != 1 {
t.Fatalf("digests %v: calls %v, kept %+v: want x/pkg/a alone tested, nothing taken kept", digests != nil, r.calls, u.Kept)
}
}
}
// ADR 0535 §2, §4: a merge that changes the build module's own code is judged by the candidate's runner: its digest says
// the candidate ran, and every package runs with nothing kept; an installed module that ignores the runner is planned as
// any other, the runner dropped. A digest that does not cover the fetch's siblings runs the level whole too.
func TestRunAskRunsWholeUnderTheRunnerOrWithoutTheSiblings(t *testing.T) {
for _, tc := range []struct{ judged, want string }{
{"candidate 999", `digest {"level":"unit","runner":"workspace","workspace":"/w"}
test {"level":"unit","runner":"workspace","workspace":"/w"}`},
{"installed", `digest {"level":"unit","runner":"workspace","workspace":"/w"}`},
} {
r := &recorder{fetch: map[string]any{"workspace": "/w", "changed": []string{"modules/mesh-build-go/x.go"}},
digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}}
steps := r.steps()
digest := steps.Digest
steps.Digest = func(ctx context.Context, args map[string]any) (any, error) {
_, _ = digest(ctx, args)
return map[string]any{"level": "unit", "packages": r.digests, "judged_by": tc.judged}, nil
}
out, err := RunAsk(context.Background(), steps, ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit}, Kept: true,
Runner: "workspace", RunnerWhen: "modules/mesh-build-go/", Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}}))
if err != nil {
t.Fatal(err)
}
if got := strings.Join(r.calls[1:], "\n"); got != tc.want {
t.Errorf("judged by %s: calls\n%s\nwant\n%s", tc.judged, got, tc.want)
}
l := out.(sdkstep.CheckAnswer).Levels[0]
if tc.judged == "installed" && (len(l.Kept) != 1 || l.Judged != "") {
t.Errorf("an installed module's level %+v, want the kept verdict taken", l)
}
if tc.judged != "installed" && (len(l.Kept) != 0 || l.Digest != nil || l.Judged != tc.judged) {
t.Errorf("the candidate's level %+v, want nothing kept and the program named", l)
}
}
r := &recorder{fetch: map[string]any{"workspace": "/w"}, digests: map[string]string{"x/pkg/a": "d-kept"}, tests: map[string]bool{}}
out, err := RunAsk(context.Background(), r.steps(), ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit}, Kept: true,
Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit, Beside: []sdkstep.FetchBeside{{Repository: "novox/mesh-sdk", Commit: runCommit}}}}))
if err != nil {
t.Fatal(err)
}
if l := out.(sdkstep.CheckAnswer).Levels[0]; len(l.Kept) != 0 || l.Digest != nil {
t.Fatalf("a digest without the siblings kept %+v", l)
}
}
// A Node check tests each module directory, the one whose tests changed first; a conflict ends the ask with the fetch
// alone, a verdict for the asker.
func TestRunAskChecksNodeModulesAndConflicts(t *testing.T) {
r := &recorder{fetch: map[string]any{"workspace": "/w", "modules": []string{"a", "b"}, "changed": []string{"b/test/x.test.ts"}},
tests: map[string]bool{"a": true, "b": true}}
s := r.steps()
s.Digest = nil
if _, err := RunAsk(context.Background(), s, ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit}, Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}})); err != nil {
t.Fatal(err)
}
if fmt.Sprint(r.calls[1:]) != `[test {"level":"unit","module":"b","workspace":"/w"} test {"level":"unit","module":"a","workspace":"/w"}]` {
t.Fatalf("calls %v", r.calls)
}
r = &recorder{fetch: map[string]any{"workspace": "/w", "conflicts": []string{"go.mod"}}}
out, err := RunAsk(context.Background(), r.steps(), ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit}, Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}}))
if err != nil || len(r.calls) != 1 || len(out.(sdkstep.CheckAnswer).Levels) != 0 {
t.Fatalf("a conflict: %v %v", err, r.calls)
}
}
// Issue 1289: the packages whose tests changed run first. Since 2026-11-02 (the operator: "A failed check shows only its
// first fault") a failure among them ends nothing: every package of the level and every level still runs, so one check
// names every failing test (issue 1921). A Node check does the same with its module directories.
func TestRunAskRunsEveryPackageAndLevelAfterAFailure(t *testing.T) {
for _, row := range []struct {
name string
changed []string
tests map[string]bool
want []string // the packages tested at each level, in order
}{
{"the change's own test fails: the rest still runs", []string{"pkg/b/b_test.go"}, map[string]bool{"x/pkg/a": false, "x/pkg/b": false}, []string{"x/pkg/b", "x/pkg/a"}},
{"the change's own test passes: the rest runs, also after a failure", []string{"pkg/b/b_test.go"}, map[string]bool{"x/pkg/a": false, "x/pkg/b": true, "x/pkg/c": false}, []string{"x/pkg/b", "x/pkg/a x/pkg/c"}},
{"no test changed: every failure is named", nil, map[string]bool{"x/pkg/a": false, "x/pkg/b": false}, []string{"x/pkg/a x/pkg/b"}},
} {
r := &recorder{fetch: map[string]any{"workspace": "/w", "changed": row.changed}, tests: row.tests,
digests: map[string]string{"x/pkg/a": "d-a", "x/pkg/b": "d-b"}}
if _, ok := row.tests["x/pkg/c"]; ok {
r.digests["x/pkg/c"] = "d-c"
}
out, err := RunAsk(context.Background(), r.steps(), ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit, sdkstep.LevelIntegration},
Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}}))
if err != nil {
t.Fatal(err)
}
var got []string
for _, c := range r.calls[1:] {
if strings.HasPrefix(c, "test ") {
var a struct{ Packages []string }
_ = json.Unmarshal([]byte(strings.TrimPrefix(c, "test ")), &a)
got = append(got, strings.Join(a.Packages, " "))
}
}
if want := append(slices.Clone(row.want), row.want...); fmt.Sprint(got) != fmt.Sprint(want) {
t.Errorf("%s: tested %v, want %v", row.name, got, want)
}
if a := out.(sdkstep.CheckAnswer); len(a.Levels) != 2 || len(a.Levels[0].Tests) != len(row.want) {
t.Errorf("%s: levels %+v, want both, each with every test result", row.name, a.Levels)
}
}
r := &recorder{fetch: map[string]any{"workspace": "/w", "modules": []string{"a", "b"}, "changed": []string{"b/test/x.test.ts"}},
tests: map[string]bool{"a": true, "b": false}}
s := r.steps()
s.Digest = nil
out, err := RunAsk(context.Background(), s, ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit, sdkstep.LevelIntegration},
Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}}))
if err != nil {
t.Fatal(err)
}
if a := out.(sdkstep.CheckAnswer); len(r.calls) != 5 || len(a.Levels) != 2 || len(a.Levels[0].Tests) != 2 {
t.Fatalf("calls %v, levels %+v: want b then a at each of the two levels", r.calls, a.Levels)
}
}
// A seat's own ask type (the generated one of any of the three build seats) is the neutral ask through the form's one
// conversion (sdkstep.AskOf), reads for its step description through the same conversion (askRead), and reaches the
// fetch tool as the fetch's arguments alone: no layout version, nothing of the ask's.
func TestASeatsAskTypeRunsAsTheNeutralAsk(t *testing.T) {
at := time.Now().UTC()
fetch := &nodebuildgo.Fetch{Commit: runCommit, Repository: "novox/x", Dir: "modules/m", Paths: []string{"src"}}
goAsk := nodebuildgo.StepAsk{V: sdkstep.StepAskVersion, ID: sdkstep.NewID(), Kind: sdkstep.AskDelivery, Language: sdkstep.AskGo, Asker: "mesh-delivery-builder", At: &at,
Build: &nodebuildgo.StepAskBuild{Module: "m", Path: "modules/m", Fetch: fetch, Artifacts: []nodebuildgo.AskArtifact{{Artifact: "bundle"}}},
Packages: map[string]string{"git.novox.be/novox/mesh-sdk/go": runCommit}}
a, err := sdkstep.AskOf(goAsk)
if err != nil {
t.Fatal(err)
}
if read := askRead(a); read.Kind != sdkstep.AskDelivery || read.Build == nil || read.Build.Module != "m" {
t.Errorf("the step description reads %+v", read)
}
r := &recorder{fetch: map[string]any{"workspace": "/w"}}
if _, err := RunAsk(context.Background(), r.steps(), a); err != nil {
t.Fatal(err)
}
want := `fetch {"commit":"` + runCommit + `","dir":"modules/m","packages":{"git.novox.be/novox/mesh-sdk/go":"` + runCommit + `"},"paths":["src"],"repository":"novox/x"}`
if len(r.calls) == 0 || r.calls[0] != want {
t.Errorf("the fetch tool was called with %v, want %s", r.calls, want)
}
if back, err := sdkstep.AskAs[nodebuildnode.StepAsk](a); err != nil || back.Build == nil || back.Build.Module != "m" {
t.Errorf("the ask as another seat's type: %+v, %v", back, err)
}
}
// 086 A-C2 (ADR 0666 §7, §8, §21; design 47 *Asked on a work queue, ended by an event*, test 65): a pipeline step ask
// runs its one level as a check of it, carries the packages the runner kept at their digest, keeps what it passed, says
// its counts, and says its end once on its own node; a level with no tests ends nothing to run, a fault of the source
// could not run, and a transport fault or an end that could not be said is handed back unsaid, to be given again.
func TestRunAskRunsAPipelineStepAndSaysItsEnd(t *testing.T) {
type said struct{ ends []sdkstep.StepEnded }
stepOf := func(step string, kept ...sdkstep.KeptResult) sdkstep.StepAsk {
return sdkstep.StepAsk{ID: sdkstep.NewID(), Kind: sdkstep.AskStep, Language: sdkstep.AskGo, Asker: "mesh-delivery-pipeline", At: time.Now(),
Step: &sdkstep.StepRun{Pipeline: "main.12.m", Step: step, Module: "m", Artifact: "m:program", Commit: runCommit, Fingerprint: "fp-1",
Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit, Dir: "modules/m"}, Kept: kept}}
}
for _, row := range []struct {
name string
ask sdkstep.StepAsk
digests map[string]string
tests map[string]bool
lint StepFunc
fetchErr error
sayErr error
outcome string // "" when nothing is said
counts int
carried []string
kept []string
passing bool // the error is a transport fault
noTests bool // the level holds no test, as the test body says it
}{
{name: "unit: the kept package carried, the other tested and kept", ask: stepOf(sdkstep.StepUnit, sdkstep.KeptResult{Package: "x/a", Digest: "d-a", Level: "unit"}),
digests: map[string]string{"x/a": "d-a", "x/b": "d-b"}, tests: map[string]bool{"x/b": true}, outcome: sdkstep.OutcomePassed, counts: 1, carried: []string{"x/a"}, kept: []string{"x/b"}},
{name: "unit failed: said with why, nothing kept", ask: stepOf(sdkstep.StepUnit), digests: map[string]string{"x/b": "d-b"}, tests: map[string]bool{"x/b": false},
outcome: sdkstep.OutcomeFailed, counts: 1},
{name: "integration with no tests: nothing to run", ask: stepOf(sdkstep.StepIntegration), digests: map[string]string{}, noTests: true, outcome: sdkstep.OutcomeNothingToRun},
{name: "lint failed", ask: stepOf(sdkstep.StepLint), lint: func(context.Context, map[string]any) (any, error) {
return LintAnswer{Failed: []sdkstep.Failure{{Package: "modules/m", Test: "gofmt", Cause: "not as gofmt writes them"}}}, nil
}, outcome: sdkstep.OutcomeFailed},
{name: "lint passed", ask: stepOf(sdkstep.StepLint), lint: func(context.Context, map[string]any) (any, error) { return LintAnswer{}, nil }, outcome: sdkstep.OutcomePassed},
{name: "no lint body: nothing to run", ask: stepOf(sdkstep.StepLint), outcome: sdkstep.OutcomeNothingToRun},
{name: "the source cannot be fetched: could not run", ask: stepOf(sdkstep.StepUnit), fetchErr: Refuse("no commit %s", runCommit), outcome: sdkstep.OutcomeCouldNotRun},
{name: "a transport fault: unsaid, given again", ask: stepOf(sdkstep.StepUnit), fetchErr: &sdkstep.Passing{Err: errors.New("no responders")}, passing: true},
{name: "the end cannot be said: given again", ask: stepOf(sdkstep.StepLint), sayErr: errors.New("bus down"), passing: true},
} {
r := &recorder{fetch: map[string]any{"workspace": "/w"}, tests: row.tests, digests: row.digests}
s := r.steps()
if row.fetchErr != nil {
s.Fetch = func(context.Context, map[string]any) (any, error) { return nil, row.fetchErr }
}
s.Lint, s.Node = row.lint, "ace"
if row.noTests {
s.Test = func(context.Context, map[string]any) (any, error) {
return map[string]any{"passed": true, "tests": 0, "no_tests": true}, nil
}
}
var got said
s.SayStep = func(e sdkstep.StepEnded) error {
if row.sayErr != nil {
return row.sayErr
}
got.ends = append(got.ends, e)
return nil
}
out, err := RunAsk(context.Background(), s, row.ask)
if row.passing {
if !sdkstep.IsPassing(err) || len(got.ends) != 0 {
t.Errorf("%s: %v, said %+v; want a transport fault, nothing said", row.name, err, got.ends)
}
continue
}
if err != nil || len(got.ends) != 1 {
t.Fatalf("%s: %v, said %+v; want one end", row.name, err, got.ends)
}
e := got.ends[0]
if e.Outcome != row.outcome || e.Node != "ace" || e.ID != row.ask.ID || e.Fingerprint != "fp-1" || out.(StepAnswer).Ended.Outcome != row.outcome {
t.Errorf("%s: said %+v, want %s on ace for the ask", row.name, e, row.outcome)
}
if (row.outcome == sdkstep.OutcomeFailed || row.outcome == sdkstep.OutcomeCouldNotRun) && e.Said == "" {
t.Errorf("%s: %s and not why", row.name, row.outcome)
}
if n := 0; e.Tests != nil {
n = e.Tests.Unit + e.Tests.Integration
if n != row.counts {
t.Errorf("%s: counts %+v, want %d", row.name, e.Tests, row.counts)
}
} else if row.counts != 0 {
t.Errorf("%s: no counts, want %d", row.name, row.counts)
}
pkgs := func(ks []sdkstep.KeptResult) (out []string) {
for _, k := range ks {
out = append(out, k.Package)
}
return out
}
if !slices.Equal(pkgs(e.Carried), row.carried) || !slices.Equal(pkgs(e.Kept), row.kept) {
t.Errorf("%s: carried %v kept %v, want %v and %v", row.name, pkgs(e.Carried), pkgs(e.Kept), row.carried, row.kept)
}
if _, err := sdkstep.EncodeStepEnded(e); err != nil {
t.Errorf("%s: the end does not encode: %v", row.name, err)
}
}
}