Files
mesh-sdk/go/views/views_test.go
T
jschoubben 6bfd14b14b CAT-RD1..RD10 sdk: views.Reader, a module's pictures of the owners' states (A8's patch 08da702)
Keys, Get, Values and Watch over the views followers, the first-view tools, the finding past the bound, and the re-read
recorded through mesh-hand-acts before it is asked; its cases are subtests of the views table.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:23:21 +01:00

294 lines
11 KiB
Go

package views
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/handacts"
)
// mapSource is a state held in memory, as a Source.
type mapSource map[string]Entry
func (m mapSource) Keys() ([]string, error) {
var out []string
for k := range m {
out = append(out, k)
}
return out, nil
}
func (m mapSource) Get(key string) (*Entry, bool, error) {
e, ok := m[key]
if !ok {
return nil, false, nil
}
return &e, true, nil
}
func markOf(state string, rev uint64, made time.Time) Entry {
raw, _ := json.Marshal(Mark{V: Version, State: state, Revision: rev, Created: made, Fill: Fill{Keys: 2, Bytes: 100, MaxBytes: 1000}})
return Entry{Value: raw, Revision: 1}
}
// The owners' and the readers' side of a first view (ADR 0615 §4-§6, ADR 0621 §2-§3, ADR 0622): one table of cases,
// since mesh-delivery is over its test budget.
func TestViews(t *testing.T) {
made := time.Date(2026, 11, 4, 12, 0, 0, 0, time.UTC)
now := func() time.Time { return made.Add(time.Hour) }
values := mapSource{"a": {Value: json.RawMessage(`{"n":1}`), Revision: 7}, "b": {Value: json.RawMessage(`{"n":2}`), Revision: 9}}
marks := mapSource{"modules": markOf("modules", 9, made)}
t.Run("a first view answers every value at the mark's revision, the mark read first", func(t *testing.T) {
a, err := FirstView("o", "modules", values, marks, "", now())
if err != nil || a.Revision != 9 || !a.Created.Equal(made) || len(a.Values) != 2 || a.Next != "" {
t.Fatalf("answer = %+v, %v", a, err)
}
if a, _ := FirstView("o", "modules", values, mapSource{}, "", now()); a.Revision != 0 {
t.Errorf("a state not marked answers revision %d, want 0", a.Revision)
}
})
t.Run("a first view larger than a page is paged by key", func(t *testing.T) {
big := mapSource{}
for i := range 4 {
big[fmt.Sprintf("k%d", i)] = Entry{Value: json.RawMessage(`"` + strings.Repeat("x", PageBytes/3) + `"`), Revision: uint64(i + 1)}
}
got := map[string]bool{}
after := ""
for pages := 0; ; pages++ {
a, err := FirstView("o", "s", big, nil, after, now())
if err != nil || pages > 4 {
t.Fatalf("page %d: %+v %v", pages, a.Next, err)
}
for k := range a.Values {
got[k] = true
}
if a.Next == "" {
break
}
after = a.Next
}
if len(got) != 4 {
t.Errorf("pages hold %v, want the four keys", got)
}
})
t.Run("each first-view tool answers its callers alone, never the owner, by the proof and not an argument", func(t *testing.T) {
callers := ParseCallers("mesh-grants, node-tools,mesh-delivery-assigner")
for caller, allowed := range map[string]bool{"novox.mesh-grants": true, "ace.node-tools": true, "g14.mesh-delivery-assigner": true,
"novox.mesh-delivery-indexer": false, "novox.mesh-config": false, "person.jochen": false, stdio.Unproven: false} {
if err := Refusal("manifests", callers, caller); (err == nil) != allowed {
t.Errorf("%q: err = %v, allowed %v", caller, err, allowed)
}
}
// A caller argument names nobody: a call with no proof is refused.
tool := FirstViewTool("mesh-delivery-indexer", "manifests", "modules", "w.", callers, values, marks, now)
if _, err := tool.Run(map[string]any{"caller": "novox.mesh-grants"}); err == nil {
t.Errorf("an unproven call naming a caller is answered")
}
})
t.Run("a read tool says the keys and the fill, one key, or a page of values", func(t *testing.T) {
r, err := Read("o", "modules", values, marks, "", false, "", now())
ra, ok := r.(ReadAnswer)
if err != nil || !ok || len(ra.Keys) != 2 || ra.Fill == nil || ra.Fill.MaxBytes != 1000 || ra.Revision != 9 {
t.Fatalf("read = %+v, %v", r, err)
}
r, _ = Read("o", "modules", values, marks, "b", false, "", now())
if k := r.(KeyAnswer); !k.Found || k.Revision != 9 {
t.Errorf("one key = %+v", k)
}
r, _ = Read("o", "modules", values, nil, "", false, "", now())
if ra := r.(ReadAnswer); ra.Fill != nil || ra.Words == "" {
t.Errorf("an unmarked state says its fill as %+v", ra)
}
})
// A follower whose owner answers from values and marks.
follower := func(ask func(context.Context, string) (Answer, error)) (*Follower, *[]string) {
var said []string
f := &Follower{Owner: "o", Tool: "o.modules", State: "modules", Ask: ask, Bound: time.Nanosecond, Wait: time.Millisecond,
Raise: func(s Status) { said = append(said, "raised "+s.Tool) }, Clear: func(s Status) { said = append(said, "cleared") }}
return f, &said
}
answer := func(context.Context, string) (Answer, error) {
return FirstView("o", "modules", values, marks, "", now())
}
ev := func(rev uint64, key, value string) Event {
e := Event{V: Version, Owner: "o", State: "modules", Key: key, Revision: rev, Created: made}
if value == "" {
e.Deleted = true
} else {
e.Value = json.RawMessage(value)
}
return e
}
t.Run("a reader applies a put and a delete from the events alone, and passes over what it holds", func(t *testing.T) {
asks := 0
f, _ := follower(func(ctx context.Context, after string) (Answer, error) { asks++; return answer(ctx, after) })
if err := f.Start(context.Background()); err != nil {
t.Fatal(err)
}
for _, e := range []Event{ev(8, "a", `{"n":0}`), ev(10, "c", `{"n":3}`), ev(11, "a", "")} {
if gap, why := f.Apply(e); gap {
t.Fatalf("revision %d is a gap: %s", e.Revision, why)
}
}
if _, ok := f.Get("a"); ok || f.Status().Revision != 11 || asks != 1 {
t.Errorf("picture = %v at %d after %d asks", f.Values(), f.Status().Revision, asks)
}
if v, _ := f.Get("c"); string(v) != `{"n":3}` {
t.Errorf("c = %s", v)
}
})
t.Run("a gap, a state made again and a value too large each ask once more", func(t *testing.T) {
for name, e := range map[string]Event{
"a revision missed": ev(12, "a", `{}`),
"made again": {V: Version, Owner: "o", State: "modules", Key: "a", Revision: 1, Created: made.Add(time.Minute), Value: json.RawMessage(`{}`)},
"too large": {V: Version, Owner: "o", State: "modules", Key: "a", Revision: 10, Created: made, Gap: true},
} {
f, _ := follower(answer)
_ = f.Start(context.Background())
if gap, _ := f.Apply(e); !gap {
t.Errorf("%s: not taken as a gap", name)
}
}
})
t.Run("a reader whose owner is down keeps asking, raises past its bound, and the answer clears it", func(t *testing.T) {
down := 3
f, said := follower(func(ctx context.Context, after string) (Answer, error) {
if down > 0 {
down--
return Answer{}, errors.New("no responders")
}
return answer(ctx, after)
})
if err := f.Start(context.Background()); err != nil {
t.Fatal(err)
}
if strings.Join(*said, ",") != "raised o.modules,cleared" || f.Status().Asks != 4 || f.Status().Raised {
t.Errorf("said %v after %d asks", *said, f.Status().Asks)
}
})
t.Run("an event heard while an ask runs is taken again after its answer", func(t *testing.T) {
var f *Follower
f, _ = follower(func(ctx context.Context, after string) (Answer, error) {
a, err := answer(ctx, after) // the picture read at 9, before revision 10 was written
if f.Status().Asks == 2 {
f.Apply(ev(10, "d", `{"n":4}`))
}
return a, err
})
_ = f.Start(context.Background())
_ = f.Again(context.Background(), "test")
if v, ok := f.Get("d"); !ok || string(v) != `{"n":4}` || f.Status().Revision != 10 || f.Status().ByHand != 1 {
t.Errorf("d = %s, %v at %d", v, ok, f.Status().Revision)
}
})
t.Run("an event heard by its data asks a gap once more as a gap, never by hand", func(t *testing.T) {
asked := make(chan struct{}, 4)
f, _ := follower(func(ctx context.Context, after string) (Answer, error) {
asked <- struct{}{}
return answer(ctx, after)
})
_ = f.Start(context.Background())
<-asked
raw, _ := ev(12, "a", `{}`).Encode()
if err := HearEvent(context.Background(), map[string]*Follower{"modules": f}, raw, t.Logf); err != nil {
t.Fatal(err)
}
select {
case <-asked:
case <-time.After(3 * time.Second):
t.Fatal("a gap heard by its data asked nothing")
}
if s := f.Status(); s.Asks != 2 || s.ByHand != 0 {
t.Errorf("a gap was counted by hand: %+v", s)
}
if err := HearEvent(context.Background(), nil, []byte("not an event"), t.Logf); err == nil {
t.Error("data that is no views event was taken")
}
})
t.Run("a module's reader answers Keys and Get from its pictures, refuses a state it does not follow or has no first view of, and writes and clears its finding", func(t *testing.T) {
acted := 0
fs := &findingsMap{}
r := &Reader{Module: "reader", Findings: fs, Bound: time.Nanosecond, Wait: time.Millisecond, Acts: handacts.Recorder{Ask: func(string, any) (json.RawMessage, error) {
acted++
return json.RawMessage(`{"log":"mesh-hand-acts"}`), nil
}}}
f := r.Follow("o", "modules", "o.indexer_read_modules")
down := 2
f.Ask = func(ctx context.Context, after string) (Answer, error) {
if down > 0 {
down--
return Answer{}, errors.New("no responders")
}
return answer(ctx, after)
}
if _, err := r.Keys("o.modules"); err == nil {
t.Errorf("a state with no first view is read")
}
if _, err := r.Keys("o.other"); err == nil {
t.Errorf("a state not followed is read")
}
if err := f.Start(context.Background()); err != nil {
t.Fatal(err)
}
keys, err := r.Keys("o.modules")
v, ok, _ := r.Get("o.modules", "b")
if err != nil || len(keys) != 2 || !ok || string(v) != `{"n":2}` || len(fs.m) != 0 || fs.puts != 1 || fs.deletes != 1 {
t.Errorf("keys %v, b %s, findings %+v", keys, v, fs)
}
handed := map[string]bool{}
if err := r.Watch("o.modules", func(k string, _ []byte, gone bool) { handed[k] = !gone }); err != nil || len(handed) != 2 || !r.Follows("o.modules") || r.Follows("o.x") {
t.Errorf("watch handed %v, %v", handed, err)
}
f.Apply(ev(10, "c", `{"n":3}`))
f.Apply(ev(11, "a", ""))
if !handed["c"] || handed["a"] {
t.Errorf("a change was not handed to the watch: %v", handed)
}
if got := r.Tools("reader"); len(got) != 2 || got[0].Name != "reader_first_views" {
t.Errorf("tools = %v", got)
}
again, err := r.Tools("reader")[1].Run(map[string]any{"state": "o.modules"})
if err != nil || len(again.(map[string]any)["views"].([]Status)) != 1 || f.Status().ByHand != 1 || acted != 1 {
t.Errorf("asked again by hand: %v, %v", again, err)
}
})
t.Run("an event names its state on its subject, and decodes as it was encoded", func(t *testing.T) {
if s := StateOfEvent("mesh.stream.mesh-delivery-indexer.event.views.config-candidates"); s != "config-candidates" {
t.Errorf("state of subject = %q", s)
}
raw, err := ev(3, "k", `{"x":1}`).Encode()
got, err2 := DecodeEvent(raw)
if err != nil || err2 != nil || got.Revision != 3 || string(got.Value) != `{"x":1}` {
t.Errorf("round trip = %+v %v %v", got, err, err2)
}
if _, err := (Event{Owner: "o", State: "s"}).Encode(); err == nil {
t.Errorf("an event with no key or revision is encoded")
}
})
}
// findingsMap is a module's own findings state in memory.
type findingsMap struct {
m map[string][]byte
puts, deletes int
}
func (f *findingsMap) Put(k string, v []byte) error {
if f.m == nil {
f.m = map[string][]byte{}
}
f.m[k] = v
f.puts++
return nil
}
func (f *findingsMap) Delete(k string) error { delete(f.m, k); f.deletes++; return nil }