github.com/koko1123/flow-go-1@v0.29.6/module/mempool/herocache/backdata/heropool/pool_test.go (about)

     1  package heropool
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/stretchr/testify/require"
     8  
     9  	"github.com/koko1123/flow-go-1/utils/unittest"
    10  )
    11  
    12  // TestStoreAndRetrieval_BelowLimit checks health of heroPool for storing and retrieval scenarios that
    13  // do not involve ejection.
    14  // The test involves cases for testing the pool below its limit, and also up to its limit. However, it never gets beyond
    15  // the limit, so no ejection will kick-in.
    16  func TestStoreAndRetrieval_BelowLimit(t *testing.T) {
    17  	for _, tc := range []struct {
    18  		limit       uint32 // capacity of entity list
    19  		entityCount uint32 // total entities to be stored
    20  	}{
    21  		{
    22  			limit:       30,
    23  			entityCount: 10,
    24  		},
    25  		{
    26  			limit:       30,
    27  			entityCount: 30,
    28  		},
    29  		{
    30  			limit:       2000,
    31  			entityCount: 1000,
    32  		},
    33  		{
    34  			limit:       1000,
    35  			entityCount: 1000,
    36  		},
    37  	} {
    38  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
    39  			withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
    40  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    41  					testInitialization(t, pool, entities)
    42  				},
    43  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    44  					testAddingEntities(t, pool, entities, LRUEjection)
    45  				},
    46  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    47  					testRetrievingEntitiesFrom(t, pool, entities, 0)
    48  				},
    49  			}...,
    50  			)
    51  		})
    52  	}
    53  }
    54  
    55  // TestStoreAndRetrieval_With_No_Ejection checks health of heroPool for storing and retrieval scenarios that involves the NoEjection mode.
    56  func TestStoreAndRetrieval_With_No_Ejection(t *testing.T) {
    57  	for _, tc := range []struct {
    58  		limit       uint32 // capacity of pool
    59  		entityCount uint32 // total entities to be stored
    60  	}{
    61  		{
    62  			limit:       30,
    63  			entityCount: 31,
    64  		},
    65  		{
    66  			limit:       30,
    67  			entityCount: 100,
    68  		},
    69  		{
    70  			limit:       1000,
    71  			entityCount: 2000,
    72  		},
    73  	} {
    74  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
    75  			withTestScenario(t, tc.limit, tc.entityCount, NoEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
    76  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    77  					testAddingEntities(t, pool, entities, NoEjection)
    78  				},
    79  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
    80  					// with the NoEjection mode, only the first "limit" entities must be retrievable.
    81  					testRetrievingEntitiesInRange(t, pool, entities, 0, EIndex(tc.limit))
    82  				},
    83  			}...,
    84  			)
    85  		})
    86  	}
    87  }
    88  
    89  // TestStoreAndRetrieval_With_LRU_Ejection checks health of heroPool for storing and retrieval scenarios that involves the LRU ejection.
    90  // The test involves cases for testing the pool beyond its limit, so the LRU ejection will kick-in.
    91  func TestStoreAndRetrieval_With_LRU_Ejection(t *testing.T) {
    92  	for _, tc := range []struct {
    93  		limit       uint32 // capacity of pool
    94  		entityCount uint32 // total entities to be stored
    95  	}{
    96  		{
    97  			limit:       30,
    98  			entityCount: 31,
    99  		},
   100  		{
   101  			limit:       30,
   102  			entityCount: 100,
   103  		},
   104  		{
   105  			limit:       1000,
   106  			entityCount: 2000,
   107  		},
   108  	} {
   109  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
   110  			withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
   111  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   112  					testAddingEntities(t, pool, entities, LRUEjection)
   113  				},
   114  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   115  					// with a limit of tc.limit, storing a total of tc.entityCount (> tc.limit) entities, results
   116  					// in ejection of the first tc.entityCount - tc.limit entities.
   117  					// Hence, we check retrieval of the last tc.limit entities, which start from index
   118  					// tc.entityCount - tc.limit entities.
   119  					testRetrievingEntitiesFrom(t, pool, entities, EIndex(tc.entityCount-tc.limit))
   120  				},
   121  			}...,
   122  			)
   123  		})
   124  	}
   125  }
   126  
   127  // TestStoreAndRetrieval_With_Random_Ejection checks health of heroPool for storing and retrieval scenarios that involves the LRU ejection.
   128  func TestStoreAndRetrieval_With_Random_Ejection(t *testing.T) {
   129  	for _, tc := range []struct {
   130  		limit       uint32 // capacity of pool
   131  		entityCount uint32 // total entities to be stored
   132  	}{
   133  		{
   134  			limit:       30,
   135  			entityCount: 31,
   136  		},
   137  		{
   138  			limit:       30,
   139  			entityCount: 100,
   140  		},
   141  	} {
   142  		t.Run(fmt.Sprintf("%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
   143  			withTestScenario(t, tc.limit, tc.entityCount, RandomEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
   144  				func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) {
   145  					testAddingEntities(t, backData, entities, RandomEjection)
   146  				},
   147  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   148  					// with a limit of tc.limit, storing a total of tc.entityCount (> tc.limit) entities, results
   149  					// in ejection of "tc.entityCount - tc.limit" entities at random.
   150  					// Hence, we check retrieval any successful total of "tc.limit" entities.
   151  					testRetrievingCount(t, pool, entities, int(tc.limit))
   152  				},
   153  			}...,
   154  			)
   155  		})
   156  	}
   157  }
   158  
   159  // TestInvalidateEntity checks the health of heroPool for invalidating entities under random, LRU, and LIFO scenarios.
   160  // Invalidating an entity removes it from the used state and moves its node to the free state.
   161  func TestInvalidateEntity(t *testing.T) {
   162  	for _, tc := range []struct {
   163  		limit       uint32 // capacity of entity pool
   164  		entityCount uint32 // total entities to be stored
   165  	}{
   166  		{
   167  			limit:       30,
   168  			entityCount: 0,
   169  		},
   170  		{
   171  			limit:       30,
   172  			entityCount: 1,
   173  		},
   174  		{
   175  			limit:       30,
   176  			entityCount: 10,
   177  		},
   178  		{
   179  			limit:       30,
   180  			entityCount: 30,
   181  		},
   182  		{
   183  			limit:       100,
   184  			entityCount: 10,
   185  		},
   186  		{
   187  			limit:       100,
   188  			entityCount: 100,
   189  		},
   190  	} {
   191  		// head invalidation test (LRU)
   192  		t.Run(fmt.Sprintf("head-invalidation-%d-limit-%d-entities", tc.limit, tc.entityCount), func(t *testing.T) {
   193  			withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
   194  				func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) {
   195  					testAddingEntities(t, backData, entities, LRUEjection)
   196  				},
   197  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   198  					testInvalidatingHead(t, pool, entities)
   199  				},
   200  			}...)
   201  		})
   202  
   203  		// tail invalidation test (LIFO)
   204  		t.Run(fmt.Sprintf("tail-invalidation-%d-limit-%d-entities-", tc.limit, tc.entityCount), func(t *testing.T) {
   205  			withTestScenario(t, tc.limit, tc.entityCount, LRUEjection, []func(*testing.T, *Pool, []*unittest.MockEntity){
   206  				func(t *testing.T, backData *Pool, entities []*unittest.MockEntity) {
   207  					testAddingEntities(t, backData, entities, LRUEjection)
   208  				},
   209  				func(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   210  					testInvalidatingTail(t, pool, entities)
   211  				},
   212  			}...)
   213  		})
   214  	}
   215  }
   216  
   217  // testInvalidatingHead keeps invalidating the head and evaluates the linked-list keeps updating its head
   218  // and remains connected.
   219  func testInvalidatingHead(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   220  	// total number of entities to store
   221  	totalEntitiesStored := len(entities)
   222  	// freeListInitialSize is total number of empty nodes after
   223  	// storing all items in the list
   224  	freeListInitialSize := len(pool.poolEntities) - totalEntitiesStored
   225  
   226  	// (i+1) keeps total invalidated (head) entities.
   227  	for i := 0; i < totalEntitiesStored; i++ {
   228  		headIndex := pool.invalidateUsedHead()
   229  		// head index should be moved to the next index after each head invalidation.
   230  		require.Equal(t, EIndex(i), headIndex)
   231  		// size of list should be decremented after each invalidation.
   232  		require.Equal(t, uint32(totalEntitiesStored-i-1), pool.Size())
   233  		// invalidated head should be appended to free entities
   234  		require.Equal(t, pool.free.tail.getSliceIndex(), headIndex)
   235  
   236  		if freeListInitialSize != 0 {
   237  			// number of entities is below limit, hence free list is not empty.
   238  			// invalidating used head must not change the free head.
   239  			require.Equal(t, EIndex(totalEntitiesStored), pool.free.head.getSliceIndex())
   240  		} else {
   241  			// number of entities is greater than or equal to limit, hence free list is empty.
   242  			// free head must be updated to the first invalidated head (index 0),
   243  			// and must be kept there for entire test (as we invalidate head not tail).
   244  			require.Equal(t, EIndex(0), pool.free.head.getSliceIndex())
   245  		}
   246  
   247  		// except when the list is empty, head must be updated after invalidation,
   248  		// except when the list is empty, head and tail must be accessible after each invalidation`
   249  		// i.e., the linked list remains connected despite invalidation.
   250  		if i != totalEntitiesStored-1 {
   251  			// used linked-list
   252  			tailAccessibleFromHead(t,
   253  				pool.used.head.getSliceIndex(),
   254  				pool.used.tail.getSliceIndex(),
   255  				pool,
   256  				pool.Size())
   257  
   258  			headAccessibleFromTail(t,
   259  				pool.used.head.getSliceIndex(),
   260  				pool.used.tail.getSliceIndex(),
   261  				pool,
   262  				pool.Size())
   263  
   264  			// free lined-list
   265  			//
   266  			// after invalidating each item, size of free linked-list is incremented by one.
   267  			tailAccessibleFromHead(t,
   268  				pool.free.head.getSliceIndex(),
   269  				pool.free.tail.getSliceIndex(),
   270  				pool,
   271  				uint32(i+1+freeListInitialSize))
   272  
   273  			headAccessibleFromTail(t,
   274  				pool.free.head.getSliceIndex(),
   275  				pool.free.tail.getSliceIndex(),
   276  				pool,
   277  				uint32(i+1+freeListInitialSize))
   278  		}
   279  
   280  		// checking the status of head and tail in used linked-list after each head invalidation.
   281  		usedTail, _ := pool.getTails()
   282  		usedHead, _ := pool.getHeads()
   283  		if i != totalEntitiesStored-1 {
   284  			// pool is not empty yet, we still have entities to invalidate.
   285  			//
   286  			// used tail should point to the last element in pool, since we are
   287  			// invalidating head.
   288  			require.Equal(t, entities[totalEntitiesStored-1].ID(), usedTail.id)
   289  			require.Equal(t, EIndex(totalEntitiesStored-1), pool.used.tail.getSliceIndex())
   290  
   291  			// used head must point to the next element in the pool,
   292  			// i.e., invalidating head moves it forward.
   293  			require.Equal(t, entities[i+1].ID(), usedHead.id)
   294  			require.Equal(t, EIndex(i+1), pool.used.head.getSliceIndex())
   295  		} else {
   296  			// pool is empty
   297  			// used head and tail must be nil and their corresponding
   298  			// pointer indices must be undefined.
   299  			require.Nil(t, usedHead)
   300  			require.Nil(t, usedTail)
   301  			require.True(t, pool.used.tail.isUndefined())
   302  			require.True(t, pool.used.head.isUndefined())
   303  		}
   304  	}
   305  }
   306  
   307  // testInvalidatingHead keeps invalidating the tail and evaluates the underlying free and used linked-lists keep updating its tail and remains connected.
   308  func testInvalidatingTail(t *testing.T, pool *Pool, entities []*unittest.MockEntity) {
   309  	size := len(entities)
   310  	offset := len(pool.poolEntities) - size
   311  	for i := 0; i < size; i++ {
   312  		// invalidates tail index
   313  		tailIndex := pool.used.tail.getSliceIndex()
   314  		require.Equal(t, EIndex(size-1-i), tailIndex)
   315  
   316  		pool.invalidateEntityAtIndex(tailIndex)
   317  		// old head index must be invalidated
   318  		require.True(t, pool.isInvalidated(tailIndex))
   319  		// unclaimed head should be appended to free entities
   320  		require.Equal(t, pool.free.tail.getSliceIndex(), tailIndex)
   321  
   322  		if offset != 0 {
   323  			// number of entities is below limit
   324  			// free must head keeps pointing to first empty index after
   325  			// adding all entities.
   326  			require.Equal(t, EIndex(size), pool.free.head.getSliceIndex())
   327  		} else {
   328  			// number of entities is greater than or equal to limit
   329  			// free head must be updated to last element in the pool (size - 1),
   330  			// and must be kept there for entire test (as we invalidate tail not head).
   331  			require.Equal(t, EIndex(size-1), pool.free.head.getSliceIndex())
   332  		}
   333  
   334  		// size of pool should be shrunk after each invalidation.
   335  		require.Equal(t, uint32(size-i-1), pool.Size())
   336  
   337  		// except when the pool is empty, tail must be updated after invalidation,
   338  		// and also head and tail must be accessible after each invalidation
   339  		// i.e., the linked-list remains connected despite invalidation.
   340  		if i != size-1 {
   341  
   342  			// used linked-list
   343  			tailAccessibleFromHead(t,
   344  				pool.used.head.getSliceIndex(),
   345  				pool.used.tail.getSliceIndex(),
   346  				pool,
   347  				pool.Size())
   348  
   349  			headAccessibleFromTail(t,
   350  				pool.used.head.getSliceIndex(),
   351  				pool.used.tail.getSliceIndex(),
   352  				pool,
   353  				pool.Size())
   354  
   355  			// free linked-list
   356  			tailAccessibleFromHead(t,
   357  				pool.free.head.getSliceIndex(),
   358  				pool.free.tail.getSliceIndex(),
   359  				pool,
   360  				uint32(i+1+offset))
   361  
   362  			headAccessibleFromTail(t,
   363  				pool.free.head.getSliceIndex(),
   364  				pool.free.tail.getSliceIndex(),
   365  				pool,
   366  				uint32(i+1+offset))
   367  		}
   368  
   369  		usedTail, _ := pool.getTails()
   370  		usedHead, _ := pool.getHeads()
   371  		if i != size-1 {
   372  			// pool is not empty yet
   373  			//
   374  			// used tail should move backward after each invalidation
   375  			require.Equal(t, entities[size-i-2].ID(), usedTail.id)
   376  			require.Equal(t, EIndex(size-i-2), pool.used.tail.getSliceIndex())
   377  
   378  			// used head must point to the first element in the pool,
   379  			require.Equal(t, entities[0].ID(), usedHead.id)
   380  			require.Equal(t, EIndex(0), pool.used.head.getSliceIndex())
   381  		} else {
   382  			// pool is empty
   383  			// used head and tail must be nil and their corresponding
   384  			// pointer indices must be undefined.
   385  			require.Nil(t, usedHead)
   386  			require.Nil(t, usedTail)
   387  			require.True(t, pool.used.tail.isUndefined())
   388  			require.True(t, pool.used.head.isUndefined())
   389  		}
   390  	}
   391  }
   392  
   393  // testInitialization evaluates the state of an initialized pool before adding any element to it.
   394  func testInitialization(t *testing.T, pool *Pool, _ []*unittest.MockEntity) {
   395  	// head and tail of "used" linked-list must be undefined at initialization time, since we have no elements in the list.
   396  	require.True(t, pool.used.head.isUndefined())
   397  	require.True(t, pool.used.tail.isUndefined())
   398  
   399  	for i := 0; i < len(pool.poolEntities); i++ {
   400  		if i == 0 {
   401  			// head of "free" linked-list should point to index 0 of entities slice.
   402  			require.Equal(t, EIndex(i), pool.free.head.getSliceIndex())
   403  			// previous element of head must be undefined (linked-list head feature).
   404  			require.True(t, pool.poolEntities[i].node.prev.isUndefined())
   405  		}
   406  
   407  		if i != 0 {
   408  			// except head, any element should point back to its previous index in slice.
   409  			require.Equal(t, EIndex(i-1), pool.poolEntities[i].node.prev.getSliceIndex())
   410  		}
   411  
   412  		if i != len(pool.poolEntities)-1 {
   413  			// except tail, any element should point forward to its next index in slice.
   414  			require.Equal(t, EIndex(i+1), pool.poolEntities[i].node.next.getSliceIndex())
   415  		}
   416  
   417  		if i == len(pool.poolEntities)-1 {
   418  			// tail of "free" linked-list should point to the last index in entities slice.
   419  			require.Equal(t, EIndex(i), pool.free.tail.getSliceIndex())
   420  			// next element of tail must be undefined.
   421  			require.True(t, pool.poolEntities[i].node.next.isUndefined())
   422  		}
   423  	}
   424  }
   425  
   426  // testAddingEntities evaluates health of pool for storing new elements.
   427  func testAddingEntities(t *testing.T, pool *Pool, entitiesToBeAdded []*unittest.MockEntity, ejectionMode EjectionMode) {
   428  	// initially head must be empty
   429  	e, ok := pool.Head()
   430  	require.False(t, ok)
   431  	require.Nil(t, e)
   432  
   433  	// adding elements
   434  	for i, e := range entitiesToBeAdded {
   435  		// adding each element must be successful.
   436  		entityIndex, slotAvailable, ejectionHappened := pool.Add(e.ID(), e, uint64(i))
   437  
   438  		if i < len(pool.poolEntities) {
   439  			// in case of no over limit, size of entities linked list should be incremented by each addition.
   440  			require.Equal(t, pool.Size(), uint32(i+1))
   441  
   442  			require.True(t, slotAvailable)
   443  			require.False(t, ejectionHappened)
   444  			require.Equal(t, entityIndex, EIndex(i))
   445  
   446  			// in case pool is not full, the head should retrieve the first added entity.
   447  			headEntity, headExists := pool.Head()
   448  			require.True(t, headExists)
   449  			require.Equal(t, headEntity.ID(), entitiesToBeAdded[0].ID())
   450  		}
   451  
   452  		if ejectionMode == LRUEjection {
   453  			// under LRU ejection mode, new entity should be placed at index i in back data
   454  			_, entity, _ := pool.Get(EIndex(i % len(pool.poolEntities)))
   455  			require.Equal(t, e, entity)
   456  
   457  			if i >= len(pool.poolEntities) {
   458  				require.True(t, slotAvailable)
   459  				require.True(t, ejectionHappened)
   460  				// when pool is full and with LRU ejection, the head should move forward with each element added.
   461  				headEntity, headExists := pool.Head()
   462  				require.True(t, headExists)
   463  				require.Equal(t, headEntity.ID(), entitiesToBeAdded[i+1-len(pool.poolEntities)].ID())
   464  			}
   465  		}
   466  
   467  		if ejectionMode == RandomEjection {
   468  			if i >= len(pool.poolEntities) {
   469  				require.True(t, slotAvailable)
   470  				require.True(t, ejectionHappened)
   471  			}
   472  		}
   473  
   474  		if ejectionMode == NoEjection {
   475  			if i >= len(pool.poolEntities) {
   476  				require.False(t, slotAvailable)
   477  				require.False(t, ejectionHappened)
   478  				require.Equal(t, entityIndex, EIndex(0))
   479  
   480  				// when pool is full and with NoEjection, the head must keep pointing to the first added element.
   481  				headEntity, headExists := pool.Head()
   482  				require.True(t, headExists)
   483  				require.Equal(t, headEntity.ID(), entitiesToBeAdded[0].ID())
   484  			}
   485  		}
   486  
   487  		// underlying linked-lists sanity check
   488  		// first insertion forward, head of used list should always point to first entity in the list.
   489  		usedHead, freeHead := pool.getHeads()
   490  		usedTail, freeTail := pool.getTails()
   491  
   492  		if ejectionMode == LRUEjection {
   493  			expectedUsedHead := 0
   494  			if i >= len(pool.poolEntities) {
   495  				// we are beyond limit, so LRU ejection must happen and used head must
   496  				// be moved.
   497  				expectedUsedHead = (i + 1) % len(pool.poolEntities)
   498  			}
   499  			require.Equal(t, pool.poolEntities[expectedUsedHead].entity, usedHead.entity)
   500  			// head must be healthy and point back to undefined.
   501  			require.True(t, usedHead.node.prev.isUndefined())
   502  		}
   503  
   504  		if ejectionMode != NoEjection || i < len(pool.poolEntities) {
   505  			// new entity must be successfully added to tail of used linked-list
   506  			require.Equal(t, entitiesToBeAdded[i], usedTail.entity)
   507  			// used tail must be healthy and point back to undefined.
   508  			require.True(t, usedTail.node.next.isUndefined())
   509  		}
   510  
   511  		if ejectionMode == NoEjection && i >= len(pool.poolEntities) {
   512  			// used tail must not move
   513  			require.Equal(t, entitiesToBeAdded[len(pool.poolEntities)-1], usedTail.entity)
   514  			// used tail must be healthy and point back to undefined.
   515  			require.True(t, usedTail.node.next.isUndefined())
   516  		}
   517  
   518  		// free head
   519  		if i < len(pool.poolEntities)-1 {
   520  			// as long as we are below limit, after adding i element, free head
   521  			// should move to i+1 element.
   522  			require.Equal(t, EIndex(i+1), pool.free.head.getSliceIndex())
   523  			// head must be healthy and point back to undefined.
   524  			require.True(t, freeHead.node.prev.isUndefined())
   525  		} else {
   526  			// once we go beyond limit,
   527  			// we run out of free slots,
   528  			// and free head must be kept at undefined.
   529  			require.Nil(t, freeHead)
   530  		}
   531  
   532  		// free tail
   533  		if i < len(pool.poolEntities)-1 {
   534  			// as long as we are below limit, after adding i element, free tail
   535  			// must keep pointing to last index of the array-based linked-list. In other
   536  			// words, adding element must not change free tail (since only free head is
   537  			// updated).
   538  			require.Equal(t, EIndex(len(pool.poolEntities)-1), pool.free.tail.getSliceIndex())
   539  			// head tail be healthy and point next to undefined.
   540  			require.True(t, freeTail.node.next.isUndefined())
   541  		} else {
   542  			// once we go beyond limit, we run out of free slots, and
   543  			// free tail must be kept at undefined.
   544  			require.Nil(t, freeTail)
   545  		}
   546  
   547  		// used linked-list
   548  		// if we are still below limit, head to tail of used linked-list
   549  		// must be reachable within i + 1 steps.
   550  		// +1 is since we start from index 0 not 1.
   551  		usedTraverseStep := uint32(i + 1)
   552  		if i >= len(pool.poolEntities) {
   553  			// if we are above the limit, head to tail of used linked-list
   554  			// must be reachable within as many steps as the actual capacity of pool.
   555  			usedTraverseStep = uint32(len(pool.poolEntities))
   556  		}
   557  		tailAccessibleFromHead(t,
   558  			pool.used.head.getSliceIndex(),
   559  			pool.used.tail.getSliceIndex(),
   560  			pool,
   561  			usedTraverseStep)
   562  		headAccessibleFromTail(t,
   563  			pool.used.head.getSliceIndex(),
   564  			pool.used.tail.getSliceIndex(),
   565  			pool,
   566  			usedTraverseStep)
   567  
   568  		// free linked-list
   569  		// if we are still below limit, head to tail of used linked-list
   570  		// must be reachable within "limit - i - 1" steps. "limit - i" part is since
   571  		// when we have i elements in pool, we have "limit - i" free slots, and -1 is
   572  		// since we start from index 0 not 1.
   573  		freeTraverseStep := uint32(len(pool.poolEntities) - i - 1)
   574  		if i >= len(pool.poolEntities) {
   575  			// if we are above the limit, head and tail of free linked-list must be reachable
   576  			// within 0 steps.
   577  			// The reason is linked-list is full and adding new elements is done
   578  			// by ejecting existing ones, remaining no free slot.
   579  			freeTraverseStep = uint32(0)
   580  		}
   581  		tailAccessibleFromHead(t,
   582  			pool.free.head.getSliceIndex(),
   583  			pool.free.tail.getSliceIndex(),
   584  			pool,
   585  			freeTraverseStep)
   586  		headAccessibleFromTail(t,
   587  			pool.free.head.getSliceIndex(),
   588  			pool.free.tail.getSliceIndex(),
   589  			pool,
   590  			freeTraverseStep)
   591  	}
   592  }
   593  
   594  // testRetrievingEntitiesFrom evaluates that all entities starting from given index are retrievable from pool.
   595  func testRetrievingEntitiesFrom(t *testing.T, pool *Pool, entities []*unittest.MockEntity, from EIndex) {
   596  	testRetrievingEntitiesInRange(t, pool, entities, from, EIndex(len(entities)))
   597  }
   598  
   599  // testRetrievingEntitiesInRange evaluates that all entities in the given range are retrievable from pool.
   600  func testRetrievingEntitiesInRange(t *testing.T, pool *Pool, entities []*unittest.MockEntity, from EIndex, to EIndex) {
   601  	for i := from; i < to; i++ {
   602  		actualID, actual, _ := pool.Get(i % EIndex(len(pool.poolEntities)))
   603  		require.Equal(t, entities[i].ID(), actualID, i)
   604  		require.Equal(t, entities[i], actual, i)
   605  	}
   606  }
   607  
   608  // testRetrievingCount evaluates that exactly expected number of entities are retrievable from underlying pool.
   609  func testRetrievingCount(t *testing.T, pool *Pool, entities []*unittest.MockEntity, expected int) {
   610  	actualRetrievable := 0
   611  
   612  	for i := EIndex(0); i < EIndex(len(entities)); i++ {
   613  		for j := EIndex(0); j < EIndex(len(pool.poolEntities)); j++ {
   614  			actualID, actual, _ := pool.Get(j % EIndex(len(pool.poolEntities)))
   615  			if entities[i].ID() == actualID && entities[i] == actual {
   616  				actualRetrievable++
   617  			}
   618  		}
   619  	}
   620  
   621  	require.Equal(t, expected, actualRetrievable)
   622  }
   623  
   624  // withTestScenario creates a new pool, and then runs helpers on it sequentially.
   625  func withTestScenario(t *testing.T,
   626  	limit uint32,
   627  	entityCount uint32,
   628  	ejectionMode EjectionMode,
   629  	helpers ...func(*testing.T, *Pool, []*unittest.MockEntity)) {
   630  
   631  	pool := NewHeroPool(limit, ejectionMode)
   632  
   633  	// head on underlying linked-list value should be uninitialized
   634  	require.True(t, pool.used.head.isUndefined())
   635  	require.Equal(t, pool.Size(), uint32(0))
   636  
   637  	entities := unittest.EntityListFixture(uint(entityCount))
   638  
   639  	for _, helper := range helpers {
   640  		helper(t, pool, entities)
   641  	}
   642  }
   643  
   644  // tailAccessibleFromHead checks tail of given entities linked-list is reachable from its head by traversing expected number of steps.
   645  func tailAccessibleFromHead(t *testing.T, headSliceIndex EIndex, tailSliceIndex EIndex, pool *Pool, steps uint32) {
   646  	seen := make(map[EIndex]struct{})
   647  
   648  	index := headSliceIndex
   649  	for i := uint32(0); i < steps; i++ {
   650  		if i == steps-1 {
   651  			require.Equal(t, tailSliceIndex, index, "tail not reachable after steps steps")
   652  			return
   653  		}
   654  
   655  		require.NotEqual(t, tailSliceIndex, index, "tail visited in less expected steps (potential inconsistency)", i, steps)
   656  		_, ok := seen[index]
   657  		require.False(t, ok, "duplicate identifiers found")
   658  
   659  		require.False(t, pool.poolEntities[index].node.next.isUndefined(), "tail not found, and reached end of list")
   660  		index = pool.poolEntities[index].node.next.getSliceIndex()
   661  	}
   662  }
   663  
   664  // headAccessibleFromTail checks head of given entities linked list is reachable from its tail by traversing expected number of steps.
   665  func headAccessibleFromTail(t *testing.T, headSliceIndex EIndex, tailSliceIndex EIndex, pool *Pool, total uint32) {
   666  	seen := make(map[EIndex]struct{})
   667  
   668  	index := tailSliceIndex
   669  	for i := uint32(0); i < total; i++ {
   670  		if i == total-1 {
   671  			require.Equal(t, headSliceIndex, index, "head not reachable after total steps")
   672  			return
   673  		}
   674  
   675  		require.NotEqual(t, headSliceIndex, index, "head visited in less expected steps (potential inconsistency)", i, total)
   676  		_, ok := seen[index]
   677  		require.False(t, ok, "duplicate identifiers found")
   678  
   679  		index = pool.poolEntities[index].node.prev.getSliceIndex()
   680  	}
   681  }