kevo/pkg/memtable/skiplist_test.go
Jeremy Tregunna 6fc3be617d
Some checks failed
Go Tests / Run Tests (1.24.2) (push) Has been cancelled
feat: Initial release of kevo storage engine.
Adds a complete LSM-based storage engine with these features:
- Single-writer based architecture for the storage engine
- WAL for durability, and hey it's configurable
- MemTable with skip list implementation for fast read/writes
- SSTable with block-based structure for on-disk level-based storage
- Background compaction with tiered strategy
- ACID transactions
- Good documentation (I hope)
2025-04-20 14:06:50 -06:00

233 lines
5.6 KiB
Go

package memtable
import (
"bytes"
"testing"
)
func TestSkipListBasicOperations(t *testing.T) {
sl := NewSkipList()
// Test insertion
e1 := newEntry([]byte("key1"), []byte("value1"), TypeValue, 1)
e2 := newEntry([]byte("key2"), []byte("value2"), TypeValue, 2)
e3 := newEntry([]byte("key3"), []byte("value3"), TypeValue, 3)
sl.Insert(e1)
sl.Insert(e2)
sl.Insert(e3)
// Test lookup
found := sl.Find([]byte("key2"))
if found == nil {
t.Fatalf("expected to find key2, but got nil")
}
if string(found.value) != "value2" {
t.Errorf("expected value to be 'value2', got '%s'", string(found.value))
}
// Test lookup of non-existent key
notFound := sl.Find([]byte("key4"))
if notFound != nil {
t.Errorf("expected nil for non-existent key, got %v", notFound)
}
}
func TestSkipListSequenceNumbers(t *testing.T) {
sl := NewSkipList()
// Insert same key with different sequence numbers
e1 := newEntry([]byte("key"), []byte("value1"), TypeValue, 1)
e2 := newEntry([]byte("key"), []byte("value2"), TypeValue, 2)
e3 := newEntry([]byte("key"), []byte("value3"), TypeValue, 3)
// Insert in reverse order to test ordering
sl.Insert(e3)
sl.Insert(e2)
sl.Insert(e1)
// Find should return the entry with the highest sequence number
found := sl.Find([]byte("key"))
if found == nil {
t.Fatalf("expected to find key, but got nil")
}
if string(found.value) != "value3" {
t.Errorf("expected value to be 'value3' (highest seq num), got '%s'", string(found.value))
}
if found.seqNum != 3 {
t.Errorf("expected sequence number to be 3, got %d", found.seqNum)
}
}
func TestSkipListIterator(t *testing.T) {
sl := NewSkipList()
// Insert entries
entries := []struct {
key string
value string
seq uint64
}{
{"apple", "red", 1},
{"banana", "yellow", 2},
{"cherry", "red", 3},
{"date", "brown", 4},
{"elderberry", "purple", 5},
}
for _, e := range entries {
sl.Insert(newEntry([]byte(e.key), []byte(e.value), TypeValue, e.seq))
}
// Test iteration
it := sl.NewIterator()
it.SeekToFirst()
count := 0
for it.Valid() {
if count >= len(entries) {
t.Fatalf("iterator returned more entries than expected")
}
expectedKey := entries[count].key
expectedValue := entries[count].value
if string(it.Key()) != expectedKey {
t.Errorf("at position %d, expected key '%s', got '%s'", count, expectedKey, string(it.Key()))
}
if string(it.Value()) != expectedValue {
t.Errorf("at position %d, expected value '%s', got '%s'", count, expectedValue, string(it.Value()))
}
it.Next()
count++
}
if count != len(entries) {
t.Errorf("expected to iterate through %d entries, but got %d", len(entries), count)
}
}
func TestSkipListSeek(t *testing.T) {
sl := NewSkipList()
// Insert entries
entries := []struct {
key string
value string
seq uint64
}{
{"apple", "red", 1},
{"banana", "yellow", 2},
{"cherry", "red", 3},
{"date", "brown", 4},
{"elderberry", "purple", 5},
}
for _, e := range entries {
sl.Insert(newEntry([]byte(e.key), []byte(e.value), TypeValue, e.seq))
}
testCases := []struct {
seek string
expected string
valid bool
}{
// Before first entry
{"a", "apple", true},
// Exact match
{"cherry", "cherry", true},
// Between entries
{"blueberry", "cherry", true},
// After last entry
{"zebra", "", false},
}
for _, tc := range testCases {
t.Run(tc.seek, func(t *testing.T) {
it := sl.NewIterator()
it.Seek([]byte(tc.seek))
if it.Valid() != tc.valid {
t.Errorf("expected Valid() to be %v, got %v", tc.valid, it.Valid())
}
if tc.valid {
if string(it.Key()) != tc.expected {
t.Errorf("expected key '%s', got '%s'", tc.expected, string(it.Key()))
}
}
})
}
}
func TestEntryComparison(t *testing.T) {
testCases := []struct {
e1, e2 *entry
expected int
}{
// Different keys
{
newEntry([]byte("a"), []byte("val"), TypeValue, 1),
newEntry([]byte("b"), []byte("val"), TypeValue, 1),
-1,
},
{
newEntry([]byte("b"), []byte("val"), TypeValue, 1),
newEntry([]byte("a"), []byte("val"), TypeValue, 1),
1,
},
// Same key, different sequence numbers (higher seq should be "less")
{
newEntry([]byte("same"), []byte("val1"), TypeValue, 2),
newEntry([]byte("same"), []byte("val2"), TypeValue, 1),
-1,
},
{
newEntry([]byte("same"), []byte("val1"), TypeValue, 1),
newEntry([]byte("same"), []byte("val2"), TypeValue, 2),
1,
},
// Same key, same sequence number
{
newEntry([]byte("same"), []byte("val"), TypeValue, 1),
newEntry([]byte("same"), []byte("val"), TypeValue, 1),
0,
},
}
for i, tc := range testCases {
result := tc.e1.compareWithEntry(tc.e2)
expected := tc.expected
// We just care about the sign
if (result < 0 && expected >= 0) || (result > 0 && expected <= 0) || (result == 0 && expected != 0) {
t.Errorf("case %d: expected comparison result %d, got %d", i, expected, result)
}
}
}
func TestSkipListApproximateSize(t *testing.T) {
sl := NewSkipList()
// Initial size should be 0
if size := sl.ApproximateSize(); size != 0 {
t.Errorf("expected initial size to be 0, got %d", size)
}
// Add some entries
e1 := newEntry([]byte("key1"), []byte("value1"), TypeValue, 1)
e2 := newEntry([]byte("key2"), bytes.Repeat([]byte("v"), 100), TypeValue, 2)
sl.Insert(e1)
expectedSize := int64(e1.size())
if size := sl.ApproximateSize(); size != expectedSize {
t.Errorf("expected size to be %d after first insert, got %d", expectedSize, size)
}
sl.Insert(e2)
expectedSize += int64(e2.size())
if size := sl.ApproximateSize(); size != expectedSize {
t.Errorf("expected size to be %d after second insert, got %d", expectedSize, size)
}
}