github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/voteaggregator/vote_collectors_test.go (about) 1 package voteaggregator 2 3 import ( 4 "errors" 5 "fmt" 6 "sync" 7 "testing" 8 9 "github.com/gammazero/workerpool" 10 "github.com/stretchr/testify/require" 11 "github.com/stretchr/testify/suite" 12 "go.uber.org/atomic" 13 14 "github.com/onflow/flow-go/consensus/hotstuff" 15 "github.com/onflow/flow-go/consensus/hotstuff/mocks" 16 "github.com/onflow/flow-go/module/mempool" 17 "github.com/onflow/flow-go/utils/unittest" 18 ) 19 20 var factoryError = errors.New("factory error") 21 22 func TestVoteCollectors(t *testing.T) { 23 suite.Run(t, new(VoteCollectorsTestSuite)) 24 } 25 26 // VoteCollectorsTestSuite is a test suite for isolated testing of VoteCollectors. 27 // Contains helper methods and mocked state which is used to verify correct behavior of VoteCollectors. 28 type VoteCollectorsTestSuite struct { 29 suite.Suite 30 31 mockedCollectors map[uint64]*mocks.VoteCollector 32 factoryMethod NewCollectorFactoryMethod 33 collectors *VoteCollectors 34 lowestLevel uint64 35 workerPool *workerpool.WorkerPool 36 } 37 38 func (s *VoteCollectorsTestSuite) SetupTest() { 39 s.lowestLevel = 1000 40 s.mockedCollectors = make(map[uint64]*mocks.VoteCollector) 41 s.workerPool = workerpool.New(2) 42 s.factoryMethod = func(view uint64, _ hotstuff.Workers) (hotstuff.VoteCollector, error) { 43 if collector, found := s.mockedCollectors[view]; found { 44 return collector, nil 45 } 46 return nil, fmt.Errorf("mocked collector %v not found: %w", view, factoryError) 47 } 48 s.collectors = NewVoteCollectors(unittest.Logger(), s.lowestLevel, s.workerPool, s.factoryMethod) 49 } 50 51 func (s *VoteCollectorsTestSuite) TearDownTest() { 52 s.workerPool.StopWait() 53 } 54 55 // prepareMockedCollector prepares a mocked collector and stores it in map, later it will be used 56 // to mock behavior of vote collectors. 57 func (s *VoteCollectorsTestSuite) prepareMockedCollector(view uint64) *mocks.VoteCollector { 58 collector := &mocks.VoteCollector{} 59 collector.On("View").Return(view).Maybe() 60 s.mockedCollectors[view] = collector 61 return collector 62 } 63 64 // TestGetOrCreatorCollector_ViewLowerThanLowest tests a scenario where caller tries to create a collector with view 65 // lower than already pruned one. This should result in sentinel error `BelowPrunedThresholdError` 66 func (s *VoteCollectorsTestSuite) TestGetOrCreatorCollector_ViewLowerThanLowest() { 67 collector, created, err := s.collectors.GetOrCreateCollector(s.lowestLevel - 10) 68 require.Nil(s.T(), collector) 69 require.False(s.T(), created) 70 require.Error(s.T(), err) 71 require.True(s.T(), mempool.IsBelowPrunedThresholdError(err)) 72 } 73 74 // TestGetOrCreateCollector_ValidCollector tests a happy path scenario where we try first to create and then retrieve cached collector. 75 func (s *VoteCollectorsTestSuite) TestGetOrCreateCollector_ValidCollector() { 76 view := s.lowestLevel + 10 77 s.prepareMockedCollector(view) 78 collector, created, err := s.collectors.GetOrCreateCollector(view) 79 require.NoError(s.T(), err) 80 require.True(s.T(), created) 81 require.Equal(s.T(), view, collector.View()) 82 83 cached, cachedCreated, err := s.collectors.GetOrCreateCollector(view) 84 require.NoError(s.T(), err) 85 require.False(s.T(), cachedCreated) 86 require.Equal(s.T(), collector, cached) 87 } 88 89 // TestGetOrCreateCollector_FactoryError tests that error from factory method is propagated to caller. 90 func (s *VoteCollectorsTestSuite) TestGetOrCreateCollector_FactoryError() { 91 // creating collector without calling prepareMockedCollector will yield factoryError. 92 collector, created, err := s.collectors.GetOrCreateCollector(s.lowestLevel + 10) 93 require.Nil(s.T(), collector) 94 require.False(s.T(), created) 95 require.ErrorIs(s.T(), err, factoryError) 96 } 97 98 // TestGetOrCreateCollectors_ConcurrentAccess tests that concurrently accessing of GetOrCreateCollector creates 99 // only one collector and all other instances are retrieved from cache. 100 func (s *VoteCollectorsTestSuite) TestGetOrCreateCollectors_ConcurrentAccess() { 101 createdTimes := atomic.NewUint64(0) 102 view := s.lowestLevel + 10 103 s.prepareMockedCollector(view) 104 var wg sync.WaitGroup 105 for i := 0; i < 10; i++ { 106 wg.Add(1) 107 go func() { 108 _, created, err := s.collectors.GetOrCreateCollector(view) 109 require.NoError(s.T(), err) 110 if created { 111 createdTimes.Add(1) 112 } 113 wg.Done() 114 }() 115 } 116 117 wg.Wait() 118 require.Equal(s.T(), uint64(1), createdTimes.Load()) 119 } 120 121 // TestPruneUpToView tests pruning removes item below pruning height and leaves unmodified other items. 122 func (s *VoteCollectorsTestSuite) TestPruneUpToView() { 123 numberOfCollectors := uint64(10) 124 prunedViews := make([]uint64, 0) 125 for i := uint64(0); i < numberOfCollectors; i++ { 126 view := s.lowestLevel + i 127 s.prepareMockedCollector(view) 128 _, _, err := s.collectors.GetOrCreateCollector(view) 129 require.NoError(s.T(), err) 130 prunedViews = append(prunedViews, view) 131 } 132 133 pruningHeight := s.lowestLevel + numberOfCollectors 134 135 expectedCollectors := make([]hotstuff.VoteCollector, 0) 136 for i := uint64(0); i < numberOfCollectors; i++ { 137 view := pruningHeight + i 138 s.prepareMockedCollector(view) 139 collector, _, err := s.collectors.GetOrCreateCollector(view) 140 require.NoError(s.T(), err) 141 expectedCollectors = append(expectedCollectors, collector) 142 } 143 144 // after this operation collectors below pruning height should be pruned and everything higher 145 // should be left unmodified 146 s.collectors.PruneUpToView(pruningHeight) 147 148 for _, prunedView := range prunedViews { 149 _, _, err := s.collectors.GetOrCreateCollector(prunedView) 150 require.Error(s.T(), err) 151 require.True(s.T(), mempool.IsBelowPrunedThresholdError(err)) 152 } 153 154 for _, collector := range expectedCollectors { 155 cached, _, _ := s.collectors.GetOrCreateCollector(collector.View()) 156 require.Equal(s.T(), collector, cached) 157 } 158 }