tests: cache

This commit is contained in:
Felipe M 2025-03-23 23:37:26 +01:00
parent 92732f9682
commit 597feb7b54
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8
8 changed files with 822 additions and 32 deletions

285
cache/memory_test.go vendored Normal file
View file

@ -0,0 +1,285 @@
package cache
import (
"sync"
"testing"
"time"
"git.nakama.town/fmartingr/gotoolkit/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewMemoryCache(t *testing.T) {
cache := NewMemoryCache()
require.NotNil(t, cache, "NewMemoryCache returned nil")
require.NotNil(t, cache.data, "Cache data map was not initialized")
}
func TestMemoryCache_Set(t *testing.T) {
cache := NewMemoryCache()
// Test basic set
err := cache.Set("key1", "value1")
require.NoError(t, err, "Set returned unexpected error")
// Verify the item was stored correctly
cache.dataMu.RLock()
item, exists := cache.data["key1"]
cache.dataMu.RUnlock()
require.True(t, exists, "Set did not store the item in the cache")
assert.Equal(t, "key1", item.Key, "Wrong key stored")
assert.Equal(t, "value1", item.Value, "Wrong value stored")
assert.Nil(t, item.TTL, "TTL should be nil")
// Test set with TTL
withTTL := func(item *model.CacheItem) {
expiry := time.Now().Add(10 * time.Minute)
item.TTL = &expiry
}
err = cache.Set("key2", "value2", withTTL)
require.NoError(t, err, "Set with TTL returned unexpected error")
// Verify the item with TTL was stored correctly
cache.dataMu.RLock()
itemWithTTL, exists := cache.data["key2"]
cache.dataMu.RUnlock()
require.True(t, exists, "Set did not store the item with TTL in the cache")
assert.NotNil(t, itemWithTTL.TTL, "TTL option was not applied")
}
func TestMemoryCache_SetTableDriven(t *testing.T) {
tests := []struct {
name string
key string
value interface{}
ttl *time.Duration
}{
{
name: "string value without TTL",
key: "string-key",
value: "string-value",
ttl: nil,
},
{
name: "int value without TTL",
key: "int-key",
value: 42,
ttl: nil,
},
{
name: "struct value without TTL",
key: "struct-key",
value: struct{ Name string }{"test"},
ttl: nil,
},
{
name: "string value with TTL",
key: "string-key-ttl",
value: "string-value-ttl",
ttl: ptrDuration(5 * time.Minute),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache := NewMemoryCache()
var opts []model.CacheOption
if tt.ttl != nil {
opts = append(opts, func(item *model.CacheItem) {
expiry := time.Now().Add(*tt.ttl)
item.TTL = &expiry
})
}
err := cache.Set(tt.key, tt.value, opts...)
assert.NoError(t, err)
cache.dataMu.RLock()
item, exists := cache.data[tt.key]
cache.dataMu.RUnlock()
assert.True(t, exists)
assert.Equal(t, tt.key, item.Key)
assert.Equal(t, tt.value, item.Value)
if tt.ttl != nil {
assert.NotNil(t, item.TTL)
} else {
assert.Nil(t, item.TTL)
}
})
}
}
func TestMemoryCache_Get(t *testing.T) {
cache := NewMemoryCache()
// Test getting non-existent key
_, err := cache.Get("nonexistent")
assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Wrong error returned for non-existent key")
// Test getting existing key
require.NoError(t, cache.Set("key1", "value1"))
value, err := cache.Get("key1")
require.NoError(t, err, "Get returned unexpected error")
assert.Equal(t, "value1", value, "Wrong value returned")
// Test getting expired key
expiredTTL := func(item *model.CacheItem) {
expiry := time.Now().Add(-1 * time.Minute) // Past time
item.TTL = &expiry
}
require.NoError(t, cache.Set("expired", "value", expiredTTL))
_, err = cache.Get("expired")
assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Wrong error returned for expired item")
// Verify the expired key was deleted
cache.dataMu.RLock()
_, exists := cache.data["expired"]
cache.dataMu.RUnlock()
assert.False(t, exists, "Expired item was not deleted during Get")
}
func TestMemoryCache_GetTableDriven(t *testing.T) {
tests := []struct {
name string
setupKey string
setupValue any
setupTTL *time.Duration
getKey string
expectValue any
expectError error
}{
{
name: "get existing key",
setupKey: "test-key",
setupValue: "test-value",
getKey: "test-key",
expectValue: "test-value",
expectError: nil,
},
{
name: "get non-existent key",
setupKey: "test-key",
setupValue: "test-value",
getKey: "different-key",
expectValue: nil,
expectError: model.ErrCacheKeyDontExist,
},
{
name: "get expired key",
setupKey: "expired-key",
setupValue: "expired-value",
setupTTL: ptrDuration(-1 * time.Minute), // Negative to create an expired key
getKey: "expired-key",
expectValue: nil,
expectError: model.ErrCacheKeyDontExist,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache := NewMemoryCache()
// Setup
var opts []model.CacheOption
if tt.setupTTL != nil {
opts = append(opts, func(item *model.CacheItem) {
expiry := time.Now().Add(*tt.setupTTL)
item.TTL = &expiry
})
}
if tt.setupKey != "" {
err := cache.Set(tt.setupKey, tt.setupValue, opts...)
require.NoError(t, err)
}
// Test
value, err := cache.Get(tt.getKey)
if tt.expectError != nil {
assert.ErrorIs(t, err, tt.expectError)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectValue, value)
// If it was an expired key, ensure it's deleted
if tt.setupTTL != nil && *tt.setupTTL < 0 {
cache.dataMu.RLock()
_, exists := cache.data[tt.setupKey]
cache.dataMu.RUnlock()
assert.False(t, exists, "Expired item was not deleted during Get")
}
})
}
}
func TestMemoryCache_Delete(t *testing.T) {
cache := NewMemoryCache()
// Setup test data
require.NoError(t, cache.Set("key1", "value1"))
// Test deleting existing key
err := cache.Delete("key1")
require.NoError(t, err, "Delete returned unexpected error")
// Verify the key was deleted
cache.dataMu.RLock()
_, exists := cache.data["key1"]
cache.dataMu.RUnlock()
assert.False(t, exists, "Item was not deleted from cache")
// Test deleting non-existent key (should not error)
err = cache.Delete("nonexistent")
assert.NoError(t, err, "Delete on non-existent key returned error")
}
func TestMemoryCache_Concurrency(t *testing.T) {
cache := NewMemoryCache()
const goroutines = 10
const operations = 100
wg := sync.WaitGroup{}
wg.Add(goroutines * 2)
// Writers
for i := range goroutines {
go func(id int) {
for j := range operations {
key := "key" + string(rune('A'+id)) + string(rune('0'+j%10))
err := cache.Set(key, j)
assert.NoError(t, err)
}
wg.Done()
}(i)
}
// Readers
for i := range goroutines {
go func(id int) {
for j := range operations {
key := "key" + string(rune('A'+id)) + string(rune('0'+j%10))
_, _ = cache.Get(key) // We ignore errors here as keys might not exist yet
}
wg.Done()
}(i)
}
// Wait for all goroutines to complete
wg.Wait()
// If we reached here without deadlock or panic, the test passes
}
// Helper function to create duration pointers
func ptrDuration(d time.Duration) *time.Duration {
return &d
}