Files

237 lines
10 KiB
Go

package meshmoduleassignerclient
import (
"errors"
"fmt"
"reflect"
"strings"
"testing"
seat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
func TestAssignmentsClient(t *testing.T) {
t.Run("proposal id", func(t *testing.T) {
for id, ok := range map[string]bool{"p-20261028t183000-1a2b3c": true, "abc": true, "ab": false, "A-bc": false, "-abc": false, "a.bc": false, "": false} {
if (CheckProposalID(id) == nil) != ok {
t.Errorf("CheckProposalID(%q) = %v", id, CheckProposalID(id))
}
}
})
t.Run("key and file", func(t *testing.T) {
// Research 072 row 37: one node key and one key per module, any other refused.
for key, want := range map[string]struct {
node, module string
kind KeyKind
}{"g14.node": {"g14", "", NodeKind}, "g14.app": {"g14", "app", ModuleKind}} {
if n, m, k, err := ParseKey(key); err != nil || n != want.node || m != want.module || k != want.kind {
t.Errorf("ParseKey(%q) = %q, %q, %d, %v", key, n, m, k, err)
}
}
for _, bad := range []string{"want.g14.x", "g14", "g14.a.b", ".node"} {
if _, _, _, err := ParseKey(bad); err == nil {
t.Errorf("ParseKey(%q) took it", bad)
}
}
if k, err := NodeKey("g14"); err != nil || k != "g14.node" {
t.Errorf("NodeKey = %q, %v", k, err)
}
if k, err := ModuleKey("g14", "app"); err != nil || k != "g14.app" {
t.Errorf("ModuleKey = %q, %v", k, err)
}
if k, err := NodeKeys("g14"); err != nil || k != "g14.>" {
t.Errorf("NodeKeys = %q, %v", k, err)
}
for name, err := range map[string]error{"a dotted node": func() error { _, err := NodeKey("a.b"); return err }(),
"a module named node": func() error { _, err := ModuleKey("g14", "node"); return err }()} {
if err == nil {
t.Errorf("%s was taken", name)
}
}
})
g14 := seat.Assignments{Node: "g14", Revision: "abc",
Decided: seat.NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}},
Modules: []seat.PlacedModule{
{Module: "zsh"},
{Module: "app", Ports: map[string]int64{"8080": 18080}, Bindings: map[string]seat.Provider{"postgres": {Node: "novox", Module: "postgres"}}},
},
Pins: map[string]seat.Provider{"shell": {Node: "g14", Module: "zsh"}},
Holds: []seat.Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}},
}
novox := seat.Assignments{Node: "novox", Decided: seat.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []seat.PlacedModule{{Module: "postgres", Ports: map[string]int64{"5432": 5432}}},
Holds: []seat.Holding{{Claim: "mesh-example", Scope: "mesh", Module: "postgres"}}}
t.Run("value", func(t *testing.T) {
raw, err := EncodeAssignments(g14)
if err != nil {
t.Fatal(err)
}
back, err := DecodeAssignments(raw)
if err != nil || back.V != Version || !SameAssignments(back, g14) {
t.Fatalf("round trip: %+v, %v", back, err)
}
moved := g14
moved.Revision = "def"
if !SameAssignments(g14, moved) {
t.Error("a new revision alone counts as a change")
}
if _, err := DecodeAssignments([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) {
t.Errorf("version 9: %v", err)
}
for name, bad := range map[string]string{
"module twice": `{"v":1,"node":"g14","modules":[{"module":"a"},{"module":"a"}]}`,
"port not number": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"http":80}}]}`,
"host port zero": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"80":0}}]}`,
"binding no node": `{"v":1,"node":"g14","modules":[{"module":"a","bindings":{"pg":{"module":"pg"}}}]}`,
"pin no module": `{"v":1,"node":"g14","modules":[],"pins":{"pg":{"node":"x"}}}`,
"hold not placed": `{"v":1,"node":"g14","modules":[],"holds":[{"claim":"s","module":"a"}]}`,
"node not a name": `{"v":1,"node":"g.14","modules":[]}`,
"value not shaped": `{"v":1,"node":"g14","modules":{}}`,
} {
if _, err := DecodeAssignments([]byte(bad)); err == nil {
t.Errorf("%s: taken", name)
}
}
})
t.Run("as the composition reads it", func(t *testing.T) {
if got := Assigned(g14); !reflect.DeepEqual(got, []string{"zsh", "app"}) {
t.Errorf("Assigned = %v, want the file's order", got)
}
if got := ChosenPins(g14); !reflect.DeepEqual(got, map[string]catalogue.Chosen{"shell": {Node: "g14", Module: "zsh"}}) {
t.Errorf("ChosenPins = %v", got)
}
if got := Bound(g14); !reflect.DeepEqual(got, map[string]map[string]catalogue.Chosen{"app": {"postgres": {Node: "novox", Module: "postgres"}}}) {
t.Errorf("Bound = %v", got)
}
if got := HostPorts(g14); !reflect.DeepEqual(got, map[string]map[int]int{"app": {8080: 18080}}) {
t.Errorf("HostPorts = %v", got)
}
if got := Held(g14); !reflect.DeepEqual(got, []catalogue.Held{{Claim: "node-shell", Scope: "node", Node: "g14", Module: "zsh"}}) {
t.Errorf("Held = %v", got)
}
layered := seat.PlacedModule{Module: "app", HasMeshWide: true, Layer: map[string]any{"expose": map[string]any{"80": "mesh"}}}
want := []catalogue.Layer{
{From: catalogue.MeshWideLayer, Values: map[string]any{}},
{From: "g14", Values: map[string]any{"mode": "x"}, Placement: map[string]any{"expose": map[string]any{"80": "mesh"}}},
}
if got := Layers(layered, "g14", map[string]any{"mode": "x"}); !reflect.DeepEqual(got, want) {
t.Errorf("Layers = %#v, want %#v", got, want)
}
if got := Layers(seat.PlacedModule{Module: "a"}, "g14", map[string]any{"k": 1}); len(got) != 1 || got[0].From != "g14" {
t.Errorf("values alone make the node's layer: %#v", got)
}
if got := Layers(seat.PlacedModule{Module: "a", HasLayer: true}, "g14", nil); len(got) != 1 || got[0].Values == nil {
t.Errorf("an empty layer of the node's own is kept, with empty values: %#v", got)
}
if got := Layers(seat.PlacedModule{Module: "a"}, "g14", nil); got != nil {
t.Errorf("no layer and no value is no layer: %#v", got)
}
empty := seat.Assignments{Node: "x"}
if ChosenPins(empty) != nil || Bound(empty) != nil || HostPorts(empty) != nil || len(Held(empty)) != 0 {
t.Error("an empty assignments reads as something")
}
})
// Research 072 row 37: a node's assignments as its node key and module keys, written so a reader never takes a part of
// a write,
t.Run("one key per module and node", func(t *testing.T) {
values, err := AssignmentsValues(g14)
if err != nil || len(values) != 3 || values["g14.node"] == nil || values["g14.zsh"] == nil || values["g14.app"] == nil {
t.Fatalf("AssignmentsValues = %v, %v", values, err)
}
got, err := AssignmentsOf(values)
if err != nil || !SameAssignments(got["g14"], g14) || len(got) != 1 {
t.Fatalf("AssignmentsOf = %+v, %v", got, err)
}
if got := InsideAddresses(map[string]seat.Assignments{"g14": g14, "novox": novox, "x": {Node: "x"}, "y": {Node: "y", Decided: seat.NodeDecided{Address: " 10.77.0.1"}}}); strings.Join(got, ",") != "10.77.0.1,10.77.0.2" {
t.Errorf("InsideAddresses = %v", got)
}
keys := NewKeys()
rev := uint64(0)
apply := func(writes []KeyWrite, upTo int) {
t.Helper()
for i, w := range writes {
if i == upTo {
return
}
rev++
if _, err := keys.Put(w.Key, w.Value, rev, w.Value == nil); err != nil {
t.Fatalf("write %d (%s): %v", i, w.Key, err)
}
}
}
// A node written whole onto no keys: its module keys, then its node key.
rev++
first, err := keys.Writes("g14", &g14)
if err != nil || len(first) != 3 || !first[2].Commit {
t.Fatalf("Writes = %+v, %v", first, err)
}
apply(first, -1)
if p, r, ok := keys.Of("g14"); !ok || !SameAssignments(p, g14) || r != rev {
t.Fatalf("Of after the write = %+v, %d, %v", p, r, ok)
}
// A write under way is never read half: app's new port is not read before the node key names it.
after := cloneAssignments(g14)
after.Modules = []seat.PlacedModule{{Module: "app", Ports: map[string]int64{"8080": 18081}}, {Module: "new"}}
after.Holds, after.Pins = nil, nil
writes, err := keys.Writes("g14", &after)
if err != nil {
t.Fatal(err)
}
var kinds []string
for _, w := range writes {
kinds = append(kinds, fmt.Sprintf("%s:%v:%d", w.Key, w.Value != nil, w.Base))
}
if strings.Join(kinds, " ") != "g14.app:true:3 g14.new:true:0 g14.node:true:4 g14.zsh:false:2" {
t.Fatalf("Writes = %v", kinds)
}
apply(writes, 2)
if p, _, _ := keys.Of("g14"); !SameAssignments(p, g14) {
t.Errorf("the module keys written before the node key are read: %+v", p)
}
apply(writes[2:], -1)
if p, _, _ := keys.Of("g14"); !SameAssignments(p, after) {
t.Errorf("the write done is not read whole: %+v", p)
}
// A module key that stays is not written again.
if again, _ := keys.Writes("g14", &after); len(again) != 1 || !again[0].Commit {
t.Errorf("an unchanged assignments writes %+v", again)
}
// Keys that do not agree keep what the node had, and say so.
stray, _ := EncodeModule(seat.AssignmentsByNodeModuleValue{Node: "g14", Placed: seat.PlacedModule{Module: "app"}})
if _, err := keys.Put("g14.app", stray, 99, false); err == nil {
t.Error("a module key the node key does not name the value of was taken silently")
}
if p, _, _ := keys.Of("g14"); !SameAssignments(p, after) {
t.Errorf("a stray module key moved the assignments: %+v", p)
}
// The holder's next write puts the module key right again.
repair, _ := keys.Writes("g14", &after)
if len(repair) != 2 || repair[0].Key != "g14.app" || repair[0].Base != 99 || !repair[1].Commit {
t.Fatalf("the repair writes %+v", repair)
}
apply(repair, -1)
// No assignments: the node key first, then the module keys; none is read after.
gone, _ := keys.Writes("g14", nil)
kinds = nil
for _, w := range gone {
kinds = append(kinds, w.Key)
}
if strings.Join(kinds, " ") != "g14.node g14.app g14.new" {
t.Errorf("Writes(nil) = %v", kinds)
}
apply(gone, 1)
if _, _, ok := keys.Of("g14"); ok {
t.Error("a node key deleted still places its node")
}
if _, err := DecodeNode([]byte(`{"v":1,"node":"g14","modules":[{"module":"a","digest":"x"},{"module":"a","digest":"x"}]}`)); err == nil {
t.Error("a node key naming a module twice reads")
}
})
}