A node away during the switch finds its last declaration on the stream; once every node's have names the node-engine registered, no node reads one, so the stream and the nodes' consumers on it go, said once, and a raise leaves them out (ADR 0539 clause 11).
113 lines
5.3 KiB
Go
113 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// fakeBuckets is the bus's buckets by name, for deleting.
|
|
type fakeBuckets map[string]bool
|
|
|
|
func (f fakeBuckets) DeleteKeyValue(_ context.Context, bucket string) error {
|
|
if !f[bucket] {
|
|
return jetstream.ErrBucketNotFound
|
|
}
|
|
delete(f, bucket)
|
|
return nil
|
|
}
|
|
|
|
// 1945/AC14 (unit half): the first start deletes the merge window's, the silent sends' and the building plans' buckets
|
|
// and says each once; a later start finds none and says nothing; a bucket of anything else is left alone.
|
|
func TestTheFirstStartDeletesThePlansBucketsAndSaysEachOnce(t *testing.T) {
|
|
bus := fakeBuckets{"mesh-controller_merge-window": true, "mesh-controller_rollout-silent": true,
|
|
"mesh-controller_plans-building": true, "mesh-controller_grants": true}
|
|
var said []string
|
|
deleted, err := deletePlanStateBuckets(context.Background(), bus, func(b string) { said = append(said, b) })
|
|
if err != nil || len(deleted) != 3 || strings.Join(said, " ") != strings.Join(broker.PlanStateBuckets, " ") {
|
|
t.Fatalf("first start: deleted %v, said %v, %v", deleted, said, err)
|
|
}
|
|
if !bus["mesh-controller_grants"] || len(bus) != 1 {
|
|
t.Errorf("the bus holds %v after the first start", bus)
|
|
}
|
|
said = nil
|
|
if deleted, err := deletePlanStateBuckets(context.Background(), bus, func(b string) { said = append(said, b) }); err != nil || len(deleted)+len(said) != 0 {
|
|
t.Errorf("a later start deleted %v and said %v (%v)", deleted, said, err)
|
|
}
|
|
}
|
|
|
|
// 1945/AC6 (unit half): the controller this one replaced is composed on its node from its own build's manifest at its
|
|
// own fingerprint: the composition holds what that manifest declares, not what the controller registered now declares.
|
|
func TestTheReplacedControllerIsComposedFromItsOwnManifest(t *testing.T) {
|
|
now := catalogue.Manifest{Module: controllerModule, Version: "6", Resources: []map[string]any{
|
|
{"id": "settings", "type": "file", "path": "/etc/mesh-controller/settings", "content": "six\n"}}}
|
|
before := catalogue.Manifest{Module: controllerModule, Version: "5", Resources: []map[string]any{
|
|
{"id": "settings", "type": "file", "path": "/etc/mesh-controller/settings", "content": "five\n"}}}
|
|
in := compose.Input{Catalogue: map[string]catalogue.Manifest{controllerModule: now},
|
|
Nodes: []compose.Node{{Name: "anchor", Assigned: []string{controllerModule}, Stated: true}}}
|
|
fp := sdkdelivery.SourcePrefix2 + strings.Repeat("5", 64)
|
|
c, err := replacedComposition(in, replacedController{node: "anchor", fp: fp, manifest: before})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Module != controllerModule || c.FP != fp || c.Node != "anchor" || len(c.Resources) != 1 {
|
|
t.Fatalf("composed %s at %s on %s with %d resources", c.Module, c.FP, c.Node, len(c.Resources))
|
|
}
|
|
var f struct{ Content string }
|
|
_ = json.Unmarshal(c.Resources[0], &f)
|
|
if f.Content != "five\n" {
|
|
t.Errorf("the replaced controller's composition holds %q, its own manifest says five", f.Content)
|
|
}
|
|
if in.Catalogue[controllerModule].Version != "6" {
|
|
t.Errorf("composing the replaced controller changed the Input's catalogue")
|
|
}
|
|
}
|
|
|
|
// 1945/AC6 (unit half): the controller replaced is the newest earlier build of the controller that worked and names a
|
|
// fingerprint of the new form, not the one registered now.
|
|
func TestTheReplacedControllerIsItsNewestEarlierBuild(t *testing.T) {
|
|
now, before, older := sdkdelivery.SourcePrefix2+strings.Repeat("6", 64), sdkdelivery.SourcePrefix2+strings.Repeat("5", 64),
|
|
sdkdelivery.SourcePrefix2+strings.Repeat("4", 64)
|
|
m := []byte(`{"module":"mesh-controller","version":"5"}`)
|
|
builds := []inventory.Build{
|
|
{ID: "c6", SourceFingerprint: now, Manifest: m},
|
|
{ID: "c5-failed", SourceFingerprint: before, Failed: "no", Manifest: m},
|
|
{ID: "c5", SourceFingerprint: before, Manifest: m},
|
|
{ID: "c4", SourceFingerprint: older, Manifest: m},
|
|
}
|
|
if b, ok := replacedBuild(builds, now); !ok || b.ID != "c5" {
|
|
t.Errorf("the replaced build is %+v (%v), want c5", b, ok)
|
|
}
|
|
if _, ok := replacedBuild(builds[:1], now); ok {
|
|
t.Errorf("a controller with no earlier build replaced one")
|
|
}
|
|
}
|
|
|
|
// 1945/AC14 (unit half): the declarations' stream stays while one node's have names another node-engine than the one
|
|
// registered, or none, and goes once every node's names it.
|
|
func TestTheDeclarationsGoOnlyOnceEveryNodeRunsTheNewEngine(t *testing.T) {
|
|
n6 := sdkdelivery.SourcePrefix2 + strings.Repeat("6", 64)
|
|
nodes := []string{"ace", "g14", "novox"}
|
|
if declarationsDone(nodes, map[string]string{"ace": n6, "novox": n6}, n6) {
|
|
t.Errorf("done while g14 says nothing (it sleeps)")
|
|
}
|
|
if declarationsDone(nodes, map[string]string{"ace": n6, "g14": "src2:old", "novox": n6}, n6) {
|
|
t.Errorf("done while g14 runs the old node-engine")
|
|
}
|
|
if !declarationsDone(nodes, map[string]string{"ace": n6, "g14": n6, "novox": n6}, n6) {
|
|
t.Errorf("not done with every node on the new node-engine")
|
|
}
|
|
if declarationsDone(nodes, map[string]string{"ace": "", "g14": "", "novox": ""}, "") || declarationsDone(nil, nil, n6) {
|
|
t.Errorf("done with no fingerprint registered, or no node")
|
|
}
|
|
}
|