asg-server/pkg/cache/memory_edge_test.go
2025-09-24 13:47:55 +07:00

392 lines
8.9 KiB
Go

package cache_test
import (
"context"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/android-sms-gateway/server/pkg/cache"
)
func TestMemoryCache_ZeroTTL(t *testing.T) {
// Test cache with zero TTL (no expiration)
cache := cache.NewMemory(0)
ctx := context.Background()
key := "zero-ttl-key"
value := "zero-ttl-value"
err := cache.Set(ctx, key, value)
if err != nil {
t.Fatalf("Set failed: %v", err)
}
// Wait some time to ensure no expiration
time.Sleep(100 * time.Millisecond)
retrieved, err := cache.Get(ctx, key)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if retrieved != value {
t.Errorf("Expected %s, got %s", value, retrieved)
}
}
func TestMemoryCache_ImmediateExpiration(t *testing.T) {
// Test c with very short TTL
c := cache.NewMemory(0)
ctx := context.Background()
key := "expiring-key"
value := "expiring-value"
ttl := 1 * time.Millisecond
err := c.Set(ctx, key, value, cache.WithTTL(ttl))
if err != nil {
t.Fatalf("Set failed: %v", err)
}
// Wait for expiration
time.Sleep(2 * ttl)
_, err = c.Get(ctx, key)
if err != cache.ErrKeyExpired {
t.Errorf("Expected ErrKeyExpired, got %v", err)
}
}
func TestMemoryCache_NilContext(t *testing.T) {
// Test cache operations with nil context
cache := cache.NewMemory(0)
key := "nil-context-key"
value := "nil-context-value"
err := cache.Set(nil, key, value) //nolint:staticcheck
if err != nil {
t.Fatalf("Set with nil context failed: %v", err)
}
retrieved, err := cache.Get(nil, key) //nolint:staticcheck
if err != nil {
t.Fatalf("Get with nil context failed: %v", err)
}
if retrieved != value {
t.Errorf("Expected %s, got %s", value, retrieved)
}
}
func TestMemoryCache_EmptyKey(t *testing.T) {
// Test cache operations with empty key
cache := cache.NewMemory(0)
ctx := context.Background()
key := ""
value := "empty-key-value"
err := cache.Set(ctx, key, value)
if err != nil {
t.Fatalf("Set with empty key failed: %v", err)
}
retrieved, err := cache.Get(ctx, key)
if err != nil {
t.Fatalf("Get with empty key failed: %v", err)
}
if retrieved != value {
t.Errorf("Expected %s, got %s", value, retrieved)
}
}
func TestMemoryCache_OverwriteWithDifferentTTL(t *testing.T) {
// Test overwriting a key with different TTL
c := cache.NewMemory(0)
ctx := context.Background()
key := "ttl-key"
value1 := "value1"
value2 := "value2"
// Set with short TTL
err := c.Set(ctx, key, value1, cache.WithTTL(100*time.Millisecond))
if err != nil {
t.Fatalf("Set failed: %v", err)
}
// Overwrite with longer TTL
err = c.Set(ctx, key, value2, cache.WithTTL(1*time.Second))
if err != nil {
t.Fatalf("Set overwrite failed: %v", err)
}
retrieved, err := c.Get(ctx, key)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if retrieved != value2 {
t.Errorf("Expected %s, got %s", value2, retrieved)
}
// Wait for short TTL to expire but not long TTL
time.Sleep(200 * time.Millisecond)
retrieved, err = c.Get(ctx, key)
if err != nil {
t.Fatalf("Get after partial wait failed: %v", err)
}
if retrieved != value2 {
t.Errorf("Expected %s after partial wait, got %s", value2, retrieved)
}
}
func TestMemoryCache_MixedTTLScenarios(t *testing.T) {
// Test various TTL scenarios
c := cache.NewMemory(0)
ctx := context.Background()
// Set multiple keys with different TTLs
keys := map[string]time.Duration{
"no-ttl": 0,
"short-ttl": 50 * time.Millisecond,
"medium-ttl": 200 * time.Millisecond,
"long-ttl": 500 * time.Millisecond,
}
for key, ttl := range keys {
value := "value-" + key
var err error
if ttl > 0 {
err = c.Set(ctx, key, value, cache.WithTTL(ttl))
} else {
err = c.Set(ctx, key, value)
}
if err != nil {
t.Fatalf("Set %s failed: %v", key, err)
}
}
// Verify all keys are present initially
for key := range keys {
_, err := c.Get(ctx, key)
if err != nil {
t.Fatalf("Get %s failed: %v", key, err)
}
}
// Wait for short TTL to expire
time.Sleep(100 * time.Millisecond)
// Short TTL key should be expired, others should still be there
_, err := c.Get(ctx, "short-ttl")
if err != cache.ErrKeyExpired {
t.Errorf("Expected ErrKeyExpired for short-ttl, got %v", err)
}
for key := range keys {
if key == "short-ttl" {
continue
}
_, err := c.Get(ctx, key)
if err != nil {
t.Errorf("Get %s failed: %v", key, err)
}
}
// Wait for medium TTL to expire
time.Sleep(150 * time.Millisecond)
// Medium TTL key should be expired, others should still be there
_, err = c.Get(ctx, "medium-ttl")
if err != cache.ErrKeyExpired {
t.Errorf("Expected ErrKeyExpired for medium-ttl, got %v", err)
}
for key := range keys {
if key == "short-ttl" || key == "medium-ttl" {
continue
}
_, err := c.Get(ctx, key)
if err != nil {
t.Errorf("Get %s failed: %v", key, err)
}
}
}
func TestMemoryCache_RapidOperations(t *testing.T) {
// Test rapid c operations
c := cache.NewMemory(0)
ctx := context.Background()
const numOperations = 1000
const duration = 100 * time.Millisecond
start := time.Now()
opsCompleted := 0
for i := range numOperations {
// Alternate between set and get
if i%2 == 0 {
key := "rapid-key-" + strconv.Itoa(i)
value := "rapid-value-" + strconv.Itoa(i)
err := c.Set(ctx, key, value)
if err != nil {
t.Errorf("Set failed: %v", err)
}
} else {
key := "rapid-key-" + strconv.Itoa(i-1)
_, err := c.Get(ctx, key)
if err != nil && err != cache.ErrKeyNotFound {
t.Errorf("Get failed: %v", err)
}
}
opsCompleted++
}
durationTaken := time.Since(start)
t.Logf("Completed %d operations in %v (%.2f ops/ms)", opsCompleted, durationTaken, float64(opsCompleted)/float64(durationTaken.Milliseconds()))
// Verify operations completed within reasonable time
if durationTaken > 2*duration {
t.Errorf("Operations took too long: %v", durationTaken)
}
}
func TestMemoryCache_CleanupOnEmptyCache(t *testing.T) {
// Test cleanup operation on empty cache
cache := cache.NewMemory(0)
ctx := context.Background()
err := cache.Cleanup(ctx)
if err != nil {
t.Fatalf("Cleanup failed: %v", err)
}
// Should still work normally after cleanup
key := "post-cleanup-key"
value := "post-cleanup-value"
err = cache.Set(ctx, key, value)
if err != nil {
t.Fatalf("Set after cleanup failed: %v", err)
}
retrieved, err := cache.Get(ctx, key)
if err != nil {
t.Fatalf("Get after cleanup failed: %v", err)
}
if retrieved != value {
t.Errorf("Expected %s, got %s", value, retrieved)
}
}
func TestMemoryCache_DrainWithExpiredItems(t *testing.T) {
// Test drain operation with mix of expired and non-expired items
c := cache.NewMemory(0)
ctx := context.Background()
// Set non-expired item
err := c.Set(ctx, "valid-key", "valid-value")
if err != nil {
t.Fatalf("Set valid key failed: %v", err)
}
// Set expired item
err = c.Set(ctx, "expired-key", "expired-value", cache.WithTTL(1*time.Millisecond))
if err != nil {
t.Fatalf("Set expired key failed: %v", err)
}
// Wait for expiration
time.Sleep(10 * time.Millisecond)
// Drain should only return non-expired items
items, err := c.Drain(ctx)
if err != nil {
t.Fatalf("Drain failed: %v", err)
}
if len(items) != 1 {
t.Errorf("Expected 1 item in drain result, got %d", len(items))
}
if items["valid-key"] != "valid-value" {
t.Errorf("Expected valid-value, got %s", items["valid-key"])
}
// Verify expired item is gone (should be completely removed, not just expired)
_, err = c.Get(ctx, "expired-key")
if err != cache.ErrKeyNotFound {
t.Errorf("Expected ErrKeyNotFound, got %v", err)
}
}
func TestMemoryCache_ExtremeKeyLength(t *testing.T) {
// Test with very long keys
cache := cache.NewMemory(0)
ctx := context.Background()
// Create a very long key (1KB)
longKey := strings.Repeat("a", 1024)
value := "extreme-key-value"
err := cache.Set(ctx, longKey, value)
if err != nil {
t.Fatalf("Set with long key failed: %v", err)
}
retrieved, err := cache.Get(ctx, longKey)
if err != nil {
t.Fatalf("Get with long key failed: %v", err)
}
if retrieved != value {
t.Errorf("Expected %s, got %s", value, retrieved)
}
}
func TestMemoryCache_RaceConditionWithExpiration(t *testing.T) {
// Test race conditions between expiration and access
c := cache.NewMemory(0)
ctx := context.Background()
key := "race-expire-key"
value := "race-expire-value"
ttl := 10 * time.Millisecond
// Set item with short TTL
err := c.Set(ctx, key, value, cache.WithTTL(ttl))
if err != nil {
t.Fatalf("Set failed: %v", err)
}
const numGoroutines = 50
var wg sync.WaitGroup
// Launch goroutines that try to access the key while it's expiring
for i := range numGoroutines {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Wait for the item to be close to expiration with some jitter
jitter := time.Duration(id%3) * time.Millisecond
time.Sleep(ttl - 2*time.Millisecond + jitter)
// Try to get the item
_, err := c.Get(ctx, key)
if err != nil && err != cache.ErrKeyExpired && err != cache.ErrKeyNotFound {
t.Errorf("Get failed: %v", err)
}
}(i)
}
wg.Wait()
}