added the notify module
This commit is contained in:
369
notify/close_test.go
Normal file
369
notify/close_test.go
Normal file
@@ -0,0 +1,369 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user