Files
mesh-sdk/go/stateobjects/fakes_test.go
T

233 lines
6.3 KiB
Go

package stateobjects
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"strings"
"sync"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
// memory is a state and its object store in memory, behind the two interfaces.
type memory struct {
mu sync.Mutex
rev uint64
keys map[string]Entry
objects map[string][]byte
failPuts bool
most int
}
func newMemory() *memory { return &memory{keys: map[string]Entry{}, objects: map[string][]byte{}} }
func (m *memory) Get(key string) (*Entry, error) {
m.mu.Lock()
defer m.mu.Unlock()
if e, ok := m.keys[key]; ok {
return &e, nil
}
return nil, nil
}
func (m *memory) write(key string, value []byte, revision *uint64) error {
m.mu.Lock()
defer m.mu.Unlock()
e, has := m.keys[key]
if (revision == nil && has) || (revision != nil && (!has || e.Revision != *revision)) {
return ErrChanged
}
m.rev++
m.keys[key] = Entry{Value: value, Revision: m.rev}
return nil
}
func (m *memory) Create(key string, value []byte) error { return m.write(key, value, nil) }
func (m *memory) Update(key string, value []byte, revision uint64) error {
return m.write(key, value, &revision)
}
func (m *memory) Delete(key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.keys, key)
return nil
}
func (m *memory) Keys() ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
var out []string
for k := range m.keys {
out = append(out, k)
}
sort.Strings(out)
return out, nil
}
func (m *memory) Put(name string, data []byte) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.failPuts {
return errors.New("the store is full")
}
m.objects[name] = data
return nil
}
func (m *memory) GetObject(name string) ([]byte, error) { return m.Object(name) }
func (m *memory) values() Values { return Values{KV: m, Objects: memObjects{m}, Store: "s-objects"} }
// Object is an object's bytes.
func (m *memory) Object(name string) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
d, ok := m.objects[name]
if !ok {
return nil, fmt.Errorf("no object %s", name)
}
return d, nil
}
// memObjects is the memory's object store side (Put is the memory's own).
type memObjects struct{ m *memory }
func (o memObjects) MostBytes() int { return o.m.most }
func (o memObjects) Put(name string, data []byte) error { return o.m.Put(name, data) }
func (o memObjects) Get(name string) ([]byte, error) { return o.m.Object(name) }
func (o memObjects) Delete(name string) error {
o.m.mu.Lock()
defer o.m.mu.Unlock()
delete(o.m.objects, name)
return nil
}
func (o memObjects) List() ([]Object, error) {
o.m.mu.Lock()
defer o.m.mu.Unlock()
var out []Object
for n, d := range o.m.objects {
out = append(out, Object{Name: n, Size: int64(len(d))})
}
return out, nil
}
// runtimeOf serves one bundle tool, "run", against the memory as the tool runner would answer the state's and the object
// store's calls, and returns what the tool answered. The tool runs run in the bundle, which reaches the memory through
// stdio.State and busstate.Objects.
func runtimeOf(t *testing.T, m *memory, run func() (any, error)) any {
t.Helper()
t.Setenv(stdio.ServedAs, "owner")
inR, inW := io.Pipe()
outR, outW := io.Pipe()
go func() {
_ = stdio.ServeOn("", []stdio.Tool{{Name: "run", Run: func(map[string]any) (any, error) { return run() }}}, inR, outW)
}()
var wmu sync.Mutex
send := func(v map[string]any) {
v["jsonrpc"] = "2.0"
b, _ := json.Marshal(v)
wmu.Lock()
defer wmu.Unlock()
_, _ = inW.Write(append(b, '\n'))
}
answered := make(chan any, 1)
go func() {
lines := bufio.NewScanner(outR)
lines.Buffer(make([]byte, 1<<20), 1<<28)
for lines.Scan() {
var in struct {
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
Result json.RawMessage `json:"result"`
}
if json.Unmarshal(lines.Bytes(), &in) != nil {
continue
}
if !strings.HasPrefix(in.Method, "mesh/") {
if string(in.ID) == "1" {
answered <- string(in.Result)
return
}
continue
}
go func() {
res, err := m.serve(in.Method, in.Params)
a := map[string]any{"id": json.RawMessage(in.ID)}
if err != nil {
code := -32000
if errors.Is(err, ErrChanged) {
code = stdio.CodeChanged
}
a["error"] = map[string]any{"code": code, "message": err.Error()}
} else {
a["result"] = res
}
send(a)
}()
}
}()
send(map[string]any{"id": 1, "method": "tools/call", "params": map[string]any{"name": "run"}})
select {
case a := <-answered:
return a
case <-time.After(20 * time.Second):
t.Fatal("the bundle said nothing")
return nil
}
}
func (m *memory) serve(method string, raw json.RawMessage) (any, error) {
var p struct {
Key string `json:"key"`
Value json.RawMessage `json:"value"`
Revision uint64 `json:"revision"`
Name string `json:"name"`
Data []byte `json:"data"`
Object string `json:"object"`
}
_ = json.Unmarshal(raw, &p)
switch method {
case "mesh/state.get":
e, _ := m.Get(p.Key)
if e == nil {
return nil, nil
}
return map[string]any{"key": p.Key, "value": json.RawMessage(e.Value), "revision": e.Revision}, nil
case "mesh/state.create":
return map[string]any{"revision": 1}, m.Create(p.Key, p.Value)
case "mesh/state.update":
return map[string]any{"revision": 1}, m.Update(p.Key, p.Value, p.Revision)
case "mesh/state.delete":
return buscalls.Done{}, m.Delete(p.Key)
case "mesh/state.keys":
return m.Keys()
case buscalls.ObjectsPut:
return buscalls.ObjectInfo{Name: p.Name, Size: uint64(len(p.Data))}, m.Put(p.Name, p.Data)
case buscalls.ObjectsGet:
d, err := m.Object(p.Name)
return buscalls.ObjectData{Info: buscalls.ObjectInfo{Name: p.Name}, Data: d}, err
case buscalls.ObjectsSentGet:
d, err := m.Object(p.Object)
return buscalls.ObjectData{Info: buscalls.ObjectInfo{Name: p.Object}, Data: d}, err
case buscalls.ObjectsDelete:
return buscalls.Done{}, memObjects{m}.Delete(p.Name)
case buscalls.ObjectsList:
l, _ := memObjects{m}.List()
var out []buscalls.ObjectInfo
for _, o := range l {
out = append(out, buscalls.ObjectInfo{Name: o.Name, Size: uint64(o.Size)})
}
return buscalls.ObjectsListed{Objects: out}, nil
}
return nil, fmt.Errorf("no %s", method)
}