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