370 lines
8.3 KiB
Go
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")
|
|
}
|