Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

285 lines
7.5 KiB
Go

package server
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBroadcaster_EmitFansOutToAllSubscribers(t *testing.T) {
b := NewBroadcaster(10 * time.Second)
sub1, unsub1 := b.Subscribe()
defer unsub1()
sub2, unsub2 := b.Subscribe()
defer unsub2()
b.Emit("messages")
for i, sub := range []<-chan Event{sub1, sub2} {
select {
case ev := <-sub:
assert.Equal(t, "messages", ev.Scope, "sub %d", i)
case <-time.After(time.Second):
require.Fail(t, "timed out waiting for event", "sub %d", i)
}
}
}
func TestBroadcaster_EmitIsNonBlockingOnSlowSubscriber(t *testing.T) {
// Disable rate limiting so every Emit attempts a broadcast, which
// is what exercises the non-blocking select-default path against
// a slow subscriber.
b := NewBroadcaster(0)
slow, unsub := b.Subscribe()
defer unsub()
// Don't read from slow. Fill its buffer + one extra; Emit must not block.
const extra = 5
done := make(chan struct{})
go func() {
for range broadcasterBufferCap + extra {
b.Emit("messages")
}
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
require.Fail(t, "Emit blocked on slow subscriber")
}
// Drain what we can — drop count >= extra, exact count not guaranteed.
drained := 0
for {
select {
case <-slow:
drained++
case <-time.After(50 * time.Millisecond):
require.NotZero(t, drained, "slow subscriber received nothing")
return
}
}
}
func TestBroadcaster_UnsubscribeStopsDelivery(t *testing.T) {
b := NewBroadcaster(10 * time.Second)
sub, unsub := b.Subscribe()
unsub()
b.Emit("messages")
select {
case ev, ok := <-sub:
require.False(t, ok, "got event after unsubscribe: %v", ev)
// channel closed by unsubscribe — acceptable
case <-time.After(100 * time.Millisecond):
// no delivery — also acceptable
}
}
func TestBroadcaster_ConcurrentSubscribeAndEmit(t *testing.T) {
// Disable rate limiting so each subscriber's Emit reliably
// produces a broadcast during the race.
b := NewBroadcaster(0)
var wg sync.WaitGroup
for range 20 {
wg.Go(func() {
sub, unsub := b.Subscribe()
defer unsub()
b.Emit("sessions")
select {
case <-sub:
case <-time.After(time.Second):
assert.Fail(t, "concurrent subscriber did not receive event")
}
})
}
wg.Wait()
}
func TestBroadcaster_LeadingEdgeEmitsImmediately(t *testing.T) {
b := NewBroadcaster(time.Second)
sub, unsub := b.Subscribe()
defer unsub()
b.Emit("messages")
select {
case ev := <-sub:
assert.Equal(t, "messages", ev.Scope)
case <-time.After(100 * time.Millisecond):
require.Fail(t, "first emit did not broadcast immediately")
}
}
func TestBroadcaster_CoalescesWithinWindow(t *testing.T) {
const interval = 100 * time.Millisecond
b := NewBroadcaster(interval)
sub, unsub := b.Subscribe()
defer unsub()
// Leading-edge broadcast drains the first emit.
b.Emit("sessions")
select {
case <-sub:
case <-time.After(50 * time.Millisecond):
require.Fail(t, "leading-edge emit did not broadcast immediately")
}
// Bursts within the window are coalesced; no broadcast yet.
b.Emit("messages")
b.Emit("sync")
b.Emit("sessions")
select {
case ev := <-sub:
require.Fail(t, "got early broadcast during rate-limit window", "ev=%v", ev)
case <-time.After(interval / 2):
}
// After the window elapses a single trailing broadcast arrives
// carrying the most recent scope.
select {
case ev := <-sub:
assert.Equal(t, "sessions", ev.Scope, "trailing scope")
case <-time.After(interval * 3):
require.Fail(t, "trailing broadcast never arrived")
}
// The three coalesced emits produce exactly one trailing broadcast.
select {
case ev := <-sub:
require.Fail(t, "got duplicate broadcast after trailing fire", "ev=%v", ev)
case <-time.After(interval):
}
}
func TestBroadcaster_LeadingEdgeCancelsPendingTrailing(t *testing.T) {
const interval = 50 * time.Millisecond
b := NewBroadcaster(interval)
sub, unsub := b.Subscribe()
defer unsub()
// Leading broadcast fills the window.
b.Emit("a")
select {
case <-sub:
case <-time.After(interval):
require.Fail(t, "leading emit did not broadcast")
}
// Rate-limited emit schedules a trailing broadcast of "b".
b.Emit("b")
// Simulate the race: another Emit arrives just after the window
// boundary but before the in-flight trailing timer can acquire
// the lock. Backdating lastEmit forces the next Emit to take the
// leading branch while pending is still set and the timer is
// still armed.
b.mu.Lock()
b.lastEmit = time.Now().Add(-2 * interval)
b.mu.Unlock()
b.Emit("c")
select {
case ev := <-sub:
assert.Equal(t, "c", ev.Scope, "leading broadcast scope")
case <-time.After(interval):
require.Fail(t, "second leading emit did not broadcast")
}
// The pre-existing trailing timer for "b" may still fire. If the
// leading branch did not cancel pending/timer, flushTrailing
// would now deliver a stale "b" broadcast. Wait past the
// original deadline and assert no extra event arrives.
select {
case ev := <-sub:
require.Fail(t, "stale trailing broadcast after leading edge", "ev=%v", ev)
case <-time.After(2 * interval):
}
}
func TestBroadcaster_StaleTrailingCallbackDoesNotConsumeNewerPending(t *testing.T) {
// Narrow race: a trailing callback whose timer already fired is
// waiting for b.mu; a leading-edge Emit runs first and a follow-up
// rate-limited Emit installs a new pending+timer. Without a
// generation token, the stale callback clobbers b.timer and
// broadcasts the newer pending event immediately, violating the
// rate limit.
const interval = 50 * time.Millisecond
b := NewBroadcaster(interval)
sub, unsub := b.Subscribe()
defer unsub()
b.Emit("a")
<-sub
// Rate-limited emit schedules a timer; capture the generation
// the scheduled callback will check against when it runs.
b.Emit("b")
b.mu.Lock()
staleGen := b.timerGen
b.mu.Unlock()
// Force the next Emit into the leading branch, invalidating the
// prior timer's generation.
b.mu.Lock()
b.lastEmit = time.Now().Add(-2 * interval)
b.mu.Unlock()
b.Emit("c")
<-sub
// Rate-limited emit after the leading edge installs a fresh
// pending+timer under the new generation.
b.Emit("d")
// Simulate the stale callback from the "b" timer finally acquiring
// the lock after being blocked. With the generation check it must
// return without touching state; without it, the stale callback
// would clear b.timer and broadcast "d" prematurely.
b.flushTrailing(staleGen)
// No premature broadcast in the window right after the stale
// callback supposedly ran.
select {
case ev := <-sub:
require.Fail(t, "stale callback consumed newer pending", "ev=%v", ev)
case <-time.After(interval / 2):
}
// The new timer scheduled for "d" must still be live and deliver
// "d" on its original schedule. A bug that lets the stale callback
// null out b.timer would orphan the new timer here — in which case
// the callback still fires, finds pending == nil, and no event
// arrives.
select {
case ev := <-sub:
assert.Equal(t, "d", ev.Scope)
case <-time.After(interval * 3):
require.Fail(t, "new trailing timer did not fire with pending scope")
}
}
func TestBroadcaster_EmitAfterIntervalBroadcastsImmediately(t *testing.T) {
const interval = 50 * time.Millisecond
b := NewBroadcaster(interval)
sub, unsub := b.Subscribe()
defer unsub()
b.Emit("first")
<-sub
time.Sleep(interval * 2)
b.Emit("second")
select {
case ev := <-sub:
assert.Equal(t, "second", ev.Scope)
case <-time.After(interval):
require.Fail(t, "emit after quiet interval did not broadcast immediately")
}
}