156 lines
6.5 KiB
Go
156 lines
6.5 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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} }),
|
|
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) delivery.StepAsk {
|
|
a := delivery.StepAsk{ID: NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: "t", At: time.Now()}
|
|
switch j := job.(type) {
|
|
case delivery.BuildJob:
|
|
a.Build = &j
|
|
case delivery.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.
|
|
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 }
|
|
out, err := RunAsk(context.Background(), steps, ask(delivery.BuildJob{Module: "m", Path: "modules/m", Publish: true,
|
|
Fetch: map[string]any{"repository": "novox/x", "commit": runCommit},
|
|
Artifacts: []delivery.AskArtifact{{Artifact: "base", From: "registry/b@sha256:00"}, {Artifact: "bundle"}, {Artifact: "lib"}}}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := strings.Join(r.calls, "\n")
|
|
want := `copy {"artifact":"base","from":"registry/b@sha256:00","module":"m"}
|
|
fetch {"commit":"` + runCommit + `","repository":"novox/x"}
|
|
build {"artifact":"bundle","module":"modules/m","publish":true,"workspace":"/w/1"}
|
|
build {"artifact":"lib","module":"modules/m","publish":true,"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)
|
|
}
|
|
a := out.(delivery.BuildAnswer)
|
|
if len(a.Artifacts) != 3 || a.Artifacts[1].Reference != "artifact-store://bundle" {
|
|
t.Fatalf("answer %+v", a)
|
|
}
|
|
}
|
|
|
|
// A build ask for another toolchain is refused, so its step ends refused and the asker reads why.
|
|
func TestRunAskRefusesAnotherToolchain(t *testing.T) {
|
|
r := &recorder{fetch: map[string]any{"workspace": "/w"}}
|
|
s := r.steps()
|
|
s.Toolchain = func(context.Context) (delivery.ToolchainUsed, error) {
|
|
return delivery.ToolchainUsed{Version: "go1.27", Recipe: "3"}, nil
|
|
}
|
|
_, err := RunAsk(context.Background(), s, ask(delivery.BuildJob{Module: "m", Path: ".", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit},
|
|
Artifacts: []delivery.AskArtifact{{Artifact: "bundle"}}, Toolchain: &delivery.ToolchainUsed{Version: "go1.26", Recipe: "3"}}))
|
|
var ref *Refusal
|
|
if !errors.As(err, &ref) || len(r.calls) != 0 {
|
|
t.Fatalf("err %v, calls %v", err, r.calls)
|
|
}
|
|
}
|
|
|
|
// 1887/A3, issue 1887 design limit 1: a check ask digests the level, takes the kept verdicts, tests the packages whose
|
|
// tests changed first, and stops at the first failure.
|
|
func TestRunAskChecksAGoLevel(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(delivery.CheckJob{Level: delivery.LevelUnit, Kept: true,
|
|
Runner: "workspace", RunnerWhen: "pkg/b/", Fetch: map[string]any{"repository": "novox/x", "commit": runCommit, "base": runCommit}}))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := strings.Join(r.calls[1:], "\n")
|
|
want := `digest {"level":"unit","runner":"workspace","workspace":"/w"}
|
|
test {"level":"unit","packages":["x/pkg/b"],"runner":"workspace","workspace":"/w"}`
|
|
if got != want {
|
|
t.Fatalf("calls:\n%s\nwant:\n%s", got, want)
|
|
}
|
|
a := out.(delivery.CheckAnswer)
|
|
if len(a.Kept) != 1 || a.Kept[0].Package != "x/pkg/k" || a.Kept[0].Tests != 7 || len(a.Tests) != 1 || len(a.Digest) == 0 {
|
|
t.Fatalf("answer %+v", a)
|
|
}
|
|
}
|
|
|
|
// 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(delivery.CheckJob{Level: delivery.LevelUnit, Fetch: map[string]any{"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(delivery.CheckJob{Level: delivery.LevelUnit, Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}}))
|
|
if err != nil || len(r.calls) != 1 || len(out.(delivery.CheckAnswer).Tests) != 0 {
|
|
t.Fatalf("a conflict: %v %v", err, r.calls)
|
|
}
|
|
}
|