From 70ea74cc13bd9360383a034f97ef87258098e879 Mon Sep 17 00:00:00 2001 From: paul cannon Date: Wed, 2 Apr 2025 20:23:18 -0500 Subject: [PATCH 1/6] Add randomness to tests Rather than relying on Seed(0), try things in a different order every time. The seed is logged so that if something fails, the problem can be reproduced using the -seed flag. --- heap_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/heap_test.go b/heap_test.go index bedc04d..d82f6f9 100644 --- a/heap_test.go +++ b/heap_test.go @@ -1,11 +1,52 @@ package minmaxheap import ( + "crypto/sha256" + "encoding/binary" + "flag" "math/rand" + "os" "sort" + "sync" "testing" + "time" ) +var ( + seed int64 + globalRand *rand.Rand + randMu sync.Mutex +) + +func init() { + flag.Int64Var(&seed, "seed", 0, "Random seed (default is current time)") +} + +func TestMain(m *testing.M) { + flag.Parse() + + randMu.Lock() + if seed == 0 { + seed = time.Now().UnixNano() + } + globalRand = rand.New(rand.NewSource(seed)) // seeded once for all test-local RNGs + randMu.Unlock() + + os.Exit(m.Run()) +} + +// newTestRand creates a deterministic *rand.Rand for the given test based on the test name. +func newTestRand(t *testing.T) *rand.Rand { + randMu.Lock() + defer randMu.Unlock() + + t.Logf("using global seed %d", seed) + h := sha256.Sum256([]byte(t.Name())) + namePart := int64(binary.BigEndian.Uint64(h[:8])) + nameSeed := seed ^ namePart // xor to combine them + return rand.New(rand.NewSource(nameSeed)) +} + type myHeap []int func (h myHeap) Len() int { return len(h) } @@ -191,12 +232,12 @@ func TestMax(t *testing.T) { } func TestRandomSorted(t *testing.T) { - rand.Seed(0) + rng := newTestRand(t) const n = 1_000 h := new(myHeap) for i := 0; i < n; i++ { - *h = append(*h, rand.Intn(n/2)) + *h = append(*h, rng.Intn(n/2)) } Init(h) @@ -213,12 +254,12 @@ func TestRandomSorted(t *testing.T) { } func TestRandomSortedMax(t *testing.T) { - rand.Seed(0) + rng := newTestRand(t) const n = 1_000 h := new(myHeap) for i := 0; i < n; i++ { - *h = append(*h, rand.Intn(n/2)) + *h = append(*h, rng.Intn(n/2)) } Init(h) @@ -233,6 +274,7 @@ func TestRandomSortedMax(t *testing.T) { t.Fatal("max pop order invalid") } } + func TestRemove0(t *testing.T) { h := new(myHeap) for i := 0; i < 10; i++ { @@ -307,7 +349,7 @@ func BenchmarkDup(b *testing.B) { } func TestFix(t *testing.T) { - rand.Seed(0) + rng := newTestRand(t) h := new(myHeap) h.verify(t, 0) @@ -325,7 +367,7 @@ func TestFix(t *testing.T) { h.verify(t, 0) for i := 100; i > 0; i-- { - elem := rand.Intn(h.Len()) + elem := rng.Intn(h.Len()) if i&1 == 0 { (*h)[elem] *= 2 } else { From 826d009664869a077ba7fd79056e87a010da3d09 Mon Sep 17 00:00:00 2001 From: paul cannon Date: Wed, 2 Apr 2025 20:26:01 -0500 Subject: [PATCH 2/6] Test Remove more rigorously Removes elements from a larger minmax heap in a random order, making sure all elements were yielded and no non-elements were yielded. --- heap_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/heap_test.go b/heap_test.go index d82f6f9..7af56d9 100644 --- a/heap_test.go +++ b/heap_test.go @@ -334,6 +334,37 @@ func TestRemove2(t *testing.T) { } } +func TestRemove3(t *testing.T) { + rng := newTestRand(t) + N := 200 + + h := new(myHeap) + for i := 0; i < N; i++ { + Push(h, i) + } + h.verify(t, 0) + + // remove all in random order + removed := make(map[int]struct{}) + for h.Len() > 0 { + i := rng.Intn(h.Len()) + x := Remove(h, i).(int) + h.verify(t, 0) + removed[x] = struct{}{} + } + + // make sure all were removed + for i := 0; i < N; i++ { + if _, ok := removed[i]; !ok { + t.Errorf("value %d was never removed", i) + } + delete(removed, i) + } + for k := range removed { + t.Errorf("value %d was removed but never added", k) + } +} + func BenchmarkDup(b *testing.B) { const n = 10000 h := make(myHeap, 0, n) From 861b6e1134f5bbc8438ff06b046ff496fb110912 Mon Sep 17 00:00:00 2001 From: paul cannon Date: Wed, 2 Apr 2025 20:30:47 -0500 Subject: [PATCH 3/6] Add code for rendering a minmax heap as a tree in SVG This can be used when a test fails unexpectedly to show the structure of the tree and where values are. Perhaps this is overkill, but I found it very useful. --- heap_test.go | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/heap_test.go b/heap_test.go index 7af56d9..14f5b82 100644 --- a/heap_test.go +++ b/heap_test.go @@ -4,9 +4,12 @@ import ( "crypto/sha256" "encoding/binary" "flag" + "fmt" "math/rand" "os" + "path/filepath" "sort" + "strings" "sync" "testing" "time" @@ -100,6 +103,213 @@ func (h myHeap) verify(t *testing.T, i int) { } } +func (h myHeap) Format(t *testing.T, highlight int) (filename string) { + if h.Len() == 0 { + return "" + } + + // Generate SVG representation of the heap + svgContent := h.FormatSVG(highlight) + + // Create the output directory if it doesn't exist + outputDir := "heap_visualizations" + if err := os.MkdirAll(outputDir, 0755); err != nil { + t.Errorf("Error creating output directory: %v", err) + return + } + + // Write SVG to a file in the output directory + timestamp := time.Now().Format("20060102_150405.000000") + filename = filepath.Join(outputDir, fmt.Sprintf("%s_nodes%d_highlight%d_%s.svg", + t.Name(), h.Len(), highlight, timestamp)) + + if err := os.WriteFile(filename, []byte(svgContent), 0644); err != nil { + t.Errorf("Error writing SVG file: %v", err) + return + } + + return filename +} + +// FormatSVG generates an SVG representation of the heap tree +func (h myHeap) FormatSVG(highlight int) string { + // Determine SVG parameters based on heap size + var nodeDiameter, levelHeight, leftMargin, topMargin int + + // Adjust parameters based on heap size + if h.Len() <= 31 { // Small heap (5 levels or fewer) + nodeDiameter = 40 + levelHeight = 80 + leftMargin = 10 + topMargin = 20 + } else if h.Len() <= 127 { // Medium heap (6-7 levels) + nodeDiameter = 30 + levelHeight = 60 + leftMargin = 10 + topMargin = 20 + } else { // Large heap (8+ levels) + nodeDiameter = 24 + levelHeight = 50 + leftMargin = 10 + topMargin = 20 + } + + // Calculate the total width needed for the tree + levels := level(h.Len()) + + // For very large heaps, limit the width by not showing all levels + maxLevelsToShow := levels + if levels > 7 { + maxLevelsToShow = 7 // Only show top 7 levels for very large heaps + } + + maxNodesInLevel := 1 << maxLevelsToShow // Maximum nodes in the last level we'll show + totalWidth := maxNodesInLevel*(nodeDiameter*2) + leftMargin*2 + totalHeight := (maxLevelsToShow+1)*levelHeight + topMargin*2 + + // Start the SVG + var buf strings.Builder + buf.WriteString(fmt.Sprintf("\n", + totalWidth, totalHeight)) + + // Add a title + buf.WriteString(fmt.Sprintf(" MinMaxHeap Visualization (%d nodes)\n", h.Len())) + + // Style definitions + buf.WriteString(` +`) + + // Add a legend + buf.WriteString(` + + Min Level + + Max Level +`) + + // Calculate positions for each node + nodesCount := h.Len() + if maxLevelsToShow < levels { + // Calculate how many nodes we're showing if we limited levels + nodesCount = (1 << (maxLevelsToShow + 1)) - 1 + if nodesCount > h.Len() { + nodesCount = h.Len() + } + } + + nodePositions := make([]struct{ x, y int }, nodesCount) + for i := 0; i < nodesCount; i++ { + lev := level(i) + if lev > maxLevelsToShow { + continue // Skip nodes beyond our display level + } + + nodesInLevel := 1 << lev + + // Calculate width of this level + levelWidth := totalWidth - (leftMargin * 2) + + // Calculate position in this level (0-based) + posInLevel := i - ((1 << lev) - 1) + + // Calculate x position (centered in its segment) + segmentWidth := levelWidth / nodesInLevel + x := leftMargin + (posInLevel * segmentWidth) + (segmentWidth / 2) + + // Calculate y position + y := topMargin + (lev * levelHeight) + (nodeDiameter / 2) + + nodePositions[i] = struct{ x, y int }{x, y} + } + + // Draw edges first (so they'll be behind nodes) + buf.WriteString(" \n") + for i := 0; i < nodesCount; i++ { + lev := level(i) + if lev >= maxLevelsToShow { + continue // Skip drawing edges from nodes in the last level + } + + leftChild := 2*i + 1 + rightChild := 2*i + 2 + + if leftChild < nodesCount { + buf.WriteString(fmt.Sprintf(" \n", + nodePositions[i].x, nodePositions[i].y+(nodeDiameter/2), + nodePositions[i].x, nodePositions[i].y+levelHeight/3, + nodePositions[leftChild].x, nodePositions[leftChild].y-levelHeight/3, + nodePositions[leftChild].x, nodePositions[leftChild].y-(nodeDiameter/2))) + } + + if rightChild < nodesCount { + buf.WriteString(fmt.Sprintf(" \n", + nodePositions[i].x, nodePositions[i].y+(nodeDiameter/2), + nodePositions[i].x, nodePositions[i].y+levelHeight/3, + nodePositions[rightChild].x, nodePositions[rightChild].y-levelHeight/3, + nodePositions[rightChild].x, nodePositions[rightChild].y-(nodeDiameter/2))) + } + } + + // Draw all nodes + buf.WriteString(" \n") + for i := 0; i < nodesCount; i++ { + lev := level(i) + if lev > maxLevelsToShow { + continue // Skip nodes beyond our display level + } + + x := nodePositions[i].x + y := nodePositions[i].y + + // Determine node class based on min/max level + nodeClass := "node-min" + if !isMinLevel(i) { + nodeClass = "node-max" + } + + // Add highlight class if needed + if i == highlight { + nodeClass += " node-highlight" + } + + // Draw the node + buf.WriteString(fmt.Sprintf(" \n", + x, y, nodeDiameter/2, nodeClass)) + + // Add the value text + buf.WriteString(fmt.Sprintf(" %d\n", + x, y, h[i])) + + // Add node index for all nodes as a reference + fontSize := 10 + if nodeDiameter < 30 { + fontSize = 8 // Smaller font for smaller nodes + } + + buf.WriteString(fmt.Sprintf(" [%d]\n", + x, y, -nodeDiameter/2-2, fontSize, i)) + } + + // If we limited the display, add a note + if maxLevelsToShow < levels { + buf.WriteString(fmt.Sprintf(" Note: Only showing %d of %d levels. Total nodes: %d\n", + totalWidth/2, totalHeight-20, maxLevelsToShow, levels, h.Len())) + } + + // End the SVG + buf.WriteString("") + + return buf.String() +} + func TestInit0(t *testing.T) { h := new(myHeap) for i := 20; i > 0; i-- { From fde071f464091cba5d46dd737afe7cabe6829d2e Mon Sep 17 00:00:00 2001 From: paul cannon Date: Wed, 2 Apr 2025 20:32:53 -0500 Subject: [PATCH 4/6] Add invariants to verify() method This now checks that grandchildren have the correct relationship to nodes. Without this, it was possible for an invalid tree to pass verify(). --- heap_test.go | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/heap_test.go b/heap_test.go index 14f5b82..b3af2c1 100644 --- a/heap_test.go +++ b/heap_test.go @@ -73,33 +73,34 @@ func (h myHeap) verify(t *testing.T, i int) { n := h.Len() l := 2*i + 1 r := 2*i + 2 - if l < n { - if isMinLevel(i) { - if h.Less(l, i) { - t.Errorf("heap invariant violated [%d] = %d > [%d] = %d", i, h[i], l, h[l]) - return - } - } else { - if h.Less(i, l) { - t.Errorf("heap invariant violated [%d] = %d > [%d] = %d", l, h[l], i, h[i]) - return - } + childrenAndGrandchildren := []int{ + l, // left child + r, // right child + 2*l + 1, // left child of left child + 2*l + 2, // right child of left child + 2*r + 1, // left child of right child + 2*r + 2, // right child of right child + } + + for cNum, descendant := range childrenAndGrandchildren { + if descendant >= n { + continue } - h.verify(t, l) - } - if r < n { if isMinLevel(i) { - if h.Less(r, i) { - t.Errorf("heap invariant violated [%d] = %d > [%d] = %d", i, h[i], r, h[r]) - return + if h.Less(descendant, i) { + filename := h.Format(t, i) + t.Fatalf("heap invariant violated [%d] = %d >= [%d] = %d\n SVG rendering of tree can be found at %s", i, h[i], descendant, h[descendant], filename) } } else { - if h.Less(i, r) { - t.Errorf("heap invariant violated [%d] = %d > [%d] = %d", r, h[r], i, h[i]) - return + if h.Less(i, descendant) { + filename := h.Format(t, descendant) + t.Fatalf("heap invariant violated [%d] = %d >= [%d] = %d\n SVG rendering of tree can be found at %s", descendant, h[descendant], i, h[i], filename) } } - h.verify(t, r) + if cNum < 2 { + // only recurse to immediate children + h.verify(t, descendant) + } } } From dfdf1198879161bfbc341218efcfa27ffcff23b6 Mon Sep 17 00:00:00 2001 From: paul cannon Date: Wed, 2 Apr 2025 20:34:54 -0500 Subject: [PATCH 5/6] Add a specific test for Fix() that currently fails --- heap_test.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/heap_test.go b/heap_test.go index b3af2c1..ecc4af2 100644 --- a/heap_test.go +++ b/heap_test.go @@ -590,7 +590,7 @@ func BenchmarkDup(b *testing.B) { } } -func TestFix(t *testing.T) { +func TestFix0(t *testing.T) { rng := newTestRand(t) h := new(myHeap) @@ -619,3 +619,19 @@ func TestFix(t *testing.T) { h.verify(t, 0) } } + +func TestFix1(t *testing.T) { + h := new(myHeap) + + for i := 0; i < 100; i++ { + Push(h, 100-i) + h.verify(t, 0) + } + (*h)[48] = -1 + Fix(h, 48) + h.verify(t, 0) + got := Pop(h).(int) + if got != -1 { + t.Fatalf("expected -1 as minimum, got %d", got) + } +} From e7ea832959e7c3561ab406c5bf2f56becb01209e Mon Sep 17 00:00:00 2001 From: paul cannon Date: Wed, 2 Apr 2025 20:35:24 -0500 Subject: [PATCH 6/6] Fix Remove() and Fix() This appears to fix all of the ordering problems I have been experiencing so far. --- heap.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/heap.go b/heap.go index 3da8c5b..0cbc334 100644 --- a/heap.go +++ b/heap.go @@ -172,9 +172,8 @@ func Remove(h Interface, i int) interface{} { n := h.Len() - 1 if n != i { h.Swap(i, n) - if !down(h, i, n) { - up(h, i) - } + up(h, i) + down(h, i, n) } return h.Pop() } @@ -185,7 +184,6 @@ func Remove(h Interface, i int) interface{} { // followed by a Push of the new value. // The complexity is O(log n) where n = h.Len(). func Fix(h Interface, i int) { - if !down(h, i, h.Len()) { - up(h, i) - } + up(h, i) + down(h, i, h.Len()) }