github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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  		collection := unittest.CollectionFixture(0)
   173  		ed := &execution_data.BlockExecutionData{
   174  			BlockID: ID,
   175  			ChunkExecutionDatas: []*execution_data.ChunkExecutionData{{
   176  				Collection: &collection,
   177  				TrieUpdate: trie,
   178  			}},
   179  		}
   180  
   181  		// create this to capture the closure of the creation of block execution data, so we can for each returned
   182  		// block execution data make sure the store of registers will match what the execution data returned and
   183  		// also that the height was correct
   184  		test.indexTest.setStoreRegisters(func(t *testing.T, entries flow.RegisterEntries, height uint64) error {
   185  			var blockHeight uint64
   186  			for _, b := range test.blocks {
   187  				if b.ID() == ID {
   188  					blockHeight = b.Header.Height
   189  				}
   190  			}
   191  
   192  			assert.Equal(t, blockHeight, height)
   193  			trieRegistersPayloadComparer(t, trie.Payloads, entries)
   194  			return nil
   195  		})
   196  
   197  		return execution_data.NewBlockExecutionDataEntity(ID, ed), true
   198  	})
   199  
   200  	signalerCtx, cancel := irrecoverable.NewMockSignalerContextWithCancel(t, context.Background())
   201  	lastHeight := test.blocks[len(test.blocks)-1].Header.Height
   202  	test.run(signalerCtx, lastHeight, cancel)
   203  
   204  	// make sure store was called correct number of times
   205  	test.indexTest.registers.AssertNumberOfCalls(t, "Store", blocks-lastIndexedIndex-1)
   206  }
   207  
   208  func TestIndexer_Failure(t *testing.T) {
   209  	// we use 5th index as the latest indexed height, so we leave 5 more blocks to be indexed by the indexer in this test
   210  	blocks := 10
   211  	lastIndexedIndex := 5
   212  	test := newIndexerTest(t, blocks, lastIndexedIndex)
   213  
   214  	test.setBlockDataByID(func(ID flow.Identifier) (*execution_data.BlockExecutionDataEntity, bool) {
   215  		trie := trieUpdateFixture(t)
   216  		collection := unittest.CollectionFixture(0)
   217  		ed := &execution_data.BlockExecutionData{
   218  			BlockID: ID,
   219  			ChunkExecutionDatas: []*execution_data.ChunkExecutionData{{
   220  				Collection: &collection,
   221  				TrieUpdate: trie,
   222  			}},
   223  		}
   224  
   225  		// fail when trying to persist registers
   226  		test.indexTest.setStoreRegisters(func(t *testing.T, entries flow.RegisterEntries, height uint64) error {
   227  			return fmt.Errorf("error persisting data")
   228  		})
   229  
   230  		return execution_data.NewBlockExecutionDataEntity(ID, ed), true
   231  	})
   232  
   233  	// make sure the error returned is as expected
   234  	expectedErr := fmt.Errorf(
   235  		"failed to index block data at height %d: %w",
   236  		test.blocks[lastIndexedIndex].Header.Height+1,
   237  		fmt.Errorf(
   238  			"could not index register payloads at height %d: %w", test.blocks[lastIndexedIndex].Header.Height+1, fmt.Errorf("error persisting data")),
   239  	)
   240  
   241  	_, cancel := context.WithCancel(context.Background())
   242  	signalerCtx := irrecoverable.NewMockSignalerContextExpectError(t, context.Background(), expectedErr)
   243  	lastHeight := test.blocks[lastIndexedIndex].Header.Height
   244  	test.run(signalerCtx, lastHeight, cancel)
   245  
   246  	// make sure store was called correct number of times
   247  	test.indexTest.registers.AssertNumberOfCalls(t, "Store", 1) // it fails after first run
   248  }