mirror of
https://github.com/luxfi/vm.git
synced 2026-10-30 15:13:40 +00:00
NewNotificationForwarder spawns the forwarding goroutine itself, so a caller that leaves Log unset has no window to set it, and forwardNotification logs before it does anything else. The nil dereference lands on a goroutine whose panic no caller can recover, so it takes the process rather than failing one VM. start() is the one place the loop begins, so defaulting Log there covers both ways to leave it unset — the exported field and the constructor argument. Noop discards what it is given, so a caller that wanted silence still gets it. Co-authored-by: Hanzo Dev <[email protected]>
209 lines
4.5 KiB
Go
209 lines
4.5 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/log"
|
|
)
|
|
|
|
type notifier func(_ context.Context, msg Message) error
|
|
|
|
func (n notifier) Notify(ctx context.Context, msg Message) error {
|
|
return n(ctx, msg)
|
|
}
|
|
|
|
func TestNotifier(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
notifier := notifier(func(_ context.Context, msg Message) error {
|
|
defer wg.Done()
|
|
require.Equal(t, PendingTxs, msg)
|
|
return nil
|
|
})
|
|
|
|
c := make(chan Message)
|
|
|
|
subscriber := func(ctx context.Context) (Message, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
case msg := <-c:
|
|
return msg, nil
|
|
}
|
|
}
|
|
|
|
nf := NewNotificationForwarder(
|
|
Notifier(notifier),
|
|
subscriber,
|
|
&log.NoLog{})
|
|
|
|
defer nf.Close()
|
|
|
|
c <- PendingTxs
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestNotifierStopWhileSubscribing(_ *testing.T) {
|
|
notifier := notifier(func(ctx context.Context, _ Message) error {
|
|
<-ctx.Done()
|
|
return nil
|
|
})
|
|
|
|
var subscribed sync.WaitGroup
|
|
subscribed.Add(1)
|
|
|
|
subscribe := func(ctx context.Context) (Message, error) {
|
|
subscribed.Done()
|
|
<-ctx.Done()
|
|
return 0, nil
|
|
}
|
|
|
|
nf := NewNotificationForwarder(
|
|
Notifier(notifier),
|
|
subscribe,
|
|
&log.NoLog{})
|
|
|
|
subscribed.Wait()
|
|
nf.Close()
|
|
}
|
|
|
|
func TestNotifierWaitForPrefChangeAfterNotify(t *testing.T) {
|
|
var notifiedCount uint32
|
|
|
|
engine := Notifier(notifier(func(_ context.Context, _ Message) error {
|
|
atomic.AddUint32(¬ifiedCount, 1)
|
|
return nil
|
|
}))
|
|
|
|
subscribe := func(context.Context) (Message, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
nf := NewNotificationForwarder(engine, subscribe, &log.NoLog{})
|
|
defer nf.Close()
|
|
|
|
require.Eventually(t, func() bool {
|
|
return atomic.LoadUint32(¬ifiedCount) == 1
|
|
}, time.Minute, 10*time.Millisecond)
|
|
|
|
require.Never(t, func() bool {
|
|
return atomic.LoadUint32(¬ifiedCount) != 1
|
|
}, time.Millisecond*100, 10*time.Millisecond)
|
|
|
|
nf.CheckForEvent()
|
|
|
|
require.Eventually(t, func() bool {
|
|
return atomic.LoadUint32(¬ifiedCount) == 2
|
|
}, time.Minute, 10*time.Millisecond)
|
|
|
|
require.Never(t, func() bool {
|
|
return atomic.LoadUint32(¬ifiedCount) != 2
|
|
}, time.Millisecond*100, 10*time.Millisecond)
|
|
}
|
|
|
|
func TestNotifierReSubscribeAtPrefChange(t *testing.T) {
|
|
notifications := make(chan Message)
|
|
|
|
engine := Notifier(notifier(func(ctx context.Context, msg Message) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case notifications <- msg:
|
|
}
|
|
|
|
<-ctx.Done()
|
|
return ctx.Err()
|
|
}))
|
|
|
|
messages := make(chan Message)
|
|
|
|
signal := make(chan struct{})
|
|
|
|
subscriber := func(ctx context.Context) (Message, error) {
|
|
select {
|
|
case <-signal:
|
|
default:
|
|
close(signal)
|
|
<-ctx.Done()
|
|
return 0, ctx.Err()
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
case msg := <-messages:
|
|
return msg, nil
|
|
}
|
|
}
|
|
|
|
nf := NewNotificationForwarder(engine, subscriber, &log.NoLog{})
|
|
defer nf.Close()
|
|
|
|
select {
|
|
case <-signal:
|
|
nf.CheckForEvent()
|
|
case <-time.After(time.Minute):
|
|
require.FailNow(t, "Timed out waiting for the notification forwarder to subscribe")
|
|
}
|
|
|
|
select {
|
|
case messages <- PendingTxs:
|
|
case <-time.After(time.Minute):
|
|
require.FailNow(t, "Timed out waiting to send message to notification forwarder")
|
|
}
|
|
|
|
select {
|
|
case msg := <-notifications:
|
|
require.Equal(t, PendingTxs, msg)
|
|
case <-time.After(time.Minute):
|
|
require.FailNow(t, "Timed out waiting for notification forwarder to forward message")
|
|
}
|
|
}
|
|
|
|
// TestNotifierWithoutLoggerDoesNotPanic pins the forwarder to surviving an
|
|
// unset Log.
|
|
//
|
|
// The constructor spawns the forwarding goroutine itself, so a caller cannot
|
|
// set Log afterwards — passing nil is fatal at the instant of construction.
|
|
// And forwardNotification logs before it does anything else, on a goroutine
|
|
// whose panic no caller can recover, so the nil dereference takes the whole
|
|
// process with it rather than failing one VM.
|
|
func TestNotifierWithoutLoggerDoesNotPanic(t *testing.T) {
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
notifier := notifier(func(_ context.Context, msg Message) error {
|
|
defer wg.Done()
|
|
require.Equal(t, PendingTxs, msg)
|
|
return nil
|
|
})
|
|
|
|
c := make(chan Message)
|
|
subscriber := func(ctx context.Context) (Message, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return 0, ctx.Err()
|
|
case msg := <-c:
|
|
return msg, nil
|
|
}
|
|
}
|
|
|
|
// No logger. The forwarder still forwards.
|
|
nf := NewNotificationForwarder(Notifier(notifier), subscriber, nil)
|
|
defer nf.Close()
|
|
|
|
c <- PendingTxs
|
|
wg.Wait()
|
|
|
|
require.NotNil(t, nf.Log, "an unset Log must be answered before the loop starts")
|
|
}
|