Files
mesh-sdk/go/stateobjects/values_test.go
T
2026-11-11 10:51:06 +01:00

189 lines
5.6 KiB
Go

package stateobjects
import (
"bytes"
"encoding/json"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/kvlimit"
)
// big is a JSON value of about n bytes.
func big(n int) map[string]string { return map[string]string{"text": strings.Repeat("x", n)} }
// valueRows is what every backend of Values holds: a value of 1x, 10x and 100x the limit is read back whole, a replace
// leaves one object, a delete leaves one listed key-gone (ADR 0666 §16, C-Q1).
func valueRows(t *testing.T, v Values) {
t.Helper()
limit := kvlimit.ModuleStateValueBytes
for _, times := range []int{1, 10, 100} {
key := map[int]string{1: "one", 10: "ten", 100: "hundred"}[times]
want := big(times * limit)
if err := v.Put(key, want); err != nil {
t.Errorf("%dx: put: %v", times, err)
return
}
var got map[string]string
if ok, err := v.Get(key, &got); err != nil || !ok || got["text"] != want["text"] {
t.Errorf("%dx: read back %v, found %v, %v", times, len(got["text"]), ok, err)
return
}
e, _ := v.KV.Get(key)
if _, isRef := ReadRef(e.Value); !isRef {
t.Errorf("%dx: the key holds the value, not a reference", times)
return
}
}
if err := v.Put("small", map[string]int{"a": 1}); err != nil {
t.Error(err)
return
}
if e, _ := v.KV.Get("small"); !bytes.Equal(e.Value, []byte(`{"a":1}`)) {
t.Errorf("a small value is kept in its key, got %s", e.Value)
return
}
// a replace removes the object it replaced; the same value again leaves its object
before, _ := v.Objects.List()
if err := v.Put("one", big(2*limit)); err != nil { // 10x key
t.Error(err)
return
}
if err := v.Put("one", big(2*limit)); err != nil {
t.Error(err)
return
}
after, _ := v.Objects.List()
if len(after) != len(before) {
t.Errorf("a replace left %d objects, %d before it", len(after), len(before))
return
}
// a replace by a small value removes the object too
if err := v.Put("one", "small now"); err != nil {
t.Error(err)
return
}
if now, _ := v.Objects.List(); len(now) != len(before)-1 {
t.Errorf("replacing a large value by a small one left %d objects, want %d", len(now), len(before)-1)
return
}
// a delete leaves the object, listed key-gone, and the drop removes it alone
if err := v.Delete("ten"); err != nil {
t.Error(err)
return
}
kept, err := v.Kept()
if err != nil {
t.Error(err)
return
}
var gone []string
for _, k := range kept {
if k.KeyGone {
gone = append(gone, k.Key)
}
}
if len(gone) != 1 || gone[0] != "ten" {
t.Errorf("the objects whose key is gone: %v, want ten's", gone)
return
}
dropped, err := v.DropKeyGone()
if err != nil || len(dropped) != 1 {
t.Errorf("dropped %v, %v", dropped, err)
return
}
if left, _ := v.Objects.List(); len(left) != len(kept)-1 {
t.Errorf("the drop left %d objects of %d", len(left), len(kept))
return
}
if err := v.Put("ref", json.RawMessage(Wrap(Ref{Store: "s", Object: "o", Digest: strings.Repeat("a", 64), Form: FormVersion}))); err == nil {
t.Error("a value that is exactly a reference was accepted")
return
}
}
// ADR 0666 §16, C-S3, C-S5: values of 1x, 10x and 100x the limit through a bundle's runtime, whole.
func TestValuesOfAnySizeThroughTheRuntime(t *testing.T) {
m := newMemory()
out := runtimeOf(t, m, func() (any, error) {
v := ForState("views")
valueRows(t, v)
return "done", nil
})
if !strings.Contains(out.(string), "done") {
t.Fatalf("the bundle answered %v", out)
}
}
// A write another writer won first leaves the object it put removed and the key as it found it.
func TestAWriteLostLeavesNoObjectBehind(t *testing.T) {
m := newMemory()
v := m.values()
if err := v.Put("k", big(3*kvlimit.ModuleStateValueBytes)); err != nil {
t.Fatal(err)
}
// another writer changes the key between this writer's read and write
racing := &raceKV{KV: m, then: func() { _ = m.Update("k", []byte(`"other"`), 1) }}
lost := Values{KV: racing, Objects: memObjects{m}, Store: "s"}
if err := lost.Put("k", big(4*kvlimit.ModuleStateValueBytes)); err == nil || err != ErrChanged {
t.Fatalf("the lost write: %v", err)
}
if l, _ := (memObjects{m}).List(); len(l) != 1 {
t.Fatalf("%d objects, want the first value's only", len(l))
}
m.failPuts = true
if err := v.Put("k2", big(3*kvlimit.ModuleStateValueBytes)); err == nil {
t.Fatal("a put into a full store was accepted")
}
if e, _ := m.Get("k2"); e != nil {
t.Fatal("the key was written though its object was not")
}
}
// raceKV runs then once, after the first Get, as another writer would.
type raceKV struct {
KV
then func()
}
func (r *raceKV) Get(key string) (*Entry, error) {
e, err := r.KV.Get(key)
if r.then != nil {
then := r.then
r.then = nil
then()
}
return e, err
}
// A store that takes no object as large as the value gets it split over objects, read back whole, replaced and dropped as
// one (a bundle's runtime carries a message of 16 MiB).
func TestAValueLargerThanTheStoreTakesIsSplitOverObjects(t *testing.T) {
m := newMemory()
m.most = 100_000
v := m.values()
want := big(5*kvlimit.ModuleStateValueBytes + 10)
if err := v.Put("k", want); err != nil {
t.Fatal(err)
}
if l, _ := (memObjects{m}).List(); len(l) < 13 {
t.Fatalf("%d objects, want the value split over at least 13", len(l))
}
var got map[string]string
if ok, err := v.Get("k", &got); err != nil || !ok || got["text"] != want["text"] {
t.Fatalf("read back %d, %v, %v", len(got["text"]), ok, err)
}
if err := v.Delete("k"); err != nil {
t.Fatal(err)
}
kept, _ := v.Kept()
for _, k := range kept {
if !k.KeyGone || k.Key != "k" {
t.Fatalf("a part %+v is not listed as key-gone for k", k)
}
}
if dropped, err := v.DropKeyGone(); err != nil || len(dropped) != len(kept) {
t.Fatalf("dropped %v of %d, %v", dropped, len(kept), err)
}
}