github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/mempool/herocache/backdata/heropool/pool_test.go (about)

     1  package heropool
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"testing"
     7  
     8  	"github.com/onflow/flow-go/utils/rand"
     9  
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/onflow/flow-go/model/flow"
    13  	"github.com/onflow/flow-go/utils/unittest"
    14  )
    15  
    16  // TestStoreAndRetrieval_BelowLimit checks health of heroPool for storing and retrieval scenarios that
    17  // do not involve ejection.
    18  // The test involves cases for testing the pool below its limit, and also up to its limit. However, it never gets beyond
    19  // the limit, so no ejection will kick-in.
    20  func TestStoreAndRetrieval_BelowLimit(t *testing.T) {
    21  	for _, tc := range []struct {
    22  		limit       uint32 // capacity of entity list
    23  		entityCount uint32 // total entities to be stored
    24  	}{
    25  		{
    26  			limit:       30,
    27  			entityCount: 10,
    28  		},
    29  		{
    30  			limit:       30,
    31  			entityCount: 30,
    32  		},
    33  		{
    34  			limit:       2000,
    35  			entityCount: 1000,
    36  		},
    37  		{
    38  			limit:       1000,
    39  			entityCount: 1000,
    40  		},
    41  	} {
    42  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
    43  			withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
    44  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    45  					testInitialization(t, pool, entities)
    46  				},
    47  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    48  					testAddingEntities(t, pool, entities, LRUEjection)
    49  				},
    50  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    51  					testRetrievingEntitiesFrom(t, pool, entities, 0)
    52  				},
    53  			}...,
    54  			)
    55  		})
    56  	}
    57  }
    58  
    59  // TestStoreAndRetrieval_With_No_Ejection checks health of heroPool for storing and retrieval scenarios that involves the NoEjection mode.
    60  func TestStoreAndRetrieval_With_No_Ejection(t *testing.T) {
    61  	for _, tc := range []struct {
    62  		limit       uint32 // capacity of pool
    63  		entityCount uint32 // total entities to be stored
    64  	}{
    65  		{
    66  			limit:       30,
    67  			entityCount: 31,
    68  		},
    69  		{
    70  			limit:       30,
    71  			entityCount: 100,
    72  		},
    73  		{
    74  			limit:       1000,
    75  			entityCount: 2000,
    76  		},
    77  	} {
    78  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
    79  			withTestScenario(t, tc.limit, tc.entityCount, NoEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
    80  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    81  					testAddingEntities(t, pool, entities, NoEjection)
    82  				},
    83  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    84  					// with the NoEjection mode, only the first "limit" entities must be retrievable.
    85  					testRetrievingEntitiesInRange(t, pool, entities, 0, EIndex(tc.limit))
    86  				},
    87  			}...,
    88  			)
    89  		})
    90  	}
    91  }
    92  
    93  // TestStoreAndRetrieval_With_LRU_Ejection checks health of heroPool for storing and retrieval scenarios that involves the LRU ejection.
    94  // The test involves cases for testing the pool beyond its limit, so the LRU ejection will kick-in.
    95  func TestStoreAndRetrieval_With_LRU_Ejection(t *testing.T) {
    96  	for _, tc := range []struct {
    97  		limit       uint32 // capacity of pool
    98  		entityCount uint32 // total entities to be stored
    99  	}{
   100  		{
   101  			limit:       30,
   102  			entityCount: 31,
   103  		},
   104  		{
   105  			limit:       30,
   106  			entityCount: 100,
   107  		},
   108  		{
   109  			limit:       1000,
   110  			entityCount: 2000,
   111  		},
   112  	} {
   113  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
   114  			withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
   115  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   116  					testAddingEntities(t, pool, entities, LRUEjection)
   117  				},
   118  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   119  					// with a limit of tc.limit, storing a total of tc.entityCount (> tc.limit) entities, results
   120  					// in ejection of the first tc.entityCount - tc.limit entities.
   121  					// Hence, we check retrieval of the last tc.limit entities, which start from index
   122  					// tc.entityCount - tc.limit entities.
   123  					testRetrievingEntitiesFrom(t, pool, entities, EIndex(tc.entityCount-tc.limit))
   124  				},
   125  			}...,
   126  			)
   127  		})
   128  	}
   129  }
   130  
   131  // TestStoreAndRetrieval_With_Random_Ejection checks health of heroPool for storing and retrieval scenarios that involves the LRU ejection.
   132  func TestStoreAndRetrieval_With_Random_Ejection(t *testing.T) {
   133  	for _, tc := range []struct {
   134  		limit       uint32 // capacity of pool
   135  		entityCount uint32 // total entities to be stored
   136  	}{
   137  		{
   138  			limit:       30,
   139  			entityCount: 31,
   140  		},
   141  		{
   142  			limit:       30,
   143  			entityCount: 100,
   144  		},
   145  	} {
   146  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
   147  			withTestScenario(t, tc.limit, tc.entityCount, RandomEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
   148  				func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) {
   149  					testAddingEntities(t, backData, entities, RandomEjection)
   150  				},
   151  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   152  					// with a limit of tc.limit, storing a total of tc.entityCount (> tc.limit) entities, results
   153  					// in ejection of "tc.entityCount - tc.limit" entities at random.
   154  					// Hence, we check retrieval any successful total of "tc.limit" entities.
   155  					testRetrievingCount(t, pool, entities, int(tc.limit))
   156  				},
   157  			}...,
   158  			)
   159  		})
   160  	}
   161  }
   162  
   163  // TestInvalidateEntity checks the health of heroPool for invalidating entities under random, LRU, and LIFO scenarios.
   164  // Invalidating an entity removes it from the used state and moves its node to the free state.
   165  func TestInvalidateEntity(t *testing.T) {
   166  	for _, tc := range []struct {
   167  		limit       uint32 // capacity of entity pool
   168  		entityCount uint32 // total entities to be stored
   169  	}{
   170  		{
   171  			limit:       30,
   172  			entityCount: 0,
   173  		},
   174  		{
   175  			limit:       30,
   176  			entityCount: 1,
   177  		},
   178  		{
   179  			limit:       30,
   180  			entityCount: 10,
   181  		},
   182  		{
   183  			limit:       30,
   184  			entityCount: 30,
   185  		},
   186  		{
   187  			limit:       100,
   188  			entityCount: 10,
   189  		},
   190  		{
   191  			limit:       100,
   192  			entityCount: 100,
   193  		},
   194  	} {
   195  		// head invalidation test (LRU)
   196  		t.Run(fmt.Sprintf("head-invalidation-%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
   197  			withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
   198  				func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) {
   199  					testAddingEntities(t, backData, entities, LRUEjection)
   200  				},
   201  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   202  					testInvalidatingHead(t, pool, entities)
   203  				},
   204  			}...)
   205  		})
   206  
   207  		// tail invalidation test (LIFO)
   208  		t.Run(fmt.Sprintf("tail-invalidation-%d-limit-%d-entities-", tc.limit, tc.entityCount), func(t *testing.T) {
   209  			withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
   210  				func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) {
   211  					testAddingEntities(t, backData, entities, LRUEjection)
   212  				},
   213  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   214  					testInvalidatingTail(t, pool, entities)
   215  				},
   216  			}...)
   217  		})
   218  	}
   219  }
   220  
   221  // TestAddAndRemoveEntities checks health of heroPool for scenario where entitites are stored and removed in a predetermined order.
   222  // LRUEjection, NoEjection and RandomEjection are tested. RandomEjection doesn't allow to provide a final state of the pool to check.
   223  func TestAddAndRemoveEntities(t *testing.T) {
   224  	for _, tc := range []struct {
   225  		limit               uint32       // capacity of the pool
   226  		entityCount         uint32       // total entities to be stored
   227  		ejectionMode        EjectionMode // ejection mode
   228  		numberOfOperations  int
   229  		probabilityOfAdding float32
   230  	}{
   231  		{
   232  			limit:               500,
   233  			entityCount:         1000,
   234  			ejectionMode:        LRUEjection,
   235  			numberOfOperations:  1000,
   236  			probabilityOfAdding: 0.8,
   237  		},
   238  		{
   239  			limit:               500,
   240  			entityCount:         1000,
   241  			ejectionMode:        NoEjection,
   242  			numberOfOperations:  1000,
   243  			probabilityOfAdding: 0.8,
   244  		},
   245  		{
   246  			limit:               500,
   247  			entityCount:         1000,
   248  			ejectionMode:        RandomEjection,
   249  			numberOfOperations:  1000,
   250  			probabilityOfAdding: 0.8,
   251  		},
   252  	} {
   253  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
   254  			testAddRemoveEntities(t, tc.limit, tc.entityCount, tc.ejectionMode, tc.numberOfOperations, tc.probabilityOfAdding)
   255  		})
   256  	}
   257  }
   258  
   259  // testAddRemoveEntities adds and removes randomly elements in the pool, probabilityOfAdding and its counterpart 1-probabilityOfAdding are probabilities
   260  // for an operation to be add or remove. Current timestamp is taken as a seed for the random number generator.
   261  func testAddRemoveEntities(t *testing.T, limit uint32, entityCount uint32, ejectionMode EjectionMode, numberOfOperations int, probabilityOfAdding float32) {
   262  
   263  	require.GreaterOrEqual(t, entityCount, 2*limit, "entityCount must be greater or equal to 2*limit to test add/remove operations")
   264  
   265  	randomIntN := func(length int) int {
   266  		random, err := rand.Uintn(uint(length))
   267  		require.NoError(t, err)
   268  		return int(random)
   269  	}
   270  
   271  	pool := NewHeroPool(limit, ejectionMode, unittest.Logger())
   272  	entities := unittest.EntityListFixture(uint(entityCount))
   273  	// retryLimit is the max number of retries to find an entity that is not already in the pool to add it.
   274  	// The test fails if it reaches this limit.
   275  	retryLimit := 100
   276  	// an array of random owner Ids.
   277  	ownerIds := make([]uint64, entityCount)
   278  	// generate ownerId to index in the entities array.
   279  	for i := 0; i < int(entityCount); i++ {
   280  		randomOwnerId, err := rand.Uint64()
   281  		require.Nil(t, err)
   282  		ownerIds[i] = randomOwnerId
   283  	}
   284  	// this map maintains entities currently stored in the pool.
   285  	addedEntities := make(map[flow.Identifier]int)
   286  	addedEntitiesInPool := make(map[flow.Identifier]EIndex)
   287  	for i := 0; i < numberOfOperations; i++ {
   288  		// choose between Add and Remove with a probability of probabilityOfAdding and 1-probabilityOfAdding respectively.
   289  		if float32(randomIntN(math.MaxInt32))/math.MaxInt32 < probabilityOfAdding || len(addedEntities) == 0 {
   290  			// keeps finding an entity to add until it finds one that is not already in the pool.
   291  			found := false
   292  			for retryTime := 0; retryTime < retryLimit; retryTime++ {
   293  				toAddIndex := randomIntN(int(entityCount))
   294  				_, found = addedEntities[entities[toAddIndex].ID()]
   295  				if !found {
   296  					// found an entity that is not in the pool, add it.
   297  					indexInThePool, _, ejectedEntity := pool.Add(entities[toAddIndex].ID(), entities[toAddIndex], ownerIds[toAddIndex])
   298  					if ejectionMode != NoEjection || len(addedEntities) < int(limit) {
   299  						// when there is an ejection mode in place, or the pool is not full, the index should be valid.
   300  						require.NotEqual(t, InvalidIndex, indexInThePool)
   301  					}
   302  					require.LessOrEqual(t, len(addedEntities), int(limit), "pool should not contain more elements than its limit")
   303  					if ejectionMode != NoEjection && len(addedEntities) == int(limit) {
   304  						// when there is an ejection mode in place, the ejected entity should be valid.
   305  						require.NotNil(t, ejectedEntity)
   306  					}
   307  					if ejectionMode != NoEjection && len(addedEntities) >= int(limit) {
   308  						// when there is an ejection mode in place, the ejected entity should be valid.
   309  						require.NotNil(t, ejectedEntity)
   310  					}
   311  					if indexInThePool != InvalidIndex {
   312  						entityId := entities[toAddIndex].ID()
   313  						// tracks the index of the entity in the pool and the index of the entity in the entities array.
   314  						addedEntities[entityId] = int(toAddIndex)
   315  						addedEntitiesInPool[entityId] = indexInThePool
   316  						// any entity added to the pool should be in the pool, and must be retrievable.
   317  						actualFlowId, actualEntity, actualOwnerId := pool.Get(indexInThePool)
   318  						require.Equal(t, entityId, actualFlowId)
   319  						require.Equal(t, entities[toAddIndex], actualEntity, "pool returned a different entity than the one added")
   320  						require.Equal(t, ownerIds[toAddIndex], actualOwnerId, "pool returned a different owner than the one added")
   321  					}
   322  					if ejectedEntity != nil {
   323  						require.Contains(t, addedEntities, ejectedEntity.ID(), "pool ejected an entity that was not added before")
   324  						delete(addedEntities, ejectedEntity.ID())
   325  						delete(addedEntitiesInPool, ejectedEntity.ID())
   326  					}
   327  					break
   328  				}
   329  			}
   330  			require.Falsef(t, found, "could not find an entity to add after %d retries", retryLimit)
   331  		} else {
   332  			// randomly select an index of an entity to remove.
   333  			entityToRemove := randomIntN(len(addedEntities))
   334  			i := 0
   335  			var indexInPoolToRemove EIndex = 0
   336  			var indexInEntitiesArray int = 0
   337  			for k, v := range addedEntities {
   338  				if i == entityToRemove {
   339  					indexInPoolToRemove = addedEntitiesInPool[k]
   340  					indexInEntitiesArray = v
   341  					break
   342  				}
   343  				i++
   344  			}
   345  			// remove the selected entity from the pool.
   346  			removedEntity := pool.Remove(indexInPoolToRemove)
   347  			expectedRemovedEntityId := entities[indexInEntitiesArray].ID()
   348  			require.Equal(t, expectedRemovedEntityId, removedEntity.ID(), "removed wrong entity")
   349  			delete(addedEntities, expectedRemovedEntityId)
   350  			delete(addedEntitiesInPool, expectedRemovedEntityId)
   351  			actualFlowId, actualEntity, _ := pool.Get(indexInPoolToRemove)
   352  			require.Equal(t, flow.ZeroID, actualFlowId)
   353  			require.Equal(t, nil, actualEntity)
   354  		}
   355  	}
   356  	for k, v := range addedEntities {
   357  		indexInPool := addedEntitiesInPool[k]
   358  		actualFlowId, actualEntity, actualOwnerId := pool.Get(indexInPool)
   359  		require.Equal(t, entities[v].ID(), actualFlowId)
   360  		require.Equal(t, entities[v], actualEntity)
   361  		require.Equal(t, ownerIds[v], actualOwnerId)
   362  	}
   363  	require.Equalf(t, len(addedEntities), int(pool.Size()), "pool size is not correct, expected %d, actual %d", len(addedEntities), pool.Size())
   364  }
   365  
   366  // testInvalidatingHead keeps invalidating the head and evaluates the linked-list keeps updating its head
   367  // and remains connected.
   368  func testInvalidatingHead(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   369  	// total number of entities to store
   370  	totalEntitiesStored := len(entities)
   371  	// freeListInitialSize is total number of empty nodes after
   372  	// storing all items in the list
   373  	freeListInitialSize := len(pool.poolEntities) - totalEntitiesStored
   374  
   375  	// (i+1) keeps total invalidated (head) entities.
   376  	for i := 0; i < totalEntitiesStored; i++ {
   377  		headIndex := pool.invalidateUsedHead()
   378  		// head index should be moved to the next index after each head invalidation.
   379  		require.Equal(t, entities[i], headIndex)
   380  		// size of list should be decremented after each invalidation.
   381  		require.Equal(t, uint32(totalEntitiesStored-i-1), pool.Size())
   382  		// invalidated head should be appended to free entities
   383  		require.Equal(t, pool.states[stateFree].tail, EIndex(i))
   384  
   385  		if freeListInitialSize != 0 {
   386  			// number of entities is below limit, hence free list is not empty.
   387  			// invalidating used head must not change the free head.
   388  			require.Equal(t, EIndex(totalEntitiesStored), pool.states[stateFree].head)
   389  		} else {
   390  			// number of entities is greater than or equal to limit, hence free list is empty.
   391  			// free head must be updated to the first invalidated head (index 0),
   392  			// and must be kept there for entire test (as we invalidate head not tail).
   393  			require.Equal(t, EIndex(0), pool.states[stateFree].head)
   394  		}
   395  
   396  		// except when the list is empty, head must be updated after invalidation,
   397  		// except when the list is empty, head and tail must be accessible after each invalidation`
   398  		// i.e., the linked list remains connected despite invalidation.
   399  		if i != totalEntitiesStored-1 {
   400  			// used linked-list
   401  			tailAccessibleFromHead(t,
   402  				pool.states[stateUsed].head,
   403  				pool.states[stateUsed].tail,
   404  				pool,
   405  				pool.Size())
   406  
   407  			headAccessibleFromTail(t,
   408  				pool.states[stateUsed].head,
   409  				pool.states[stateUsed].tail,
   410  				pool,
   411  				pool.Size())
   412  
   413  			// free lined-list
   414  			//
   415  			// after invalidating each item, size of free linked-list is incremented by one.
   416  			tailAccessibleFromHead(t,
   417  				pool.states[stateFree].head,
   418  				pool.states[stateFree].tail,
   419  				pool,
   420  				uint32(i+1+freeListInitialSize))
   421  
   422  			headAccessibleFromTail(t,
   423  				pool.states[stateFree].head,
   424  				pool.states[stateFree].tail,
   425  				pool,
   426  				uint32(i+1+freeListInitialSize))
   427  		}
   428  
   429  		// checking the status of head and tail in used linked-list after each head invalidation.
   430  		usedTail, _ := pool.getTails()
   431  		usedHead, _ := pool.getHeads()
   432  		if i != totalEntitiesStored-1 {
   433  			// pool is not empty yet, we still have entities to invalidate.
   434  			//
   435  			// used tail should point to the last element in pool, since we are
   436  			// invalidating head.
   437  			require.Equal(t, entities[totalEntitiesStored-1].ID(), usedTail.id)
   438  			require.Equal(t, EIndex(totalEntitiesStored-1), pool.states[stateUsed].tail)
   439  
   440  			// used head must point to the next element in the pool,
   441  			// i.e., invalidating head moves it forward.
   442  			require.Equal(t, entities[i+1].ID(), usedHead.id)
   443  			require.Equal(t, EIndex(i+1), pool.states[stateUsed].head)
   444  		} else {
   445  			// pool is empty
   446  			// used head and tail must be nil and their corresponding
   447  			// pointer indices must be undefined.
   448  			require.Nil(t, usedHead)
   449  			require.Nil(t, usedTail)
   450  			require.True(t, pool.states[stateUsed].size == 0)
   451  			require.Equal(t, pool.states[stateUsed].tail, InvalidIndex)
   452  			require.Equal(t, pool.states[stateUsed].head, InvalidIndex)
   453  		}
   454  		checkEachEntityIsInFreeOrUsedState(t, pool)
   455  	}
   456  }
   457  
   458  // testInvalidatingHead keeps invalidating the tail and evaluates the underlying free and used linked-lists keep updating its tail and remains connected.
   459  func testInvalidatingTail(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   460  	size := len(entities)
   461  	offset := len(pool.poolEntities) - size
   462  	for i := 0; i < size; i++ {
   463  		// invalidates tail index
   464  		tailIndex := pool.states[stateUsed].tail
   465  		require.Equal(t, EIndex(size-1-i), tailIndex)
   466  
   467  		pool.invalidateEntityAtIndex(tailIndex)
   468  		// old head index must be invalidated
   469  		require.True(t, pool.isInvalidated(tailIndex))
   470  		// unclaimed head should be appended to free entities
   471  		require.Equal(t, pool.states[stateFree].tail, tailIndex)
   472  
   473  		if offset != 0 {
   474  			// number of entities is below limit
   475  			// free must head keeps pointing to first empty index after
   476  			// adding all entities.
   477  			require.Equal(t, EIndex(size), pool.states[stateFree].head)
   478  		} else {
   479  			// number of entities is greater than or equal to limit
   480  			// free head must be updated to last element in the pool (size - 1),
   481  			// and must be kept there for entire test (as we invalidate tail not head).
   482  			require.Equal(t, EIndex(size-1), pool.states[stateFree].head)
   483  		}
   484  
   485  		// size of pool should be shrunk after each invalidation.
   486  		require.Equal(t, uint32(size-i-1), pool.Size())
   487  
   488  		// except when the pool is empty, tail must be updated after invalidation,
   489  		// and also head and tail must be accessible after each invalidation
   490  		// i.e., the linked-list remains connected despite invalidation.
   491  		if i != size-1 {
   492  
   493  			// used linked-list
   494  			tailAccessibleFromHead(t,
   495  				pool.states[stateUsed].head,
   496  				pool.states[stateUsed].tail,
   497  				pool,
   498  				pool.Size())
   499  
   500  			headAccessibleFromTail(t,
   501  				pool.states[stateUsed].head,
   502  				pool.states[stateUsed].tail,
   503  				pool,
   504  				pool.Size())
   505  
   506  			// free linked-list
   507  			tailAccessibleFromHead(t,
   508  				pool.states[stateFree].head,
   509  				pool.states[stateFree].tail,
   510  				pool,
   511  				uint32(i+1+offset))
   512  
   513  			headAccessibleFromTail(t,
   514  				pool.states[stateFree].head,
   515  				pool.states[stateFree].tail,
   516  				pool,
   517  				uint32(i+1+offset))
   518  		}
   519  
   520  		usedTail, _ := pool.getTails()
   521  		usedHead, _ := pool.getHeads()
   522  		if i != size-1 {
   523  			// pool is not empty yet
   524  			//
   525  			// used tail should move backward after each invalidation
   526  			require.Equal(t, entities[size-i-2].ID(), usedTail.id)
   527  			require.Equal(t, EIndex(size-i-2), pool.states[stateUsed].tail)
   528  
   529  			// used head must point to the first element in the pool,
   530  			require.Equal(t, entities[0].ID(), usedHead.id)
   531  			require.Equal(t, EIndex(0), pool.states[stateUsed].head)
   532  		} else {
   533  			// pool is empty
   534  			// used head and tail must be nil and their corresponding
   535  			// pointer indices must be undefined.
   536  			require.Nil(t, usedHead)
   537  			require.Nil(t, usedTail)
   538  			require.True(t, pool.states[stateUsed].size == 0)
   539  			require.Equal(t, pool.states[stateUsed].head, InvalidIndex)
   540  			require.Equal(t, pool.states[stateUsed].tail, InvalidIndex)
   541  		}
   542  		checkEachEntityIsInFreeOrUsedState(t, pool)
   543  	}
   544  }
   545  
   546  // testInitialization evaluates the state of an initialized pool before adding any element to it.
   547  func testInitialization(t *testing.T, pool *Pool, _ []*unittest.MockEntity) {
   548  	// "used" linked-list must have a zero size, since we have no elements in the list.
   549  	require.True(t, pool.states[stateUsed].size == 0)
   550  	require.Equal(t, pool.states[stateUsed].head, InvalidIndex)
   551  	require.Equal(t, pool.states[stateUsed].tail, InvalidIndex)
   552  
   553  	for i := 0; i < len(pool.poolEntities); i++ {
   554  		if i == 0 {
   555  			// head of "free" linked-list should point to InvalidIndex of entities slice.
   556  			require.Equal(t, EIndex(i), pool.states[stateFree].head)
   557  			// previous element of head must be undefined (linked-list head feature).
   558  			require.Equal(t, pool.poolEntities[i].node.prev, InvalidIndex)
   559  		}
   560  
   561  		if i != 0 {
   562  			// except head, any element should point back to its previous index in slice.
   563  			require.Equal(t, EIndex(i-1), pool.poolEntities[i].node.prev)
   564  		}
   565  
   566  		if i != len(pool.poolEntities)-1 {
   567  			// except tail, any element should point forward to its next index in slice.
   568  			require.Equal(t, EIndex(i+1), pool.poolEntities[i].node.next)
   569  		}
   570  
   571  		if i == len(pool.poolEntities)-1 {
   572  			// tail of "free" linked-list should point to the last index in entities slice.
   573  			require.Equal(t, EIndex(i), pool.states[stateFree].tail)
   574  			// next element of tail must be undefined.
   575  			require.Equal(t, pool.poolEntities[i].node.next, InvalidIndex)
   576  		}
   577  	}
   578  }
   579  
   580  // testAddingEntities evaluates health of pool for storing new elements.
   581  func testAddingEntities(t *testing.T, pool *Pool, entitiesToBeAdded []*unittest.MockEntity, ejectionMode EjectionMode) {
   582  	// initially head must be empty
   583  	e, ok := pool.Head()
   584  	require.False(t, ok)
   585  	require.Nil(t, e)
   586  
   587  	var uniqueEntities map[flow.Identifier]struct{}
   588  	if ejectionMode != NoEjection {
   589  		uniqueEntities = make(map[flow.Identifier]struct{})
   590  		for _, entity := range entitiesToBeAdded {
   591  			uniqueEntities[entity.ID()] = struct{}{}
   592  		}
   593  		require.Equalf(t, len(uniqueEntities), len(entitiesToBeAdded), "entitesToBeAdded must be constructed of unique entities")
   594  	}
   595  
   596  	// adding elements
   597  	lruEjectedIndex := 0
   598  	for i, e := range entitiesToBeAdded {
   599  		// adding each element must be successful.
   600  		entityIndex, slotAvailable, ejectedEntity := pool.Add(e.ID(), e, uint64(i))
   601  
   602  		if i < len(pool.poolEntities) {
   603  			// in case of no over limit, size of entities linked list should be incremented by each addition.
   604  			require.Equal(t, pool.Size(), uint32(i+1))
   605  
   606  			require.True(t, slotAvailable)
   607  			require.Nil(t, ejectedEntity)
   608  			require.Equal(t, entityIndex, EIndex(i))
   609  
   610  			// in case pool is not full, the head should retrieve the first added entity.
   611  			headEntity, headExists := pool.Head()
   612  			require.True(t, headExists)
   613  			require.Equal(t, headEntity.ID(), entitiesToBeAdded[0].ID())
   614  		}
   615  
   616  		if ejectionMode == LRUEjection {
   617  			// under LRU ejection mode, new entity should be placed at index i in back data
   618  			_, entity, _ := pool.Get(EIndex(i % len(pool.poolEntities)))
   619  			require.Equal(t, e, entity)
   620  
   621  			if i >= len(pool.poolEntities) {
   622  				require.True(t, slotAvailable)
   623  				require.NotNil(t, ejectedEntity)
   624  				// confirm that ejected entity is the oldest entity
   625  				require.Equal(t, entitiesToBeAdded[lruEjectedIndex], ejectedEntity)
   626  				lruEjectedIndex++
   627  				// when pool is full and with LRU ejection, the head should move forward with each element added.
   628  				headEntity, headExists := pool.Head()
   629  				require.True(t, headExists)
   630  				require.Equal(t, headEntity.ID(), entitiesToBeAdded[i+1-len(pool.poolEntities)].ID())
   631  			}
   632  		}
   633  
   634  		if ejectionMode == RandomEjection {
   635  			if i >= len(pool.poolEntities) {
   636  				require.True(t, slotAvailable)
   637  				require.NotNil(t, ejectedEntity)
   638  				// confirm that ejected entity is from list of entitiesToBeAdded
   639  				_, ok := uniqueEntities[ejectedEntity.ID()]
   640  				require.True(t, ok)
   641  			}
   642  		}
   643  
   644  		if ejectionMode == NoEjection {
   645  			if i >= len(pool.poolEntities) {
   646  				require.False(t, slotAvailable)
   647  				require.Nil(t, ejectedEntity)
   648  				require.Equal(t, entityIndex, InvalidIndex)
   649  
   650  				// when pool is full and with NoEjection, the head must keep pointing to the first added element.
   651  				headEntity, headExists := pool.Head()
   652  				require.True(t, headExists)
   653  				require.Equal(t, headEntity.ID(), entitiesToBeAdded[0].ID())
   654  			}
   655  		}
   656  
   657  		// underlying linked-lists sanity check
   658  		// first insertion forward, head of used list should always point to first entity in the list.
   659  		usedHead, freeHead := pool.getHeads()
   660  		usedTail, freeTail := pool.getTails()
   661  
   662  		if ejectionMode == LRUEjection {
   663  			expectedUsedHead := 0
   664  			if i >= len(pool.poolEntities) {
   665  				// we are beyond limit, so LRU ejection must happen and used head must
   666  				// be moved.
   667  				expectedUsedHead = (i + 1) % len(pool.poolEntities)
   668  			}
   669  			require.Equal(t, pool.poolEntities[expectedUsedHead].entity, usedHead.entity)
   670  			// head must be healthy and point back to undefined.
   671  			require.Equal(t, usedHead.node.prev, InvalidIndex)
   672  		}
   673  
   674  		if ejectionMode != NoEjection || i < len(pool.poolEntities) {
   675  			// new entity must be successfully added to tail of used linked-list
   676  			require.Equal(t, entitiesToBeAdded[i], usedTail.entity)
   677  			// used tail must be healthy and point back to undefined.
   678  			require.Equal(t, usedTail.node.next, InvalidIndex)
   679  		}
   680  
   681  		if ejectionMode == NoEjection && i >= len(pool.poolEntities) {
   682  			// used tail must not move
   683  			require.Equal(t, entitiesToBeAdded[len(pool.poolEntities)-1], usedTail.entity)
   684  			// used tail must be healthy and point back to undefined.
   685  			// This is not needed anymore as tail's next is now ignored
   686  			require.Equal(t, usedTail.node.next, InvalidIndex)
   687  		}
   688  
   689  		// free head
   690  		if i < len(pool.poolEntities)-1 {
   691  			// as long as we are below limit, after adding i element, free head
   692  			// should move to i+1 element.
   693  			require.Equal(t, EIndex(i+1), pool.states[stateFree].head)
   694  			// head must be healthy and point back to undefined.
   695  			require.Equal(t, freeHead.node.prev, InvalidIndex)
   696  		} else {
   697  			// once we go beyond limit,
   698  			// we run out of free slots,
   699  			// and free head must be kept at undefined.
   700  			require.Nil(t, freeHead)
   701  		}
   702  
   703  		// free tail
   704  		if i < len(pool.poolEntities)-1 {
   705  			// as long as we are below limit, after adding i element, free tail
   706  			// must keep pointing to last index of the array-based linked-list. In other
   707  			// words, adding element must not change free tail (since only free head is
   708  			// updated).
   709  			require.Equal(t, EIndex(len(pool.poolEntities)-1), pool.states[stateFree].tail)
   710  			// head tail be healthy and point next to undefined.
   711  			require.Equal(t, freeTail.node.next, InvalidIndex)
   712  		} else {
   713  			// once we go beyond limit, we run out of free slots, and
   714  			// free tail must be kept at undefined.
   715  			require.Nil(t, freeTail)
   716  		}
   717  
   718  		// used linked-list
   719  		// if we are still below limit, head to tail of used linked-list
   720  		// must be reachable within i + 1 steps.
   721  		// +1 is since we start from index 0 not 1.
   722  		usedTraverseStep := uint32(i + 1)
   723  		if i >= len(pool.poolEntities) {
   724  			// if we are above the limit, head to tail of used linked-list
   725  			// must be reachable within as many steps as the actual capacity of pool.
   726  			usedTraverseStep = uint32(len(pool.poolEntities))
   727  		}
   728  		tailAccessibleFromHead(t,
   729  			pool.states[stateUsed].head,
   730  			pool.states[stateUsed].tail,
   731  			pool,
   732  			usedTraverseStep)
   733  		headAccessibleFromTail(t,
   734  			pool.states[stateUsed].head,
   735  			pool.states[stateUsed].tail,
   736  			pool,
   737  			usedTraverseStep)
   738  
   739  		// free linked-list
   740  		// if we are still below limit, head to tail of used linked-list
   741  		// must be reachable within "limit - i - 1" steps. "limit - i" part is since
   742  		// when we have i elements in pool, we have "limit - i" free slots, and -1 is
   743  		// since we start from index 0 not 1.
   744  		freeTraverseStep := uint32(len(pool.poolEntities) - i - 1)
   745  		if i >= len(pool.poolEntities) {
   746  			// if we are above the limit, head and tail of free linked-list must be reachable
   747  			// within 0 steps.
   748  			// The reason is linked-list is full and adding new elements is done
   749  			// by ejecting existing ones, remaining no free slot.
   750  			freeTraverseStep = uint32(0)
   751  		}
   752  		tailAccessibleFromHead(t,
   753  			pool.states[stateFree].head,
   754  			pool.states[stateFree].tail,
   755  			pool,
   756  			freeTraverseStep)
   757  		headAccessibleFromTail(t,
   758  			pool.states[stateFree].head,
   759  			pool.states[stateFree].tail,
   760  			pool,
   761  			freeTraverseStep)
   762  
   763  		checkEachEntityIsInFreeOrUsedState(t, pool)
   764  	}
   765  }
   766  
   767  // testRetrievingEntitiesFrom evaluates that all entities starting from given index are retrievable from pool.
   768  func testRetrievingEntitiesFrom(t *testing.T, pool *Pool, entities []*unittest.MockEntity, from EIndex) {
   769  	testRetrievingEntitiesInRange(t, pool, entities, from, EIndex(len(entities)))
   770  }
   771  
   772  // testRetrievingEntitiesInRange evaluates that all entities in the given range are retrievable from pool.
   773  func testRetrievingEntitiesInRange(t *testing.T, pool *Pool, entities []*unittest.MockEntity, from EIndex, to EIndex) {
   774  	for i := from; i < to; i++ {
   775  		actualID, actual, _ := pool.Get(i % EIndex(len(pool.poolEntities)))
   776  		require.Equal(t, entities[i].ID(), actualID, i)
   777  		require.Equal(t, entities[i], actual, i)
   778  	}
   779  }
   780  
   781  // testRetrievingCount evaluates that exactly expected number of entities are retrievable from underlying pool.
   782  func testRetrievingCount(t *testing.T, pool *Pool, entities []*unittest.MockEntity, expected int) {
   783  	actualRetrievable := 0
   784  
   785  	for i := EIndex(0); i < EIndex(len(entities)); i++ {
   786  		for j := EIndex(0); j < EIndex(len(pool.poolEntities)); j++ {
   787  			actualID, actual, _ := pool.Get(j % EIndex(len(pool.poolEntities)))
   788  			if entities[i].ID() == actualID && entities[i] == actual {
   789  				actualRetrievable++
   790  			}
   791  		}
   792  	}
   793  
   794  	require.Equal(t, expected, actualRetrievable)
   795  }
   796  
   797  // withTestScenario creates a new pool, and then runs helpers on it sequentially.
   798  func withTestScenario(t *testing.T,
   799  	limit uint32,
   800  	entityCount uint32,
   801  	ejectionMode EjectionMode,
   802  	helpers ...func(*testing.T, *Pool, []*unittest.MockEntity)) {
   803  
   804  	pool := NewHeroPool(limit, ejectionMode, unittest.Logger())
   805  
   806  	// head on underlying linked-list value should be uninitialized
   807  	require.True(t, pool.states[stateUsed].size == 0)
   808  	require.Equal(t, pool.Size(), uint32(0))
   809  
   810  	entities := unittest.EntityListFixture(uint(entityCount))
   811  
   812  	for _, helper := range helpers {
   813  		helper(t, pool, entities)
   814  	}
   815  }
   816  
   817  // tailAccessibleFromHead checks tail of given entities linked-list is reachable from its head by traversing expected number of steps.
   818  func tailAccessibleFromHead(t *testing.T, headSliceIndex EIndex, tailSliceIndex EIndex, pool *Pool, steps uint32) {
   819  	seen := make(map[EIndex]struct{})
   820  
   821  	index := headSliceIndex
   822  	for i := uint32(0); i < steps; i++ {
   823  		if i == steps-1 {
   824  			require.Equal(t, tailSliceIndex, index, "tail not reachable after steps steps")
   825  			return
   826  		}
   827  
   828  		require.NotEqual(t, tailSliceIndex, index, "tail visited in less expected steps (potential inconsistency)", i, steps)
   829  		_, ok := seen[index]
   830  		require.False(t, ok, "duplicate identifiers found")
   831  
   832  		require.NotEqual(t, pool.poolEntities[index].node.next, InvalidIndex, "tail not found, and reached end of list")
   833  		index = pool.poolEntities[index].node.next
   834  	}
   835  }
   836  
   837  // headAccessibleFromTail checks head of given entities linked list is reachable from its tail by traversing expected number of steps.
   838  func headAccessibleFromTail(t *testing.T, headSliceIndex EIndex, tailSliceIndex EIndex, pool *Pool, total uint32) {
   839  	seen := make(map[EIndex]struct{})
   840  
   841  	index := tailSliceIndex
   842  	for i := uint32(0); i < total; i++ {
   843  		if i == total-1 {
   844  			require.Equal(t, headSliceIndex, index, "head not reachable after total steps")
   845  			return
   846  		}
   847  
   848  		require.NotEqual(t, headSliceIndex, index, "head visited in less expected steps (potential inconsistency)", i, total)
   849  		_, ok := seen[index]
   850  		require.False(t, ok, "duplicate identifiers found")
   851  
   852  		index = pool.poolEntities[index].node.prev
   853  	}
   854  }
   855  
   856  // checkEachEntityIsInFreeOrUsedState checks if each entity in the pool belongs exactly to one of the state lists.
   857  func checkEachEntityIsInFreeOrUsedState(t *testing.T, pool *Pool) {
   858  	pool_capacity := len(pool.poolEntities)
   859  	// check size
   860  	require.Equal(t, int(pool.states[stateFree].size+pool.states[stateUsed].size), pool_capacity, "Pool capacity is not equal to the sum of used and free sizes")
   861  	// check elelments
   862  	nodesInFree := discoverEntitiesBelongingToStateList(t, pool, stateFree)
   863  	nodesInUsed := discoverEntitiesBelongingToStateList(t, pool, stateUsed)
   864  	for i := 0; i < pool_capacity; i++ {
   865  		require.False(t, !nodesInFree[i] && !nodesInUsed[i], "Node is not in any state list")
   866  		require.False(t, nodesInFree[i] && nodesInUsed[i], "Node is in two state lists at the same time")
   867  	}
   868  }
   869  
   870  // discoverEntitiesBelongingToStateList discovers all entities in the pool that belong to the given list.
   871  func discoverEntitiesBelongingToStateList(t *testing.T, pool *Pool, stateType StateIndex) []bool {
   872  	result := make([]bool, len(pool.poolEntities))
   873  	for node_index := pool.states[stateType].head; node_index != InvalidIndex; {
   874  		require.False(t, result[node_index], "A node is present two times in the same state list")
   875  		result[node_index] = true
   876  		node_index = pool.poolEntities[node_index].node.next
   877  	}
   878  	return result
   879  }