diff --git a/x/sync/workheap_test.go b/x/sync/workheap_test.go index 0e48f66be26e..7f50468a1fbd 100644 --- a/x/sync/workheap_test.go +++ b/x/sync/workheap_test.go @@ -4,13 +4,17 @@ package sync import ( + "math/rand" "testing" - - "github.com/ava-labs/avalanchego/utils/maybe" + "time" "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/maybe" ) // Tests heap.Interface methods Push, Pop, Swap, Len, Less. @@ -271,3 +275,135 @@ func Test_WorkHeap_Merge_Insert(t *testing.T) { syncHeap.MergeInsert(&workItem{start: maybe.Some([]byte{63}), end: maybe.Some([]byte{127}), priority: lowPriority}) require.Equal(t, 1, syncHeap.Len()) } + +func TestWorkHeapMergeInsertRandom(t *testing.T) { + var ( + require = require.New(t) + seed = time.Now().UnixNano() + rand = rand.New(rand.NewSource(seed)) // #nosec G404 + numRanges = 1_000 + bounds = [][]byte{} + rootID = ids.GenerateTestID() + ) + t.Logf("seed: %d", seed) + + // Create start and end bounds + for i := 0; i < numRanges; i++ { + bound := make([]byte, 32) + _, _ = rand.Read(bound) + bounds = append(bounds, bound) + } + utils.SortBytes(bounds) + + // Note that start < end for all ranges. + // It is possible but extremely unlikely that + // two elements of [bounds] are equal. + ranges := []workItem{} + for i := 0; i < numRanges/2; i++ { + start := bounds[i*2] + end := bounds[i*2+1] + ranges = append(ranges, workItem{ + start: maybe.Some(start), + end: maybe.Some(end), + priority: lowPriority, + // Note they all share the same root ID. + localRootID: rootID, + }) + } + // Set beginning of first range to Nothing. + ranges[0].start = maybe.Nothing[[]byte]() + // Set end of last range to Nothing. + ranges[len(ranges)-1].end = maybe.Nothing[[]byte]() + + setup := func() *workHeap { + // Insert all the ranges into the heap. + h := newWorkHeap() + for i, r := range ranges { + require.Equal(i, h.Len()) + rCopy := r + h.MergeInsert(&rCopy) + } + return h + } + + { + // Case 1: Merging an item with the range before and after + h := setup() + // Keep merging ranges until there's only one range left. + for i := 0; i < len(ranges)-1; i++ { + // Merge ranges[i] with ranges[i+1] + h.MergeInsert(&workItem{ + start: ranges[i].end, + end: ranges[i+1].start, + priority: lowPriority, + localRootID: rootID, + }) + require.Equal(len(ranges)-i-1, h.Len()) + } + got := h.GetWork() + require.True(got.start.IsNothing()) + require.True(got.end.IsNothing()) + } + + { + // Case 2: Merging an item with the range before + h := setup() + for i := 0; i < len(ranges)-1; i++ { + // Extend end of ranges[i] + newEnd := slices.Clone(ranges[i].end.Value()) + newEnd = append(newEnd, 0) + h.MergeInsert(&workItem{ + start: ranges[i].end, + end: maybe.Some(newEnd), + priority: lowPriority, + localRootID: rootID, + }) + + // Shouldn't cause number of elements to change + require.Equal(len(ranges), h.Len()) + + start := ranges[i].start + if i == 0 { + start = maybe.Nothing[[]byte]() + } + // Make sure end is updated + got, ok := h.sortedItems.Get(&heapItem{ + workItem: &workItem{ + start: start, + }, + }) + require.True(ok) + require.Equal(newEnd, got.workItem.end.Value()) + } + } + + { + // Case 3: Merging an item with the range after + h := setup() + for i := 1; i < len(ranges); i++ { + // Extend start of ranges[i] + newStartBytes := slices.Clone(ranges[i].start.Value()) + newStartBytes = newStartBytes[:len(newStartBytes)-1] + newStart := maybe.Some(newStartBytes) + + h.MergeInsert(&workItem{ + start: newStart, + end: ranges[i].start, + priority: lowPriority, + localRootID: rootID, + }) + + // Shouldn't cause number of elements to change + require.Equal(len(ranges), h.Len()) + + // Make sure start is updated + got, ok := h.sortedItems.Get(&heapItem{ + workItem: &workItem{ + start: newStart, + }, + }) + require.True(ok) + require.Equal(newStartBytes, got.workItem.start.Value()) + } + } +}