github.com/onflow/flow-go@v0.33.17/module/state_synchronization/indexer/indexer_test.go (about) 1 package indexer 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 mocks "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12 "go.uber.org/atomic" 13 14 "github.com/onflow/flow-go/model/flow" 15 "github.com/onflow/flow-go/module/executiondatasync/execution_data" 16 "github.com/onflow/flow-go/module/executiondatasync/execution_data/cache" 17 "github.com/onflow/flow-go/module/executiondatasync/execution_data/mock" 18 "github.com/onflow/flow-go/module/irrecoverable" 19 mempool "github.com/onflow/flow-go/module/mempool/mock" 20 storagemock "github.com/onflow/flow-go/storage/mock" 21 "github.com/onflow/flow-go/utils/unittest" 22 ) 23 24 const testTimeout = 300 * time.Millisecond 25 26 type indexerTest struct { 27 blocks []*flow.Block 28 progress *mockProgress 29 registers *storagemock.RegisterIndex 30 indexTest *indexCoreTest 31 worker *Indexer 32 executionData *mempool.ExecutionData 33 t *testing.T 34 } 35 36 // newIndexerTest set up a jobqueue integration test with the worker. 37 // It will create blocks fixtures with the length provided as availableBlocks, and it will set heights already 38 // indexed to lastIndexedIndex value. Using run it should index all the remaining blocks up to all available blocks. 39 func newIndexerTest(t *testing.T, availableBlocks int, lastIndexedIndex int) *indexerTest { 40 blocks := unittest.BlockchainFixture(availableBlocks) 41 // we use 5th index as the latest indexed height, so we leave 5 more blocks to be indexed by the indexer in this test 42 lastIndexedHeight := blocks[lastIndexedIndex].Header.Height 43 progress := newMockProgress() 44 err := progress.SetProcessedIndex(lastIndexedHeight) 45 require.NoError(t, err) 46 47 registers := storagemock.NewRegisterIndex(t) 48 49 indexerCoreTest := newIndexCoreTest(t, blocks, nil). 50 setLastHeight(func(t *testing.T) uint64 { 51 i, err := progress.ProcessedIndex() 52 require.NoError(t, err) 53 54 return i 55 }). 56 useDefaultBlockByHeight(). 57 useDefaultEvents(). 58 useDefaultTransactionResults(). 59 initIndexer() 60 61 executionData := mempool.NewExecutionData(t) 62 exeCache := cache.NewExecutionDataCache( 63 mock.NewExecutionDataStore(t), 64 indexerCoreTest.indexer.headers, 65 nil, 66 nil, 67 executionData, 68 ) 69 70 test := &indexerTest{ 71 t: t, 72 blocks: blocks, 73 progress: progress, 74 indexTest: indexerCoreTest, 75 executionData: executionData, 76 } 77 78 test.worker, err = NewIndexer( 79 unittest.Logger(), 80 test.first().Header.Height, 81 registers, 82 indexerCoreTest.indexer, 83 exeCache, 84 test.latestHeight, 85 progress, 86 ) 87 require.NoError(t, err) 88 89 return test 90 } 91 92 func (w *indexerTest) setBlockDataByID(f func(ID flow.Identifier) (*execution_data.BlockExecutionDataEntity, bool)) { 93 w.executionData. 94 On("ByID", mocks.AnythingOfType("flow.Identifier")). 95 Return(f) 96 } 97 98 func (w *indexerTest) latestHeight() (uint64, error) { 99 return w.last().Header.Height, nil 100 } 101 102 func (w *indexerTest) last() *flow.Block { 103 return w.blocks[len(w.blocks)-1] 104 } 105 106 func (w *indexerTest) first() *flow.Block { 107 return w.blocks[0] 108 } 109 110 func (w *indexerTest) run(ctx irrecoverable.SignalerContext, reachHeight uint64, cancel context.CancelFunc) { 111 w.worker.Start(ctx) 112 113 unittest.RequireComponentsReadyBefore(w.t, testTimeout, w.worker) 114 115 w.worker.OnExecutionData(nil) 116 117 // wait for end to be reached 118 <-w.progress.WaitForIndex(reachHeight) 119 cancel() 120 121 unittest.RequireCloseBefore(w.t, w.worker.Done(), testTimeout, "timeout waiting for the consumer to be done") 122 } 123 124 type mockProgress struct { 125 index *atomic.Uint64 126 doneIndex *atomic.Uint64 127 // signal to mark the progress reached an index set with WaitForIndex 128 doneChan chan struct{} 129 } 130 131 func newMockProgress() *mockProgress { 132 return &mockProgress{ 133 index: atomic.NewUint64(0), 134 doneIndex: atomic.NewUint64(0), 135 doneChan: make(chan struct{}), 136 } 137 } 138 139 func (w *mockProgress) ProcessedIndex() (uint64, error) { 140 return w.index.Load(), nil 141 } 142 143 func (w *mockProgress) SetProcessedIndex(index uint64) error { 144 w.index.Store(index) 145 146 if index > 0 && index == w.doneIndex.Load() { 147 close(w.doneChan) 148 } 149 150 return nil 151 } 152 153 func (w *mockProgress) InitProcessedIndex(index uint64) error { 154 w.index.Store(index) 155 return nil 156 } 157 158 // WaitForIndex will trigger a signal to the consumer, so they know the test reached a certain point 159 func (w *mockProgress) WaitForIndex(n uint64) <-chan struct{} { 160 w.doneIndex.Store(n) 161 return w.doneChan 162 } 163 164 func TestIndexer_Success(t *testing.T) { 165 // we use 5th index as the latest indexed height, so we leave 5 more blocks to be indexed by the indexer in this test 166 blocks := 10 167 lastIndexedIndex := 5 168 test := newIndexerTest(t, blocks, lastIndexedIndex) 169 170 test.setBlockDataByID(func(ID flow.Identifier) (*execution_data.BlockExecutionDataEntity, bool) { 171 trie := trieUpdateFixture(t) 172 ed := &execution_data.BlockExecutionData{ 173 BlockID: ID, 174 ChunkExecutionDatas: []*execution_data.ChunkExecutionData{ 175 {TrieUpdate: trie}, 176 }, 177 } 178 179 // create this to capture the closure of the creation of block execution data, so we can for each returned 180 // block execution data make sure the store of registers will match what the execution data returned and 181 // also that the height was correct 182 test.indexTest.setStoreRegisters(func(t *testing.T, entries flow.RegisterEntries, height uint64) error { 183 var blockHeight uint64 184 for _, b := range test.blocks { 185 if b.ID() == ID { 186 blockHeight = b.Header.Height 187 } 188 } 189 190 assert.Equal(t, blockHeight, height) 191 trieRegistersPayloadComparer(t, trie.Payloads, entries) 192 return nil 193 }) 194 195 return execution_data.NewBlockExecutionDataEntity(ID, ed), true 196 }) 197 198 signalerCtx, cancel := irrecoverable.NewMockSignalerContextWithCancel(t, context.Background()) 199 lastHeight := test.blocks[len(test.blocks)-1].Header.Height 200 test.run(signalerCtx, lastHeight, cancel) 201 202 // make sure store was called correct number of times 203 test.indexTest.registers.AssertNumberOfCalls(t, "Store", blocks-lastIndexedIndex-1) 204 } 205 206 func TestIndexer_Failure(t *testing.T) { 207 // we use 5th index as the latest indexed height, so we leave 5 more blocks to be indexed by the indexer in this test 208 blocks := 10 209 lastIndexedIndex := 5 210 test := newIndexerTest(t, blocks, lastIndexedIndex) 211 212 test.setBlockDataByID(func(ID flow.Identifier) (*execution_data.BlockExecutionDataEntity, bool) { 213 trie := trieUpdateFixture(t) 214 ed := &execution_data.BlockExecutionData{ 215 BlockID: ID, 216 ChunkExecutionDatas: []*execution_data.ChunkExecutionData{ 217 {TrieUpdate: trie}, 218 }, 219 } 220 221 // fail when trying to persist registers 222 test.indexTest.setStoreRegisters(func(t *testing.T, entries flow.RegisterEntries, height uint64) error { 223 return fmt.Errorf("error persisting data") 224 }) 225 226 return execution_data.NewBlockExecutionDataEntity(ID, ed), true 227 }) 228 229 // make sure the error returned is as expected 230 expectedErr := fmt.Errorf( 231 "failed to index block data at height %d: %w", 232 test.blocks[lastIndexedIndex].Header.Height+1, 233 fmt.Errorf( 234 "could not index register payloads at height %d: %w", test.blocks[lastIndexedIndex].Header.Height+1, fmt.Errorf("error persisting data")), 235 ) 236 237 _, cancel := context.WithCancel(context.Background()) 238 signalerCtx := irrecoverable.NewMockSignalerContextExpectError(t, context.Background(), expectedErr) 239 lastHeight := test.blocks[lastIndexedIndex].Header.Height 240 test.run(signalerCtx, lastHeight, cancel) 241 242 // make sure store was called correct number of times 243 test.indexTest.registers.AssertNumberOfCalls(t, "Store", 1) // it fails after first run 244 }