The tests G added fold into existing ones as subtests (design 47 G §626, A-306: 171 tests down to 154, integration 9 down to 7). TestMeshWaitRefusesLoudlyAndNeverLoops runs its groups of one-second rows as parallel subtests, each on a runtime of its own, 15 s down to 5 s (A-311: the one-second floor of a wait's timeout stands, no clock is injected). The go.mod no longer requires meshconditionsclient, which no package loads (T4). Node names in the lines G adds are role names (T5). Builds: design 47 G §626, design 34 every-event §19, A-306, A-311; review round 2 findings T2, T3, T4, T5 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
194 lines
7.1 KiB
Go
194 lines
7.1 KiB
Go
package runtime
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
)
|
|
|
|
// fakeObjects is the bus of a session: what was put whole, what a read gives, and what the store deleted.
|
|
type fakeObjects struct {
|
|
put map[string][]byte
|
|
description string
|
|
deleted []string
|
|
read []byte
|
|
ctxEnded chan struct{}
|
|
}
|
|
|
|
func (f *fakeObjects) ObjectsPutStream(ctx context.Context, _ string, r buscalls.ObjectsNameRequest, description string, in io.Reader) (buscalls.ObjectInfo, error) {
|
|
data, err := io.ReadAll(in)
|
|
if err != nil {
|
|
f.deleted = append(f.deleted, r.Name)
|
|
if f.ctxEnded != nil {
|
|
<-ctx.Done()
|
|
close(f.ctxEnded)
|
|
}
|
|
return buscalls.ObjectInfo{}, err
|
|
}
|
|
f.put[r.Name], f.description = data, description
|
|
return buscalls.ObjectInfo{Name: r.Name, Size: uint64(len(data)), Description: description}, nil
|
|
}
|
|
|
|
func (f *fakeObjects) ObjectsOpen(context.Context, string, buscalls.ObjectsNameRequest) (buscalls.ObjectInfo, io.ReadCloser, bool, error) {
|
|
return buscalls.ObjectInfo{Name: "o", Size: uint64(len(f.read))}, io.NopCloser(bytes.NewReader(f.read)), true, nil
|
|
}
|
|
|
|
func (f *fakeObjects) SentOpen(context.Context, buscalls.ObjectsSentGetRequest) (buscalls.ObjectInfo, io.ReadCloser, error) {
|
|
return buscalls.ObjectInfo{Size: uint64(len(f.read))}, io.NopCloser(bytes.NewReader(f.read)), nil
|
|
}
|
|
|
|
func aSession(t *testing.T, f *fakeObjects) *moduleBus {
|
|
t.Helper()
|
|
c := &consumers{}
|
|
c.sessions.store = f
|
|
return &moduleBus{all: c, module: "m"}
|
|
}
|
|
|
|
func asked(t *testing.T, b *moduleBus, method string, req any, into any) {
|
|
t.Helper()
|
|
params, _ := json.Marshal(req)
|
|
raw, err := b.BusState(method, params)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", method, err)
|
|
}
|
|
if err := json.Unmarshal(raw, into); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// §490, §518, §620: a value over one chunk is got as a session (the chunked answer, then each chunk by its call and sequence,
|
|
// the whole checked against the digest); a sent object over one chunk the same, with `chunked` true.
|
|
func TestAGetOverOneChunkRunsAsASession(t *testing.T) {
|
|
whole := bytes.Repeat([]byte("y"), 2*buscalls.ChunkBytes)
|
|
f := &fakeObjects{read: whole}
|
|
b := aSession(t, f)
|
|
sum := sha256.Sum256(whole)
|
|
want := "sha256:" + hex.EncodeToString(sum[:])
|
|
for name, run := range map[string]func() buscalls.ObjectsChunked{
|
|
"state": func() buscalls.ObjectsChunked {
|
|
var c buscalls.ObjectsChunked
|
|
asked(t, b, buscalls.ObjectsGet, buscalls.ObjectsNameRequest{Objects: "files", Name: "o"}, &c)
|
|
return c
|
|
},
|
|
"sent": func() buscalls.ObjectsChunked {
|
|
var c buscalls.ObjectsChunked
|
|
asked(t, b, buscalls.ObjectsSentGet, buscalls.ObjectsSentGetRequest{Store: "m_sent-objects", Object: "o", Size: int64(len(whole)), Digest: hex.EncodeToString(sum[:])}, &c)
|
|
if string(c.Chunked) != "true" {
|
|
t.Errorf("a sent object's chunked is %s", c.Chunked)
|
|
}
|
|
return c
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
c := run()
|
|
if c.Call == "" || c.Size != uint64(len(whole)) {
|
|
t.Fatalf("the get answered %+v", c)
|
|
}
|
|
if name == "state" {
|
|
if string(c.Chunked) != "2" {
|
|
t.Errorf("chunked is %s, want 2", c.Chunked)
|
|
}
|
|
}
|
|
var got []byte
|
|
for seq := uint32(1); ; seq++ {
|
|
var ch buscalls.ObjectsChunk
|
|
asked(t, b, buscalls.ObjectsGetChunk, buscalls.ObjectsGetChunkRequest{Call: c.Call, Seq: seq}, &ch)
|
|
if ch.Error != "" || ch.Seq != seq {
|
|
t.Fatalf("chunk %d: %+v", seq, ch)
|
|
}
|
|
got = append(got, ch.Data...)
|
|
if ch.Last {
|
|
if seq != 2 {
|
|
t.Fatalf("an object of two chunks ended at chunk %d", seq)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !bytes.Equal(got, whole) || name == "state" && c.Digest == "" {
|
|
t.Fatalf("read %d bytes, digest %s", len(got), c.Digest)
|
|
}
|
|
if name == "sent" && c.Digest != want {
|
|
t.Errorf("digest %s, want %s", c.Digest, want)
|
|
}
|
|
var again buscalls.ObjectsChunk
|
|
asked(t, b, buscalls.ObjectsGetChunk, buscalls.ObjectsGetChunkRequest{Call: c.Call, Seq: 3}, &again)
|
|
if again.Error == "" {
|
|
t.Error("a finished session answered another chunk")
|
|
}
|
|
})
|
|
}
|
|
// A value of one chunk or less is the value itself, as before.
|
|
f.read = []byte("small")
|
|
var small buscalls.ObjectData
|
|
asked(t, b, buscalls.ObjectsGet, buscalls.ObjectsNameRequest{Objects: "files", Name: "o"}, &small)
|
|
if string(small.Data) != "small" {
|
|
t.Fatalf("a small value was answered %+v", small)
|
|
}
|
|
// Design 47 G §518, A-21, A-25: a put session writes each chunk as it is taken, carries the description of its first chunk,
|
|
// and answers the put's own info for the last; a chunk out of order fails the put naming both sequences and leaves none.
|
|
t.Run("APutSessionSealsOnTheLastChunkAndDropsOnADisorder", func(t *testing.T) {
|
|
f := &fakeObjects{put: map[string][]byte{}}
|
|
b := aSession(t, f)
|
|
whole := bytes.Repeat([]byte("x"), 2*buscalls.ChunkBytes+5)
|
|
var taken buscalls.ObjectsChunkTaken
|
|
for seq, at := uint32(1), 0; ; seq++ {
|
|
end := min(at+buscalls.ChunkBytes, len(whole))
|
|
req := buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "a", Call: "c1", Seq: seq, Last: end == len(whole), Data: whole[at:end]}
|
|
if seq == 1 {
|
|
req.Description = "what it is"
|
|
}
|
|
if req.Last {
|
|
var info buscalls.ObjectInfo
|
|
asked(t, b, buscalls.ObjectsPutChunk, req, &info)
|
|
if info.Size != uint64(len(whole)) || info.Description != "what it is" {
|
|
t.Fatalf("the put answered %+v", info)
|
|
}
|
|
break
|
|
}
|
|
asked(t, b, buscalls.ObjectsPutChunk, req, &taken)
|
|
if taken.Seq != seq || taken.Error != "" {
|
|
t.Fatalf("chunk %d was answered %+v", seq, taken)
|
|
}
|
|
at = end
|
|
}
|
|
if !bytes.Equal(f.put["a"], whole) {
|
|
t.Fatalf("the store holds %d bytes, want %d", len(f.put["a"]), len(whole))
|
|
}
|
|
|
|
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 1, Data: []byte("one")}, &taken)
|
|
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 3, Data: []byte("three")}, &taken)
|
|
if !strings.Contains(taken.Error, "chunk 3") || !strings.Contains(taken.Error, "chunk 2") {
|
|
t.Fatalf("a disorder was answered %+v", taken)
|
|
}
|
|
if _, ok := f.put["b"]; ok || len(f.deleted) != 1 || f.deleted[0] != "b" {
|
|
t.Fatalf("a dropped put left %v, deleted %v", f.put, f.deleted)
|
|
}
|
|
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "b", Call: "c2", Seq: 2, Data: []byte("two")}, &taken)
|
|
if taken.Error == "" {
|
|
t.Fatal("a chunk of a dropped put was taken")
|
|
}
|
|
})
|
|
// §518 "The bound": a put not sealed within the bound is dropped, its object deleted.
|
|
t.Run("APutNotSealedWithinTheBoundIsDropped", func(t *testing.T) {
|
|
was := SessionBound
|
|
SessionBound = time.Millisecond
|
|
t.Cleanup(func() { SessionBound = was })
|
|
f := &fakeObjects{put: map[string][]byte{}, ctxEnded: make(chan struct{})}
|
|
b := aSession(t, f)
|
|
var taken buscalls.ObjectsChunkTaken
|
|
asked(t, b, buscalls.ObjectsPutChunk, buscalls.ObjectsPutChunkRequest{Objects: "files", Name: "a", Call: "c", Seq: 1, Data: []byte("x")}, &taken)
|
|
<-f.ctxEnded
|
|
if len(f.deleted) != 1 {
|
|
t.Fatalf("the unsealed object was left: %v", f.deleted)
|
|
}
|
|
})
|
|
}
|