package stateobjects import ( "bytes" "encoding/json" "slices" "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 the old object and a delete leaves its object, both listed key-gone until the drop (ADR 0666 §16, ADR 0595 §4). func valueRows(t *testing.T, v Values, multiples ...int) { t.Helper() limit := kvlimit.ModuleStateValueBytes for _, times := range multiples { 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 leaves the object it replaced, listed key-gone (ADR 0595 §4); the same value again adds none before, _ := v.Objects.List() if err := v.Put("one", big(2*limit)); err != nil { 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)+1 { t.Errorf("a replace left %d objects, %d before it: the old one must stay", len(after), len(before)) return } if err := v.Put("one", "small now"); err != nil { t.Error(err) return } if now, _ := v.Objects.List(); len(now) != len(after) { t.Errorf("replacing by a small value changed the objects: %d, want %d", len(now), len(after)) 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) } } slices.Sort(gone) if !slices.Equal(gone, []string{"one", "one", "ten"}) { t.Errorf("the objects whose key is gone: %v, want the two replaced of one and ten's", gone) return } dropped, err := v.DropKeyGone() if err != nil || len(dropped) != 3 { t.Errorf("dropped %v, %v", dropped, err) return } if left, _ := v.Objects.List(); len(left) != len(kept)-3 { 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, 1, 10) // a bundle puts at most 8 MiB in an object: 100x is a process's 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 refuses it, loudly, and writes nothing. func TestAValueLargerThanTheStoreTakesIsRefused(t *testing.T) { m := newMemory() m.most = 100_000 v := m.values() err := v.Put("k", big(5*kvlimit.ModuleStateValueBytes)) if err == nil || !strings.Contains(err.Error(), "refused") { t.Fatalf("the put: %v", err) } if e, _ := m.Get("k"); e != nil || len(m.objects) != 0 { t.Fatal("a refused value left a key or an object") } }