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

     1  package herocache
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/onflow/flow-go/model/flow"
    12  	"github.com/onflow/flow-go/module/mempool/herocache/backdata/heropool"
    13  	"github.com/onflow/flow-go/module/metrics"
    14  	"github.com/onflow/flow-go/utils/unittest"
    15  )
    16  
    17  // TestArrayBackData_SingleBucket evaluates health of state transition for storing 10 entities in a Cache with only
    18  // a single bucket (of 16). It also evaluates all stored items are retrievable.
    19  func TestArrayBackData_SingleBucket(t *testing.T) {
    20  	limit := 16
    21  
    22  	bd := NewCache(uint32(limit),
    23  		1,
    24  		heropool.LRUEjection,
    25  		unittest.Logger(),
    26  		metrics.NewNoopCollector())
    27  
    28  	entities := unittest.EntityListFixture(uint(limit))
    29  
    30  	// adds all entities to backdata
    31  	testAddEntities(t, bd, entities, heropool.LRUEjection)
    32  
    33  	// sanity checks
    34  	for i := heropool.EIndex(0); i < heropool.EIndex(len(entities)); i++ {
    35  		// since we are below limit, elements should be added sequentially at bucket 0.
    36  		// the ith added element has a key index of i+1,
    37  		// since 0 means unused key index in implementation.
    38  		require.Equal(t, bd.buckets[0].slots[i].slotAge, uint64(i+1))
    39  		// also, since we have not yet over-limited,
    40  		// entities are assigned their entityIndex in the same order they are added.
    41  		require.Equal(t, bd.buckets[0].slots[i].entityIndex, i)
    42  		_, _, owner := bd.entities.Get(i)
    43  		require.Equal(t, owner, uint64(i))
    44  	}
    45  
    46  	// all stored items must be retrievable
    47  	testRetrievableFrom(t, bd, entities, 0)
    48  }
    49  
    50  // TestArrayBackData_Adjust evaluates that Adjust method correctly updates the value of
    51  // the desired entity while preserving the integrity of BackData.
    52  func TestArrayBackData_Adjust(t *testing.T) {
    53  	limit := 100_000
    54  
    55  	bd := NewCache(uint32(limit),
    56  		8,
    57  		heropool.LRUEjection,
    58  		unittest.Logger(),
    59  		metrics.NewNoopCollector())
    60  
    61  	entities := unittest.EntityListFixture(uint(limit))
    62  
    63  	// adds all entities to backdata
    64  	testAddEntities(t, bd, entities, heropool.LRUEjection)
    65  
    66  	// picks a random entity from BackData and adjusts its identifier to a new one.
    67  	entityIndex := rand.Int() % limit
    68  	// checking integrity of retrieving entity
    69  	oldEntity, ok := bd.ByID(entities[entityIndex].ID())
    70  	require.True(t, ok)
    71  	oldEntityID := oldEntity.ID()
    72  	require.Equal(t, entities[entityIndex].ID(), oldEntityID)
    73  	require.Equal(t, entities[entityIndex], oldEntity)
    74  
    75  	// picks a new identifier for the entity and makes sure it is different than its current one.
    76  	newEntityID := unittest.IdentifierFixture()
    77  	require.NotEqual(t, oldEntityID, newEntityID)
    78  
    79  	// adjusts old entity to a new entity with a new identifier
    80  	newEntity, ok := bd.Adjust(oldEntity.ID(), func(entity flow.Entity) flow.Entity {
    81  		mockEntity, ok := entity.(*unittest.MockEntity)
    82  		require.True(t, ok)
    83  		// oldEntity must be passed to func parameter of adjust.
    84  		require.Equal(t, oldEntityID, mockEntity.ID())
    85  		require.Equal(t, oldEntity, mockEntity)
    86  
    87  		return &unittest.MockEntity{Identifier: newEntityID}
    88  	})
    89  
    90  	// adjustment must be successful, and identifier must be updated.
    91  	require.True(t, ok)
    92  	require.Equal(t, newEntityID, newEntity.ID())
    93  	newMockEntity, ok := newEntity.(*unittest.MockEntity)
    94  	require.True(t, ok)
    95  
    96  	// replaces new entity in the original reference list and
    97  	// retrieves all.
    98  	entities[entityIndex] = newMockEntity
    99  	testRetrievableFrom(t, bd, entities, 0)
   100  
   101  	// re-adjusting old entity must fail, since its identifier must no longer exist
   102  	entity, ok := bd.Adjust(oldEntityID, func(entity flow.Entity) flow.Entity {
   103  		require.Fail(t, "function must not be invoked on a non-existing entity")
   104  		return entity
   105  	})
   106  	require.False(t, ok)
   107  	require.Nil(t, entity)
   108  
   109  	// similarly, retrieving old entity must fail
   110  	entity, ok = bd.ByID(oldEntityID)
   111  	require.False(t, ok)
   112  	require.Nil(t, entity)
   113  
   114  	ok = bd.Has(oldEntityID)
   115  	require.False(t, ok)
   116  
   117  	// adjusting any random non-existing identifier must fail
   118  	entity, ok = bd.Adjust(unittest.IdentifierFixture(), func(entity flow.Entity) flow.Entity {
   119  		require.Fail(t, "function must not be invoked on a non-existing entity")
   120  		return entity
   121  	})
   122  	require.False(t, ok)
   123  	require.Nil(t, entity)
   124  
   125  	// adjustment must be idempotent for size
   126  	require.Equal(t, bd.Size(), uint(limit))
   127  }
   128  
   129  // TestArrayBackData_AdjustWitInit evaluates that AdjustWithInit method. It should initialize and then adjust the value of
   130  // non-existing entity while preserving the integrity of BackData on just adjusting the value of existing entity.
   131  func TestArrayBackData_AdjustWitInit(t *testing.T) {
   132  	limit := 100_000
   133  
   134  	bd := NewCache(uint32(limit),
   135  		8,
   136  		heropool.LRUEjection,
   137  		unittest.Logger(),
   138  		metrics.NewNoopCollector())
   139  
   140  	entities := unittest.EntityListFixture(uint(limit))
   141  	for _, e := range entities {
   142  		adjustedEntity, adjusted := bd.AdjustWithInit(e.ID(), func(entity flow.Entity) flow.Entity {
   143  			// adjust logic, increments the nonce of the entity
   144  			mockEntity, ok := entity.(*unittest.MockEntity)
   145  			require.True(t, ok)
   146  			mockEntity.Nonce++
   147  			return mockEntity
   148  		}, func() flow.Entity {
   149  			return e // initialize with the entity
   150  		})
   151  		require.True(t, adjusted)
   152  		require.Equal(t, e.ID(), adjustedEntity.ID())
   153  		require.Equal(t, uint64(1), adjustedEntity.(*unittest.MockEntity).Nonce)
   154  	}
   155  
   156  	// picks a random entity from BackData and adjusts its identifier to a new one.
   157  	entityIndex := rand.Int() % limit
   158  	// checking integrity of retrieving entity
   159  	oldEntity, ok := bd.ByID(entities[entityIndex].ID())
   160  	require.True(t, ok)
   161  	oldEntityID := oldEntity.ID()
   162  	require.Equal(t, entities[entityIndex].ID(), oldEntityID)
   163  	require.Equal(t, entities[entityIndex], oldEntity)
   164  
   165  	// picks a new identifier for the entity and makes sure it is different than its current one.
   166  	newEntityID := unittest.IdentifierFixture()
   167  	require.NotEqual(t, oldEntityID, newEntityID)
   168  
   169  	// adjusts old entity to a new entity with a new identifier
   170  	newEntity, ok := bd.Adjust(oldEntity.ID(), func(entity flow.Entity) flow.Entity {
   171  		mockEntity, ok := entity.(*unittest.MockEntity)
   172  		require.True(t, ok)
   173  		// oldEntity must be passed to func parameter of adjust.
   174  		require.Equal(t, oldEntityID, mockEntity.ID())
   175  		require.Equal(t, oldEntity, mockEntity)
   176  
   177  		// adjust logic, adjsuts the nonce of the entity
   178  		return &unittest.MockEntity{Identifier: newEntityID, Nonce: 2}
   179  	})
   180  
   181  	// adjustment must be successful, and identifier must be updated.
   182  	require.True(t, ok)
   183  	require.Equal(t, newEntityID, newEntity.ID())
   184  	require.Equal(t, uint64(2), newEntity.(*unittest.MockEntity).Nonce)
   185  	newMockEntity, ok := newEntity.(*unittest.MockEntity)
   186  	require.True(t, ok)
   187  
   188  	// replaces new entity in the original reference list and
   189  	// retrieves all.
   190  	entities[entityIndex] = newMockEntity
   191  	testRetrievableFrom(t, bd, entities, 0)
   192  
   193  	// re-adjusting old entity must fail, since its identifier must no longer exist
   194  	entity, ok := bd.Adjust(oldEntityID, func(entity flow.Entity) flow.Entity {
   195  		require.Fail(t, "function must not be invoked on a non-existing entity")
   196  		return entity
   197  	})
   198  	require.False(t, ok)
   199  	require.Nil(t, entity)
   200  
   201  	// similarly, retrieving old entity must fail
   202  	entity, ok = bd.ByID(oldEntityID)
   203  	require.False(t, ok)
   204  	require.Nil(t, entity)
   205  
   206  	ok = bd.Has(oldEntityID)
   207  	require.False(t, ok)
   208  }
   209  
   210  // TestArrayBackData_GetWithInit evaluates that GetWithInit method. It should initialize and then retrieve the value of
   211  // non-existing entity while preserving the integrity of BackData on just retrieving the value of existing entity.
   212  func TestArrayBackData_GetWithInit(t *testing.T) {
   213  	limit := 1000
   214  
   215  	bd := NewCache(uint32(limit), 8, heropool.LRUEjection, unittest.Logger(), metrics.NewNoopCollector())
   216  
   217  	entities := unittest.EntityListFixture(uint(limit))
   218  
   219  	// GetWithInit
   220  	for _, e := range entities {
   221  		// all entities must be initialized retrieved successfully
   222  		actual, ok := bd.GetWithInit(e.ID(), func() flow.Entity {
   223  			return e // initialize with the entity
   224  		})
   225  		require.True(t, ok)
   226  		require.Equal(t, e, actual)
   227  	}
   228  
   229  	// All
   230  	all := bd.All()
   231  	require.Equal(t, len(entities), len(all))
   232  	for _, expected := range entities {
   233  		actual, ok := bd.ByID(expected.ID())
   234  		require.True(t, ok)
   235  		require.Equal(t, expected, actual)
   236  	}
   237  
   238  	// Identifiers
   239  	ids := bd.Identifiers()
   240  	require.Equal(t, len(entities), len(ids))
   241  	for _, id := range ids {
   242  		require.True(t, bd.Has(id))
   243  	}
   244  
   245  	// Entities
   246  	actualEntities := bd.Entities()
   247  	require.Equal(t, len(entities), len(actualEntities))
   248  	require.ElementsMatch(t, entities, actualEntities)
   249  
   250  	// Adjust
   251  	for _, e := range entities {
   252  		// all entities must be adjusted successfully
   253  		actual, ok := bd.Adjust(e.ID(), func(entity flow.Entity) flow.Entity {
   254  			// increment nonce of the entity
   255  			entity.(*unittest.MockEntity).Nonce++
   256  			return entity
   257  		})
   258  		require.True(t, ok)
   259  		require.Equal(t, e, actual)
   260  	}
   261  
   262  	// ByID; should return the latest version of the entity
   263  	for _, e := range entities {
   264  		// all entities must be retrieved successfully
   265  		actual, ok := bd.ByID(e.ID())
   266  		require.True(t, ok)
   267  		require.Equal(t, e.ID(), actual.ID())
   268  		require.Equal(t, uint64(1), actual.(*unittest.MockEntity).Nonce)
   269  	}
   270  
   271  	// GetWithInit; should return the latest version of the entity, than increment the nonce
   272  	for _, e := range entities {
   273  		// all entities must be retrieved successfully
   274  		actual, ok := bd.GetWithInit(e.ID(), func() flow.Entity {
   275  			require.Fail(t, "should not be called") // entity has already been initialized
   276  			return e
   277  		})
   278  		require.True(t, ok)
   279  		require.Equal(t, e.ID(), actual.ID())
   280  	}
   281  }
   282  
   283  // TestArrayBackData_WriteHeavy evaluates correctness of Cache under the writing and retrieving
   284  // a heavy load of entities up to its limit. All data must be written successfully and then retrievable.
   285  func TestArrayBackData_WriteHeavy(t *testing.T) {
   286  	limit := 100_000
   287  
   288  	bd := NewCache(uint32(limit),
   289  		8,
   290  		heropool.LRUEjection,
   291  		unittest.Logger(),
   292  		metrics.NewNoopCollector())
   293  
   294  	entities := unittest.EntityListFixture(uint(limit))
   295  
   296  	// adds all entities to backdata
   297  	testAddEntities(t, bd, entities, heropool.LRUEjection)
   298  
   299  	// retrieves all entities from backdata
   300  	testRetrievableFrom(t, bd, entities, 0)
   301  }
   302  
   303  // TestArrayBackData_LRU_Ejection evaluates correctness of Cache under the writing and retrieving
   304  // a heavy load of entities beyond its limit. With LRU ejection, only most recently written data must be maintained
   305  // by mempool.
   306  func TestArrayBackData_LRU_Ejection(t *testing.T) {
   307  	// mempool has the limit of 100K, but we put 1M
   308  	// (10 time more than its capacity)
   309  	limit := 100_000
   310  	items := uint(1_000_000)
   311  
   312  	bd := NewCache(uint32(limit),
   313  		8,
   314  		heropool.LRUEjection,
   315  		unittest.Logger(),
   316  		metrics.NewNoopCollector())
   317  
   318  	entities := unittest.EntityListFixture(items)
   319  
   320  	// adds all entities to backdata
   321  	testAddEntities(t, bd, entities, heropool.LRUEjection)
   322  
   323  	// only last 100K (i.e., 900Kth forward) items must be retrievable, and
   324  	// the rest must be ejected.
   325  	testRetrievableFrom(t, bd, entities, 900_000)
   326  }
   327  
   328  // TestArrayBackData_No_Ejection evaluates correctness of Cache under the writing and retrieving
   329  // a heavy load of entities beyond its limit. With NoEjection mode, the cache should refuse to add extra entities beyond
   330  // its limit.
   331  func TestArrayBackData_No_Ejection(t *testing.T) {
   332  	// mempool has the limit of 100K, but we put 1M
   333  	// (10 time more than its capacity)
   334  	limit := 100_000
   335  	items := uint(1_000_000)
   336  
   337  	bd := NewCache(uint32(limit),
   338  		8,
   339  		heropool.NoEjection,
   340  		unittest.Logger(),
   341  		metrics.NewNoopCollector())
   342  
   343  	entities := unittest.EntityListFixture(items)
   344  
   345  	// adds all entities to backdata
   346  	testAddEntities(t, bd, entities, heropool.NoEjection)
   347  
   348  	// only last 100K (i.e., 900Kth forward) items must be retrievable, and
   349  	// the rest must be ejected.
   350  	testRetrievableInRange(t, bd, entities, 0, limit)
   351  }
   352  
   353  // TestArrayBackData_Random_Ejection evaluates correctness of Cache under the writing and retrieving
   354  // a heavy load of entities beyond its limit. With random ejection, only as many entities as capacity of
   355  // Cache must be retrievable.
   356  func TestArrayBackData_Random_Ejection(t *testing.T) {
   357  	// mempool has the limit of 100K, but we put 1M
   358  	// (10 time more than its capacity)
   359  	limit := 100_000
   360  	items := uint(1_000_000)
   361  
   362  	bd := NewCache(uint32(limit),
   363  		8,
   364  		heropool.RandomEjection,
   365  		unittest.Logger(),
   366  		metrics.NewNoopCollector())
   367  
   368  	entities := unittest.EntityListFixture(items)
   369  
   370  	// adds all entities to backdata
   371  	testAddEntities(t, bd, entities, heropool.RandomEjection)
   372  
   373  	// only 100K (random) items must be retrievable, as the rest
   374  	// are randomly ejected to make room.
   375  	testRetrievableCount(t, bd, entities, 100_000)
   376  }
   377  
   378  // TestArrayBackData_AddDuplicate evaluates that adding duplicate entity to Cache will fail without
   379  // altering the internal state of it.
   380  func TestArrayBackData_AddDuplicate(t *testing.T) {
   381  	limit := 100
   382  
   383  	bd := NewCache(uint32(limit),
   384  		8,
   385  		heropool.LRUEjection,
   386  		unittest.Logger(),
   387  		metrics.NewNoopCollector())
   388  
   389  	entities := unittest.EntityListFixture(uint(limit))
   390  
   391  	// adds all entities to backdata
   392  	testAddEntities(t, bd, entities, heropool.LRUEjection)
   393  
   394  	// adding duplicate entity should fail
   395  	for _, entity := range entities {
   396  		require.False(t, bd.Add(entity.ID(), entity))
   397  	}
   398  
   399  	// still all entities must be retrievable from Cache.
   400  	testRetrievableFrom(t, bd, entities, 0)
   401  }
   402  
   403  // TestArrayBackData_Clear evaluates that calling Clear method removes all entities stored in BackData.
   404  func TestArrayBackData_Clear(t *testing.T) {
   405  	limit := 100
   406  
   407  	bd := NewCache(uint32(limit),
   408  		8,
   409  		heropool.LRUEjection,
   410  		unittest.Logger(),
   411  		metrics.NewNoopCollector())
   412  
   413  	entities := unittest.EntityListFixture(uint(limit))
   414  
   415  	// adds all entities to backdata
   416  	testAddEntities(t, bd, entities, heropool.LRUEjection)
   417  
   418  	// still all must be retrievable from backdata
   419  	testRetrievableFrom(t, bd, entities, 0)
   420  	require.Equal(t, bd.Size(), uint(limit))
   421  	require.Len(t, bd.All(), limit)
   422  
   423  	// calling clear must shrink size of BackData to zero
   424  	bd.Clear()
   425  	require.Equal(t, bd.Size(), uint(0))
   426  	require.Len(t, bd.All(), 0)
   427  
   428  	// none of stored elements must be retrievable any longer
   429  	testRetrievableCount(t, bd, entities, 0)
   430  }
   431  
   432  // TestArrayBackData_All checks correctness of All method in returning all stored entities in it.
   433  func TestArrayBackData_All(t *testing.T) {
   434  	tt := []struct {
   435  		limit        uint32
   436  		items        uint32
   437  		ejectionMode heropool.EjectionMode
   438  	}{
   439  		{ // mempool has the limit of 1000, but we put 100.
   440  			limit:        1000,
   441  			items:        100,
   442  			ejectionMode: heropool.LRUEjection,
   443  		},
   444  		{ // mempool has the limit of 1000, and we put exactly 1000 items.
   445  			limit:        1000,
   446  			items:        1000,
   447  			ejectionMode: heropool.LRUEjection,
   448  		},
   449  		{ // mempool has the limit of 1000, and we put 10K items with LRU ejection.
   450  			limit:        1000,
   451  			items:        10_000,
   452  			ejectionMode: heropool.LRUEjection,
   453  		},
   454  		{ // mempool has the limit of 1000, and we put 10K items with random ejection.
   455  			limit:        1000,
   456  			items:        10_000,
   457  			ejectionMode: heropool.RandomEjection,
   458  		},
   459  	}
   460  
   461  	for _, tc := range tt {
   462  		t.Run(fmt.Sprintf("%d-limit-%d-items-%s-ejection", tc.limit, tc.items, tc.ejectionMode), func(t *testing.T) {
   463  			bd := NewCache(tc.limit,
   464  				8,
   465  				tc.ejectionMode,
   466  				unittest.Logger(),
   467  				metrics.NewNoopCollector())
   468  			entities := unittest.EntityListFixture(uint(tc.items))
   469  
   470  			testAddEntities(t, bd, entities, tc.ejectionMode)
   471  
   472  			if tc.ejectionMode == heropool.RandomEjection {
   473  				// in random ejection mode we count total number of matched entities
   474  				// with All map.
   475  				testMapMatchCount(t, bd.All(), entities, int(tc.limit))
   476  				testEntitiesMatchCount(t, bd.Entities(), entities, int(tc.limit))
   477  				testIdentifiersMatchCount(t, bd.Identifiers(), entities, int(tc.limit))
   478  			} else {
   479  				// in LRU ejection mode we match All items based on a from index (i.e., last "from" items).
   480  				from := int(tc.items) - int(tc.limit)
   481  				if from < 0 {
   482  					// we are below limit, hence we start matching from index 0
   483  					from = 0
   484  				}
   485  				testMapMatchFrom(t, bd.All(), entities, from)
   486  				testEntitiesMatchFrom(t, bd.Entities(), entities, from)
   487  				testIdentifiersMatchFrom(t, bd.Identifiers(), entities, from)
   488  			}
   489  		})
   490  	}
   491  }
   492  
   493  // TestArrayBackData_Remove checks correctness of removing elements from Cache.
   494  func TestArrayBackData_Remove(t *testing.T) {
   495  	tt := []struct {
   496  		limit uint32
   497  		items uint32
   498  		from  int // index start to be removed (set -1 to remove randomly)
   499  		count int // total elements to be removed
   500  	}{
   501  		{ // removing range with total items below the limit
   502  			limit: 100_000,
   503  			items: 10_000,
   504  			from:  188,
   505  			count: 2012,
   506  		},
   507  		{ // removing range from full Cache
   508  			limit: 100_000,
   509  			items: 100_000,
   510  			from:  50_333,
   511  			count: 6667,
   512  		},
   513  		{ // removing random from Cache with total items below its limit
   514  			limit: 100_000,
   515  			items: 10_000,
   516  			from:  -1,
   517  			count: 6888,
   518  		},
   519  		{ // removing random from full Cache
   520  			limit: 100_000,
   521  			items: 10_000,
   522  			from:  -1,
   523  			count: 7328,
   524  		},
   525  	}
   526  
   527  	for _, tc := range tt {
   528  		t.Run(fmt.Sprintf("%d-limit-%d-items-%dfrom-%dcount", tc.limit, tc.items, tc.from, tc.count), func(t *testing.T) {
   529  			bd := NewCache(
   530  				tc.limit,
   531  				8,
   532  				heropool.RandomEjection,
   533  				unittest.Logger(),
   534  				metrics.NewNoopCollector())
   535  			entities := unittest.EntityListFixture(uint(tc.items))
   536  
   537  			testAddEntities(t, bd, entities, heropool.RandomEjection)
   538  
   539  			if tc.from == -1 {
   540  				// random removal
   541  				testRemoveAtRandom(t, bd, entities, tc.count)
   542  				// except removed ones, the rest must be retrievable
   543  				testRetrievableCount(t, bd, entities, uint64(int(tc.items)-tc.count))
   544  			} else {
   545  				// removing a range
   546  				testRemoveRange(t, bd, entities, tc.from, tc.from+tc.count)
   547  				testCheckRangeRemoved(t, bd, entities, tc.from, tc.from+tc.count)
   548  			}
   549  		})
   550  	}
   551  }
   552  
   553  // testAddEntities is a test helper that checks entities are added successfully to the Cache.
   554  // and each entity is retrievable right after it is written to backdata.
   555  func testAddEntities(t *testing.T, bd *Cache, entities []*unittest.MockEntity, ejection heropool.EjectionMode) {
   556  	// initially, head should be undefined
   557  	e, ok := bd.Head()
   558  	require.False(t, ok)
   559  	require.Nil(t, e)
   560  
   561  	// adding elements
   562  	for i, e := range entities {
   563  		if ejection == heropool.NoEjection && uint32(i) >= bd.sizeLimit {
   564  			// with no ejection when it goes beyond limit, the writes should be unsuccessful.
   565  			require.False(t, bd.Add(e.ID(), e))
   566  
   567  			// the head should retrieve the first added entity.
   568  			headEntity, headExists := bd.Head()
   569  			require.True(t, headExists)
   570  			require.Equal(t, headEntity.ID(), entities[0].ID())
   571  		} else {
   572  			// adding each element must be successful.
   573  			require.True(t, bd.Add(e.ID(), e))
   574  
   575  			if uint32(i) < bd.sizeLimit {
   576  				// when we are below limit the size of
   577  				// Cache should be incremented by each addition.
   578  				require.Equal(t, bd.Size(), uint(i+1))
   579  
   580  				// in case cache is not full, the head should retrieve the first added entity.
   581  				headEntity, headExists := bd.Head()
   582  				require.True(t, headExists)
   583  				require.Equal(t, headEntity.ID(), entities[0].ID())
   584  			} else {
   585  				// when we cross the limit, the ejection kicks in, and
   586  				// size must be steady at the limit.
   587  				require.Equal(t, uint32(bd.Size()), bd.sizeLimit)
   588  			}
   589  
   590  			// entity should be immediately retrievable
   591  			actual, ok := bd.ByID(e.ID())
   592  			require.True(t, ok)
   593  			require.Equal(t, e, actual)
   594  		}
   595  	}
   596  }
   597  
   598  // testRetrievableInRange is a test helper that evaluates that all entities starting from given index are retrievable from Cache.
   599  func testRetrievableFrom(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int) {
   600  	testRetrievableInRange(t, bd, entities, from, len(entities))
   601  }
   602  
   603  // testRetrievableInRange is a test helper that evaluates within given range [from, to) are retrievable from Cache.
   604  func testRetrievableInRange(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) {
   605  	for i := range entities {
   606  		expected := entities[i]
   607  		actual, ok := bd.ByID(expected.ID())
   608  		if i < from || i >= to {
   609  			require.False(t, ok, i)
   610  			require.Nil(t, actual)
   611  		} else {
   612  			require.True(t, ok)
   613  			require.Equal(t, expected, actual)
   614  		}
   615  	}
   616  }
   617  
   618  // testRemoveAtRandom is a test helper removes specified number of entities from Cache at random.
   619  func testRemoveAtRandom(t *testing.T, bd *Cache, entities []*unittest.MockEntity, count int) {
   620  	for removedCount := 0; removedCount < count; {
   621  		unittest.RequireReturnsBefore(t, func() {
   622  			index := rand.Int() % len(entities)
   623  			expected, removed := bd.Remove(entities[index].ID())
   624  			if !removed {
   625  				return
   626  			}
   627  			require.Equal(t, entities[index], expected)
   628  			removedCount++
   629  			// size sanity check after removal
   630  			require.Equal(t, bd.Size(), uint(len(entities)-removedCount))
   631  		}, 100*time.Millisecond, "could not find element to remove")
   632  	}
   633  }
   634  
   635  // testRemoveRange is a test helper that removes specified range of entities from Cache.
   636  func testRemoveRange(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) {
   637  	for i := from; i < to; i++ {
   638  		expected, removed := bd.Remove(entities[i].ID())
   639  		require.True(t, removed)
   640  		require.Equal(t, entities[i], expected)
   641  		// size sanity check after removal
   642  		require.Equal(t, bd.Size(), uint(len(entities)-(i-from)-1))
   643  	}
   644  }
   645  
   646  // testCheckRangeRemoved is a test helper that evaluates the specified range of entities have been removed from Cache.
   647  func testCheckRangeRemoved(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) {
   648  	for i := from; i < to; i++ {
   649  		// both removal and retrieval must fail
   650  		expected, removed := bd.Remove(entities[i].ID())
   651  		require.False(t, removed)
   652  		require.Nil(t, expected)
   653  
   654  		expected, exists := bd.ByID(entities[i].ID())
   655  		require.False(t, exists)
   656  		require.Nil(t, expected)
   657  	}
   658  }
   659  
   660  // testMapMatchFrom is a test helper that checks entities are retrievable from entitiesMap starting specified index.
   661  func testMapMatchFrom(t *testing.T, entitiesMap map[flow.Identifier]flow.Entity, entities []*unittest.MockEntity, from int) {
   662  	require.Len(t, entitiesMap, len(entities)-from)
   663  
   664  	for i := range entities {
   665  		expected := entities[i]
   666  		actual, ok := entitiesMap[expected.ID()]
   667  		if i < from {
   668  			require.False(t, ok, i)
   669  			require.Nil(t, actual)
   670  		} else {
   671  			require.True(t, ok)
   672  			require.Equal(t, expected, actual)
   673  		}
   674  	}
   675  }
   676  
   677  // testEntitiesMatchFrom is a test helper that checks entities are retrievable from given list starting specified index.
   678  func testEntitiesMatchFrom(t *testing.T, expectedEntities []flow.Entity, actualEntities []*unittest.MockEntity, from int) {
   679  	require.Len(t, expectedEntities, len(actualEntities)-from)
   680  
   681  	for i, actual := range actualEntities {
   682  		if i < from {
   683  			require.NotContains(t, expectedEntities, actual)
   684  		} else {
   685  			require.Contains(t, expectedEntities, actual)
   686  		}
   687  	}
   688  }
   689  
   690  // testIdentifiersMatchFrom is a test helper that checks identifiers of entities are retrievable from given list starting specified index.
   691  func testIdentifiersMatchFrom(t *testing.T, expectedIdentifiers flow.IdentifierList, actualEntities []*unittest.MockEntity, from int) {
   692  	require.Len(t, expectedIdentifiers, len(actualEntities)-from)
   693  
   694  	for i, actual := range actualEntities {
   695  		if i < from {
   696  			require.NotContains(t, expectedIdentifiers, actual.ID())
   697  		} else {
   698  			require.Contains(t, expectedIdentifiers, actual.ID())
   699  		}
   700  	}
   701  }
   702  
   703  // testMapMatchFrom is a test helper that checks specified number of entities are retrievable from entitiesMap.
   704  func testMapMatchCount(t *testing.T, entitiesMap map[flow.Identifier]flow.Entity, entities []*unittest.MockEntity, count int) {
   705  	require.Len(t, entitiesMap, count)
   706  	actualCount := 0
   707  	for i := range entities {
   708  		expected := entities[i]
   709  		actual, ok := entitiesMap[expected.ID()]
   710  		if !ok {
   711  			continue
   712  		}
   713  		require.Equal(t, expected, actual)
   714  		actualCount++
   715  	}
   716  	require.Equal(t, count, actualCount)
   717  }
   718  
   719  // testEntitiesMatchCount is a test helper that checks specified number of entities are retrievable from given list.
   720  func testEntitiesMatchCount(t *testing.T, expectedEntities []flow.Entity, actualEntities []*unittest.MockEntity, count int) {
   721  	entitiesMap := make(map[flow.Identifier]flow.Entity)
   722  
   723  	// converts expected entities list to a map in order to utilize a test helper.
   724  	for _, expected := range expectedEntities {
   725  		entitiesMap[expected.ID()] = expected
   726  	}
   727  
   728  	testMapMatchCount(t, entitiesMap, actualEntities, count)
   729  }
   730  
   731  // testIdentifiersMatchCount is a test helper that checks specified number of entities are retrievable from given list.
   732  func testIdentifiersMatchCount(t *testing.T, expectedIdentifiers flow.IdentifierList, actualEntities []*unittest.MockEntity, count int) {
   733  	idMap := make(map[flow.Identifier]struct{})
   734  
   735  	// converts expected identifiers to a map.
   736  	for _, expectedId := range expectedIdentifiers {
   737  		idMap[expectedId] = struct{}{}
   738  	}
   739  
   740  	require.Len(t, idMap, count)
   741  	actualCount := 0
   742  	for _, e := range actualEntities {
   743  		_, ok := idMap[e.ID()]
   744  		if !ok {
   745  			continue
   746  		}
   747  		actualCount++
   748  	}
   749  	require.Equal(t, count, actualCount)
   750  }
   751  
   752  // testRetrievableCount is a test helper that checks the number of retrievable entities from backdata exactly matches
   753  // the expectedCount.
   754  func testRetrievableCount(t *testing.T, bd *Cache, entities []*unittest.MockEntity, expectedCount uint64) {
   755  	actualCount := 0
   756  
   757  	for i := range entities {
   758  		expected := entities[i]
   759  		actual, ok := bd.ByID(expected.ID())
   760  		if !ok {
   761  			continue
   762  		}
   763  		require.Equal(t, expected, actual)
   764  		actualCount++
   765  	}
   766  
   767  	require.Equal(t, int(expectedCount), actualCount)
   768  }