github.com/koko1123/flow-go-1@v0.29.6/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/koko1123/flow-go-1/consensus/hotstuff/mocks"
    17  	"github.com/koko1123/flow-go-1/consensus/hotstuff/model"
    18  	"github.com/koko1123/flow-go-1/module/irrecoverable"
    19  	"github.com/koko1123/flow-go-1/module/metrics"
    20  	"github.com/koko1123/flow-go-1/utils/unittest"
    21  )
    22  
    23  // TestEventLoop performs unit testing of event loop, checks if submitted events are propagated
    24  // to event handler as well as handling of timeouts.
    25  func TestEventLoop(t *testing.T) {
    26  	suite.Run(t, new(EventLoopTestSuite))
    27  }
    28  
    29  type EventLoopTestSuite struct {
    30  	suite.Suite
    31  
    32  	eh     *mocks.EventHandlerV2
    33  	cancel context.CancelFunc
    34  
    35  	eventLoop *EventLoop
    36  }
    37  
    38  func (s *EventLoopTestSuite) SetupTest() {
    39  	s.eh = &mocks.EventHandlerV2{}
    40  	s.eh.On("Start").Return(nil).Maybe()
    41  	s.eh.On("TimeoutChannel").Return(time.NewTimer(10 * time.Second).C).Maybe()
    42  	s.eh.On("OnLocalTimeout").Return(nil).Maybe()
    43  
    44  	log := zerolog.New(io.Discard)
    45  
    46  	eventLoop, err := NewEventLoop(log, metrics.NewNoopCollector(), s.eh, time.Time{})
    47  	require.NoError(s.T(), err)
    48  	s.eventLoop = eventLoop
    49  
    50  	ctx, cancel := context.WithCancel(context.Background())
    51  	s.cancel = cancel
    52  	signalerCtx, _ := irrecoverable.WithSignaler(ctx)
    53  
    54  	s.eventLoop.Start(signalerCtx)
    55  	unittest.RequireCloseBefore(s.T(), s.eventLoop.Ready(), 100*time.Millisecond, "event loop not started")
    56  }
    57  
    58  func (s *EventLoopTestSuite) TearDownTest() {
    59  	s.cancel()
    60  	unittest.RequireCloseBefore(s.T(), s.eventLoop.Done(), 100*time.Millisecond, "event loop not stopped")
    61  }
    62  
    63  // TestReadyDone tests if event loop stops internal worker thread
    64  func (s *EventLoopTestSuite) TestReadyDone() {
    65  	time.Sleep(1 * time.Second)
    66  	go func() {
    67  		s.cancel()
    68  	}()
    69  
    70  	unittest.RequireCloseBefore(s.T(), s.eventLoop.Done(), 100*time.Millisecond, "event loop not stopped")
    71  }
    72  
    73  // Test_SubmitQC tests that submitted proposal is eventually sent to event handler for processing
    74  func (s *EventLoopTestSuite) Test_SubmitProposal() {
    75  	proposal := unittest.BlockHeaderFixture()
    76  	expectedProposal := model.ProposalFromFlow(proposal, proposal.View-1)
    77  	processed := atomic.NewBool(false)
    78  	s.eh.On("OnReceiveProposal", expectedProposal).Run(func(args mock.Arguments) {
    79  		processed.Store(true)
    80  	}).Return(nil).Once()
    81  	s.eventLoop.SubmitProposal(proposal, proposal.View-1)
    82  	require.Eventually(s.T(), processed.Load, time.Millisecond*100, time.Millisecond*10)
    83  	s.eh.AssertExpectations(s.T())
    84  }
    85  
    86  // Test_SubmitQC tests that submitted QC is eventually sent to event handler for processing
    87  func (s *EventLoopTestSuite) Test_SubmitQC() {
    88  	qc := unittest.QuorumCertificateFixture()
    89  	processed := atomic.NewBool(false)
    90  	s.eh.On("OnQCConstructed", qc).Run(func(args mock.Arguments) {
    91  		processed.Store(true)
    92  	}).Return(nil).Once()
    93  	s.eventLoop.SubmitTrustedQC(qc)
    94  	require.Eventually(s.T(), processed.Load, time.Millisecond*100, time.Millisecond*10)
    95  	s.eh.AssertExpectations(s.T())
    96  }
    97  
    98  // TestEventLoop_Timeout tests that event loop delivers timeout events to event handler under pressure
    99  func TestEventLoop_Timeout(t *testing.T) {
   100  	eh := &mocks.EventHandlerV2{}
   101  	processed := atomic.NewBool(false)
   102  	eh.On("Start").Return(nil).Once()
   103  	eh.On("TimeoutChannel").Return(time.NewTimer(100 * time.Millisecond).C)
   104  	eh.On("OnQCConstructed", mock.Anything).Return(nil).Maybe()
   105  	eh.On("OnReceiveProposal", mock.Anything).Return(nil).Maybe()
   106  	eh.On("OnLocalTimeout").Run(func(args mock.Arguments) {
   107  		processed.Store(true)
   108  	}).Return(nil).Once()
   109  
   110  	log := zerolog.New(io.Discard)
   111  
   112  	eventLoop, err := NewEventLoop(log, metrics.NewNoopCollector(), eh, time.Time{})
   113  	require.NoError(t, err)
   114  
   115  	ctx, cancel := context.WithCancel(context.Background())
   116  	signalerCtx, _ := irrecoverable.WithSignaler(ctx)
   117  	eventLoop.Start(signalerCtx)
   118  
   119  	unittest.RequireCloseBefore(t, eventLoop.Ready(), 100*time.Millisecond, "event loop not stopped")
   120  
   121  	time.Sleep(10 * time.Millisecond)
   122  
   123  	var wg sync.WaitGroup
   124  	wg.Add(2)
   125  
   126  	// spam with proposals and QCs
   127  	go func() {
   128  		defer wg.Done()
   129  		for !processed.Load() {
   130  			qc := unittest.QuorumCertificateFixture()
   131  			eventLoop.SubmitTrustedQC(qc)
   132  		}
   133  	}()
   134  
   135  	go func() {
   136  		defer wg.Done()
   137  		for !processed.Load() {
   138  			proposal := unittest.BlockHeaderFixture()
   139  			eventLoop.SubmitProposal(proposal, proposal.View-1)
   140  		}
   141  	}()
   142  
   143  	require.Eventually(t, processed.Load, time.Millisecond*200, time.Millisecond*10)
   144  	wg.Wait()
   145  
   146  	cancel()
   147  	unittest.RequireCloseBefore(t, eventLoop.Done(), 100*time.Millisecond, "event loop not stopped")
   148  }
   149  
   150  // TestReadyDoneWithStartTime tests that event loop correctly starts and schedules start of processing
   151  // when startTime argument is used
   152  func TestReadyDoneWithStartTime(t *testing.T) {
   153  	eh := &mocks.EventHandlerV2{}
   154  	eh.On("Start").Return(nil)
   155  	eh.On("TimeoutChannel").Return(time.NewTimer(10 * time.Second).C)
   156  	eh.On("OnLocalTimeout").Return(nil)
   157  
   158  	metrics := metrics.NewNoopCollector()
   159  
   160  	log := zerolog.New(io.Discard)
   161  
   162  	startTimeDuration := 2 * time.Second
   163  	startTime := time.Now().Add(startTimeDuration)
   164  	eventLoop, err := NewEventLoop(log, metrics, eh, startTime)
   165  	require.NoError(t, err)
   166  
   167  	done := make(chan struct{})
   168  	eh.On("OnReceiveProposal", mock.AnythingOfType("*model.Proposal")).Run(func(args mock.Arguments) {
   169  		require.True(t, time.Now().After(startTime))
   170  		close(done)
   171  	}).Return(nil).Once()
   172  
   173  	ctx, cancel := context.WithCancel(context.Background())
   174  	signalerCtx, _ := irrecoverable.WithSignaler(ctx)
   175  	eventLoop.Start(signalerCtx)
   176  
   177  	unittest.RequireCloseBefore(t, eventLoop.Ready(), 100*time.Millisecond, "event loop not started")
   178  
   179  	parentBlock := unittest.BlockHeaderFixture()
   180  	block := unittest.BlockHeaderWithParentFixture(parentBlock)
   181  	eventLoop.SubmitProposal(block, parentBlock.View)
   182  
   183  	unittest.RequireCloseBefore(t, done, startTimeDuration+100*time.Millisecond, "proposal wasn't received")
   184  	cancel()
   185  	unittest.RequireCloseBefore(t, eventLoop.Done(), 100*time.Millisecond, "event loop not stopped")
   186  }