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 }