186 lines
7.3 KiB
Go
186 lines
7.3 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
"github.com/novox/mesh-tools/node-tools/internal/launch"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// historyBuses are three modules on one runtime, each reached as a bundle reaches it (novox/hq ADR
|
|
// 0307): issues keeps a state `issues` in a bucket keeping 8 values a key, triage reads it as
|
|
// `issues.issues`, and other was issued no state.
|
|
func historyBuses(t *testing.T) (owner, reader, other launch.Bus) {
|
|
t.Helper()
|
|
mesh := mt.New(t)
|
|
mesh.BucketKeeping(t, "issues_issues", 8)
|
|
m := mt.MembershipOf("issues", "anchor", false, nil)
|
|
m.State = []bus.StateIssued{{Name: "issues", Bucket: "issues_issues", Writes: true}}
|
|
mesh.Issue(t, m)
|
|
r := mt.MembershipOf("triage", "anchor", false, nil)
|
|
r.State = []bus.StateIssued{{Name: "issues.issues", Bucket: "issues_issues"}}
|
|
mesh.Issue(t, r)
|
|
mesh.Issue(t, mt.MembershipOf("other", "anchor", false, nil))
|
|
conn := connect(t, "node-tools", "anchor")
|
|
for _, module := range []string{"issues", "triage", "other"} {
|
|
conn.Follow(module)
|
|
}
|
|
mt.Until(t, func() error {
|
|
for _, module := range []string{"issues", "triage", "other"} {
|
|
if conn.Membership(module) == nil {
|
|
return errors.New(module + " not issued yet")
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
all := &consumers{conn: conn, logf: t.Logf, of: map[string]*moduleEvents{}}
|
|
return all.forModule("issues"), all.forModule("triage"), all.forModule("other")
|
|
}
|
|
|
|
// kept is one value of a history answer, by the field names on the wire.
|
|
type kept struct {
|
|
Key string `json:"key"`
|
|
Value json.RawMessage `json:"value"`
|
|
Revision uint64 `json:"revision"`
|
|
Op string `json:"op"`
|
|
At string `json:"at"`
|
|
}
|
|
|
|
func historyOf(t *testing.T, b launch.Bus, state, key string) []kept {
|
|
t.Helper()
|
|
raw, err := ask(b, "history", map[string]any{"state": state, "key": key})
|
|
if err != nil {
|
|
t.Fatalf("history of %s %s: %v", state, key, err)
|
|
}
|
|
var h []kept
|
|
if err := json.Unmarshal(raw, &h); err != nil || h == nil {
|
|
t.Fatalf("history answered %s, not an array", raw)
|
|
}
|
|
return h
|
|
}
|
|
|
|
// A key's history is every value the bucket keeps, oldest first, each with its revision, op and time
|
|
// in RFC 3339 — a delete with no value; an unknown key's is []. The owner and a reader are served it,
|
|
// and reading it writes nothing.
|
|
func TestAKeysHistoryIsEveryValueKeptOldestFirst(t *testing.T) {
|
|
owner, reader, _ := historyBuses(t)
|
|
before := time.Now().Add(-time.Second)
|
|
var revisions []uint64
|
|
for _, v := range []string{`{"state":"open"}`, `{"state":"fixing"}`, `{"state":"resolved"}`} {
|
|
raw, err := ask(owner, "put", map[string]any{"state": "issues", "key": "502", "value": json.RawMessage(v)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var r struct {
|
|
Revision uint64 `json:"revision"`
|
|
}
|
|
_ = json.Unmarshal(raw, &r)
|
|
revisions = append(revisions, r.Revision)
|
|
}
|
|
if _, err := ask(owner, "put", map[string]any{"state": "issues", "key": "503", "value": 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ask(owner, "delete", map[string]any{"state": "issues", "key": "502"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for who, b := range map[string]launch.Bus{"the owner": owner, "a reader": reader} {
|
|
name := "issues"
|
|
if b == reader {
|
|
name = "issues.issues"
|
|
}
|
|
h := historyOf(t, b, name, "502")
|
|
if len(h) != 4 {
|
|
t.Fatalf("%s read %d values of 502, not 4: %+v", who, len(h), h)
|
|
}
|
|
for i, v := range []string{`{"state":"open"}`, `{"state":"fixing"}`, `{"state":"resolved"}`} {
|
|
if h[i].Op != "put" || string(h[i].Value) != v || h[i].Revision != revisions[i] || h[i].Key != "502" {
|
|
t.Errorf("%s: value %d is %+v", who, i, h[i])
|
|
}
|
|
}
|
|
if h[3].Op != "delete" || len(h[3].Value) != 0 && string(h[3].Value) != "null" || h[3].Revision <= revisions[2] {
|
|
t.Errorf("%s: the delete reads %+v", who, h[3])
|
|
}
|
|
for i, k := range h {
|
|
at, err := time.Parse(time.RFC3339, k.At)
|
|
if err != nil || at.Before(before) || at.After(time.Now().Add(time.Second)) {
|
|
t.Errorf("%s: value %d's time %q is not RFC 3339 and now: %v", who, i, k.At, err)
|
|
}
|
|
if i > 0 && k.Revision <= h[i-1].Revision {
|
|
t.Errorf("%s: not oldest first: %+v", who, h)
|
|
}
|
|
}
|
|
if h := historyOf(t, b, name, "999"); len(h) != 0 {
|
|
t.Errorf("%s: an unknown key's history is %+v, not []", who, h)
|
|
}
|
|
}
|
|
// The history is read, never written: the key is still deleted and keeps the same four values.
|
|
if raw, err := ask(owner, "get", map[string]any{"state": "issues", "key": "502"}); err != nil || string(raw) != "null" {
|
|
t.Fatalf("after history, 502 reads %s %v", raw, err)
|
|
}
|
|
if h := historyOf(t, owner, "issues", "502"); len(h) != 4 {
|
|
t.Fatalf("reading the history changed it: %+v", h)
|
|
}
|
|
// A key the bucket holds nothing for answers [] on the wire, not null.
|
|
if raw, _ := ask(owner, "history", map[string]any{"state": "issues", "key": "999"}); string(raw) != "[]" {
|
|
t.Fatalf("an unknown key's history is %s on the wire", raw)
|
|
}
|
|
}
|
|
|
|
// A module the bucket was not issued to is refused its history with the reason, as it is a get.
|
|
func TestAKeysHistoryIsRefusedToAModuleNotIssuedTheState(t *testing.T) {
|
|
owner, reader, other := historyBuses(t)
|
|
if _, err := ask(owner, "put", map[string]any{"state": "issues", "key": "1", "value": 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, name := range []string{"issues", "issues.issues"} {
|
|
_, err := ask(other, "history", map[string]any{"state": name, "key": "1"})
|
|
if err == nil || !strings.Contains(err.Error(), "keeps and reads no state called") ||
|
|
!strings.Contains(err.Error(), "issued: none") {
|
|
t.Errorf("history of %s to a module not issued it: %v", name, err)
|
|
}
|
|
}
|
|
if _, err := ask(owner, "history", map[string]any{"state": "issues", "key": "a b"}); err == nil ||
|
|
!strings.Contains(err.Error(), "not a key") {
|
|
t.Errorf("history of a key the bus cannot hold: %v", err)
|
|
}
|
|
// A page is the values in key order after a key, in one request, to the owner and to a reader, never to a
|
|
// module not issued the state; a page too small for two values still gives one and says where it goes on.
|
|
for k, v := range map[string]string{"2": `"bb"`, "3": `"cc"`} {
|
|
if _, err := ask(owner, "put", map[string]any{"state": "issues", "key": k, "value": json.RawMessage(v)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
type page struct {
|
|
Entries []kept `json:"entries"`
|
|
Next string `json:"next"`
|
|
}
|
|
for _, c := range []struct {
|
|
who, state, after string
|
|
max int
|
|
keys []string
|
|
next string
|
|
}{{"owner", "issues", "", 1 << 20, []string{"1", "2", "3"}, ""}, {"reader", "issues.issues", "1", 1 << 20, []string{"2", "3"}, ""},
|
|
{"owner", "issues", "", 1, []string{"1"}, "1"}, {"owner", "issues", "1", 9, []string{"2"}, "2"}} {
|
|
b := map[string]launch.Bus{"owner": owner, "reader": reader}[c.who]
|
|
raw, err := ask(b, "page", map[string]any{"state": c.state, "after": c.after, "max_bytes": c.max})
|
|
var p page
|
|
if err != nil || json.Unmarshal(raw, &p) != nil || p.Next != c.next || len(p.Entries) != len(c.keys) {
|
|
t.Fatalf("page %+v = %s, %v", c, raw, err)
|
|
}
|
|
for i, k := range c.keys {
|
|
if p.Entries[i].Key != k {
|
|
t.Errorf("page %+v: entry %d is %s, want %s", c, i, p.Entries[i].Key, k)
|
|
}
|
|
}
|
|
}
|
|
if _, err := ask(other, "page", map[string]any{"state": "issues.issues"}); err == nil {
|
|
t.Errorf("a page given to a module not issued the state")
|
|
}
|
|
}
|