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
118 lines
4.7 KiB
Go
118 lines
4.7 KiB
Go
package bus_test
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// Design 47 G A-249, §681: the runner asks a chunked verb with a call token of its own, receives the chunks the answer counts
|
|
// (each acknowledged before the next) and answers the verb's own answer with the whole file, so the asker receives none.
|
|
func TestAChunkedSeatAnswerIsReceivedByTheRunnerAndHandedWhole(t *testing.T) {
|
|
url := mt.URL(t)
|
|
holder := dial(t, url, "mesh-delivery-indexer", "anchor", "")
|
|
asker := dial(t, url, "mesh-build-go", "anchor", "")
|
|
raw, err := nats.Connect(url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(raw.Close)
|
|
file := make([]byte, buscalls.SeatChunkBytes*2+5)
|
|
for i := range file {
|
|
file[i] = byte(i)
|
|
}
|
|
// the holder's process: answers the count, then sends each chunk and waits for its acknowledgement
|
|
stop, err := holder.Handle("seat:mesh-catalog.input", func(body json.RawMessage) (any, error) {
|
|
var in struct{ Call, Digest string }
|
|
if err := json.Unmarshal(body, &in); err != nil || in.Call == "" {
|
|
t.Errorf("the verb was asked with no call token: %s (%v)", body, err)
|
|
return nil, err
|
|
}
|
|
send := buscalls.SeatChunks{Seat: "mesh-catalog", Verb: "input", Call: in.Call}
|
|
go func() {
|
|
for n := int64(1); n <= 3; n++ {
|
|
end := min(int64(len(file)), n*buscalls.SeatChunkBytes)
|
|
data, _ := json.Marshal(buscalls.SeatChunk{N: n, Of: 3, Data: file[(n-1)*buscalls.SeatChunkBytes : end]})
|
|
ackSub, _ := raw.SubscribeSync(send.Ack(n))
|
|
_ = raw.Flush()
|
|
if err := raw.Publish(send.Chunk(n), data); err != nil {
|
|
return
|
|
}
|
|
m, err := ackSub.NextMsg(buscalls.SeatAckWithin)
|
|
if err != nil {
|
|
t.Errorf("chunk %d not acknowledged: %v", n, err)
|
|
return
|
|
}
|
|
_ = m.Respond([]byte("{}"))
|
|
}
|
|
}()
|
|
return map[string]any{"digest": in.Digest, "chunks": 3}, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stop()
|
|
holder.Flush() // the bus holds the verb's subscription before it is asked: the asker is another connection
|
|
got, err := asker.AskAsChunked("mesh-build-go", "seat:mesh-catalog.input", map[string]any{"digest": "d1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var ans struct {
|
|
Digest string `json:"digest"`
|
|
Chunks int `json:"chunks"`
|
|
File string `json:"file"`
|
|
}
|
|
if err := json.Unmarshal(got.Result, &ans); err != nil || ans.Digest != "d1" || ans.Chunks != 3 {
|
|
t.Fatalf("the answer %s: %v", got.Result, err)
|
|
}
|
|
if body, err := base64.StdEncoding.DecodeString(ans.File); err != nil || string(body) != string(file) {
|
|
t.Errorf("the file handed is %d bytes, want the %d sent: %v", len(body), len(file), err)
|
|
}
|
|
// Design 47 G A-257, A-254: a bundle answers a chunked verb with the whole file in the member `file`; the runner that serves the
|
|
// verb sends the file in chunks (each acknowledged before the next) and answers the count, so the bundle sends none, and the asker
|
|
// of one node's verb receives it whole.
|
|
t.Run("ABundlesChunkedAnswerIsSentByTheRunner", func(t *testing.T) {
|
|
url := mt.URL(t)
|
|
holder := dial(t, url, "mesh-build-go", "anchor", "")
|
|
asker := dial(t, url, "mesh-delivery-indexer", "anchor", "")
|
|
file := make([]byte, buscalls.SeatChunkBytes*2+7)
|
|
for i := range file {
|
|
file[i] = byte(i * 7)
|
|
}
|
|
stop, err := holder.Handle("seat:node-build-go.output@anchor", func(body json.RawMessage) (any, error) {
|
|
answer := map[string]any{"ask": "a1", "file": base64.StdEncoding.EncodeToString(file)}
|
|
return holder.AnswerChunked("node-build-go", "output", body, answer)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stop()
|
|
holder.Flush() // the bus holds the verb's subscription before it is asked: the asker is another connection
|
|
got, err := asker.AskAsChunked("mesh-delivery-indexer", "seat:node-build-go.output@anchor", map[string]any{"ask": "a1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var ans struct {
|
|
Ask string `json:"ask"`
|
|
Chunks int `json:"chunks"`
|
|
File string `json:"file"`
|
|
}
|
|
if err := json.Unmarshal(got.Result, &ans); err != nil || ans.Ask != "a1" || ans.Chunks != 3 {
|
|
t.Fatalf("the answer %s: %v", got.Result, err)
|
|
}
|
|
if body, err := base64.StdEncoding.DecodeString(ans.File); err != nil || string(body) != string(file) {
|
|
t.Errorf("the file handed is %d bytes, want the %d sent: %v", len(body), len(file), err)
|
|
}
|
|
// an answer with no file is the verb's own: no chunks are sent
|
|
plain, err := holder.AnswerChunked("node-build-go", "output", json.RawMessage(`{"call":"c"}`), map[string]any{"ask": "x", "chunks": 0})
|
|
if m, _ := plain.(map[string]any); err != nil || m["chunks"] != 0 {
|
|
t.Fatalf("an answer with no file was %v, %v", plain, err)
|
|
}
|
|
})
|
|
}
|