github.com/koko1123/flow-go-1@v0.29.6/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/koko1123/flow-go-1/model/flow"
    12  	"github.com/koko1123/flow-go-1/module/mempool/herocache/backdata/heropool"
    13  	"github.com/koko1123/flow-go-1/module/metrics"
    14  	"github.com/koko1123/flow-go-1/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 := 10
    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_WriteHeavy evaluates correctness of Cache under the writing and retrieving
   130  // a heavy load of entities up to its limit. All data must be written successfully and then retrievable.
   131  func TestArrayBackData_WriteHeavy(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  
   142  	// adds all entities to backdata
   143  	testAddEntities(t, bd, entities, heropool.LRUEjection)
   144  
   145  	// retrieves all entities from backdata
   146  	testRetrievableFrom(t, bd, entities, 0)
   147  }
   148  
   149  // TestArrayBackData_LRU_Ejection evaluates correctness of Cache under the writing and retrieving
   150  // a heavy load of entities beyond its limit. With LRU ejection, only most recently written data must be maintained
   151  // by mempool.
   152  func TestArrayBackData_LRU_Ejection(t *testing.T) {
   153  	// mempool has the limit of 100K, but we put 1M
   154  	// (10 time more than its capacity)
   155  	limit := 100_000
   156  	items := uint(1_000_000)
   157  
   158  	bd := NewCache(uint32(limit),
   159  		8,
   160  		heropool.LRUEjection,
   161  		unittest.Logger(),
   162  		metrics.NewNoopCollector())
   163  
   164  	entities := unittest.EntityListFixture(items)
   165  
   166  	// adds all entities to backdata
   167  	testAddEntities(t, bd, entities, heropool.LRUEjection)
   168  
   169  	// only last 100K (i.e., 900Kth forward) items must be retrievable, and
   170  	// the rest must be ejected.
   171  	testRetrievableFrom(t, bd, entities, 900_000)
   172  }
   173  
   174  // TestArrayBackData_No_Ejection evaluates correctness of Cache under the writing and retrieving
   175  // a heavy load of entities beyond its limit. With NoEjection mode, the cache should refuse to add extra entities beyond
   176  // its limit.
   177  func TestArrayBackData_No_Ejection(t *testing.T) {
   178  	// mempool has the limit of 100K, but we put 1M
   179  	// (10 time more than its capacity)
   180  	limit := 100_000
   181  	items := uint(1_000_000)
   182  
   183  	bd := NewCache(uint32(limit),
   184  		8,
   185  		heropool.NoEjection,
   186  		unittest.Logger(),
   187  		metrics.NewNoopCollector())
   188  
   189  	entities := unittest.EntityListFixture(items)
   190  
   191  	// adds all entities to backdata
   192  	testAddEntities(t, bd, entities, heropool.NoEjection)
   193  
   194  	// only last 100K (i.e., 900Kth forward) items must be retrievable, and
   195  	// the rest must be ejected.
   196  	testRetrievableInRange(t, bd, entities, 0, limit)
   197  }
   198  
   199  // TestArrayBackData_Random_Ejection evaluates correctness of Cache under the writing and retrieving
   200  // a heavy load of entities beyond its limit. With random ejection, only as many entities as capacity of
   201  // Cache must be retrievable.
   202  func TestArrayBackData_Random_Ejection(t *testing.T) {
   203  	// mempool has the limit of 100K, but we put 1M
   204  	// (10 time more than its capacity)
   205  	limit := 100_000
   206  	items := uint(1_000_000)
   207  
   208  	bd := NewCache(uint32(limit),
   209  		8,
   210  		heropool.RandomEjection,
   211  		unittest.Logger(),
   212  		metrics.NewNoopCollector())
   213  
   214  	entities := unittest.EntityListFixture(items)
   215  
   216  	// adds all entities to backdata
   217  	testAddEntities(t, bd, entities, heropool.RandomEjection)
   218  
   219  	// only 100K (random) items must be retrievable, as the rest
   220  	// are randomly ejected to make room.
   221  	testRetrievableCount(t, bd, entities, 100_000)
   222  }
   223  
   224  // TestArrayBackData_AddDuplicate evaluates that adding duplicate entity to Cache will fail without
   225  // altering the internal state of it.
   226  func TestArrayBackData_AddDuplicate(t *testing.T) {
   227  	limit := 100
   228  
   229  	bd := NewCache(uint32(limit),
   230  		8,
   231  		heropool.LRUEjection,
   232  		unittest.Logger(),
   233  		metrics.NewNoopCollector())
   234  
   235  	entities := unittest.EntityListFixture(uint(limit))
   236  
   237  	// adds all entities to backdata
   238  	testAddEntities(t, bd, entities, heropool.LRUEjection)
   239  
   240  	// adding duplicate entity should fail
   241  	for _, entity := range entities {
   242  		require.False(t, bd.Add(entity.ID(), entity))
   243  	}
   244  
   245  	// still all entities must be retrievable from Cache.
   246  	testRetrievableFrom(t, bd, entities, 0)
   247  }
   248  
   249  // TestArrayBackData_Clear evaluates that calling Clear method removes all entities stored in BackData.
   250  func TestArrayBackData_Clear(t *testing.T) {
   251  	limit := 100
   252  
   253  	bd := NewCache(uint32(limit),
   254  		8,
   255  		heropool.LRUEjection,
   256  		unittest.Logger(),
   257  		metrics.NewNoopCollector())
   258  
   259  	entities := unittest.EntityListFixture(uint(limit))
   260  
   261  	// adds all entities to backdata
   262  	testAddEntities(t, bd, entities, heropool.LRUEjection)
   263  
   264  	// still all must be retrievable from backdata
   265  	testRetrievableFrom(t, bd, entities, 0)
   266  	require.Equal(t, bd.Size(), uint(limit))
   267  	require.Len(t, bd.All(), limit)
   268  
   269  	// calling clear must shrink size of BackData to zero
   270  	bd.Clear()
   271  	require.Equal(t, bd.Size(), uint(0))
   272  	require.Len(t, bd.All(), 0)
   273  
   274  	// none of stored elements must be retrievable any longer
   275  	testRetrievableCount(t, bd, entities, 0)
   276  }
   277  
   278  // TestArrayBackData_All checks correctness of All method in returning all stored entities in it.
   279  func TestArrayBackData_All(t *testing.T) {
   280  	tt := []struct {
   281  		limit        uint32
   282  		items        uint32
   283  		ejectionMode heropool.EjectionMode
   284  	}{
   285  		{ // mempool has the limit of 1000, but we put 100.
   286  			limit:        1000,
   287  			items:        100,
   288  			ejectionMode: heropool.LRUEjection,
   289  		},
   290  		{ // mempool has the limit of 1000, and we put exactly 1000 items.
   291  			limit:        1000,
   292  			items:        1000,
   293  			ejectionMode: heropool.LRUEjection,
   294  		},
   295  		{ // mempool has the limit of 1000, and we put 10K items with LRU ejection.
   296  			limit:        1000,
   297  			items:        10_000,
   298  			ejectionMode: heropool.LRUEjection,
   299  		},
   300  		{ // mempool has the limit of 1000, and we put 10K items with random ejection.
   301  			limit:        1000,
   302  			items:        10_000,
   303  			ejectionMode: heropool.RandomEjection,
   304  		},
   305  	}
   306  
   307  	for _, tc := range tt {
   308  		t.Run(fmt.Sprintf("%d-limit-%d-items-%s-ejection", tc.limit, tc.items, tc.ejectionMode), func(t *testing.T) {
   309  			bd := NewCache(tc.limit,
   310  				8,
   311  				tc.ejectionMode,
   312  				unittest.Logger(),
   313  				metrics.NewNoopCollector())
   314  			entities := unittest.EntityListFixture(uint(tc.items))
   315  
   316  			testAddEntities(t, bd, entities, tc.ejectionMode)
   317  
   318  			if tc.ejectionMode == heropool.RandomEjection {
   319  				// in random ejection mode we count total number of matched entities
   320  				// with All map.
   321  				testMapMatchCount(t, bd.All(), entities, int(tc.limit))
   322  				testEntitiesMatchCount(t, bd.Entities(), entities, int(tc.limit))
   323  				testIdentifiersMatchCount(t, bd.Identifiers(), entities, int(tc.limit))
   324  			} else {
   325  				// in LRU ejection mode we match All items based on a from index (i.e., last "from" items).
   326  				from := int(tc.items) - int(tc.limit)
   327  				if from < 0 {
   328  					// we are below limit, hence we start matching from index 0
   329  					from = 0
   330  				}
   331  				testMapMatchFrom(t, bd.All(), entities, from)
   332  				testEntitiesMatchFrom(t, bd.Entities(), entities, from)
   333  				testIdentifiersMatchFrom(t, bd.Identifiers(), entities, from)
   334  			}
   335  		})
   336  	}
   337  }
   338  
   339  // TestArrayBackData_Remove checks correctness of removing elements from Cache.
   340  func TestArrayBackData_Remove(t *testing.T) {
   341  	tt := []struct {
   342  		limit uint32
   343  		items uint32
   344  		from  int // index start to be removed (set -1 to remove randomly)
   345  		count int // total elements to be removed
   346  	}{
   347  		{ // removing range with total items below the limit
   348  			limit: 100_000,
   349  			items: 10_000,
   350  			from:  188,
   351  			count: 2012,
   352  		},
   353  		{ // removing range from full Cache
   354  			limit: 100_000,
   355  			items: 100_000,
   356  			from:  50_333,
   357  			count: 6667,
   358  		},
   359  		{ // removing random from Cache with total items below its limit
   360  			limit: 100_000,
   361  			items: 10_000,
   362  			from:  -1,
   363  			count: 6888,
   364  		},
   365  		{ // removing random from full Cache
   366  			limit: 100_000,
   367  			items: 10_000,
   368  			from:  -1,
   369  			count: 7328,
   370  		},
   371  	}
   372  
   373  	for _, tc := range tt {
   374  		t.Run(fmt.Sprintf("%d-limit-%d-items-%dfrom-%dcount", tc.limit, tc.items, tc.from, tc.count), func(t *testing.T) {
   375  			bd := NewCache(
   376  				tc.limit,
   377  				8,
   378  				heropool.RandomEjection,
   379  				unittest.Logger(),
   380  				metrics.NewNoopCollector())
   381  			entities := unittest.EntityListFixture(uint(tc.items))
   382  
   383  			testAddEntities(t, bd, entities, heropool.RandomEjection)
   384  
   385  			if tc.from == -1 {
   386  				// random removal
   387  				testRemoveAtRandom(t, bd, entities, tc.count)
   388  				// except removed ones, the rest must be retrievable
   389  				testRetrievableCount(t, bd, entities, uint64(int(tc.items)-tc.count))
   390  			} else {
   391  				// removing a range
   392  				testRemoveRange(t, bd, entities, tc.from, tc.from+tc.count)
   393  				testCheckRangeRemoved(t, bd, entities, tc.from, tc.from+tc.count)
   394  			}
   395  		})
   396  	}
   397  }
   398  
   399  // testAddEntities is a test helper that checks entities are added successfully to the Cache.
   400  // and each entity is retrievable right after it is written to backdata.
   401  func testAddEntities(t *testing.T, bd *Cache, entities []*unittest.MockEntity, ejection heropool.EjectionMode) {
   402  	// initially, head should be undefined
   403  	e, ok := bd.Head()
   404  	require.False(t, ok)
   405  	require.Nil(t, e)
   406  
   407  	// adding elements
   408  	for i, e := range entities {
   409  		if ejection == heropool.NoEjection && uint32(i) >= bd.sizeLimit {
   410  			// with no ejection when it goes beyond limit, the writes should be unsuccessful.
   411  			require.False(t, bd.Add(e.ID(), e))
   412  
   413  			// the head should retrieve the first added entity.
   414  			headEntity, headExists := bd.Head()
   415  			require.True(t, headExists)
   416  			require.Equal(t, headEntity.ID(), entities[0].ID())
   417  		} else {
   418  			// adding each element must be successful.
   419  			require.True(t, bd.Add(e.ID(), e))
   420  
   421  			if uint32(i) < bd.sizeLimit {
   422  				// when we are below limit the size of
   423  				// Cache should be incremented by each addition.
   424  				require.Equal(t, bd.Size(), uint(i+1))
   425  
   426  				// in case cache is not full, the head should retrieve the first added entity.
   427  				headEntity, headExists := bd.Head()
   428  				require.True(t, headExists)
   429  				require.Equal(t, headEntity.ID(), entities[0].ID())
   430  			} else {
   431  				// when we cross the limit, the ejection kicks in, and
   432  				// size must be steady at the limit.
   433  				require.Equal(t, uint32(bd.Size()), bd.sizeLimit)
   434  			}
   435  
   436  			// entity should be immediately retrievable
   437  			actual, ok := bd.ByID(e.ID())
   438  			require.True(t, ok)
   439  			require.Equal(t, e, actual)
   440  		}
   441  	}
   442  }
   443  
   444  // testRetrievableInRange is a test helper that evaluates that all entities starting from given index are retrievable from Cache.
   445  func testRetrievableFrom(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int) {
   446  	testRetrievableInRange(t, bd, entities, from, len(entities))
   447  }
   448  
   449  // testRetrievableInRange is a test helper that evaluates within given range [from, to) are retrievable from Cache.
   450  func testRetrievableInRange(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) {
   451  	for i := range entities {
   452  		expected := entities[i]
   453  		actual, ok := bd.ByID(expected.ID())
   454  		if i < from || i >= to {
   455  			require.False(t, ok, i)
   456  			require.Nil(t, actual)
   457  		} else {
   458  			require.True(t, ok)
   459  			require.Equal(t, expected, actual)
   460  		}
   461  	}
   462  }
   463  
   464  // testRemoveAtRandom is a test helper removes specified number of entities from Cache at random.
   465  func testRemoveAtRandom(t *testing.T, bd *Cache, entities []*unittest.MockEntity, count int) {
   466  	for removedCount := 0; removedCount < count; {
   467  		unittest.RequireReturnsBefore(t, func() {
   468  			index := rand.Int() % len(entities)
   469  			expected, removed := bd.Remove(entities[index].ID())
   470  			if !removed {
   471  				return
   472  			}
   473  			require.Equal(t, entities[index], expected)
   474  			removedCount++
   475  			// size sanity check after removal
   476  			require.Equal(t, bd.Size(), uint(len(entities)-removedCount))
   477  		}, 100*time.Millisecond, "could not find element to remove")
   478  	}
   479  }
   480  
   481  // testRemoveRange is a test helper that removes specified range of entities from Cache.
   482  func testRemoveRange(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) {
   483  	for i := from; i < to; i++ {
   484  		expected, removed := bd.Remove(entities[i].ID())
   485  		require.True(t, removed)
   486  		require.Equal(t, entities[i], expected)
   487  		// size sanity check after removal
   488  		require.Equal(t, bd.Size(), uint(len(entities)-(i-from)-1))
   489  	}
   490  }
   491  
   492  // testCheckRangeRemoved is a test helper that evaluates the specified range of entities have been removed from Cache.
   493  func testCheckRangeRemoved(t *testing.T, bd *Cache, entities []*unittest.MockEntity, from int, to int) {
   494  	for i := from; i < to; i++ {
   495  		// both removal and retrieval must fail
   496  		expected, removed := bd.Remove(entities[i].ID())
   497  		require.False(t, removed)
   498  		require.Nil(t, expected)
   499  
   500  		expected, exists := bd.ByID(entities[i].ID())
   501  		require.False(t, exists)
   502  		require.Nil(t, expected)
   503  	}
   504  }
   505  
   506  // testMapMatchFrom is a test helper that checks entities are retrievable from entitiesMap starting specified index.
   507  func testMapMatchFrom(t *testing.T, entitiesMap map[flow.Identifier]flow.Entity, entities []*unittest.MockEntity, from int) {
   508  	require.Len(t, entitiesMap, len(entities)-from)
   509  
   510  	for i := range entities {
   511  		expected := entities[i]
   512  		actual, ok := entitiesMap[expected.ID()]
   513  		if i < from {
   514  			require.False(t, ok, i)
   515  			require.Nil(t, actual)
   516  		} else {
   517  			require.True(t, ok)
   518  			require.Equal(t, expected, actual)
   519  		}
   520  	}
   521  }
   522  
   523  // testEntitiesMatchFrom is a test helper that checks entities are retrievable from given list starting specified index.
   524  func testEntitiesMatchFrom(t *testing.T, expectedEntities []flow.Entity, actualEntities []*unittest.MockEntity, from int) {
   525  	require.Len(t, expectedEntities, len(actualEntities)-from)
   526  
   527  	for i, actual := range actualEntities {
   528  		if i < from {
   529  			require.NotContains(t, expectedEntities, actual)
   530  		} else {
   531  			require.Contains(t, expectedEntities, actual)
   532  		}
   533  	}
   534  }
   535  
   536  // testIdentifiersMatchFrom is a test helper that checks identifiers of entities are retrievable from given list starting specified index.
   537  func testIdentifiersMatchFrom(t *testing.T, expectedIdentifiers flow.IdentifierList, actualEntities []*unittest.MockEntity, from int) {
   538  	require.Len(t, expectedIdentifiers, len(actualEntities)-from)
   539  
   540  	for i, actual := range actualEntities {
   541  		if i < from {
   542  			require.NotContains(t, expectedIdentifiers, actual.ID())
   543  		} else {
   544  			require.Contains(t, expectedIdentifiers, actual.ID())
   545  		}
   546  	}
   547  }
   548  
   549  // testMapMatchFrom is a test helper that checks specified number of entities are retrievable from entitiesMap.
   550  func testMapMatchCount(t *testing.T, entitiesMap map[flow.Identifier]flow.Entity, entities []*unittest.MockEntity, count int) {
   551  	require.Len(t, entitiesMap, count)
   552  	actualCount := 0
   553  	for i := range entities {
   554  		expected := entities[i]
   555  		actual, ok := entitiesMap[expected.ID()]
   556  		if !ok {
   557  			continue
   558  		}
   559  		require.Equal(t, expected, actual)
   560  		actualCount++
   561  	}
   562  	require.Equal(t, count, actualCount)
   563  }
   564  
   565  // testEntitiesMatchCount is a test helper that checks specified number of entities are retrievable from given list.
   566  func testEntitiesMatchCount(t *testing.T, expectedEntities []flow.Entity, actualEntities []*unittest.MockEntity, count int) {
   567  	entitiesMap := make(map[flow.Identifier]flow.Entity)
   568  
   569  	// converts expected entities list to a map in order to utilize a test helper.
   570  	for _, expected := range expectedEntities {
   571  		entitiesMap[expected.ID()] = expected
   572  	}
   573  
   574  	testMapMatchCount(t, entitiesMap, actualEntities, count)
   575  }
   576  
   577  // testIdentifiersMatchCount is a test helper that checks specified number of entities are retrievable from given list.
   578  func testIdentifiersMatchCount(t *testing.T, expectedIdentifiers flow.IdentifierList, actualEntities []*unittest.MockEntity, count int) {
   579  	idMap := make(map[flow.Identifier]struct{})
   580  
   581  	// converts expected identifiers to a map.
   582  	for _, expectedId := range expectedIdentifiers {
   583  		idMap[expectedId] = struct{}{}
   584  	}
   585  
   586  	require.Len(t, idMap, count)
   587  	actualCount := 0
   588  	for _, e := range actualEntities {
   589  		_, ok := idMap[e.ID()]
   590  		if !ok {
   591  			continue
   592  		}
   593  		actualCount++
   594  	}
   595  	require.Equal(t, count, actualCount)
   596  }
   597  
   598  // testRetrievableCount is a test helper that checks the number of retrievable entities from backdata exactly matches
   599  // the expectedCount.
   600  func testRetrievableCount(t *testing.T, bd *Cache, entities []*unittest.MockEntity, expectedCount uint64) {
   601  	actualCount := 0
   602  
   603  	for i := range entities {
   604  		expected := entities[i]
   605  		actual, ok := bd.ByID(expected.ID())
   606  		if !ok {
   607  			continue
   608  		}
   609  		require.Equal(t, expected, actual)
   610  		actualCount++
   611  	}
   612  
   613  	require.Equal(t, int(expectedCount), actualCount)
   614  }