- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留 - 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
358 lines
8.4 KiB
Go
358 lines
8.4 KiB
Go
package comm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
|
)
|
|
|
|
// recvEvent 阻塞到 ch 上到达一个事件或 d 到期,超时则判测试失败。它让投递断言确定化,
|
|
// 而不必依赖干等的 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 没有任何 ask 处理函数,因此永远不会有人回复。
|
|
|
|
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)
|
|
}
|
|
|
|
// 定向给 bob 的事件不得抵达 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()
|
|
|
|
// 混合发射广播与定向给 recv 的事件,使两个事件订阅(及其各自的工作 goroutine)
|
|
// 并发调用处理函数。
|
|
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()
|
|
|
|
// 向没有订阅者的 subject 发布是空操作,不是错误。
|
|
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)
|
|
}
|
|
}
|