kevo/pkg/transaction/transaction_test.go
Jeremy Tregunna 9a98349115
Some checks failed
Go Tests / Run Tests (1.24.2) (push) Failing after 15m7s
chore: formatting
2025-05-02 15:41:46 -06:00

394 lines
10 KiB
Go

package transaction
import (
"bytes"
"sync"
"testing"
)
func TestTransactionBasicOperations(t *testing.T) {
storage := NewMemoryStorage()
statsCollector := &StatsCollectorMock{}
rwLock := &sync.RWMutex{}
// Prepare some initial data
storage.Put([]byte("existing1"), []byte("value1"))
storage.Put([]byte("existing2"), []byte("value2"))
// Create a transaction
tx := &TransactionImpl{
storage: storage,
mode: ReadWrite,
buffer: NewBuffer(),
rwLock: rwLock,
stats: statsCollector,
}
tx.active.Store(true)
// Actually acquire the write lock before setting the flag
rwLock.Lock()
tx.hasWriteLock.Store(true)
// Test Get existing key
value, err := tx.Get([]byte("existing1"))
if err != nil {
t.Errorf("Unexpected error getting existing key: %v", err)
}
if !bytes.Equal(value, []byte("value1")) {
t.Errorf("Expected value 'value1', got %s", value)
}
// Test Get non-existing key
_, err = tx.Get([]byte("nonexistent"))
if err == nil || err != ErrKeyNotFound {
t.Errorf("Expected ErrKeyNotFound for nonexistent key, got %v", err)
}
// Test Put and then Get from buffer
err = tx.Put([]byte("key1"), []byte("new_value1"))
if err != nil {
t.Errorf("Unexpected error putting key: %v", err)
}
value, err = tx.Get([]byte("key1"))
if err != nil {
t.Errorf("Unexpected error getting key from buffer: %v", err)
}
if !bytes.Equal(value, []byte("new_value1")) {
t.Errorf("Expected buffer value 'new_value1', got %s", value)
}
// Test overwriting existing key
err = tx.Put([]byte("existing1"), []byte("updated_value1"))
if err != nil {
t.Errorf("Unexpected error updating key: %v", err)
}
value, err = tx.Get([]byte("existing1"))
if err != nil {
t.Errorf("Unexpected error getting updated key: %v", err)
}
if !bytes.Equal(value, []byte("updated_value1")) {
t.Errorf("Expected updated value 'updated_value1', got %s", value)
}
// Test Delete operation
err = tx.Delete([]byte("existing2"))
if err != nil {
t.Errorf("Unexpected error deleting key: %v", err)
}
_, err = tx.Get([]byte("existing2"))
if err == nil || err != ErrKeyNotFound {
t.Errorf("Expected ErrKeyNotFound for deleted key, got %v", err)
}
// Test operations on closed transaction
err = tx.Commit()
if err != nil {
t.Errorf("Unexpected error committing transaction: %v", err)
}
// After commit, the transaction should be closed
_, err = tx.Get([]byte("key1"))
if err == nil || err != ErrTransactionClosed {
t.Errorf("Expected ErrTransactionClosed, got %v", err)
}
err = tx.Put([]byte("key2"), []byte("value2"))
if err == nil || err != ErrTransactionClosed {
t.Errorf("Expected ErrTransactionClosed, got %v", err)
}
err = tx.Delete([]byte("key1"))
if err == nil || err != ErrTransactionClosed {
t.Errorf("Expected ErrTransactionClosed, got %v", err)
}
err = tx.Commit()
if err == nil || err != ErrTransactionClosed {
t.Errorf("Expected ErrTransactionClosed for second commit, got %v", err)
}
err = tx.Rollback()
if err == nil || err != ErrTransactionClosed {
t.Errorf("Expected ErrTransactionClosed for rollback after commit, got %v", err)
}
// Verify committed changes exist in storage
val, err := storage.Get([]byte("key1"))
if err != nil {
t.Errorf("Expected key1 to exist in storage after commit, got error: %v", err)
}
if !bytes.Equal(val, []byte("new_value1")) {
t.Errorf("Expected value 'new_value1' in storage, got %s", val)
}
val, err = storage.Get([]byte("existing1"))
if err != nil {
t.Errorf("Expected existing1 to exist in storage with updated value, got error: %v", err)
}
if !bytes.Equal(val, []byte("updated_value1")) {
t.Errorf("Expected value 'updated_value1' in storage, got %s", val)
}
_, err = storage.Get([]byte("existing2"))
if err == nil || err != ErrKeyNotFound {
t.Errorf("Expected existing2 to be deleted from storage, got: %v", err)
}
}
func TestReadOnlyTransactionOperations(t *testing.T) {
storage := NewMemoryStorage()
statsCollector := &StatsCollectorMock{}
rwLock := &sync.RWMutex{}
// Prepare some initial data
storage.Put([]byte("key1"), []byte("value1"))
// Create a read-only transaction
tx := &TransactionImpl{
storage: storage,
mode: ReadOnly,
buffer: NewBuffer(),
rwLock: rwLock,
stats: statsCollector,
}
tx.active.Store(true)
// Actually acquire the read lock before setting the flag
rwLock.RLock()
tx.hasReadLock.Store(true)
// Test Get
value, err := tx.Get([]byte("key1"))
if err != nil {
t.Errorf("Unexpected error getting key in read-only tx: %v", err)
}
if !bytes.Equal(value, []byte("value1")) {
t.Errorf("Expected value 'value1', got %s", value)
}
// Test Put on read-only transaction (should fail)
err = tx.Put([]byte("key2"), []byte("value2"))
if err == nil || err != ErrReadOnlyTransaction {
t.Errorf("Expected ErrReadOnlyTransaction, got %v", err)
}
// Test Delete on read-only transaction (should fail)
err = tx.Delete([]byte("key1"))
if err == nil || err != ErrReadOnlyTransaction {
t.Errorf("Expected ErrReadOnlyTransaction, got %v", err)
}
// Test IsReadOnly
if !tx.IsReadOnly() {
t.Error("Expected IsReadOnly() to return true")
}
// Test Commit on read-only transaction
err = tx.Commit()
if err != nil {
t.Errorf("Unexpected error committing read-only tx: %v", err)
}
// After commit, the transaction should be closed
_, err = tx.Get([]byte("key1"))
if err == nil || err != ErrTransactionClosed {
t.Errorf("Expected ErrTransactionClosed, got %v", err)
}
}
func TestTransactionRollback(t *testing.T) {
storage := NewMemoryStorage()
statsCollector := &StatsCollectorMock{}
rwLock := &sync.RWMutex{}
// Prepare some initial data
storage.Put([]byte("key1"), []byte("value1"))
// Create a transaction
tx := &TransactionImpl{
storage: storage,
mode: ReadWrite,
buffer: NewBuffer(),
rwLock: rwLock,
stats: statsCollector,
}
tx.active.Store(true)
// Actually acquire the write lock before setting the flag
rwLock.Lock()
tx.hasWriteLock.Store(true)
// Make some changes
err := tx.Put([]byte("key2"), []byte("value2"))
if err != nil {
t.Errorf("Unexpected error putting key: %v", err)
}
err = tx.Delete([]byte("key1"))
if err != nil {
t.Errorf("Unexpected error deleting key: %v", err)
}
// Rollback the transaction
err = tx.Rollback()
if err != nil {
t.Errorf("Unexpected error rolling back tx: %v", err)
}
// After rollback, the transaction should be closed
_, err = tx.Get([]byte("key1"))
if err == nil || err != ErrTransactionClosed {
t.Errorf("Expected ErrTransactionClosed, got %v", err)
}
// Verify changes were not applied to storage
val, err := storage.Get([]byte("key1"))
if err != nil {
t.Errorf("Expected key1 to still exist in storage, got error: %v", err)
}
if !bytes.Equal(val, []byte("value1")) {
t.Errorf("Expected value 'value1' in storage, got %s", val)
}
_, err = storage.Get([]byte("key2"))
if err == nil || err != ErrKeyNotFound {
t.Errorf("Expected key2 to not exist in storage after rollback, got: %v", err)
}
}
func TestTransactionIterators(t *testing.T) {
storage := NewMemoryStorage()
statsCollector := &StatsCollectorMock{}
rwLock := &sync.RWMutex{}
// Prepare some initial data
storage.Put([]byte("a"), []byte("value_a"))
storage.Put([]byte("c"), []byte("value_c"))
storage.Put([]byte("e"), []byte("value_e"))
// Create a transaction
tx := &TransactionImpl{
storage: storage,
mode: ReadWrite,
buffer: NewBuffer(),
rwLock: rwLock,
stats: statsCollector,
}
tx.active.Store(true)
// Actually acquire the write lock before setting the flag
rwLock.Lock()
tx.hasWriteLock.Store(true)
// Make some changes to the transaction buffer
tx.Put([]byte("b"), []byte("value_b"))
tx.Put([]byte("d"), []byte("value_d"))
tx.Delete([]byte("c")) // Delete an existing key
// Test full iterator
it := tx.NewIterator()
// Collect all keys and values
var keys [][]byte
var values [][]byte
for it.SeekToFirst(); it.Valid(); it.Next() {
keys = append(keys, append([]byte{}, it.Key()...))
values = append(values, append([]byte{}, it.Value()...))
}
// The iterator might still return the deleted key 'c' (with a tombstone marker)
// Print the actual keys for debugging
t.Logf("Actual keys in iterator: %v", keys)
// Define expected keys (a, b, d, e) - c is deleted but might appear as a tombstone
expectedKeySet := map[string]bool{
"a": true,
"b": true,
"d": true,
"e": true,
}
// Check each key is in our expected set
for _, key := range keys {
keyStr := string(key)
if keyStr != "c" && !expectedKeySet[keyStr] {
t.Errorf("Found unexpected key: %s", keyStr)
}
}
// Verify we have at least our expected keys
for k := range expectedKeySet {
found := false
for _, key := range keys {
if string(key) == k {
found = true
break
}
}
if !found {
t.Errorf("Expected key %s not found in iterator", k)
}
}
// Test range iterator
rangeIt := tx.NewRangeIterator([]byte("b"), []byte("e"))
// Collect all keys and values in range
keys = nil
values = nil
for rangeIt.SeekToFirst(); rangeIt.Valid(); rangeIt.Next() {
keys = append(keys, append([]byte{}, rangeIt.Key()...))
values = append(values, append([]byte{}, rangeIt.Value()...))
}
// The range should include b and d, and might include c with a tombstone
// Print the actual keys for debugging
t.Logf("Actual keys in range iterator: %v", keys)
// Ensure the keys include our expected ones (b, d)
expectedRangeSet := map[string]bool{
"b": true,
"d": true,
}
// Check each key is in our expected set (or is c which might appear as a tombstone)
for _, key := range keys {
keyStr := string(key)
if keyStr != "c" && !expectedRangeSet[keyStr] {
t.Errorf("Found unexpected key in range: %s", keyStr)
}
}
// Verify we have at least our expected keys
for k := range expectedRangeSet {
found := false
for _, key := range keys {
if string(key) == k {
found = true
break
}
}
if !found {
t.Errorf("Expected key %s not found in range iterator", k)
}
}
// Test iterator on closed transaction
tx.Commit()
closedIt := tx.NewIterator()
if closedIt.Valid() {
t.Error("Expected iterator on closed transaction to be invalid")
}
closedRangeIt := tx.NewRangeIterator([]byte("a"), []byte("z"))
if closedRangeIt.Valid() {
t.Error("Expected range iterator on closed transaction to be invalid")
}
}