The ask carried the reserved version (§573), but the build modules' shared run gave each artifact's build only the artifact's own version, which no asker sets, so no build got MESH_VERSION and a Go package was packed as a candidate and never published. The run now gives every build the ask's version, and the builder names the registry seat each artifact of a versioned build is published through, so a Go package is published under its reserved version on go-package-registry. Builds: design 47 G §370, §573, §458; design 51 *Publishing* Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
487 lines
25 KiB
Go
487 lines
25 KiB
Go
package steprun
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
nodebuildgo "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo"
|
|
nodebuildnode "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildnode"
|
|
sdkstep "git.novox.be/novox/mesh-delivery/modules/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}
|
|
// Design 47 G §573, §370: the version the ask carries reaches each artifact's build; one the artifact names stays its own.
|
|
a.Version = "0.1.16"
|
|
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,"version":"0.1.16","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)
|
|
}
|
|
|
|
// Design 47 G A-286: a level that errors after the fetch merged ends the step failed with its answer all the same, the
|
|
// fetch and the levels so far beside the error, so the checker previews the merge from it.
|
|
r.calls = nil
|
|
steps := r.steps()
|
|
steps.Test = func(context.Context, map[string]any) (any, error) {
|
|
return nil, errors.New("the seat definitions in . do not generate")
|
|
}
|
|
out, err = RunAsk(context.Background(), steps, ask(sdkstep.CheckJob{Levels: []string{sdkstep.LevelUnit, sdkstep.LevelIntegration},
|
|
Fetch: sdkstep.Fetch{Repository: "novox/x", Commit: runCommit}}))
|
|
if failed, _ := out.(sdkstep.CheckAnswer); err == nil || !strings.Contains(err.Error(), "the unit level") || failed.Fetch.Workspace != "/w" ||
|
|
len(failed.Levels) != 1 || failed.Levels[0].Level != sdkstep.LevelUnit {
|
|
t.Fatalf("a level that errors: answer %+v, err %v; want the fetch and the unit level beside the error", out, err)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|