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 }