Files
mesh-catalog/modules/mesh-build-go/buildstep/askrun_test.go
T
jschoubben 119e0a6f13 A-C2: a build module runs the pipeline step kind step and says its end on its node (issue 1966)
The runner asks a lint or test step of an artifact as the build seat's accepted kind step, and
hears its end as the seat's step-ended on the node that ran it (ADR 0666 sections 7, 8, 21;
design 47 *The steps and who serves them* line 571 and *Asked on a work queue, ended by an event*
lines 583-590 at hq b229f5c4). The build modules' shared package runs a test step as a check of
its one level, carries the packages whose digest the ask names kept, keeps what it passed and
says the counts; a level with no tests ends nothing to run, a source fault could not run, and a
transport fault or an end that could not be said is handed back unsaid, so the work queue gives
the step again and it ends once under its ask's id. The neutral forms are the domain SDK's
(sdk/buildstep pipelinestep.go, mesh-delivery dc0df1b5); the Go module says the end through the
runtime's seat publish.

Milestone 47/G, research 086 area A.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ

(Joined from 72fee3c6.)
Builds: design 47 G §22, §24, §86, §116, §140 (BLD3, BLD4)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 14:32:36 +01:00

472 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,
Names: sdkstep.StepNames{Modules: []string{"m"}, Repositories: []string{"novox/x"}, Branches: []string{"main"}}}}
}
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)
}
}
}