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