Files
Cyrene_v2/internal/runtime/orchestration/comm/comm_test.go
T

360 lines
8.5 KiB
Go

package comm
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
)
// recvEvent blocks until an event arrives on ch or d elapses, failing the test
// on timeout. It makes delivery assertions deterministic instead of relying on a
// bare sleep.
func recvEvent(t *testing.T, ch <-chan Event, d time.Duration) Event {
t.Helper()
select {
case e := <-ch:
return e
case <-time.After(d):
t.Fatalf("timed out waiting for an event after %v", d)
return Event{}
}
}
func TestSubjectConvention(t *testing.T) {
t.Parallel()
cases := []struct {
name string
got string
want string
}{
{"ask", askSubject("alice", "bob"), "cyrene.agent.alice.bob.ask"},
{"reply carries ask id", replySubject("bob", "alice", "alice-3"), "cyrene.agent.bob.alice.reply.alice-3"},
{"event addressed", eventSubject("alice", "bob"), "cyrene.agent.alice.bob.event"},
{"event broadcast", broadcastEventSubject("alice"), "cyrene.agent.alice.event"},
{"ask receive pattern", askReceivePattern("bob"), "cyrene.agent.*.bob.ask"},
{"agent ids sanitized", askSubject("a.b", "c d"), "cyrene.agent.a_b.c_d.ask"},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
if c.got != c.want {
t.Fatalf("subject = %q, want %q", c.got, c.want)
}
})
}
}
func TestEventReceivePatterns(t *testing.T) {
t.Parallel()
got := eventReceivePatterns("bob")
want := []string{"cyrene.agent.*.event", "cyrene.agent.*.bob.event"}
if len(got) != len(want) {
t.Fatalf("patterns = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("patterns[%d] = %q, want %q", i, got[i], want[i])
}
}
}
func TestAskReply_RoundTrip(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
alice := New(b, "alice")
bob := New(b, "bob")
unsub, err := bob.SubscribeAsk(context.Background(), func(ctx context.Context, a Ask) error {
return bob.Answer(ctx, a.From, a.ID, "pong:"+a.Question, a.Data)
})
if err != nil {
t.Fatalf("SubscribeAsk: %v", err)
}
defer unsub()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
reply, err := alice.Ask(ctx, "bob", "ping", "payload")
if err != nil {
t.Fatalf("Ask: %v", err)
}
if reply.Text != "pong:ping" {
t.Fatalf("reply.Text = %q, want pong:ping", reply.Text)
}
if reply.From != "bob" {
t.Fatalf("reply.From = %q, want bob", reply.From)
}
if reply.To != "alice" {
t.Fatalf("reply.To = %q, want alice", reply.To)
}
if reply.Data != "payload" {
t.Fatalf("reply.Data = %#v, want payload", reply.Data)
}
}
func TestAsk_TimeoutNoReply(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
alice := New(b, "alice")
// bob has no ask handler, so nothing ever replies.
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err := alice.Ask(ctx, "bob", "hello", nil)
if !errors.Is(err, ErrTimeout) {
t.Fatalf("Ask err = %v, want ErrTimeout", err)
}
}
func TestAsk_ContextCanceled(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
alice := New(b, "alice")
ctx, cancel := context.WithCancel(context.Background())
got := make(chan error, 1)
go func() {
_, err := alice.Ask(ctx, "bob", "hello", nil)
got <- err
}()
time.Sleep(50 * time.Millisecond)
cancel()
select {
case err := <-got:
if !errors.Is(err, context.Canceled) {
t.Fatalf("Ask err = %v, want context.Canceled", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Ask did not return after ctx cancel")
}
}
func TestAsk_ContextPreCanceled(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
alice := New(b, "alice")
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := alice.Ask(ctx, "bob", "q", nil)
if !errors.Is(err, context.Canceled) {
t.Fatalf("Ask err = %v, want context.Canceled", err)
}
}
func TestEventBroadcast(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
alice := New(b, "alice")
bob := New(b, "bob")
events := make(chan Event, 4)
unsub, err := bob.SubscribeEvent(context.Background(), func(ctx context.Context, e Event) error {
events <- e
return nil
})
if err != nil {
t.Fatalf("SubscribeEvent: %v", err)
}
defer unsub()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := alice.PublishEvent(ctx, Event{Name: "tick", Data: 7}); err != nil {
t.Fatalf("PublishEvent: %v", err)
}
e := recvEvent(t, events, 2*time.Second)
if e.Name != "tick" || e.Data != 7 || e.From != "alice" {
t.Fatalf("event = %+v, want name=tick data=7 from=alice", e)
}
}
func TestEventSend_AddressedAndIsolated(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
alice := New(b, "alice")
bob := New(b, "bob")
carol := New(b, "carol")
bobEvents := make(chan Event, 4)
carolEvents := make(chan Event, 4)
unsubBob, err := bob.SubscribeEvent(context.Background(), func(ctx context.Context, e Event) error {
bobEvents <- e
return nil
})
if err != nil {
t.Fatalf("SubscribeEvent bob: %v", err)
}
defer unsubBob()
unsubCarol, err := carol.SubscribeEvent(context.Background(), func(ctx context.Context, e Event) error {
carolEvents <- e
return nil
})
if err != nil {
t.Fatalf("SubscribeEvent carol: %v", err)
}
defer unsubCarol()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := alice.Send(ctx, "bob", Event{Name: "direct"}); err != nil {
t.Fatalf("Send: %v", err)
}
e := recvEvent(t, bobEvents, 2*time.Second)
if e.Name != "direct" || e.From != "alice" {
t.Fatalf("bob event = %+v, want name=direct from=alice", e)
}
// An event addressed to bob must not reach carol.
select {
case e := <-carolEvents:
t.Fatalf("carol unexpectedly received %+v", e)
case <-time.After(100 * time.Millisecond):
}
}
func TestEventHandler_Concurrent(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
emitter := New(b, "emit")
recv := New(b, "recv")
const n = 100
var got atomic.Int64
done := make(chan struct{})
unsub, err := recv.SubscribeEvent(context.Background(), func(ctx context.Context, e Event) error {
if got.Add(1) == n {
close(done)
}
return nil
})
if err != nil {
t.Fatalf("SubscribeEvent: %v", err)
}
defer unsub()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Fire a mix of broadcasts and events addressed to recv so both event
// subscriptions (and their separate worker goroutines) invoke the handler
// concurrently.
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if i%2 == 0 {
_ = emitter.PublishEvent(ctx, Event{Name: "bc", Data: i})
} else {
_ = emitter.Send(ctx, "recv", Event{Name: "to", Data: i})
}
}(i)
}
wg.Wait()
select {
case <-done:
case <-time.After(3 * time.Second):
t.Fatalf("only %d of %d events delivered", got.Load(), n)
}
}
func TestAsk_ConcurrentDistinct(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
alice := New(b, "alice")
bob := New(b, "bob")
unsub, err := bob.SubscribeAsk(context.Background(), func(ctx context.Context, a Ask) error {
return bob.Answer(ctx, a.From, a.ID, "ack:"+a.Question, a.Data)
})
if err != nil {
t.Fatalf("SubscribeAsk: %v", err)
}
defer unsub()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
const n = 20
var wg sync.WaitGroup
errs := make(chan error, n)
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
q := fmt.Sprintf("q%d", i)
reply, err := alice.Ask(ctx, "bob", q, i)
if err != nil {
errs <- fmt.Errorf("ask %d: %w", i, err)
return
}
if reply.Text != "ack:"+q {
errs <- fmt.Errorf("ask %d: reply.Text = %q, want %q", i, reply.Text, "ack:"+q)
return
}
if reply.Data != i {
errs <- fmt.Errorf("ask %d: reply.Data = %#v, want %d", i, reply.Data, i)
return
}
}(i)
}
wg.Wait()
close(errs)
for err := range errs {
t.Fatal(err)
}
}
func TestSend_NoErrorAndNoReceiver(t *testing.T) {
t.Parallel()
b := bus.New()
defer b.Close()
alice := New(b, "alice")
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Publishing to a subject with no subscriber is a no-op, not an error.
if err := alice.Send(ctx, "ghost", Event{Name: "nobody"}); err != nil {
t.Fatalf("Send to lost receiver: %v", err)
}
if err := alice.PublishEvent(ctx, Event{Name: "nobody"}); err != nil {
t.Fatalf("PublishEvent with no subscriber: %v", err)
}
}