219 lines
5.0 KiB
Go
219 lines
5.0 KiB
Go
package broker_test
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/uopi/uopi/internal/broker"
|
|
"github.com/uopi/uopi/internal/datasource/stub"
|
|
)
|
|
|
|
func newBroker(tb testing.TB) (*broker.Broker, context.CancelFunc) {
|
|
tb.Helper()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
log := slog.Default()
|
|
b := broker.New(ctx, log)
|
|
ds := stub.New()
|
|
if err := ds.Connect(ctx); err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
b.Register(ds)
|
|
return b, cancel
|
|
}
|
|
|
|
func recv(t *testing.T, ch <-chan broker.Update, timeout time.Duration) broker.Update {
|
|
t.Helper()
|
|
select {
|
|
case u := <-ch:
|
|
return u
|
|
case <-time.After(timeout):
|
|
t.Fatalf("timed out waiting for update after %s", timeout)
|
|
return broker.Update{}
|
|
}
|
|
}
|
|
|
|
func TestFanOut(t *testing.T) {
|
|
b, cancel := newBroker(t)
|
|
defer cancel()
|
|
|
|
ref := broker.SignalRef{DS: "stub", Name: "sine_1hz"}
|
|
ch1 := make(chan broker.Update, 10)
|
|
ch2 := make(chan broker.Update, 10)
|
|
|
|
unsub1, err := b.Subscribe(ref, ch1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unsub2, err := b.Subscribe(ref, ch2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer unsub2()
|
|
|
|
// Both channels must receive an update
|
|
recv(t, ch1, 2*time.Second)
|
|
recv(t, ch2, 2*time.Second)
|
|
|
|
// Only one upstream subscription should exist
|
|
if n := b.ActiveSubscriptions(); n != 1 {
|
|
t.Errorf("expected 1 active subscription, got %d", n)
|
|
}
|
|
|
|
// After unsubscribing ch1, ch2 should still receive updates
|
|
unsub1()
|
|
// drain any buffered updates
|
|
for len(ch2) > 0 {
|
|
<-ch2
|
|
}
|
|
recv(t, ch2, 2*time.Second)
|
|
}
|
|
|
|
func TestUnsubscribeLastClientTearsDownUpstream(t *testing.T) {
|
|
b, cancel := newBroker(t)
|
|
defer cancel()
|
|
|
|
ref := broker.SignalRef{DS: "stub", Name: "ramp_10s"}
|
|
ch := make(chan broker.Update, 10)
|
|
|
|
unsub, err := b.Subscribe(ref, ch)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recv(t, ch, 2*time.Second)
|
|
|
|
if n := b.ActiveSubscriptions(); n != 1 {
|
|
t.Errorf("expected 1 active subscription before unsub, got %d", n)
|
|
}
|
|
|
|
unsub()
|
|
// Give the broker a moment to process the teardown
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
if n := b.ActiveSubscriptions(); n != 0 {
|
|
t.Errorf("expected 0 active subscriptions after unsub, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestUnknownDataSource(t *testing.T) {
|
|
b, cancel := newBroker(t)
|
|
defer cancel()
|
|
|
|
ref := broker.SignalRef{DS: "nonexistent", Name: "signal"}
|
|
ch := make(chan broker.Update, 1)
|
|
|
|
_, err := b.Subscribe(ref, ch)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown data source, got nil")
|
|
}
|
|
}
|
|
|
|
func TestUnknownSignal(t *testing.T) {
|
|
b, cancel := newBroker(t)
|
|
defer cancel()
|
|
|
|
ref := broker.SignalRef{DS: "stub", Name: "does_not_exist"}
|
|
ch := make(chan broker.Update, 1)
|
|
|
|
_, err := b.Subscribe(ref, ch)
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown signal, got nil")
|
|
}
|
|
}
|
|
|
|
func TestMaxUpdateRate(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
const maxHz = 5.0 // 5 Hz → 200 ms interval
|
|
|
|
ds := stub.New() // counter_fast fires at 1 kHz
|
|
if err := ds.Connect(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := broker.New(ctx, slog.Default(), broker.WithMaxUpdateRate(maxHz))
|
|
b.Register(ds)
|
|
|
|
ref := broker.SignalRef{DS: "stub", Name: "counter_fast"}
|
|
ch := make(chan broker.Update, 512)
|
|
unsub, err := b.Subscribe(ref, ch)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer unsub()
|
|
|
|
// Collect updates for 2 seconds.
|
|
var count int
|
|
deadline := time.After(2 * time.Second)
|
|
loop:
|
|
for {
|
|
select {
|
|
case <-ch:
|
|
count++
|
|
case <-deadline:
|
|
break loop
|
|
}
|
|
}
|
|
|
|
// At 5 Hz for 2 s we expect ~10 updates (±3 for timer jitter + coalescing).
|
|
t.Logf("MaxUpdateRate: received %d updates at %.0f Hz limit over 2s (expect ~10)", count, maxHz)
|
|
if count < 5 || count > 20 {
|
|
t.Errorf("expected ~10 updates at 5 Hz, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestMaxUpdateRate_UnlimitedByDefault(t *testing.T) {
|
|
b, cancel := newBroker(t) // no WithMaxUpdateRate → unlimited
|
|
defer cancel()
|
|
|
|
ref := broker.SignalRef{DS: "stub", Name: "counter_fast"}
|
|
ch := make(chan broker.Update, 4096)
|
|
unsub, err := b.Subscribe(ref, ch)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer unsub()
|
|
|
|
// counter_fast fires at 1 kHz; over 500 ms we should see ≥ 400 updates.
|
|
time.Sleep(500 * time.Millisecond)
|
|
got := len(ch)
|
|
t.Logf("Unlimited: received %d updates in 500ms from 1kHz signal", got)
|
|
if got < 400 {
|
|
t.Errorf("expected >= 400 updates with no rate limit, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestMultipleSignals(t *testing.T) {
|
|
b, cancel := newBroker(t)
|
|
defer cancel()
|
|
|
|
signals := []string{"sine_1hz", "sine_01hz", "ramp_10s", "toggle_1hz"}
|
|
channels := make([]chan broker.Update, len(signals))
|
|
unsubs := make([]func(), len(signals))
|
|
|
|
for i, name := range signals {
|
|
ch := make(chan broker.Update, 10)
|
|
channels[i] = ch
|
|
unsub, err := b.Subscribe(broker.SignalRef{DS: "stub", Name: name}, ch)
|
|
if err != nil {
|
|
t.Fatalf("subscribe %s: %v", name, err)
|
|
}
|
|
unsubs[i] = unsub
|
|
}
|
|
defer func() {
|
|
for _, u := range unsubs {
|
|
u()
|
|
}
|
|
}()
|
|
|
|
if n := b.ActiveSubscriptions(); n != len(signals) {
|
|
t.Errorf("expected %d active subscriptions, got %d", len(signals), n)
|
|
}
|
|
|
|
for i, ch := range channels {
|
|
recv(t, ch, 2*time.Second)
|
|
_ = i
|
|
}
|
|
}
|