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…
201 lines
6.1 KiB
Go
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")
|
|
}
|
|
}
|