github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/eventloop/event_loop_test.go (about)

     1  package eventloop
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/rs/zerolog"
    11  	"github.com/stretchr/testify/mock"
    12  	"github.com/stretchr/testify/require"
    13  	"github.com/stretchr/testify/suite"
    14  	"go.uber.org/atomic"
    15  
    16  	"github.com/onflow/flow-go/consensus/hotstuff"
    17  	"github.com/onflow/flow-go/consensus/hotstuff/helper"
    18  	"github.com/onflow/flow-go/consensus/hotstuff/mocks"
    19  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    20  	"github.com/onflow/flow-go/model/flow"
    21  	"github.com/onflow/flow-go/module/irrecoverable"
    22  	"github.com/onflow/flow-go/module/metrics"
    23  	"github.com/onflow/flow-go/utils/unittest"
    24  )
    25  
    26  // TestEventLoop performs unit testing of event loop, checks if submitted events are propagated
    27  // to event handler as well as handling of timeouts.
    28  func TestEventLoop(t *testing.T) {
    29  	suite.Run(t, new(EventLoopTestSuite))
    30  }
    31  
    32  type EventLoopTestSuite struct {
    33  	suite.Suite
    34  
    35  	eh     *mocks.EventHandler
    36  	cancel context.CancelFunc
    37  
    38  	eventLoop *EventLoop
    39  }
    40  
    41  func (s *EventLoopTestSuite) SetupTest() {
    42  	s.eh = mocks.NewEventHandler(s.T())
    43  	s.eh.On("Start", mock.Anything).Return(nil).Maybe()
    44  	s.eh.On("TimeoutChannel").Return(make(<-chan time.Time, 1)).Maybe()
    45  	s.eh.On("OnLocalTimeout").Return(nil).Maybe()
    46  
    47  	log := zerolog.New(io.Discard)
    48  
    49  	eventLoop, err := NewEventLoop(log, metrics.NewNoopCollector(), metrics.NewNoopCollector(), s.eh, time.Time{})
    50  	require.NoError(s.T(), err)
    51  	s.eventLoop = eventLoop
    52  
    53  	ctx, cancel := context.WithCancel(context.Background())
    54  	s.cancel = cancel
    55  	signalerCtx, _ := irrecoverable.WithSignaler(ctx)
    56  
    57  	s.eventLoop.Start(signalerCtx)
    58  	unittest.RequireCloseBefore(s.T(), s.eventLoop.Ready(), 100*time.Millisecond, "event loop not started")
    59  }
    60  
    61  func (s *EventLoopTestSuite) TearDownTest() {
    62  	s.cancel()
    63  	unittest.RequireCloseBefore(s.T(), s.eventLoop.Done(), 100*time.Millisecond, "event loop not stopped")
    64  }
    65  
    66  // TestReadyDone tests if event loop stops internal worker thread
    67  func (s *EventLoopTestSuite) TestReadyDone() {
    68  	time.Sleep(1 * time.Second)
    69  	go func() {
    70  		s.cancel()
    71  	}()
    72  
    73  	unittest.RequireCloseBefore(s.T(), s.eventLoop.Done(), 100*time.Millisecond, "event loop not stopped")
    74  }
    75  
    76  // Test_SubmitQC tests that submitted proposal is eventually sent to event handler for processing
    77  func (s *EventLoopTestSuite) Test_SubmitProposal() {
    78  	proposal := helper.MakeProposal()
    79  	processed := atomic.NewBool(false)
    80  	s.eh.On("OnReceiveProposal", proposal).Run(func(args mock.Arguments) {
    81  		processed.Store(true)
    82  	}).Return(nil).Once()
    83  	s.eventLoop.SubmitProposal(proposal)
    84  	require.Eventually(s.T(), processed.Load, time.Millisecond*100, time.Millisecond*10)
    85  }
    86  
    87  // Test_SubmitQC tests that submitted QC is eventually sent to `EventHandler.OnReceiveQc` for processing
    88  func (s *EventLoopTestSuite) Test_SubmitQC() {
    89  	// qcIngestionFunction is the archetype for EventLoop.OnQcConstructedFromVotes and EventLoop.OnNewQcDiscovered
    90  	type qcIngestionFunction func(*flow.QuorumCertificate)
    91  
    92  	testQCIngestionFunction := func(f qcIngestionFunction, qcView uint64) {
    93  		qc := helper.MakeQC(helper.WithQCView(qcView))
    94  		processed := atomic.NewBool(false)
    95  		s.eh.On("OnReceiveQc", qc).Run(func(args mock.Arguments) {
    96  			processed.Store(true)
    97  		}).Return(nil).Once()
    98  		f(qc)
    99  		require.Eventually(s.T(), processed.Load, time.Millisecond*100, time.Millisecond*10)
   100  	}
   101  
   102  	s.Run("QCs handed to EventLoop.OnQcConstructedFromVotes are forwarded to EventHandler", func() {
   103  		testQCIngestionFunction(s.eventLoop.OnQcConstructedFromVotes, 100)
   104  	})
   105  
   106  	s.Run("QCs handed to EventLoop.OnNewQcDiscovered are forwarded to EventHandler", func() {
   107  		testQCIngestionFunction(s.eventLoop.OnNewQcDiscovered, 101)
   108  	})
   109  }
   110  
   111  // Test_SubmitTC tests that submitted TC is eventually sent to `EventHandler.OnReceiveTc` for processing
   112  func (s *EventLoopTestSuite) Test_SubmitTC() {
   113  	// tcIngestionFunction is the archetype for EventLoop.OnTcConstructedFromTimeouts and EventLoop.OnNewTcDiscovered
   114  	type tcIngestionFunction func(*flow.TimeoutCertificate)
   115  
   116  	testTCIngestionFunction := func(f tcIngestionFunction, tcView uint64) {
   117  		tc := helper.MakeTC(helper.WithTCView(tcView))
   118  		processed := atomic.NewBool(false)
   119  		s.eh.On("OnReceiveTc", tc).Run(func(args mock.Arguments) {
   120  			processed.Store(true)
   121  		}).Return(nil).Once()
   122  		f(tc)
   123  		require.Eventually(s.T(), processed.Load, time.Millisecond*100, time.Millisecond*10)
   124  	}
   125  
   126  	s.Run("TCs handed to EventLoop.OnTcConstructedFromTimeouts are forwarded to EventHandler", func() {
   127  		testTCIngestionFunction(s.eventLoop.OnTcConstructedFromTimeouts, 100)
   128  	})
   129  
   130  	s.Run("TCs handed to EventLoop.OnNewTcDiscovered are forwarded to EventHandler", func() {
   131  		testTCIngestionFunction(s.eventLoop.OnNewTcDiscovered, 101)
   132  	})
   133  }
   134  
   135  // Test_SubmitTC_IngestNewestQC tests that included QC in TC is eventually sent to `EventHandler.OnReceiveQc` for processing
   136  func (s *EventLoopTestSuite) Test_SubmitTC_IngestNewestQC() {
   137  	// tcIngestionFunction is the archetype for EventLoop.OnTcConstructedFromTimeouts and EventLoop.OnNewTcDiscovered
   138  	type tcIngestionFunction func(*flow.TimeoutCertificate)
   139  
   140  	testTCIngestionFunction := func(f tcIngestionFunction, tcView, qcView uint64) {
   141  		tc := helper.MakeTC(helper.WithTCView(tcView),
   142  			helper.WithTCNewestQC(helper.MakeQC(helper.WithQCView(qcView))))
   143  		processed := atomic.NewBool(false)
   144  		s.eh.On("OnReceiveQc", tc.NewestQC).Run(func(args mock.Arguments) {
   145  			processed.Store(true)
   146  		}).Return(nil).Once()
   147  		f(tc)
   148  		require.Eventually(s.T(), processed.Load, time.Millisecond*100, time.Millisecond*10)
   149  	}
   150  
   151  	// process initial TC, this will track the newest TC
   152  	s.eh.On("OnReceiveTc", mock.Anything).Return(nil).Once()
   153  	s.eventLoop.OnTcConstructedFromTimeouts(helper.MakeTC(
   154  		helper.WithTCView(100),
   155  		helper.WithTCNewestQC(
   156  			helper.MakeQC(
   157  				helper.WithQCView(80),
   158  			),
   159  		),
   160  	))
   161  
   162  	s.Run("QCs handed to EventLoop.OnTcConstructedFromTimeouts are forwarded to EventHandler", func() {
   163  		testTCIngestionFunction(s.eventLoop.OnTcConstructedFromTimeouts, 100, 99)
   164  	})
   165  
   166  	s.Run("QCs handed to EventLoop.OnNewTcDiscovered are forwarded to EventHandler", func() {
   167  		testTCIngestionFunction(s.eventLoop.OnNewTcDiscovered, 100, 100)
   168  	})
   169  }
   170  
   171  // Test_OnPartialTcCreated tests that event loop delivers partialTcCreated events to event handler.
   172  func (s *EventLoopTestSuite) Test_OnPartialTcCreated() {
   173  	view := uint64(1000)
   174  	newestQC := helper.MakeQC(helper.WithQCView(view - 10))
   175  	lastViewTC := helper.MakeTC(helper.WithTCView(view-1), helper.WithTCNewestQC(newestQC))
   176  
   177  	processed := atomic.NewBool(false)
   178  	partialTcCreated := &hotstuff.PartialTcCreated{
   179  		View:       view,
   180  		NewestQC:   newestQC,
   181  		LastViewTC: lastViewTC,
   182  	}
   183  	s.eh.On("OnPartialTcCreated", partialTcCreated).Run(func(args mock.Arguments) {
   184  		processed.Store(true)
   185  	}).Return(nil).Once()
   186  	s.eventLoop.OnPartialTcCreated(view, newestQC, lastViewTC)
   187  	require.Eventually(s.T(), processed.Load, time.Millisecond*100, time.Millisecond*10)
   188  }
   189  
   190  // TestEventLoop_Timeout tests that event loop delivers timeout events to event handler under pressure
   191  func TestEventLoop_Timeout(t *testing.T) {
   192  	eh := &mocks.EventHandler{}
   193  	processed := atomic.NewBool(false)
   194  	eh.On("Start", mock.Anything).Return(nil).Once()
   195  	eh.On("OnReceiveQc", mock.Anything).Return(nil).Maybe()
   196  	eh.On("OnReceiveProposal", mock.Anything).Return(nil).Maybe()
   197  	eh.On("OnLocalTimeout").Run(func(args mock.Arguments) {
   198  		processed.Store(true)
   199  	}).Return(nil).Once()
   200  
   201  	log := zerolog.New(io.Discard)
   202  
   203  	metricsCollector := metrics.NewNoopCollector()
   204  	eventLoop, err := NewEventLoop(log, metricsCollector, metricsCollector, eh, time.Time{})
   205  	require.NoError(t, err)
   206  
   207  	eh.On("TimeoutChannel").Return(time.After(100 * time.Millisecond))
   208  
   209  	ctx, cancel := context.WithCancel(context.Background())
   210  	signalerCtx, _ := irrecoverable.WithSignaler(ctx)
   211  	eventLoop.Start(signalerCtx)
   212  
   213  	unittest.RequireCloseBefore(t, eventLoop.Ready(), 100*time.Millisecond, "event loop not stopped")
   214  
   215  	time.Sleep(10 * time.Millisecond)
   216  
   217  	var wg sync.WaitGroup
   218  	wg.Add(2)
   219  
   220  	// spam with proposals and QCs
   221  	go func() {
   222  		defer wg.Done()
   223  		for !processed.Load() {
   224  			qc := unittest.QuorumCertificateFixture()
   225  			eventLoop.OnQcConstructedFromVotes(qc)
   226  		}
   227  	}()
   228  
   229  	go func() {
   230  		defer wg.Done()
   231  		for !processed.Load() {
   232  			eventLoop.SubmitProposal(helper.MakeProposal())
   233  		}
   234  	}()
   235  
   236  	require.Eventually(t, processed.Load, time.Millisecond*200, time.Millisecond*10)
   237  	unittest.AssertReturnsBefore(t, func() { wg.Wait() }, time.Millisecond*200)
   238  
   239  	cancel()
   240  	unittest.RequireCloseBefore(t, eventLoop.Done(), 100*time.Millisecond, "event loop not stopped")
   241  }
   242  
   243  // TestReadyDoneWithStartTime tests that event loop correctly starts and schedules start of processing
   244  // when startTime argument is used
   245  func TestReadyDoneWithStartTime(t *testing.T) {
   246  	eh := &mocks.EventHandler{}
   247  	eh.On("Start", mock.Anything).Return(nil)
   248  	eh.On("TimeoutChannel").Return(make(<-chan time.Time, 1))
   249  	eh.On("OnLocalTimeout").Return(nil)
   250  
   251  	metrics := metrics.NewNoopCollector()
   252  
   253  	log := zerolog.New(io.Discard)
   254  
   255  	startTimeDuration := 2 * time.Second
   256  	startTime := time.Now().Add(startTimeDuration)
   257  	eventLoop, err := NewEventLoop(log, metrics, metrics, eh, startTime)
   258  	require.NoError(t, err)
   259  
   260  	done := make(chan struct{})
   261  	eh.On("OnReceiveProposal", mock.AnythingOfType("*model.Proposal")).Run(func(args mock.Arguments) {
   262  		require.True(t, time.Now().After(startTime))
   263  		close(done)
   264  	}).Return(nil).Once()
   265  
   266  	ctx, cancel := context.WithCancel(context.Background())
   267  	signalerCtx, _ := irrecoverable.WithSignaler(ctx)
   268  	eventLoop.Start(signalerCtx)
   269  
   270  	unittest.RequireCloseBefore(t, eventLoop.Ready(), 100*time.Millisecond, "event loop not started")
   271  
   272  	parentBlock := unittest.BlockHeaderFixture()
   273  	block := unittest.BlockHeaderWithParentFixture(parentBlock)
   274  	eventLoop.SubmitProposal(model.ProposalFromFlow(block))
   275  
   276  	unittest.RequireCloseBefore(t, done, startTimeDuration+100*time.Millisecond, "proposal wasn't received")
   277  	cancel()
   278  	unittest.RequireCloseBefore(t, eventLoop.Done(), 100*time.Millisecond, "event loop not stopped")
   279  }