232 lines
7.5 KiB
Go
232 lines
7.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"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", Node: "v26.10.0", Lock: "lock"},
|
|
Probe: func(context.Context) (string, error) { return installed, nil },
|
|
Toolchains: states.putToolchain, Steps: states.putStep, Ctx: context.Background()}
|
|
}
|
|
|
|
// 1488/AC3: a probe answering v26.10.0: the bundle's start writes ace.typescript with that version and the recipe's,
|
|
// and the health tool answers healthy.
|
|
func TestStartWritesTheToolchainAndHealthIsHealthy(t *testing.T) {
|
|
states := newStates()
|
|
m := moduleWith(states, "v26.10.0")
|
|
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.typescript"); ok {
|
|
tc = got
|
|
break
|
|
}
|
|
}
|
|
if tc.Version != "v26.10.0" || tc.Recipe != m.Recipe.Version() || tc.Language != "typescript" {
|
|
t.Fatalf("ace.typescript = %+v, want v26.10.0 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)
|
|
}
|
|
}
|
|
|
|
// 1488/AC3: a probe answering v26.11.0: every step tool's step ends refused naming both versions, ace.typescript
|
|
// holds v26.11.0, and the health tool answers unhealthy naming both.
|
|
func TestAnotherNodeRefusesEveryStepAndHealthIsUnhealthy(t *testing.T) {
|
|
states := newStates()
|
|
m := moduleWith(states, "v26.11.0")
|
|
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},
|
|
}, (&Module{}).StepTools()...)
|
|
for _, tool := range m.Tools(steps) {
|
|
if tool.Name == StatusTool || tool.Name == HealthTool {
|
|
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)
|
|
}
|
|
st := states.waitEnd(t)
|
|
if st.State != delivery.StepRefused {
|
|
t.Errorf("%s: the step ended %s, want refused", tool.Name, st.State)
|
|
}
|
|
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, "v26.11.0") || !strings.Contains(reason, "v26.10.0") {
|
|
t.Errorf("%s: the refusal %q does not name both versions", tool.Name, reason)
|
|
}
|
|
}
|
|
if ran {
|
|
t.Errorf("a step ran on a node that refuses")
|
|
}
|
|
if tc, _ := states.toolchain("ace.typescript"); tc.Version != "v26.11.0" {
|
|
t.Errorf("ace.typescript holds %q, want the installed v26.11.0", tc.Version)
|
|
}
|
|
a := m.Health(context.Background())
|
|
if a.State != health.StateUnhealthy || !strings.Contains(a.Why, "v26.11.0") || !strings.Contains(a.Why, "v26.10.0") {
|
|
t.Errorf("health = %+v, want unhealthy naming both versions", a)
|
|
}
|
|
}
|
|
|
|
func isLong(steps []StepTool, name string) bool {
|
|
for _, s := range steps {
|
|
if s.Name == name {
|
|
return s.Long
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 1488/AC2: the recipe version moves with one byte of the lock and with the Node version, and with the recipe's own
|
|
// version; equal recipes have the same one.
|
|
func TestRecipeVersionMovesWithEachOfItsValues(t *testing.T) {
|
|
lock := []byte(`{"lockfileVersion":3}`)
|
|
base := Recipe{Steps: "1", Node: "v26.10.0", Lock: LockHash(lock)}
|
|
if base.Version() != (Recipe{Steps: "1", Node: "v26.10.0", Lock: LockHash(lock)}).Version() {
|
|
t.Errorf("one recipe has two versions")
|
|
}
|
|
seen := map[string]string{base.Version(): "base"}
|
|
for name, r := range map[string]Recipe{
|
|
"one byte of the lock": {Steps: "1", Node: "v26.10.0", Lock: LockHash([]byte(`{"lockfileVersion":4}`))},
|
|
"node": {Steps: "1", Node: "v26.11.0", Lock: LockHash(lock)},
|
|
"steps": {Steps: "2", Node: "v26.10.0", Lock: LockHash(lock)},
|
|
} {
|
|
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
|
|
}
|
|
}
|
|
|
|
// 1488/AC2: the recipe reads the lock file's bytes: a lock changed on disk is another recipe version, a lock missing
|
|
// is an error.
|
|
func TestReadRecipeHashesTheLockFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "package-lock.json")
|
|
if _, err := ReadRecipe(path); err == nil {
|
|
t.Errorf("a missing lock was accepted")
|
|
}
|
|
os.WriteFile(path, []byte("a"), 0o600)
|
|
a, err := ReadRecipe(path)
|
|
if err != nil || a.Node != "v26.10.0" {
|
|
t.Fatalf("= %+v, %v", a, err)
|
|
}
|
|
os.WriteFile(path, []byte("b"), 0o600)
|
|
b, _ := ReadRecipe(path)
|
|
if a.Version() == b.Version() {
|
|
t.Errorf("one byte of the lock did not move the recipe version")
|
|
}
|
|
}
|
|
|
|
// 1488/AC3: a lock changed under a running module is read at the next check, and a lock that cannot be read refuses.
|
|
func TestACheckReadsTheRecipeAgain(t *testing.T) {
|
|
states := newStates()
|
|
m := moduleWith(states, "v26.10.0")
|
|
r2 := Recipe{Steps: "1", Node: "v26.10.0", Lock: "other"}
|
|
m.Reload = func() (Recipe, error) { return r2, nil }
|
|
if err := m.Check(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tc, _ := states.toolchain("ace.typescript"); tc.Recipe != r2.Version() {
|
|
t.Errorf("ace.typescript holds recipe %s, want the reloaded %s", tc.Recipe, r2.Version())
|
|
}
|
|
m.Reload = func() (Recipe, error) { return Recipe{}, errors.New("no lock") }
|
|
if err := m.Check(context.Background()); err == nil || m.Health(context.Background()).Healthy {
|
|
t.Errorf("a lock that cannot be read neither refused nor made the module unhealthy: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseNodeVersion(t *testing.T) {
|
|
if v, err := parseNodeVersion("v26.10.0\n"); err != nil || v != "v26.10.0" {
|
|
t.Errorf("= %q, %v", v, err)
|
|
}
|
|
if _, err := parseNodeVersion("not node"); err == nil {
|
|
t.Errorf("accepted a text that is not `node --version`'s")
|
|
}
|
|
}
|