package bus import ( "context" "errors" "sync" "testing" "time" ) // recvWithTimeout blocks until a value is delivered or the deadline passes, // failing the test on timeout. It makes delivery assertions deterministic // instead of relying on a bare sleep. func recvWithTimeout[T any](t *testing.T, ch <-chan T, timeout time.Duration) T { t.Helper() select { case v := <-ch: return v case <-time.After(timeout): t.Fatalf("timed out waiting for a delivery after %v", timeout) var zero T return zero } } // assertNoRecv fails if a value arrives on ch within d. It is used for the // negative cases where the registration is already gone, so nothing is ever // enqueued and the channel stays empty. func assertNoRecv[T any](t *testing.T, ch <-chan T, d time.Duration) { t.Helper() select { case v := <-ch: t.Fatalf("unexpected delivery: %v", v) case <-time.After(d): } } // waitSubs polls the registry until it holds exactly want subscriptions, with a // deadline, so context-cancellation behavior can be asserted deterministically. func waitSubs(t *testing.T, b *busImpl, want int) { t.Helper() deadline := time.Now().Add(2 * time.Second) for { b.mu.RLock() got := len(b.subs) b.mu.RUnlock() if got == want { return } if time.Now().After(deadline) { t.Fatalf("subscriber count = %d, want %d", got, want) } time.Sleep(time.Millisecond) } } func TestMatch_Rules(t *testing.T) { t.Parallel() tests := []struct { name string pattern string subject string want bool }{ {name: "exact match", pattern: "runtime.loop.start", subject: "runtime.loop.start", want: true}, {name: "exact mismatch", pattern: "runtime.loop.start", subject: "runtime.loop.stop", want: false}, {name: "exact subject has more tokens", pattern: "runtime.loop", subject: "runtime.loop.start", want: false}, {name: "exact subject has fewer tokens", pattern: "runtime.loop.start", subject: "runtime", want: false}, {name: "gt matches nested", pattern: "runtime.>", subject: "runtime.loop.start", want: true}, {name: "gt matches one child", pattern: "runtime.>", subject: "runtime.loop", want: true}, {name: "gt does not match parent", pattern: "runtime.>", subject: "runtime", want: false}, {name: "gt alone matches nested", pattern: ">", subject: "runtime.loop.start", want: true}, {name: "gt alone matches single", pattern: ">", subject: "runtime", want: true}, {name: "star matches one token", pattern: "runtime.*.start", subject: "runtime.loop.start", want: true}, {name: "star rejects extra token", pattern: "runtime.*.start", subject: "runtime.loop.x.start", want: false}, {name: "star rejects value mismatch", pattern: "runtime.*.start", subject: "runtime.loop.stop", want: false}, {name: "star alone matches single", pattern: "*", subject: "runtime", want: true}, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := match(tt.pattern, tt.subject); got != tt.want { t.Errorf("match(%q, %q) = %v, want %v", tt.pattern, tt.subject, got, tt.want) } }) } } func TestPublish_ExactMatch(t *testing.T) { t.Parallel() b := New() defer b.Close() got := make(chan any, 4) if _, err := b.Subscribe(context.Background(), "runtime.loop.start", func(ctx context.Context, e Event) error { got <- e.Payload return nil }); err != nil { t.Fatalf("Subscribe: %v", err) } if err := b.Publish(context.Background(), "runtime.loop.start", "hit"); err != nil { t.Fatalf("Publish: %v", err) } if p := recvWithTimeout(t, got, 2*time.Second); p != "hit" { t.Fatalf("payload = %v, want hit", p) } if err := b.Publish(context.Background(), "runtime.loop.stop", "miss"); err != nil { t.Fatalf("Publish: %v", err) } assertNoRecv(t, got, 100*time.Millisecond) } func TestPublish_GTailWildcard(t *testing.T) { t.Parallel() b := New() defer b.Close() got := make(chan any, 4) if _, err := b.Subscribe(context.Background(), "runtime.>", func(ctx context.Context, e Event) error { got <- e.Payload return nil }); err != nil { t.Fatalf("Subscribe: %v", err) } for _, subject := range []string{"runtime.loop.start", "runtime.loop", "runtime.loop.a.b"} { if err := b.Publish(context.Background(), subject, subject); err != nil { t.Fatalf("Publish(%q): %v", subject, err) } if p := recvWithTimeout(t, got, 2*time.Second); p != subject { t.Fatalf("payload = %v, want %q", p, subject) } } // "runtime.>" must not match the exact "runtime", and a different root // must not match at all. if err := b.Publish(context.Background(), "runtime", "miss-root"); err != nil { t.Fatalf("Publish: %v", err) } if err := b.Publish(context.Background(), "other.x", "miss-other"); err != nil { t.Fatalf("Publish: %v", err) } assertNoRecv(t, got, 100*time.Millisecond) } func TestPublish_StarWildcard(t *testing.T) { t.Parallel() b := New() defer b.Close() got := make(chan any, 4) if _, err := b.Subscribe(context.Background(), "runtime.*.start", func(ctx context.Context, e Event) error { got <- e.Payload return nil }); err != nil { t.Fatalf("Subscribe: %v", err) } if err := b.Publish(context.Background(), "runtime.loop.start", "hit"); err != nil { t.Fatalf("Publish: %v", err) } if p := recvWithTimeout(t, got, 2*time.Second); p != "hit" { t.Fatalf("payload = %v, want hit", p) } for _, subject := range []string{"runtime.loop.x.start", "runtime.loop.stop", "runtime.loop"} { if err := b.Publish(context.Background(), subject, "miss"); err != nil { t.Fatalf("Publish(%q): %v", subject, err) } } assertNoRecv(t, got, 100*time.Millisecond) } func TestSubscribe_UnsubscribeStopsDelivery(t *testing.T) { t.Parallel() b := New() defer b.Close() got := make(chan any, 2) unsub, err := b.Subscribe(context.Background(), "a.b", func(ctx context.Context, e Event) error { got <- e.Payload return nil }) if err != nil { t.Fatalf("Subscribe: %v", err) } if err := b.Publish(context.Background(), "a.b", "first"); err != nil { t.Fatalf("Publish: %v", err) } if p := recvWithTimeout(t, got, 2*time.Second); p != "first" { t.Fatalf("payload = %v, want first", p) } unsub() if err := b.Publish(context.Background(), "a.b", "second"); err != nil { t.Fatalf("Publish: %v", err) } assertNoRecv(t, got, 100*time.Millisecond) } func TestPublish_NonBlockingSlowHandler(t *testing.T) { t.Parallel() b := New() defer b.Close() started := make(chan struct{}) release := make(chan struct{}) var once sync.Once handler := func(ctx context.Context, e Event) error { once.Do(func() { close(started) }) <-release return nil } if _, err := b.Subscribe(context.Background(), "slow", handler); err != nil { t.Fatalf("Subscribe: %v", err) } if err := b.Publish(context.Background(), "slow", "payload"); err != nil { t.Fatalf("Publish: %v", err) } // The handler started and is still blocked on release. Because Publish has // already returned, it did not wait for the handler to finish. select { case <-started: case <-time.After(2 * time.Second): t.Fatal("handler did not start") } select { case <-release: t.Fatal("handler finished before release was closed") case <-time.After(50 * time.Millisecond): } close(release) } func TestBus_CloseStopsDelivery(t *testing.T) { t.Parallel() b := New() defer b.Close() got := make(chan any, 1) if _, err := b.Subscribe(context.Background(), "a.b", func(ctx context.Context, e Event) error { got <- e.Payload return nil }); err != nil { t.Fatalf("Subscribe: %v", err) } if err := b.Publish(context.Background(), "a.b", "before-close"); err != nil { t.Fatalf("Publish: %v", err) } if p := recvWithTimeout(t, got, 2*time.Second); p != "before-close" { t.Fatalf("payload = %v, want before-close", p) } if err := b.Close(); err != nil { t.Fatalf("Close: %v", err) } if err := b.Publish(context.Background(), "a.b", "after-close"); !errors.Is(err, ErrClosed) { t.Fatalf("Publish after Close error = %v, want ErrClosed", err) } assertNoRecv(t, got, 100*time.Millisecond) } func TestPublish_InvalidSubject(t *testing.T) { t.Parallel() b := New() defer b.Close() for _, subject := range []string{"", ".a", "a.", "a..b"} { err := b.Publish(context.Background(), subject, "x") if !errors.Is(err, ErrInvalidArgument) { t.Errorf("Publish(%q) error = %v, want ErrInvalidArgument", subject, err) } } } func TestSubscribe_InvalidPattern(t *testing.T) { t.Parallel() b := New() defer b.Close() for _, pattern := range []string{"", ".a", "a.", "a..b", "a.>.b"} { _, err := b.Subscribe(context.Background(), pattern, func(ctx context.Context, e Event) error { return nil }) if !errors.Is(err, ErrInvalidArgument) { t.Errorf("Subscribe(%q) error = %v, want ErrInvalidArgument", pattern, err) } } } func TestSubscribe_NilHandler(t *testing.T) { t.Parallel() b := New() defer b.Close() _, err := b.Subscribe(context.Background(), "a.b", nil) if !errors.Is(err, ErrInvalidArgument) { t.Fatalf("Subscribe(nil) error = %v, want ErrInvalidArgument", err) } } func TestBus_ClosedError(t *testing.T) { t.Parallel() b := New() if err := b.Close(); err != nil { t.Fatalf("Close: %v", err) } if err := b.Publish(context.Background(), "a.b", "x"); !errors.Is(err, ErrClosed) { t.Fatalf("Publish after Close error = %v, want ErrClosed", err) } if _, err := b.Subscribe(context.Background(), "a.b", func(ctx context.Context, e Event) error { return nil }); !errors.Is(err, ErrClosed) { t.Fatalf("Subscribe after Close error = %v, want ErrClosed", err) } if err := b.Close(); err != nil { t.Fatalf("second Close error = %v, want nil", err) } } func TestBus_DroppedCounter(t *testing.T) { t.Parallel() b := New(WithQueueSize(1)) defer b.Close() started := make(chan struct{}) release := make(chan struct{}) var once sync.Once if _, err := b.Subscribe(context.Background(), "d", func(ctx context.Context, e Event) error { once.Do(func() { close(started) }) <-release return nil }); err != nil { t.Fatalf("Subscribe: %v", err) } if err := b.Publish(context.Background(), "d", "first"); err != nil { t.Fatalf("Publish first: %v", err) } select { case <-started: case <-time.After(2 * time.Second): t.Fatal("handler did not start; worker failed to drain") } // The worker is now blocked in the handler, so it consumed "first" and the // single-slot buffer is empty. The next enqueue fills it; the two after // that find it full and are dropped. for _, payload := range []string{"fill1", "fill2", "fill3"} { if err := b.Publish(context.Background(), "d", payload); err != nil { t.Fatalf("Publish %s: %v", payload, err) } } if got := b.Dropped(); got != 2 { t.Fatalf("Dropped() = %d, want 2", got) } close(release) } func TestSubscribe_ContextCancelAutoUnsub(t *testing.T) { t.Parallel() b := New() defer b.Close() got := make(chan any, 1) ctx, cancel := context.WithCancel(context.Background()) if _, err := b.Subscribe(ctx, "a.b", func(ctx context.Context, e Event) error { got <- e.Payload return nil }); err != nil { t.Fatalf("Subscribe: %v", err) } cancel() waitSubs(t, b, 0) if err := b.Publish(context.Background(), "a.b", "after-cancel"); err != nil { t.Fatalf("Publish: %v", err) } assertNoRecv(t, got, 100*time.Millisecond) } func TestPublish_ConcurrentSafety(t *testing.T) { t.Parallel() b := New(WithQueueSize(8)) defer b.Close() const workers = 8 const iterations = 200 var wg sync.WaitGroup for w := 0; w < workers; w++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < iterations; i++ { if i%3 == 0 { unsub, err := b.Subscribe(context.Background(), "c", func(ctx context.Context, e Event) error { return nil }) if err == nil { unsub() } continue } _ = b.Publish(context.Background(), "c", i) _ = b.Publish(context.Background(), "c.x", i) } }() } wg.Wait() if err := b.Close(); err != nil { t.Fatalf("Close: %v", err) } } func TestClose_Concurrent(t *testing.T) { t.Parallel() b := New() var wg sync.WaitGroup for i := 0; i < 8; i++ { wg.Add(1) go func() { defer wg.Done() _ = b.Publish(context.Background(), "a.b", 1) _, _ = b.Subscribe(context.Background(), "a.b", func(ctx context.Context, e Event) error { return nil }) _ = b.Close() }() } wg.Wait() if err := b.Close(); err != nil { t.Fatalf("Close: %v", err) } }