Files
mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go/module_test.go
T
mesh-tools c33b83dce7
mesh/merge-gate head c33b83dce7d5 merged into trunk 940594b07478 — pass: builds mesh-build-go, mesh-build-node → g14, novox, shanks; no bus step; every mac…
mesh/repo-check head c33b83dce7d5 merged into trunk 940594b07478 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@e023f69989ab (merged as b5dbfbb7 into main, walk plan-17925769516…
Read a build module's steps and toolchains states with its own tools, so a step's result and the reported toolchain can be checked live (issue 1562)
2026-10-21 09:57:16 +00:00

201 lines
6.1 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
)
// keptStates stands in for the toolchains and the steps states.
type keptStates struct {
mu sync.Mutex
toolchains map[string]delivery.Toolchain
steps map[string][]delivery.Step
ended chan string
}
func newStates() *keptStates {
return &keptStates{toolchains: map[string]delivery.Toolchain{}, steps: map[string][]delivery.Step{}, ended: make(chan string, 16)}
}
func (s *keptStates) putToolchain(key string, value any) (uint64, error) {
tc, err := delivery.DecodeToolchain(value.(json.RawMessage))
if err != nil {
return 0, err
}
s.mu.Lock()
defer s.mu.Unlock()
s.toolchains[key] = tc
return 1, nil
}
func (s *keptStates) putStep(key string, value any) (uint64, error) {
st, err := delivery.DecodeStep(value.(json.RawMessage))
if err != nil {
return 0, err
}
s.mu.Lock()
s.steps[key] = append(s.steps[key], st)
s.mu.Unlock()
if st.State != delivery.StepRunning {
s.ended <- key
}
return 1, nil
}
func (s *keptStates) toolchain(key string) (delivery.Toolchain, bool) {
s.mu.Lock()
defer s.mu.Unlock()
tc, ok := s.toolchains[key]
return tc, ok
}
func (s *keptStates) waitEnd(t *testing.T) delivery.Step {
t.Helper()
select {
case key := <-s.ended:
s.mu.Lock()
defer s.mu.Unlock()
v := s.steps[key]
return v[len(v)-1]
case <-time.After(3 * time.Second):
t.Fatal("the step never ended on its key")
return delivery.Step{}
}
}
func moduleWith(states *keptStates, installed string) *Module {
return &Module{Node: "ace", Recipe: Recipe{Steps: "1", PostgresMajor: 18, NatsServer: "2.11.17"},
Probe: func(context.Context) (string, error) { return installed, nil },
Toolchains: states.putToolchain, Steps: states.putStep, Ctx: context.Background()}
}
// 1483/AC2: a probe answering go1.27.1: the bundle's start writes ace.go with that version and the recipe's, and
// the health tool answers healthy.
func TestStartWritesTheToolchainAndHealthIsHealthy(t *testing.T) {
states := newStates()
m := moduleWith(states, "go1.27.1")
ctx, stop := context.WithCancel(context.Background())
defer stop()
go m.Start(ctx)
var tc delivery.Toolchain
for deadline := time.Now().Add(3 * time.Second); time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) {
if got, ok := states.toolchain("ace.go"); ok {
tc = got
break
}
}
if tc.Version != "go1.27.1" || tc.Recipe != m.Recipe.Version() || tc.Language != "go" {
t.Fatalf("ace.go = %+v, want go1.27.1 and recipe %s", tc, m.Recipe.Version())
}
if a := m.Health(ctx); a.State != health.StateHealthy || !a.Healthy {
t.Errorf("health = %+v, want healthy", a)
}
}
// 1572/AC2 (ADR 0477 §2): a module whose go cannot be run refuses every step tool, naming the cause, and its health is
// unhealthy; no step runs.
func TestAGoThatCannotBeRunRefusesEveryStepAndHealthIsUnhealthy(t *testing.T) {
states := newStates()
m := moduleWith(states, "")
m.Probe = func(context.Context) (string, error) {
return "", errors.New("exec: go: executable file not found in $PATH")
}
ran := false
run := func(context.Context, map[string]any) (any, error) { ran = true; return "done", nil }
steps := append([]StepTool{
{Name: "long_probe", Long: true, Run: run},
{Name: "short_probe", Run: run},
}, m.StepTools()...)
for _, tool := range m.Tools(steps) {
if tool.Name == StatusTool || tool.Name == HealthTool || tool.Name == StepReadTool || tool.Name == StepsReadTool || tool.Name == ToolchainsReadTool {
continue
}
var reason string
out, err := tool.Run(map[string]any{})
if isLong(steps, tool.Name) {
if err != nil {
t.Fatalf("%s: %v", tool.Name, err)
}
if out.(map[string]any)["step"] == "" {
t.Fatalf("%s answered no step id: %v", tool.Name, out)
}
if st := states.waitEnd(t); st.State != delivery.StepRefused {
t.Errorf("%s: the step ended %s, want refused", tool.Name, st.State)
} else {
reason = st.Reason
}
} else {
if err == nil {
t.Fatalf("%s: answered %v, want a refusal", tool.Name, out)
}
reason = err.Error()
}
if !strings.Contains(reason, "executable file not found") {
t.Errorf("%s: the refusal %q does not name the cause", tool.Name, reason)
}
}
if ran {
t.Errorf("a step ran on a node whose go cannot be run")
}
if a := m.Health(context.Background()); a.State != health.StateUnhealthy || !strings.Contains(a.Why, "executable file not found") {
t.Errorf("health = %+v, want unhealthy naming the cause", a)
}
}
func isLong(steps []StepTool, name string) bool {
for _, s := range steps {
if s.Name == name {
return s.Long
}
}
return false
}
// 1483/AC4, 1572/AC3: recipes differing in the steps' version, the postgres major or the nats-server version have
// different recipe versions; equal recipes have the same one.
func TestRecipeVersionMovesWithEachOfItsValues(t *testing.T) {
base := Recipe{Steps: "1", PostgresMajor: 18, NatsServer: "2.11.17"}
if base.Version() != (Recipe{Steps: "1", PostgresMajor: 18, NatsServer: "2.11.17"}).Version() {
t.Errorf("one recipe has two versions")
}
seen := map[string]string{base.Version(): "base"}
for name, r := range map[string]Recipe{
"postgres major": {Steps: "1", PostgresMajor: 19, NatsServer: "2.11.17"},
"nats-server": {Steps: "1", PostgresMajor: 18, NatsServer: "2.11.18"},
"steps": {Steps: "2", PostgresMajor: 18, NatsServer: "2.11.17"},
} {
v := r.Version()
if was, dup := seen[v]; dup {
t.Errorf("the recipe differing in %s has the version of the %s recipe: %s", name, was, v)
}
seen[v] = name
}
}
func TestTheRecipeNamesPostgres18AndTheLibrarysNatsVersionAndNoGo(t *testing.T) {
r := Current()
if r.PostgresMajor != 18 {
t.Errorf("recipe = %+v", r)
}
if r.NatsServer == "" || strings.HasPrefix(r.NatsServer, "v") {
t.Errorf("nats-server version = %q", r.NatsServer)
}
}
func TestParseGoVersion(t *testing.T) {
if v, err := parseGoVersion("go version go1.27.1 linux/amd64\n"); err != nil || v != "go1.27.1" {
t.Errorf("= %q, %v", v, err)
}
if _, err := parseGoVersion("not go"); err == nil {
t.Errorf("accepted a text that is not `go version`'s")
}
}