Files
vm/rpc/notifier_test.go
zeekayandHanzo Dev 3e79aad01a answer an unset notifier Log before the forwarding loop starts
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]>
2026-09-02 04:02:48 -07:00

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(&notifiedCount, 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(&notifiedCount) == 1
}, time.Minute, 10*time.Millisecond)
require.Never(t, func() bool {
return atomic.LoadUint32(&notifiedCount) != 1
}, time.Millisecond*100, 10*time.Millisecond)
nf.CheckForEvent()
require.Eventually(t, func() bool {
return atomic.LoadUint32(&notifiedCount) == 2
}, time.Minute, 10*time.Millisecond)
require.Never(t, func() bool {
return atomic.LoadUint32(&notifiedCount) != 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")
}