Files
Cyrene_v2/internal/runtime/orchestration/comm/comm_test.go
T
AskaEth eb1828ee7a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/orchestration(29 文件,纯替换)
- 只翻注释/文档字符串:剥离注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 裁决编号与 design 节号(02.4.3 / 11.4.2 / design/05 §9 等)逐字保留
- 复核:go build 绿;orchestration 及其子包(audit/comm)测试绿;gofmt 干净
2026-10-04 13:30:04 +08:00

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)
}
}