262 lines
9.3 KiB
Go
262 lines
9.3 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
testCommit = "0123456789abcdef0123456789abcdef01234567"
|
|
testAskID = "m2kx9a1b3c4d5e"
|
|
)
|
|
|
|
func goodBuild() StepAsk {
|
|
return StepAsk{ID: testAskID, Kind: AskDelivery, Language: AskGo, Asker: "mesh-delivery-builder",
|
|
At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC),
|
|
Build: &BuildJob{Module: "nats", Path: "modules/nats", Fetch: &Fetch{Repository: "novox/mesh-catalog", Commit: testCommit},
|
|
Artifacts: []AskArtifact{{Artifact: "go"}}}}
|
|
}
|
|
|
|
func goodCheck() StepAsk {
|
|
return StepAsk{ID: testAskID, Kind: AskCheck, Language: AskGo, Asker: "mesh-delivery-checker",
|
|
At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC),
|
|
Check: &CheckJob{Fetch: Fetch{Repository: "novox/mesh-catalog", Commit: testCommit}, Levels: []string{LevelUnit}}}
|
|
}
|
|
|
|
// The ask the three build seats share is one neutral form: it round-trips, and every ill-formed ask is refused,
|
|
// naming what is wrong (the rules of the old delivery.StepAsk, now the shared runner's).
|
|
func TestAskCheckAndRoundTrip(t *testing.T) {
|
|
for _, a := range []StepAsk{goodBuild(), goodCheck()} {
|
|
raw, err := EncodeStepAsk(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := DecodeStepAsk(raw)
|
|
if err != nil || back.ID != a.ID || back.V != StepAskVersion {
|
|
t.Fatalf("round trip: %+v, %v", back, err)
|
|
}
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
edit func(*StepAsk)
|
|
want string
|
|
}{
|
|
{"no id", func(a *StepAsk) { a.ID = "x" }, "not an ask id"},
|
|
{"unknown kind", func(a *StepAsk) { a.Kind = "build" }, "ask kind"},
|
|
{"unknown language", func(a *StepAsk) { a.Language = "rust" }, "ask language"},
|
|
{"no time", func(a *StepAsk) { a.At = time.Time{} }, "names no time"},
|
|
{"recheck build", func(a *StepAsk) { a.Kind = AskRecheck }, "recheck stream"},
|
|
{"two jobs", func(a *StepAsk) { a.Check = goodCheck().Check }, "exactly one"},
|
|
{"needs on a build", func(a *StepAsk) { a.Needs = []string{NeedNodeBuildNode} }, "names needs"},
|
|
{"unknown registry seat", func(a *StepAsk) { a.Build.Artifacts[0].Seat = "somewhere" }, "no registry seat"},
|
|
{"version without seat", func(a *StepAsk) { a.Build.Artifacts[0].Version = "1.0.0" }, "names no seat"},
|
|
{"no path", func(a *StepAsk) { a.Build.Path = "" }, "names no directory"},
|
|
{"short commit", func(a *StepAsk) { a.Build.Fetch.Commit = "abc" }, "full commit"},
|
|
{"bad package pin", func(a *StepAsk) { a.Packages = PackageVersions{"m": "main"} }, "neither a full commit"},
|
|
} {
|
|
a := goodBuild()
|
|
c.edit(&a)
|
|
if _, err := EncodeStepAsk(a); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: %v, want %q", c.name, err, c.want)
|
|
}
|
|
}
|
|
chk := goodCheck()
|
|
chk.Check.Levels = []string{"nightly"}
|
|
if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "nightly") {
|
|
t.Errorf("a level: %v", err)
|
|
}
|
|
chk = goodCheck()
|
|
chk.Language = AskImage
|
|
if _, err := EncodeStepAsk(chk); err == nil || !strings.Contains(err.Error(), "no tests") {
|
|
t.Errorf("an image check: %v", err)
|
|
}
|
|
if _, err := DecodeStepAsk([]byte(`{"v":2}`)); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("another version: %v", err)
|
|
}
|
|
}
|
|
|
|
// The seat's value and the neutral form are the same JSON: AskOf takes any seat's type (here its shape, with a
|
|
// pointer time), AskAs gives one back.
|
|
func TestAskOfAnySeatsType(t *testing.T) {
|
|
type seatAsk struct {
|
|
V int64 `json:"v,omitempty"`
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
Lang string `json:"language"`
|
|
Asker string `json:"asker"`
|
|
At *time.Time `json:"at,omitempty"`
|
|
Check *struct {
|
|
Fetch Fetch `json:"fetch"`
|
|
Levels []string `json:"levels"`
|
|
} `json:"check,omitempty"`
|
|
}
|
|
at := time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)
|
|
s := seatAsk{V: 1, ID: testAskID, Kind: AskCheck, Lang: AskGo, Asker: "mesh-delivery-checker", At: &at}
|
|
s.Check = &struct {
|
|
Fetch Fetch `json:"fetch"`
|
|
Levels []string `json:"levels"`
|
|
}{Fetch{Repository: "novox/mesh-catalog", Commit: testCommit}, []string{LevelUnit}}
|
|
a, err := AskOf(s)
|
|
if err != nil || a.Check == nil || !a.At.Equal(at) || a.Check.Fetch.Commit != testCommit {
|
|
t.Fatalf("%+v %v", a, err)
|
|
}
|
|
back, err := AskAs[seatAsk](a)
|
|
if err != nil || back.ID != s.ID || !back.At.Equal(at) {
|
|
t.Fatalf("%+v %v", back, err)
|
|
}
|
|
s.Check = nil
|
|
if _, err := AskOf(s); err == nil {
|
|
t.Error("an ask with no job is accepted")
|
|
}
|
|
}
|
|
|
|
func TestAskSubjectsStreamsAndKeys(t *testing.T) {
|
|
for _, l := range AskLanguages {
|
|
for _, k := range AskKinds {
|
|
subj, err := AskSubject(l, k)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gl, gk, err := ParseAskSubject(subj)
|
|
if err != nil || gl != l || gk != k {
|
|
t.Errorf("%s: %s %s %v", subj, gl, gk, err)
|
|
}
|
|
}
|
|
}
|
|
if got, _ := AskWorker(AskGo, AskCheck); got != "SEAT_NODE_BUILD_GO_CHECK_worker" {
|
|
t.Errorf("worker %q", got)
|
|
}
|
|
if got, _ := AskStream(AskImage); got != "SEAT_NODE_BUILD_IMAGE" {
|
|
t.Errorf("stream %q", got)
|
|
}
|
|
if _, _, err := ParseAskSubject("mesh.seat.node-build-go.accept.nothing"); err == nil {
|
|
t.Error("an unknown kind parses")
|
|
}
|
|
if _, err := AskSubject("rust", AskCheck); err == nil {
|
|
t.Error("an unknown language has a subject")
|
|
}
|
|
if k, err := AskStepKey("ace", testAskID); err != nil || k != "ace."+testAskID {
|
|
t.Errorf("%q %v", k, err)
|
|
}
|
|
if w, _ := AskStepWatch(testAskID); w != "*."+testAskID {
|
|
t.Errorf("watch %q", w)
|
|
}
|
|
if k, _ := PauseKey("ace"); k != "paused.ace" {
|
|
t.Errorf("pause key %q", k)
|
|
}
|
|
if k, _ := CapacityKey("ace"); k != "capacity.ace" {
|
|
t.Errorf("capacity key %q", k)
|
|
}
|
|
if _, err := CapacityKey("a.b"); err == nil {
|
|
t.Error("a dotted node keys")
|
|
}
|
|
}
|
|
|
|
func TestDeclinesNeedsAndLevelDigest(t *testing.T) {
|
|
if got := Lacks([]string{NeedNodeBuildNode}, nil); len(got) != 1 {
|
|
t.Errorf("a nil has lacks all: %v", got)
|
|
}
|
|
if got := Lacks([]string{NeedNodeBuildNode}, func(string) bool { return true }); len(got) != 0 {
|
|
t.Errorf("%v", got)
|
|
}
|
|
step := Step{State: StepRefused, Result: json.RawMessage(`{"declined":true,"needs":["node-build-node"],"lacks":["node-build-node"]}`)}
|
|
if d, ok := DeclinedOf(step); !ok || d.Lacks[0] != NeedNodeBuildNode {
|
|
t.Errorf("%+v %v", d, ok)
|
|
}
|
|
step.State = StepFailed
|
|
if _, ok := DeclinedOf(step); ok {
|
|
t.Error("a failed step is a decline")
|
|
}
|
|
var l LevelAnswer
|
|
raw := `{"level":"unit","digest":{"packages":{"b":"2","a":"1"},"needs":[{"package":"a","needs":["node-build-node"]}]},"unmet":[{"package":"a","needs":["node-build-node"]}]}`
|
|
if err := json.Unmarshal([]byte(raw), &l); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p := l.Digest.Packages; len(p) != 2 || p[0].Package != "a" || l.Digest.Map()["b"] != "2" || l.Digest.NeedsOf("a")[0] != NeedNodeBuildNode {
|
|
t.Errorf("%+v", l.Digest)
|
|
}
|
|
if got := UnmetNeeds([]LevelAnswer{l, l}); len(got) != 1 {
|
|
t.Errorf("%v", got)
|
|
}
|
|
}
|
|
|
|
func TestPauseCapacityAndComposerKey(t *testing.T) {
|
|
raw, err := EncodePause(Pause{Paused: true, By: "operator@shanks", At: time.Date(2026, 11, 5, 10, 0, 0, 0, time.UTC)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p, err := DecodePause(raw); err != nil || !p.Paused || p.V != Version {
|
|
t.Fatalf("%+v %v", p, err)
|
|
}
|
|
if _, err := DecodePause([]byte(`{"v":9}`)); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("%v", err)
|
|
}
|
|
craw, _ := EncodeCapacity(Capacity{Effective: 2, Configured: 2, Why: "fits"})
|
|
if c, err := DecodeCapacity(craw); err != nil || c.Effective != 2 {
|
|
t.Fatalf("%+v %v", c, err)
|
|
}
|
|
tc := ToolchainUsed{Language: "go", Version: "1.26.0", Recipe: "1"}
|
|
if k := ComposerKey("novox/r", testCommit, "d", "p", tc); k != ComposerKey("novox/r", testCommit, "d", "p", tc) || !strings.HasPrefix(k, "composer-") || len(k) != len("composer-")+16 {
|
|
t.Errorf("composer key %q", k)
|
|
}
|
|
if ComposerKey("novox/r", testCommit, "d", "p", tc) == ComposerKey("novox/r", testCommit, "d2", "p", tc) {
|
|
t.Error("the directory is not in the key")
|
|
}
|
|
}
|
|
|
|
// The part `build-step` is one layout for the three build seats: each seat's fixtures read strictly in the neutral
|
|
// forms (an unknown field, or a field this package lacks, fails). Where the generator cannot share a part, this is
|
|
// what holds the three seats' types and this form equal.
|
|
func TestTheThreeSeatsFixturesFitTheNeutralForms(t *testing.T) {
|
|
strict := func(t *testing.T, raw json.RawMessage, into any) {
|
|
t.Helper()
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(into); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
seen := 0
|
|
for _, seat := range []string{"node-build-go", "node-build-image", "node-build-node"} {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "contracts", "seats", seat, "fixtures", "states.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fixtures []struct {
|
|
Name string `json:"name"`
|
|
Of string `json:"of"`
|
|
Given json.RawMessage `json:"given"`
|
|
}
|
|
if err := json.Unmarshal(raw, &fixtures); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, f := range fixtures {
|
|
t.Run(seat+"/"+f.Name, func(t *testing.T) {
|
|
switch {
|
|
case strings.HasPrefix(f.Of, "accept:"):
|
|
seen++
|
|
strict(t, f.Given, &StepAsk{})
|
|
case strings.HasPrefix(f.Of, "state:pauses/paused."):
|
|
seen++
|
|
strict(t, f.Given, &Pause{})
|
|
case strings.HasPrefix(f.Of, "state:pauses/capacity."):
|
|
seen++
|
|
strict(t, f.Given, &Capacity{})
|
|
case strings.HasPrefix(f.Of, "state:steps/"):
|
|
seen++
|
|
strict(t, f.Given, &Step{})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
if seen < 12 {
|
|
t.Errorf("only %d fixtures read", seen)
|
|
}
|
|
}
|