fix: prevent index out of range on long frag tables

Previously, reading fragment 512 would panic with index out of range.
Fix that panic by introducing an abstraction over reading blocks of
items, caching the intermediate result, and returning an item at a
particular index. The primary goal of this abstraction is to make edge
cases like requesting items on page boundaries easy to unit test for.

Additionally, fix unit tests by making t.Fatal calls protected by
nil checks on the error values.
This commit is contained in:
Will Murphy
2024-11-26 07:07:00 -05:00
committed by Caleb Gardner
parent 81b663b48a
commit 0905141013
4 changed files with 228 additions and 40 deletions
+130
View File
@@ -0,0 +1,130 @@
package squashfslow
import (
"errors"
"testing"
)
func requireNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
func assertEqual(t *testing.T, want int, got int) {
t.Helper()
if want != got {
t.Errorf("want %d, got %d", want, got)
}
}
func assertLength(t *testing.T, want int, slice []int) {
t.Helper()
if len(slice) != want {
t.Errorf("want len %d, got %d", want, len(slice))
}
}
func assertErrorIs(t *testing.T, err error, wantErr error) {
t.Helper()
if err == nil {
t.Errorf("want %s, got nil", wantErr)
return
}
if !errors.Is(err, wantErr) {
t.Errorf("want %s, got %v", wantErr, err)
}
}
func TestCachingPagedReader(t *testing.T) {
// Mock readBlocks function
mockReadNMore := func(startBlock, numItems int) ([]int, error) {
if startBlock < 0 {
return nil, errors.New("invalid block start")
}
var result []int
for i := 0; i < numItems; i++ {
result = append(result, startBlock*512+i)
}
return result, nil
}
t.Run("ValidRequestWithinFirstBlock", func(t *testing.T) {
currentItems := make([]int, 0)
item, err := readPagedItems(300, 512, &currentItems, 2048, mockReadNMore)
requireNoError(t, err)
assertEqual(t, 300, item)
assertLength(t, 512, currentItems) // Ensure one block is read
})
t.Run("ValidRequestAcrossMultipleBlocks", func(t *testing.T) {
currentItems := make([]int, 0)
item, err := readPagedItems(600, 512, &currentItems, 2048, mockReadNMore)
requireNoError(t, err)
assertEqual(t, 600, item)
assertLength(t, 1024, currentItems)
})
t.Run("SequentialRequestsWithinBlocks", func(t *testing.T) {
currentItems := make([]int, 0)
// First request
item, err := readPagedItems(300, 512, &currentItems, 2048, mockReadNMore)
requireNoError(t, err)
assertEqual(t, 300, item)
// Second request in the same block
item, err = readPagedItems(400, 512, &currentItems, 2048, mockReadNMore)
requireNoError(t, err)
assertEqual(t, 400, item)
assertLength(t, 512, currentItems)
})
t.Run("RequestExactBlockBoundary", func(t *testing.T) {
currentItems := make([]int, 0)
item, err := readPagedItems(511, 512, &currentItems, 2048, mockReadNMore)
requireNoError(t, err)
assertEqual(t, 511, item)
assertLength(t, 512, currentItems)
// Request the next block's first item
item, err = readPagedItems(512, 512, &currentItems, 2048, mockReadNMore)
requireNoError(t, err)
assertEqual(t, 512, item)
assertLength(t, 1024, currentItems)
})
t.Run("OutOfBoundsRequest", func(t *testing.T) {
currentItems := make([]int, 0)
_, err := readPagedItems(2048, 512, &currentItems, 2048, mockReadNMore)
assertErrorIs(t, err, errOutOfBounds)
})
t.Run("RequestBeyondReadBlocks", func(t *testing.T) {
readFail := errors.New("failed to read block")
failingReadBlocks := func(startBlock, numBlocks int) ([]int, error) {
if startBlock > 1 {
return nil, readFail
}
var result []int
for i := 0; i < numBlocks*512; i++ {
result = append(result, startBlock*512+i)
}
return result, nil
}
currentItems := make([]int, 0)
_, err := readPagedItems(1024, 512, &currentItems, 2048, failingReadBlocks)
assertErrorIs(t, err, readFail)
})
t.Run("partial last page", func(t *testing.T) {
currentItems := make([]int, 0)
// Request the next block's first item
item, err := readPagedItems(512, 512, &currentItems, 612, mockReadNMore)
requireNoError(t, err)
assertEqual(t, 512, item)
assertLength(t, 612, currentItems)
})
}