mesh/merge-gate head 2b420603fe83 merged into trunk f70a99dbf485 — pass, with a note: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head 2b420603fe83 merged into trunk f70a99dbf485 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
295 lines
9.5 KiB
Go
295 lines
9.5 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// 1131/AC1: the have key forms and parses back; bad names are refused, naming them.
|
|
func TestHaveKey(t *testing.T) {
|
|
if HaveBucket != "node-engine_have" {
|
|
t.Fatalf("HaveBucket = %q", HaveBucket)
|
|
}
|
|
key, err := HaveKey("ace", "plex")
|
|
if err != nil || key != "ace.plex" {
|
|
t.Fatalf("HaveKey = %q, %v", key, err)
|
|
}
|
|
node, module, isNode, err := ParseHaveKey(key)
|
|
if err != nil || node != "ace" || module != "plex" || isNode {
|
|
t.Fatalf("ParseHaveKey = %q %q %v %v", node, module, isNode, err)
|
|
}
|
|
if got, err := HaveNodeKey("ace"); err != nil || got != "ace.node" {
|
|
t.Fatalf("HaveNodeKey = %q, %v", got, err)
|
|
}
|
|
if _, err := HaveNodeKey("a b"); err == nil {
|
|
t.Error("HaveNodeKey with a space accepted")
|
|
}
|
|
node, module, isNode, err = ParseHaveKey("ace.node")
|
|
if err != nil || node != "ace" || module != "" || !isNode {
|
|
t.Fatalf("ParseHaveKey node = %q %q %v %v", node, module, isNode, err)
|
|
}
|
|
for _, c := range []struct{ node, module, named string }{
|
|
{"ace", "node", "node"},
|
|
{"ace", "", "module"},
|
|
{"", "plex", "node"},
|
|
{"a.b", "plex", "a.b"},
|
|
{"ace", "p*", "p*"},
|
|
{"ace", "p>", "p>"},
|
|
{"ace", "p x", "p x"},
|
|
{"ace", "p\tx", "module"},
|
|
} {
|
|
_, err := HaveKey(c.node, c.module)
|
|
if err == nil || !strings.Contains(err.Error(), c.named) {
|
|
t.Errorf("HaveKey(%q, %q) = %v, want an error naming %q", c.node, c.module, err, c.named)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "ace", "ace.", ".plex", "ace.plex.x", "ace.p x"} {
|
|
if _, _, _, err := ParseHaveKey(bad); err == nil {
|
|
t.Errorf("ParseHaveKey(%q) accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1131/AC2: the presence keys form and parse back; a node named part is refused.
|
|
func TestPresenceKey(t *testing.T) {
|
|
if PresenceBucket != "mesh-delivery-watchman_presence" {
|
|
t.Fatalf("PresenceBucket = %q", PresenceBucket)
|
|
}
|
|
key, err := PresenceNodeKey("ace")
|
|
if err != nil || key != "ace" {
|
|
t.Fatalf("PresenceNodeKey = %q, %v", key, err)
|
|
}
|
|
node, part, instance, err := ParsePresenceKey(key)
|
|
if err != nil || node != "ace" || part != "" || instance != "" {
|
|
t.Fatalf("ParsePresenceKey node = %q %q %q %v", node, part, instance, err)
|
|
}
|
|
key, err = PresencePartKey("view", "novox")
|
|
if err != nil || key != "part.view.novox" {
|
|
t.Fatalf("PresencePartKey = %q, %v", key, err)
|
|
}
|
|
node, part, instance, err = ParsePresenceKey(key)
|
|
if err != nil || node != "" || part != "view" || instance != "novox" {
|
|
t.Fatalf("ParsePresenceKey part = %q %q %q %v", node, part, instance, err)
|
|
}
|
|
if _, err := PresenceNodeKey("part"); err == nil || !strings.Contains(err.Error(), "part") {
|
|
t.Errorf("PresenceNodeKey(part) = %v", err)
|
|
}
|
|
if _, err := PresenceNodeKey("a.b"); err == nil {
|
|
t.Error("PresenceNodeKey(a.b) accepted")
|
|
}
|
|
if _, err := PresencePartKey("", "x"); err == nil {
|
|
t.Error("PresencePartKey with empty part accepted")
|
|
}
|
|
if _, err := PresencePartKey("view", "x y"); err == nil {
|
|
t.Error("PresencePartKey with a space accepted")
|
|
}
|
|
for _, bad := range []string{"", "part", "part.view", "part.view.a.b", "a.b", "part..x"} {
|
|
if _, _, _, err := ParsePresenceKey(bad); err == nil {
|
|
t.Errorf("ParsePresenceKey(%q) accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1131/AC3: the pulse key splits at the first dot, the key holding dots.
|
|
func TestPulseKey(t *testing.T) {
|
|
if PulseBucket != "mesh-delivery-watchman_pulse" {
|
|
t.Fatalf("PulseBucket = %q", PulseBucket)
|
|
}
|
|
key, err := PulseKey("node-engine_have", "ace.plex")
|
|
if err != nil || key != "node-engine_have.ace.plex" {
|
|
t.Fatalf("PulseKey = %q, %v", key, err)
|
|
}
|
|
bucket, rest, err := ParsePulseKey(key)
|
|
if err != nil || bucket != "node-engine_have" || rest != "ace.plex" {
|
|
t.Fatalf("ParsePulseKey = %q %q %v", bucket, rest, err)
|
|
}
|
|
for _, c := range [][2]string{{"", "k"}, {"a.b", "k"}, {"b", ""}, {"b", "k x"}, {"b", ".k"}, {"b", "k."}, {"b", "k*"}} {
|
|
if _, err := PulseKey(c[0], c[1]); err == nil {
|
|
t.Errorf("PulseKey(%q, %q) accepted", c[0], c[1])
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "nodot", ".k", "b."} {
|
|
if _, _, err := ParsePulseKey(bad); err == nil {
|
|
t.Errorf("ParsePulseKey(%q) accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1131/AC4: each value encodes, decodes equal, and carries "v":1.
|
|
func TestRoundTrip(t *testing.T) {
|
|
hasV1 := func(t *testing.T, b []byte) {
|
|
t.Helper()
|
|
var m map[string]any
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if m["v"] != float64(1) {
|
|
t.Fatalf("v = %v in %s", m["v"], b)
|
|
}
|
|
if len(b) > 500 {
|
|
t.Fatalf("value is %d bytes", len(b))
|
|
}
|
|
}
|
|
t.Run("have", func(t *testing.T) {
|
|
in := Have{Build: "b1", FP: "f1", Phase: PhaseRunning, Health: HealthOK, Detail: "up"}
|
|
b, err := EncodeHave(in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hasV1(t, b)
|
|
out, err := DecodeHave(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.V = Version
|
|
if out != in {
|
|
t.Fatalf("got %+v want %+v", out, in)
|
|
}
|
|
})
|
|
t.Run("have node", func(t *testing.T) {
|
|
in := HaveNode{Windows: json.RawMessage(`{"a":1}`), Busy: json.RawMessage(`true`)}
|
|
b, err := EncodeHaveNode(in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hasV1(t, b)
|
|
out, err := DecodeHaveNode(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !SameHaveNode(out, HaveNode{V: Version, Windows: in.Windows, Busy: in.Busy}) {
|
|
t.Fatalf("got %+v", out)
|
|
}
|
|
})
|
|
t.Run("presence", func(t *testing.T) {
|
|
b, err := EncodePresence(Presence{Online: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hasV1(t, b)
|
|
out, err := DecodePresence(b)
|
|
if err != nil || out != (Presence{V: Version, Online: true}) {
|
|
t.Fatalf("got %+v %v", out, err)
|
|
}
|
|
})
|
|
t.Run("pulse", func(t *testing.T) {
|
|
in := Pulse{Bucket: "node-engine_have", Key: "ace.plex", Revision: 7, Owes: "ace"}
|
|
b, err := EncodePulse(in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hasV1(t, b)
|
|
out, err := DecodePulse(b)
|
|
in.V = Version
|
|
if err != nil || out != in {
|
|
t.Fatalf("got %+v %v", out, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// 1131/AC5: a missing or unknown version is refused as ErrUnknownVersion, naming both versions.
|
|
func TestUnknownVersion(t *testing.T) {
|
|
decoders := map[string]func([]byte) error{
|
|
"have": func(b []byte) error { _, err := DecodeHave(b); return err },
|
|
"have node": func(b []byte) error { _, err := DecodeHaveNode(b); return err },
|
|
"presence": func(b []byte) error { _, err := DecodePresence(b); return err },
|
|
"pulse": func(b []byte) error { _, err := DecodePulse(b); return err },
|
|
}
|
|
for name, dec := range decoders {
|
|
t.Run(name, func(t *testing.T) {
|
|
err := dec([]byte(`{"v":2,"phase":"running","health":"ok"}`))
|
|
if !errors.Is(err, ErrUnknownVersion) || !strings.Contains(err.Error(), "2") || !strings.Contains(err.Error(), "1") {
|
|
t.Errorf("v 2: %v", err)
|
|
}
|
|
err = dec([]byte(`{"phase":"running","health":"ok"}`))
|
|
if !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("missing v: %v", err)
|
|
}
|
|
if err := dec([]byte(`not json`)); err == nil || errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("garbage: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// 1131/AC6: a phase or health outside the lists is refused on encode and decode.
|
|
func TestPhaseHealthRefused(t *testing.T) {
|
|
for _, p := range []Phase{PhaseBackingUp, PhaseApplying, PhaseChecking, PhaseRunning, PhaseFailed} {
|
|
for _, h := range []Health{HealthOK, HealthFailed, HealthWaits, HealthUnknown} {
|
|
if _, err := EncodeHave(Have{Phase: p, Health: h}); err != nil {
|
|
t.Errorf("%s/%s refused: %v", p, h, err)
|
|
}
|
|
}
|
|
}
|
|
if _, err := EncodeHave(Have{Phase: "sleeping", Health: HealthOK}); err == nil || !strings.Contains(err.Error(), "sleeping") {
|
|
t.Errorf("bad phase encoded: %v", err)
|
|
}
|
|
if _, err := EncodeHave(Have{Phase: PhaseRunning, Health: "fine"}); err == nil || !strings.Contains(err.Error(), "fine") {
|
|
t.Errorf("bad health encoded: %v", err)
|
|
}
|
|
if _, err := EncodeHave(Have{Phase: PhaseRunning}); err == nil {
|
|
t.Error("empty health encoded")
|
|
}
|
|
if _, err := DecodeHave([]byte(`{"v":1,"phase":"sleeping","health":"ok"}`)); err == nil {
|
|
t.Error("bad phase decoded")
|
|
}
|
|
if _, err := DecodeHave([]byte(`{"v":1,"phase":"running","health":"fine"}`)); err == nil {
|
|
t.Error("bad health decoded")
|
|
}
|
|
}
|
|
|
|
// 1131/AC7: the pulse's time to live is 120 seconds minus the move's age; at the bound the move is stalled.
|
|
func TestPulseTTL(t *testing.T) {
|
|
for _, c := range []struct {
|
|
age time.Duration
|
|
ttl time.Duration
|
|
stalled bool
|
|
}{
|
|
{0, 120 * time.Second, false},
|
|
{30 * time.Second, 90 * time.Second, false},
|
|
{119 * time.Second, time.Second, false},
|
|
{120 * time.Second, 0, true},
|
|
{200 * time.Second, 0, true},
|
|
} {
|
|
ttl, stalled := PulseTTL(c.age)
|
|
if ttl != c.ttl || stalled != c.stalled {
|
|
t.Errorf("PulseTTL(%v) = %v, %v; want %v, %v", c.age, ttl, stalled, c.ttl, c.stalled)
|
|
}
|
|
}
|
|
if PulseMoveBound != 120*time.Second || HaveHistory != 20 || PresenceHistory != 20 {
|
|
t.Error("constants")
|
|
}
|
|
}
|
|
|
|
// 1131/AC8: equal values say same, different ones do not.
|
|
func TestSame(t *testing.T) {
|
|
a := Have{V: 1, Build: "b", Phase: PhaseRunning, Health: HealthOK}
|
|
b := a
|
|
if !SameHave(a, b) {
|
|
t.Error("equal haves differ")
|
|
}
|
|
b.Detail = "x"
|
|
if SameHave(a, b) {
|
|
t.Error("different haves same")
|
|
}
|
|
n := HaveNode{V: 1, Busy: json.RawMessage(`true`)}
|
|
if !SameHaveNode(n, HaveNode{V: 1, Busy: json.RawMessage(`true`)}) || SameHaveNode(n, HaveNode{V: 1}) {
|
|
t.Error("have node")
|
|
}
|
|
if !SamePresence(Presence{V: 1, Online: true}, Presence{V: 1, Online: true}) || SamePresence(Presence{V: 1}, Presence{V: 1, Online: true}) {
|
|
t.Error("presence")
|
|
}
|
|
p := Pulse{V: 1, Bucket: "b", Key: "k", Revision: 1, Owes: "ace"}
|
|
q := p
|
|
if !SamePulse(p, q) {
|
|
t.Error("equal pulses differ")
|
|
}
|
|
q.Revision = 2
|
|
if SamePulse(p, q) {
|
|
t.Error("different pulses same")
|
|
}
|
|
}
|