Files
zeekayandHanzo Dev c6c6cc6219 Go 1.27.1, and the code it can say more simply
The module declares go 1.27.1 and CI follows. go fix applied the
modernizers; omitzero is not applied, since it changes what the JSON
says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:52:34 -07:00

1392 lines
34 KiB
Go

// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dbtest
import (
"bytes"
"io"
"maps"
"math/rand"
"slices"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"golang.org/x/sync/errgroup"
"github.com/luxfi/crypto"
db "github.com/luxfi/database"
"github.com/luxfi/database/databasemock"
)
const (
KiB = 1024
MiB = 1024 * KiB
)
var (
// Import errors from db package
ErrNotFound = db.ErrNotFound
ErrClosed = db.ErrClosed
)
// TestsBasic is a list of all basic database tests that require only
// a KeyValueReaderWriterDeleter.
var TestsBasic = map[string]func(t *testing.T, db db.KeyValueReaderWriterDeleter){
"SimpleKeyValue": TestSimpleKeyValue,
"OverwriteKeyValue": TestOverwriteKeyValue,
"EmptyKey": TestEmptyKey,
"KeyEmptyValue": TestKeyEmptyValue,
"MemorySafetyDatabase": TestMemorySafetyDatabase,
"ModifyValueAfterPut": TestModifyValueAfterPut,
"PutGetEmpty": TestPutGetEmpty,
}
// Tests is a list of all database tests
var Tests = map[string]func(t *testing.T, db db.Database){
"SimpleKeyValueClosed": TestSimpleKeyValueClosed,
"NewBatchClosed": TestNewBatchClosed,
"BatchPut": TestBatchPut,
"BatchDelete": TestBatchDelete,
"BatchReset": TestBatchReset,
"BatchReuse": TestBatchReuse,
"BatchRewrite": TestBatchRewrite,
"BatchReplay": TestBatchReplay,
"BatchReplayPropagateError": TestBatchReplayPropagateError,
"BatchInner": TestBatchInner,
"BatchLargeSize": TestBatchLargeSize,
"IteratorSnapshot": TestIteratorSnapshot,
"Iterator": TestIterator,
"IteratorStart": TestIteratorStart,
"IteratorPrefix": TestIteratorPrefix,
"IteratorStartPrefix": TestIteratorStartPrefix,
"IteratorMemorySafety": TestIteratorMemorySafety,
"IteratorClosed": TestIteratorClosed,
"IteratorError": TestIteratorError,
"IteratorErrorAfterRelease": TestIteratorErrorAfterRelease,
"CompactNoPanic": TestCompactNoPanic,
"MemorySafetyBatch": TestMemorySafetyBatch,
// These tests are commented out as they use methods not in the standard Database interface:
// "AtomicClear": TestAtomicClear,
// "Clear": TestClear,
// "AtomicClearPrefix": TestAtomicClearPrefix,
// "ClearPrefix": TestClearPrefix,
"ModifyValueAfterBatchPut": TestModifyValueAfterBatchPut,
"ModifyValueAfterBatchPutReplay": TestModifyValueAfterBatchPutReplay,
"ConcurrentBatches": TestConcurrentBatches,
"ManySmallConcurrentKVPairBatches": TestManySmallConcurrentKVPairBatches,
}
func init() {
// Add all basic database tests to the database tests
for name, test := range TestsBasic {
Tests[name] = func(t *testing.T, db db.Database) {
test(t, db)
}
}
}
// TestSimpleKeyValue tests to make sure that simple Put + Get + Delete + Has
// calls return the expected values.
func TestSimpleKeyValue(t *testing.T, db db.KeyValueReaderWriterDeleter) {
require := require.New(t)
key := []byte("hello")
value := []byte("world")
has, err := db.Has(key)
require.NoError(err)
require.False(has)
_, err = db.Get(key)
require.Equal(ErrNotFound, err)
require.NoError(db.Delete(key))
require.NoError(db.Put(key, value))
has, err = db.Has(key)
require.NoError(err)
require.True(has)
v, err := db.Get(key)
require.NoError(err)
require.Equal(value, v)
require.NoError(db.Delete(key))
has, err = db.Has(key)
require.NoError(err)
require.False(has)
_, err = db.Get(key)
require.Equal(ErrNotFound, err)
require.NoError(db.Delete(key))
}
func TestOverwriteKeyValue(t *testing.T, db db.KeyValueReaderWriterDeleter) {
require := require.New(t)
key := []byte("hello")
value1 := []byte("world1")
value2 := []byte("world2")
require.NoError(db.Put(key, value1))
require.NoError(db.Put(key, value2))
gotValue, err := db.Get(key)
require.NoError(err)
require.Equal(value2, gotValue)
}
func TestKeyEmptyValue(t *testing.T, db db.KeyValueReaderWriterDeleter) {
require := require.New(t)
key := []byte("hello")
val := []byte(nil)
_, err := db.Get(key)
require.Equal(ErrNotFound, err)
require.NoError(db.Put(key, val))
value, err := db.Get(key)
require.NoError(err)
require.Empty(value)
}
func TestEmptyKey(t *testing.T, db db.KeyValueReaderWriterDeleter) {
require := require.New(t)
var (
nilKey = []byte(nil)
emptyKey = []byte{}
val1 = []byte("hi")
val2 = []byte("hello")
)
// Test that nil key can be retrieved by empty key
_, err := db.Get(nilKey)
require.Equal(ErrNotFound, err)
require.NoError(db.Put(nilKey, val1))
value, err := db.Get(emptyKey)
require.NoError(err)
require.Equal(value, val1)
// Test that empty key can be retrieved by nil key
require.NoError(db.Put(emptyKey, val2))
value, err = db.Get(nilKey)
require.NoError(err)
require.Equal(value, val2)
}
// TestSimpleKeyValueClosed tests to make sure that Put + Get + Delete + Has
// calls return the correct error when the database has been closed.
func TestSimpleKeyValueClosed(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte("hello")
value := []byte("world")
has, err := db.Has(key)
require.NoError(err)
require.False(has)
_, err = db.Get(key)
require.Equal(ErrNotFound, err)
require.NoError(db.Delete(key))
require.NoError(db.Put(key, value))
has, err = db.Has(key)
require.NoError(err)
require.True(has)
v, err := db.Get(key)
require.NoError(err)
require.Equal(value, v)
require.NoError(db.Close())
_, err = db.Has(key)
require.Equal(ErrClosed, err)
_, err = db.Get(key)
require.Equal(ErrClosed, err)
require.Equal(ErrClosed, db.Put(key, value))
require.Equal(ErrClosed, db.Delete(key))
require.Equal(ErrClosed, db.Close())
}
// TestMemorySafetyDatabase ensures it is safe to modify a key after passing it
// to Database.Put and Database.Get.
func TestMemorySafetyDatabase(t *testing.T, db db.KeyValueReaderWriterDeleter) {
require := require.New(t)
key := []byte("1key")
keyCopy := slices.Clone(key)
value := []byte("value")
key2 := []byte("2key")
value2 := []byte("value2")
// Put key in the database directly
require.NoError(db.Put(key, value))
// Put key2 in the database by modifying key, which should be safe
// to modify after the Put call
key[0] = key2[0]
require.NoError(db.Put(key, value2))
key[0] = keyCopy[0]
// Get the value for [key]
gotVal, err := db.Get(key)
require.NoError(err)
require.Equal(value, gotVal)
// Modify [key]; make sure the value we got before hasn't changed
key[0] = key2[0]
gotVal2, err := db.Get(key)
require.NoError(err)
require.Equal(value2, gotVal2)
require.Equal(value, gotVal)
// Reset [key] to its original value and make sure it's correct
key[0] = keyCopy[0]
gotVal, err = db.Get(key)
require.NoError(err)
require.Equal(value, gotVal)
}
// TestNewBatchClosed tests to make sure that calling NewBatch on a closed
// database returns a batch that errors correctly.
func TestNewBatchClosed(t *testing.T, db db.Database) {
require := require.New(t)
require.NoError(db.Close())
batch := db.NewBatch()
require.NotNil(batch)
key := []byte("hello")
value := []byte("world")
require.NoError(batch.Put(key, value))
require.Positive(batch.Size())
require.Equal(ErrClosed, batch.Write())
}
// TestBatchPut tests to make sure that batched writes work as expected.
func TestBatchPut(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte("hello")
value := []byte("world")
batch := db.NewBatch()
require.NotNil(batch)
require.NoError(batch.Put(key, value))
require.Positive(batch.Size())
require.NoError(batch.Write())
has, err := db.Has(key)
require.NoError(err)
require.True(has)
v, err := db.Get(key)
require.NoError(err)
require.Equal(value, v)
require.NoError(db.Delete(key))
batch = db.NewBatch()
require.NotNil(batch)
require.NoError(batch.Put(key, value))
require.NoError(db.Close())
writeErr := batch.Write()
require.Error(writeErr)
require.Contains(writeErr.Error(), "closed")
}
// TestBatchDelete tests to make sure that batched deletes work as expected.
func TestBatchDelete(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte("hello")
value := []byte("world")
require.NoError(db.Put(key, value))
batch := db.NewBatch()
require.NotNil(batch)
require.NoError(batch.Delete(key))
require.NoError(batch.Write())
has, err := db.Has(key)
require.NoError(err)
require.False(has)
_, err = db.Get(key)
require.Equal(ErrNotFound, err)
require.NoError(db.Delete(key))
}
// TestMemorySafetyBatch ensures it is safe to modify a key after passing it
// to Batch.Put.
func TestMemorySafetyBatch(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte("hello")
keyCopy := slices.Clone(key)
value := []byte("world")
valueCopy := slices.Clone(value)
batch := db.NewBatch()
require.NotNil(batch)
// Put a key in the batch
require.NoError(batch.Put(key, value))
require.Positive(batch.Size())
// Modify the key
key[0] = 'j'
require.NoError(batch.Write())
// Make sure the original key was written to the database
has, err := db.Has(keyCopy)
require.NoError(err)
require.True(has)
v, err := db.Get(keyCopy)
require.NoError(err)
require.Equal(valueCopy, v)
// Make sure the new key wasn't written to the database
has, err = db.Has(key)
require.NoError(err)
require.False(has)
}
// TestBatchReset tests to make sure that a batch drops un-written operations
// when it is reset.
func TestBatchReset(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte("hello")
value := []byte("world")
require.NoError(db.Put(key, value))
batch := db.NewBatch()
require.NotNil(batch)
require.NoError(batch.Delete(key))
batch.Reset()
require.Zero(batch.Size())
require.NoError(batch.Write())
has, err := db.Has(key)
require.NoError(err)
require.True(has)
v, err := db.Get(key)
require.NoError(err)
require.Equal(value, v)
}
// TestBatchReuse tests to make sure that a batch can be reused once it is
// reset.
func TestBatchReuse(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("hello2")
value2 := []byte("world2")
batch := db.NewBatch()
require.NotNil(batch)
require.NoError(batch.Put(key1, value1))
require.NoError(batch.Write())
require.NoError(db.Delete(key1))
has, err := db.Has(key1)
require.NoError(err)
require.False(has)
batch.Reset()
require.Zero(batch.Size())
require.NoError(batch.Put(key2, value2))
require.NoError(batch.Write())
has, err = db.Has(key1)
require.NoError(err)
require.False(has)
has, err = db.Has(key2)
require.NoError(err)
require.True(has)
v, err := db.Get(key2)
require.NoError(err)
require.Equal(value2, v)
}
// TestBatchRewrite tests to make sure that write can be called multiple times
// on a batch and the values will be updated correctly.
func TestBatchRewrite(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte("hello1")
value := []byte("world1")
batch := db.NewBatch()
require.NotNil(batch)
require.NoError(batch.Put(key, value))
require.NoError(batch.Write())
require.NoError(db.Delete(key))
has, err := db.Has(key)
require.NoError(err)
require.False(has)
require.NoError(batch.Write())
has, err = db.Has(key)
require.NoError(err)
require.True(has)
v, err := db.Get(key)
require.NoError(err)
require.Equal(value, v)
}
// TestBatchReplay tests to make sure that batches will correctly replay their
// contents.
func TestBatchReplay(t *testing.T, db db.Database) {
ctrl := gomock.NewController(t)
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("hello2")
value2 := []byte("world2")
batch := db.NewBatch()
require.NotNil(batch)
require.NoError(batch.Put(key1, value1))
require.NoError(batch.Put(key2, value2))
require.NoError(batch.Delete(key1))
require.NoError(batch.Delete(key2))
require.NoError(batch.Put(key1, value2))
for range 2 {
mockBatch := databasemock.NewBatch(ctrl)
gomock.InOrder(
mockBatch.EXPECT().Put(key1, value1).Times(1),
mockBatch.EXPECT().Put(key2, value2).Times(1),
mockBatch.EXPECT().Delete(key1).Times(1),
mockBatch.EXPECT().Delete(key2).Times(1),
mockBatch.EXPECT().Put(key1, value2).Times(1),
)
require.NoError(batch.Replay(mockBatch))
}
}
// TestBatchReplayPropagateError tests to make sure that batches will correctly
// propagate any returned error during Replay.
func TestBatchReplayPropagateError(t *testing.T, db db.Database) {
ctrl := gomock.NewController(t)
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("hello2")
value2 := []byte("world2")
batch := db.NewBatch()
require.NotNil(batch)
require.NoError(batch.Put(key1, value1))
require.NoError(batch.Put(key2, value2))
mockBatch := databasemock.NewBatch(ctrl)
gomock.InOrder(
mockBatch.EXPECT().Put(key1, value1).Return(ErrClosed).Times(1),
)
require.Equal(ErrClosed, batch.Replay(mockBatch))
mockBatch = databasemock.NewBatch(ctrl)
gomock.InOrder(
mockBatch.EXPECT().Put(key1, value1).Return(io.ErrClosedPipe).Times(1),
)
require.Equal(io.ErrClosedPipe, batch.Replay(mockBatch))
}
// TestBatchInner tests to make sure that inner can be used to write to the
// database.
func TestBatchInner(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("hello2")
value2 := []byte("world2")
firstBatch := db.NewBatch()
require.NotNil(firstBatch)
require.NoError(firstBatch.Put(key1, value1))
secondBatch := db.NewBatch()
require.NotNil(firstBatch)
require.NoError(secondBatch.Put(key2, value2))
innerFirstBatch := firstBatch.Inner()
require.NotNil(innerFirstBatch)
innerSecondBatch := secondBatch.Inner()
require.NotNil(innerSecondBatch)
require.NoError(innerFirstBatch.Replay(innerSecondBatch))
require.NoError(innerSecondBatch.Write())
has, err := db.Has(key1)
require.NoError(err)
require.True(has)
v, err := db.Get(key1)
require.NoError(err)
require.Equal(value1, v)
has, err = db.Has(key2)
require.NoError(err)
require.True(has)
v, err = db.Get(key2)
require.NoError(err)
require.Equal(value2, v)
}
// TestBatchLargeSize tests to make sure that the batch can support a large
// amount of entries.
func TestBatchLargeSize(t *testing.T, db db.Database) {
require := require.New(t)
totalSize := 8 * MiB
elementSize := 4 * KiB
pairSize := 2 * elementSize // 8 KiB
bytes := crypto.RandomBytes(totalSize)
batch := db.NewBatch()
require.NotNil(batch)
for len(bytes) > pairSize {
key := bytes[:elementSize]
bytes = bytes[elementSize:]
value := bytes[:elementSize]
bytes = bytes[elementSize:]
require.NoError(batch.Put(key, value))
}
require.NoError(batch.Write())
}
// TestIteratorSnapshot tests to make sure the database iterates over a snapshot
// of the database at the time of the iterator creation.
func TestIteratorSnapshot(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("hello2")
value2 := []byte("world2")
require.NoError(db.Put(key1, value1))
iterator := db.NewIterator()
require.NotNil(iterator)
defer iterator.Release()
require.NoError(db.Put(key2, value2))
require.True(iterator.Next())
require.Equal(key1, iterator.Key())
require.Equal(value1, iterator.Value())
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
require.NoError(iterator.Error())
}
// TestIterator tests to make sure the database iterates over the database
// contents lexicographically.
func TestIterator(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("hello2")
value2 := []byte("world2")
require.NoError(db.Put(key1, value1))
require.NoError(db.Put(key2, value2))
iterator := db.NewIterator()
require.NotNil(iterator)
defer iterator.Release()
require.True(iterator.Next())
require.Equal(key1, iterator.Key())
require.Equal(value1, iterator.Value())
require.True(iterator.Next())
require.Equal(key2, iterator.Key())
require.Equal(value2, iterator.Value())
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
require.NoError(iterator.Error())
}
// TestIteratorStart tests to make sure the iterator can be configured to
// start mid way through the database.
func TestIteratorStart(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("hello2")
value2 := []byte("world2")
require.NoError(db.Put(key1, value1))
require.NoError(db.Put(key2, value2))
iterator := db.NewIteratorWithStart(key2)
require.NotNil(iterator)
defer iterator.Release()
require.True(iterator.Next())
require.Equal(key2, iterator.Key())
require.Equal(value2, iterator.Value())
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
require.NoError(iterator.Error())
}
// TestIteratorPrefix tests to make sure the iterator can be configured to skip
// keys missing the provided prefix.
func TestIteratorPrefix(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello")
value1 := []byte("world1")
key2 := []byte("goodbye")
value2 := []byte("world2")
key3 := []byte("joy")
value3 := []byte("world3")
require.NoError(db.Put(key1, value1))
require.NoError(db.Put(key2, value2))
require.NoError(db.Put(key3, value3))
iterator := db.NewIteratorWithPrefix([]byte("h"))
require.NotNil(iterator)
defer iterator.Release()
require.True(iterator.Next())
require.Equal(key1, iterator.Key())
require.Equal(value1, iterator.Value())
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
require.NoError(iterator.Error())
}
// TestIteratorStartPrefix tests to make sure that the iterator can start mid
// way through the database while skipping a prefix.
func TestIteratorStartPrefix(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("z")
value2 := []byte("world2")
key3 := []byte("hello3")
value3 := []byte("world3")
require.NoError(db.Put(key1, value1))
require.NoError(db.Put(key2, value2))
require.NoError(db.Put(key3, value3))
iterator := db.NewIteratorWithStartAndPrefix(key1, []byte("h"))
require.NotNil(iterator)
defer iterator.Release()
require.True(iterator.Next())
require.Equal(key1, iterator.Key())
require.Equal(value1, iterator.Value())
require.True(iterator.Next())
require.Equal(key3, iterator.Key())
require.Equal(value3, iterator.Value())
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
require.NoError(iterator.Error())
}
// TestIteratorMemorySafety tests to make sure that keys can values are able to
// be modified from the returned iterator.
func TestIteratorMemorySafety(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("z")
value2 := []byte("world2")
key3 := []byte("hello3")
value3 := []byte("world3")
require.NoError(db.Put(key1, value1))
require.NoError(db.Put(key2, value2))
require.NoError(db.Put(key3, value3))
iterator := db.NewIterator()
require.NotNil(iterator)
defer iterator.Release()
keys := [][]byte{}
values := [][]byte{}
for iterator.Next() {
keys = append(keys, iterator.Key())
values = append(values, iterator.Value())
}
expectedKeys := [][]byte{
key1,
key3,
key2,
}
expectedValues := [][]byte{
value1,
value3,
value2,
}
for i, key := range keys {
value := values[i]
expectedKey := expectedKeys[i]
expectedValue := expectedValues[i]
require.Equal(expectedKey, key)
require.Equal(expectedValue, value)
}
}
// TestIteratorClosed tests to make sure that an iterator that was created with
// a closed database will report a closed error correctly.
func TestIteratorClosed(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
require.NoError(db.Put(key1, value1))
require.NoError(db.Close())
{
iterator := db.NewIterator()
require.NotNil(iterator)
defer iterator.Release()
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
iterErr := iterator.Error()
require.Error(iterErr)
require.Contains(iterErr.Error(), "closed")
}
{
iterator := db.NewIteratorWithPrefix(nil)
require.NotNil(iterator)
defer iterator.Release()
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
iterErr := iterator.Error()
require.Error(iterErr)
require.Contains(iterErr.Error(), "closed")
}
{
iterator := db.NewIteratorWithStart(nil)
require.NotNil(iterator)
defer iterator.Release()
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
iterErr := iterator.Error()
require.Error(iterErr)
require.Contains(iterErr.Error(), "closed")
}
{
iterator := db.NewIteratorWithStartAndPrefix(nil, nil)
require.NotNil(iterator)
defer iterator.Release()
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
iterErr := iterator.Error()
require.Error(iterErr)
require.Contains(iterErr.Error(), "closed")
}
}
// TestIteratorError tests to make sure that an iterator on a database will report
// itself as being exhausted and return [ErrClosed] to indicate that the iteration
// was not successful.
// Additionally tests that an iterator that has already called Next() can still serve
// its current value after the underlying DB was closed.
func TestIteratorError(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("hello2")
value2 := []byte("world2")
require.NoError(db.Put(key1, value1))
require.NoError(db.Put(key2, value2))
iterator := db.NewIterator()
require.NotNil(iterator)
defer iterator.Release()
// Call Next() and ensure that if the database is closed, the iterator
// can still report the current contents.
require.True(iterator.Next())
require.NoError(db.Close())
require.Equal(key1, iterator.Key())
require.Equal(value1, iterator.Value())
// Subsequent calls to the iterator should return false and report an error
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
iterErr := iterator.Error()
require.Error(iterErr)
require.Contains(iterErr.Error(), "closed")
}
// TestIteratorErrorAfterRelease tests to make sure that an iterator that was
// released still reports the error correctly.
func TestIteratorErrorAfterRelease(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte("hello1")
value := []byte("world1")
require.NoError(db.Put(key, value))
require.NoError(db.Close())
iterator := db.NewIterator()
require.NotNil(iterator)
iterator.Release()
require.False(iterator.Next())
require.Nil(iterator.Key())
require.Nil(iterator.Value())
iterErr := iterator.Error()
require.Error(iterErr)
require.Contains(iterErr.Error(), "closed")
}
// TestCompactNoPanic tests to make sure compact never panics.
func TestCompactNoPanic(t *testing.T, db db.Database) {
require := require.New(t)
key1 := []byte("hello1")
value1 := []byte("world1")
key2 := []byte("z")
value2 := []byte("world2")
key3 := []byte("hello3")
value3 := []byte("world3")
require.NoError(db.Put(key1, value1))
require.NoError(db.Put(key2, value2))
require.NoError(db.Put(key3, value3))
// Test compacting with nil bounds
require.NoError(db.Compact(nil, nil))
// Test compacting when start > end
require.NoError(db.Compact([]byte{2}, []byte{1}))
// Test compacting when start > largest key
require.NoError(db.Compact([]byte{255}, nil))
require.NoError(db.Close())
err := db.Compact(nil, nil)
require.ErrorIs(err, ErrClosed)
}
// TestAtomicClear - commented out as AtomicClear is not part of the standard Database interface
// func TestAtomicClear(t *testing.T, db db.Database) {
// testClear(t, db, func(db database.Database) error {
// return database.AtomicClear(db, db)
// })
// }
// TestClear - commented out as Clear is not part of the standard Database interface
// func TestClear(t *testing.T, db db.Database) {
// testClear(t, db, func(db database.Database) error {
// return database.Clear(db, math.MaxInt)
// })
// }
// testClear tests to make sure the deletion helper works as expected.
// Commented out as it uses Count which is not part of the standard Database interface
// func testClear(t *testing.T, db db.Database, clearF func(database.Database) error) {
// require := require.New(t)
// key1 := []byte("hello1")
// value1 := []byte("world1")
// key2 := []byte("z")
// value2 := []byte("world2")
// key3 := []byte("hello3")
// value3 := []byte("world3")
// require.NoError(db.Put(key1, value1))
// require.NoError(db.Put(key2, value2))
// require.NoError(db.Put(key3, value3))
// count, err := database.Count(db)
// require.NoError(err)
// require.Equal(3, count)
// require.NoError(clearF(db))
// count, err = database.Count(db)
// require.NoError(err)
// require.Zero(count)
// require.NoError(db.Close())
// }
// TestAtomicClearPrefix - commented out as AtomicClearPrefix is not part of the standard Database interface
// func TestAtomicClearPrefix(t *testing.T, db db.Database) {
// testClearPrefix(t, db, func(db database.Database, prefix []byte) error {
// return database.AtomicClearPrefix(db, db, prefix)
// })
// }
// TestClearPrefix - commented out as ClearPrefix is not part of the standard Database interface
// func TestClearPrefix(t *testing.T, db db.Database) {
// testClearPrefix(t, db, func(db database.Database, prefix []byte) error {
// return database.ClearPrefix(db, prefix, math.MaxInt)
// })
// }
// testClearPrefix tests to make sure prefix deletion works as expected.
// Commented out as it uses Count which is not part of the standard Database interface
// func testClearPrefix(t *testing.T, db db.Database, clearF func(database.Database, []byte) error) {
// require := require.New(t)
// key1 := []byte("hello1")
// value1 := []byte("world1")
// key2 := []byte("z")
// value2 := []byte("world2")
// key3 := []byte("hello3")
// value3 := []byte("world3")
// require.NoError(db.Put(key1, value1))
// require.NoError(db.Put(key2, value2))
// require.NoError(db.Put(key3, value3))
// count, err := database.Count(db)
// require.NoError(err)
// require.Equal(3, count)
// require.NoError(clearF(db, []byte("hello")))
// count, err = database.Count(db)
// require.NoError(err)
// require.Equal(1, count)
// has, err := db.Has(key1)
// require.NoError(err)
// require.False(has)
// has, err = db.Has(key2)
// require.NoError(err)
// require.True(has)
// has, err = db.Has(key3)
// require.NoError(err)
// require.False(has)
// require.NoError(db.Close())
// }
func TestModifyValueAfterPut(t *testing.T, db db.KeyValueReaderWriterDeleter) {
require := require.New(t)
key := []byte{1}
value := []byte{1, 2}
originalValue := slices.Clone(value)
require.NoError(db.Put(key, value))
// Modify the value that was Put into the database
// to see if the database copied the value correctly.
value[0] = 2
retrievedValue, err := db.Get(key)
require.NoError(err)
require.Equal(originalValue, retrievedValue)
}
func TestModifyValueAfterBatchPut(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte{1}
value := []byte{1, 2}
originalValue := slices.Clone(value)
batch := db.NewBatch()
require.NoError(batch.Put(key, value))
// Modify the value that was Put into the Batch and then Write the
// batch to the database.
value[0] = 2
require.NoError(batch.Write())
// Verify that the value written to the database contains matches the original
// value of the byte slice when Put was called.
retrievedValue, err := db.Get(key)
require.NoError(err)
require.Equal(originalValue, retrievedValue)
}
func TestModifyValueAfterBatchPutReplay(t *testing.T, db db.Database) {
require := require.New(t)
key := []byte{1}
value := []byte{1, 2}
originalValue := slices.Clone(value)
batch := db.NewBatch()
require.NoError(batch.Put(key, value))
// Modify the value that was Put into the Batch and then Write the
// batch to the database.
value[0] = 2
// Create a new batch and replay the batch onto this one before writing it to the DB.
replayBatch := db.NewBatch()
require.NoError(batch.Replay(replayBatch))
require.NoError(replayBatch.Write())
// Verify that the value written to the database contains matches the original
// value of the byte slice when Put was called.
retrievedValue, err := db.Get(key)
require.NoError(err)
require.Equal(originalValue, retrievedValue)
}
func TestConcurrentBatches(t *testing.T, db db.Database) {
numBatches := 10
keysPerBatch := 50
keySize := 32
valueSize := KiB
require.NoError(t, runConcurrentBatches(
db,
numBatches,
keysPerBatch,
keySize,
valueSize,
))
}
func TestManySmallConcurrentKVPairBatches(t *testing.T, db db.Database) {
numBatches := 100
keysPerBatch := 10
keySize := 10
valueSize := 10
require.NoError(t, runConcurrentBatches(
db,
numBatches,
keysPerBatch,
keySize,
valueSize,
))
}
func runConcurrentBatches(
database db.Database,
numBatches,
keysPerBatch,
keySize,
valueSize int,
) error {
batches := make([]db.Batch, 0, numBatches)
for range numBatches {
batches = append(batches, database.NewBatch())
}
for _, batch := range batches {
for range keysPerBatch {
key := crypto.RandomBytes(keySize)
value := crypto.RandomBytes(valueSize)
if err := batch.Put(key, value); err != nil {
return err
}
}
}
var eg errgroup.Group
for _, batch := range batches {
eg.Go(batch.Write)
}
return eg.Wait()
}
func TestPutGetEmpty(t *testing.T, db db.KeyValueReaderWriterDeleter) {
require := require.New(t)
key := []byte("hello")
require.NoError(db.Put(key, nil))
value, err := db.Get(key)
require.NoError(err)
require.Empty(value) // May be nil or empty byte slice.
require.NoError(db.Put(key, []byte{}))
value, err = db.Get(key)
require.NoError(err)
require.Empty(value) // May be nil or empty byte slice.
}
func FuzzKeyValue(f *testing.F, db db.KeyValueReaderWriterDeleter) {
f.Fuzz(func(t *testing.T, key []byte, value []byte) {
require := require.New(t)
require.NoError(db.Put(key, value))
exists, err := db.Has(key)
require.NoError(err)
require.True(exists)
gotVal, err := db.Get(key)
require.NoError(err)
require.True(bytes.Equal(value, gotVal))
require.NoError(db.Delete(key))
exists, err = db.Has(key)
require.NoError(err)
require.False(exists)
_, err = db.Get(key)
require.Equal(ErrNotFound, err)
})
}
func FuzzNewIteratorWithPrefix(f *testing.F, db db.Database) {
const (
maxKeyLen = 32
maxValueLen = 32
)
f.Fuzz(func(
t *testing.T,
randSeed int64,
prefix []byte,
numKeyValues uint,
) {
require := require.New(t)
r := rand.New(rand.NewSource(randSeed)) // #nosec G404
// Put a bunch of key-values
expected := map[string][]byte{}
for i := 0; i < int(numKeyValues); i++ {
key := make([]byte, r.Intn(maxKeyLen))
_, _ = r.Read(key) // #nosec G404
value := make([]byte, r.Intn(maxValueLen))
_, _ = r.Read(value) // #nosec G404
if len(value) == 0 {
// Consistently treat zero length values as nil
// so that we can compare [expected] and [got] with
// require.Equal, which treats nil and empty byte
// as being unequal, whereas the database treats
// them as being equal.
value = nil
}
if bytes.HasPrefix(key, prefix) {
expected[string(key)] = value
}
require.NoError(db.Put(key, value))
}
expectedList := slices.Collect(maps.Keys(expected))
slices.Sort(expectedList)
iter := db.NewIteratorWithPrefix(prefix)
defer iter.Release()
// Assert the iterator returns the expected key-values.
numIterElts := 0
for iter.Next() {
val := iter.Value()
if len(val) == 0 {
val = nil
}
require.Equal(expectedList[numIterElts], string(iter.Key()))
require.Equal(expected[string(iter.Key())], val)
numIterElts++
}
require.Len(expectedList, numIterElts)
// Clear the database for the next fuzz iteration.
// Commented out as AtomicClear is not part of the standard Database interface
// require.NoError(database.AtomicClear(db, db))
})
}
func FuzzNewIteratorWithStartAndPrefix(f *testing.F, db db.Database) {
const (
maxKeyLen = 32
maxValueLen = 32
)
f.Fuzz(func(
t *testing.T,
randSeed int64,
start []byte,
prefix []byte,
numKeyValues uint,
) {
require := require.New(t)
r := rand.New(rand.NewSource(randSeed)) // #nosec G404
expected := map[string][]byte{}
// Put a bunch of key-values
for i := 0; i < int(numKeyValues); i++ {
key := make([]byte, r.Intn(maxKeyLen))
_, _ = r.Read(key) // #nosec G404
value := make([]byte, r.Intn(maxValueLen))
_, _ = r.Read(value) // #nosec G404
if len(value) == 0 {
// Consistently treat zero length values as nil
// so that we can compare [expected] and [got] with
// require.Equal, which treats nil and empty byte
// as being unequal, whereas the database treats
// them as being equal.
value = nil
}
if bytes.HasPrefix(key, prefix) && bytes.Compare(key, start) >= 0 {
expected[string(key)] = value
}
require.NoError(db.Put(key, value))
}
expectedList := slices.Collect(maps.Keys(expected))
slices.Sort(expectedList)
iter := db.NewIteratorWithStartAndPrefix(start, prefix)
defer iter.Release()
// Assert the iterator returns the expected key-values.
numIterElts := 0
for iter.Next() {
val := iter.Value()
if len(val) == 0 {
val = nil
}
keyStr := string(iter.Key())
require.Equal(expectedList[numIterElts], keyStr)
require.Equal(expected[keyStr], val)
numIterElts++
}
require.Len(expectedList, numIterElts)
// Clear the database for the next fuzz iteration.
// Commented out as AtomicClear is not part of the standard Database interface
// require.NoError(database.AtomicClear(db, db))
})
}