285 lines
6.9 KiB
Go
285 lines
6.9 KiB
Go
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
|
|
}
|