Files
golib/notify/close_test.go
2026-01-24 20:35:40 +11:00

370 lines
8.3 KiB
Go

package notify
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestClose_Basic verifies basic Close() functionality.
func TestClose_Basic(t *testing.T) {
n := NewNotifier(50)
// Create some subscribers
sub1, err := n.Subscribe()
require.NoError(t, err)
sub2, err := n.Subscribe()
require.NoError(t, err)
sub3, err := n.Subscribe()
require.NoError(t, err)
assert.Equal(t, 3, len(n.subscribers), "Should have 3 subscribers")
// Close the notifier
n.Close()
// Verify all subscribers removed
assert.Equal(t, 0, len(n.subscribers), "Should have 0 subscribers after close")
// Verify channels are closed
_, ok := <-sub1.Listen()
assert.False(t, ok, "sub1 channel should be closed")
_, ok = <-sub2.Listen()
assert.False(t, ok, "sub2 channel should be closed")
_, ok = <-sub3.Listen()
assert.False(t, ok, "sub3 channel should be closed")
}
// TestClose_IdempotentClose verifies that calling Close() multiple times is safe.
func TestClose_IdempotentClose(t *testing.T) {
n := NewNotifier(50)
sub, err := n.Subscribe()
require.NoError(t, err)
// Close multiple times - should not panic
assert.NotPanics(t, func() {
n.Close()
n.Close()
n.Close()
}, "Multiple Close() calls should not panic")
// Verify channel is still closed (not double-closed)
_, ok := <-sub.Listen()
assert.False(t, ok, "Channel should be closed")
}
// TestClose_SubscribeAfterClose verifies that Subscribe fails after Close.
func TestClose_SubscribeAfterClose(t *testing.T) {
n := NewNotifier(50)
// Subscribe before close
sub1, err := n.Subscribe()
require.NoError(t, err)
require.NotNil(t, sub1)
// Close
n.Close()
// Try to subscribe after close
sub2, err := n.Subscribe()
assert.Error(t, err, "Subscribe should return error after Close")
assert.Nil(t, sub2, "Subscribe should return nil subscriber after Close")
assert.Contains(t, err.Error(), "closed", "Error should mention notifier is closed")
}
// TestClose_NotifyAfterClose verifies that Notify after Close doesn't panic.
func TestClose_NotifyAfterClose(t *testing.T) {
n := NewNotifier(50)
sub, err := n.Subscribe()
require.NoError(t, err)
// Close
n.Close()
// Try to notify - should not panic
notification := Notification{
Target: sub.ID,
Level: LevelInfo,
Message: "Should be ignored",
}
assert.NotPanics(t, func() {
n.Notify(notification)
}, "Notify after Close should not panic")
}
// TestClose_NotifyAllAfterClose verifies that NotifyAll after Close doesn't panic.
func TestClose_NotifyAllAfterClose(t *testing.T) {
n := NewNotifier(50)
_, err := n.Subscribe()
require.NoError(t, err)
// Close
n.Close()
// Try to broadcast - should not panic
notification := Notification{
Level: LevelInfo,
Message: "Should be ignored",
}
assert.NotPanics(t, func() {
n.NotifyAll(notification)
}, "NotifyAll after Close should not panic")
}
// TestClose_WithActiveListeners verifies that listeners detect channel closure.
func TestClose_WithActiveListeners(t *testing.T) {
n := NewNotifier(50)
sub, err := n.Subscribe()
require.NoError(t, err)
var wg sync.WaitGroup
listenerExited := false
// Start listener goroutine
wg.Add(1)
go func() {
defer wg.Done()
for range sub.Listen() {
// Process notifications
}
listenerExited = true
}()
// Give listener time to start
time.Sleep(10 * time.Millisecond)
// Close notifier
n.Close()
// Wait for listener to exit
done := make(chan bool)
go func() {
wg.Wait()
done <- true
}()
select {
case <-done:
assert.True(t, listenerExited, "Listener should have exited")
case <-time.After(1 * time.Second):
t.Fatal("Listener did not exit after Close - possible hang")
}
}
// TestClose_PendingNotifications verifies behavior of pending notifications on close.
func TestClose_PendingNotifications(t *testing.T) {
n := NewNotifier(50)
sub, err := n.Subscribe()
require.NoError(t, err)
// Send some notifications
for i := 0; i < 10; i++ {
notification := Notification{
Target: sub.ID,
Level: LevelInfo,
Message: "Notification",
}
go n.Notify(notification)
}
// Wait for sends to complete
time.Sleep(50 * time.Millisecond)
// Close notifier (closes channels)
n.Close()
// Try to read any remaining notifications before closure
received := 0
for {
_, ok := <-sub.Listen()
if !ok {
break
}
received++
}
t.Logf("Received %d notifications before channel closed", received)
assert.GreaterOrEqual(t, received, 0, "Should receive at least 0 notifications")
}
// TestClose_ConcurrentSubscribeAndClose verifies thread safety.
func TestClose_ConcurrentSubscribeAndClose(t *testing.T) {
n := NewNotifier(50)
var wg sync.WaitGroup
// Goroutines trying to subscribe
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, _ = n.Subscribe() // May succeed or fail depending on timing
}()
}
// Give some time for subscriptions to start
time.Sleep(5 * time.Millisecond)
// Close concurrently
wg.Add(1)
go func() {
defer wg.Done()
n.Close()
}()
// Should complete without deadlock or panic
done := make(chan bool)
go func() {
wg.Wait()
done <- true
}()
select {
case <-done:
// Success
case <-time.After(2 * time.Second):
t.Fatal("Test timed out - possible deadlock")
}
// After close, no more subscriptions should succeed
sub, err := n.Subscribe()
assert.Error(t, err)
assert.Nil(t, sub)
}
// TestClose_ConcurrentNotifyAndClose verifies thread safety with notifications.
func TestClose_ConcurrentNotifyAndClose(t *testing.T) {
n := NewNotifier(50)
// Create some subscribers
subscribers := make([]*Subscriber, 10)
for i := 0; i < 10; i++ {
sub, err := n.Subscribe()
require.NoError(t, err)
subscribers[i] = sub
}
var wg sync.WaitGroup
// Goroutines sending notifications
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
notification := Notification{
Level: LevelInfo,
Message: "Test",
}
n.NotifyAll(notification)
}()
}
// Close concurrently
time.Sleep(5 * time.Millisecond)
wg.Add(1)
go func() {
defer wg.Done()
n.Close()
}()
// Should complete without panic or deadlock
done := make(chan bool)
go func() {
wg.Wait()
done <- true
}()
select {
case <-done:
// Success - no panic or deadlock
case <-time.After(2 * time.Second):
t.Fatal("Test timed out - possible deadlock")
}
}
// TestClose_Integration verifies the complete Close workflow.
func TestClose_Integration(t *testing.T) {
n := NewNotifier(50)
// Create subscribers
sub1, err := n.Subscribe()
require.NoError(t, err)
sub2, err := n.Subscribe()
require.NoError(t, err)
sub3, err := n.Subscribe()
require.NoError(t, err)
// Send some notifications
notification := Notification{
Level: LevelSuccess,
Message: "Before close",
}
go n.NotifyAll(notification)
// Receive notifications from all subscribers
received1, ok := receiveWithTimeout(sub1.Listen(), 100*time.Millisecond)
require.True(t, ok, "sub1 should receive notification")
assert.Equal(t, "Before close", received1.Message)
received2, ok := receiveWithTimeout(sub2.Listen(), 100*time.Millisecond)
require.True(t, ok, "sub2 should receive notification")
assert.Equal(t, "Before close", received2.Message)
received3, ok := receiveWithTimeout(sub3.Listen(), 100*time.Millisecond)
require.True(t, ok, "sub3 should receive notification")
assert.Equal(t, "Before close", received3.Message)
// Close the notifier
n.Close()
// Verify all channels closed (should return immediately with ok=false)
_, ok = <-sub1.Listen()
assert.False(t, ok, "sub1 should be closed")
_, ok = <-sub2.Listen()
assert.False(t, ok, "sub2 should be closed")
_, ok = <-sub3.Listen()
assert.False(t, ok, "sub3 should be closed")
// Verify no more subscriptions
sub4, err := n.Subscribe()
assert.Error(t, err)
assert.Nil(t, sub4)
// Verify notifications are ignored
notification2 := Notification{
Level: LevelInfo,
Message: "After close",
}
assert.NotPanics(t, func() {
n.NotifyAll(notification2)
})
}
// TestClose_UnsubscribeAfterClose verifies unsubscribe after close is safe.
func TestClose_UnsubscribeAfterClose(t *testing.T) {
n := NewNotifier(50)
sub, err := n.Subscribe()
require.NoError(t, err)
// Close notifier
n.Close()
// Try to unsubscribe after close - should be safe
assert.NotPanics(t, func() {
sub.Unsubscribe()
}, "Unsubscribe after Close should not panic")
}