package gocache import ( "bytes" "fmt" "math/rand" "strconv" "strings" "testing" "time" ) func TestNewCache(t *testing.T) { cache := NewCache().WithMaxSize(1234).WithEvictionPolicy(LeastRecentlyUsed) if cache.MaxMemoryUsage() != NoMaxMemoryUsage { t.Error("shouldn't have a max memory usage configured") } if cache.EvictionPolicy() != LeastRecentlyUsed { t.Error("should've had a LeastRecentlyUsed eviction policy") } if cache.defaultTTL != NoExpiration { t.Error("should've had a default TTL of NoExpiration") } if cache.MaxSize() != 1234 { t.Error("should've had a max cache size of 1234") } if cache.MemoryUsage() != 0 { t.Error("should've had a memory usage of 0") } } func TestCache_Stats(t *testing.T) { cache := NewCache().WithMaxSize(1234).WithEvictionPolicy(LeastRecentlyUsed) cache.Set("key", "value") if cache.Stats().Hits != 0 { t.Error("should have 0 hits") } if cache.Stats().Misses != 0 { t.Error("should have 0 misses") } cache.Get("key") if cache.Stats().Hits != 1 { t.Error("should have 1 hit") } if cache.Stats().Misses != 0 { t.Error("should have 0 misses") } cache.Get("key-that-does-not-exist") if cache.Stats().Hits != 1 { t.Error("should have 1 hit") } if cache.Stats().Misses != 1 { t.Error("should have 1 miss") } } func TestCache_Get(t *testing.T) { cache := NewCache().WithMaxSize(10) cache.Set("key", "value") value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if value != "value" { t.Errorf("expected: %s, but got: %s", "value", value) } } func TestCache_GetExpired(t *testing.T) { cache := NewCache() cache.SetWithTTL("key", "value", time.Millisecond) time.Sleep(2 * time.Millisecond) _, ok := cache.Get("key") if ok { t.Error("expected key to be expired") } } func TestCache_GetEntryThatHasNotExpiredYet(t *testing.T) { cache := NewCache() cache.SetWithTTL("key", "value", time.Hour) _, ok := cache.Get("key") if !ok { t.Error("expected key to not have expired") } } func TestCache_GetValue(t *testing.T) { cache := NewCache().WithMaxSize(10) cache.Set("key", "value") value := cache.GetValue("key") if value != "value" { t.Errorf("expected: %s, but got: %s", "value", value) } } func TestCache_GetByKeys(t *testing.T) { cache := NewCache().WithMaxSize(10) cache.Set("key1", "value1") cache.Set("key2", "value2") keyValues := cache.GetByKeys([]string{"key1", "key2", "key3"}) if len(keyValues) != 3 { t.Error("expected length of map to be 3") } if keyValues["key1"] != "value1" { t.Errorf("expected: %s, but got: %s", "value1", keyValues["key1"]) } if keyValues["key2"] != "value2" { t.Errorf("expected: %s, but got: %s", "value2", keyValues["key2"]) } if value, ok := keyValues["key3"]; !ok || value != nil { t.Errorf("expected key3 to exist and be nil, but got: %s", value) } } func TestCache_GetAll(t *testing.T) { cache := NewCache().WithMaxSize(10) cache.Set("key1", "value1") cache.Set("key2", "value2") keyValues := cache.GetAll() if len(keyValues) != 2 { t.Error("expected length of map to be 2") } if keyValues["key1"] != "value1" { t.Errorf("expected: %s, but got: %s", "value1", keyValues["key1"]) } if keyValues["key2"] != "value2" { t.Errorf("expected: %s, but got: %s", "value2", keyValues["key2"]) } } func TestCache_GetAllWhenOneKeyIsExpired(t *testing.T) { cache := NewCache().WithMaxSize(10) cache.Set("key1", "value1") cache.Set("key2", "value2") cache.SetWithTTL("key3", "value3", time.Nanosecond) time.Sleep(time.Millisecond) keyValues := cache.GetAll() if len(keyValues) != 2 { t.Error("expected length of map to be 2") } if keyValues["key1"] != "value1" { t.Errorf("expected: %s, but got: %s", "value1", keyValues["key1"]) } if keyValues["key2"] != "value2" { t.Errorf("expected: %s, but got: %s", "value2", keyValues["key2"]) } } func TestCache_GetKeysByPattern(t *testing.T) { // All keys match testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4"}, "key*", 0, 4) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4"}, "*y*", 0, 4) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4"}, "*key*", 0, 4) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4"}, "*", 0, 4) // All keys match but limit is reached testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4"}, "*", 2, 2) // Some keys match testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4", "key11"}, "key1*", 0, 2) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4", "key11"}, "*key1*", 0, 2) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4", "key11", "key111"}, "key1*", 0, 3) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4", "key11", "key111"}, "key11*", 0, 2) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4", "key11", "key111"}, "*11*", 0, 2) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4", "key11", "key111"}, "k*1*", 0, 3) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4", "key11", "key111"}, "*k*1", 0, 3) // No keys match testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4"}, "image*", 0, 0) testGetKeysByPattern(t, []string{"key1", "key2", "key3", "key4"}, "?", 0, 0) } func testGetKeysByPattern(t *testing.T, keys []string, pattern string, limit, expectedMatchingKeys int) { cache := NewCache().WithMaxSize(len(keys)) for _, key := range keys { cache.Set(key, key) } matchingKeys := cache.GetKeysByPattern(pattern, limit) if len(matchingKeys) != expectedMatchingKeys { t.Errorf("expected to have %d keys to match pattern '%s', got %d", expectedMatchingKeys, pattern, len(matchingKeys)) } } func TestCache_GetKeysByPatternWithExpiredKey(t *testing.T) { cache := NewCache().WithMaxSize(10) cache.SetWithTTL("key", "value", 10*time.Millisecond) // The cache entry shouldn't have expired yet, so GetKeysByPattern should return 1 key if matchingKeys := cache.GetKeysByPattern("*", 0); len(matchingKeys) != 1 { t.Errorf("expected to have %d keys to match pattern '%s', got %d", 1, "*", len(matchingKeys)) } time.Sleep(30 * time.Millisecond) // Since the key expired, the same call should return 0 keys instead of 1 if matchingKeys := cache.GetKeysByPattern("*", 0); len(matchingKeys) != 0 { t.Errorf("expected to have %d keys to match pattern '%s', got %d", 0, "*", len(matchingKeys)) } } func TestCache_Set(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.Set("key", "value") value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if value != "value" { t.Errorf("expected: %s, but got: %s", "value", value) } cache.Set("key", "newvalue") value, ok = cache.Get("key") if !ok { t.Error("expected key to exist") } if value != "newvalue" { t.Errorf("expected: %s, but got: %s", "newvalue", value) } } func TestCache_SetDifferentTypesOfData(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.Set("key", 1) value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if value != 1 { t.Errorf("expected: %v, but got: %v", 1, value) } cache.Set("key", struct{ Test string }{Test: "test"}) value, ok = cache.Get("key") if !ok { t.Error("expected key to exist") } if value.(struct{ Test string }) != struct{ Test string }{Test: "test"} { t.Errorf("expected: %s, but got: %s", "newvalue", value) } } func TestCache_SetGetInt(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.Set("key", 1) value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if value != 1 { t.Errorf("expected: %v, but got: %v", 1, value) } cache.Set("key", 2.1) value, ok = cache.Get("key") if !ok { t.Error("expected key to exist") } if value != 2.1 { t.Errorf("expected: %v, but got: %v", 2.1, value) } } func TestCache_SetGetBool(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.Set("key", true) value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if value != true { t.Errorf("expected: %v, but got: %v", true, value) } } func TestCache_SetGetByteSlice(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.Set("key", []byte("hey")) value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if bytes.Compare(value.([]byte), []byte("hey")) != 0 { t.Errorf("expected: %v, but got: %v", []byte("hey"), value) } } func TestCache_SetGetStringSlice(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.Set("key", []string{"john", "doe"}) value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if value.([]string)[0] != "john" { t.Errorf("expected: %v, but got: %v", "john", value) } if value.([]string)[1] != "doe" { t.Errorf("expected: %v, but got: %v", "doe", value) } } func TestCache_SetGetStruct(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) type Custom struct { Int int Uint uint Float32 float32 String string Strings []string Nested struct { String string } } cache.Set("key", Custom{ Int: 111, Uint: 222, Float32: 123.456, String: "hello", Strings: []string{"s1", "s2"}, Nested: struct{ String string }{String: "nested field"}, }) value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if ExpectedValue := 111; value.(Custom).Int != ExpectedValue { t.Errorf("expected: %v, but got: %v", ExpectedValue, value) } if ExpectedValue := uint(222); value.(Custom).Uint != ExpectedValue { t.Errorf("expected: %v, but got: %v", ExpectedValue, value) } if ExpectedValue := float32(123.456); value.(Custom).Float32 != ExpectedValue { t.Errorf("expected: %v, but got: %v", ExpectedValue, value) } if ExpectedValue := "hello"; value.(Custom).String != ExpectedValue { t.Errorf("expected: %v, but got: %v", ExpectedValue, value) } if ExpectedValue := "s1"; value.(Custom).Strings[0] != ExpectedValue { t.Errorf("expected: %v, but got: %v", ExpectedValue, value) } if ExpectedValue := "s2"; value.(Custom).Strings[1] != ExpectedValue { t.Errorf("expected: %v, but got: %v", ExpectedValue, value) } if ExpectedValue := "nested field"; value.(Custom).Nested.String != ExpectedValue { t.Errorf("expected: %v, but got: %v", ExpectedValue, value) } } func TestCache_SetAll(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.SetAll(map[string]any{"k1": "v1", "k2": "v2"}) value, ok := cache.Get("k1") if !ok { t.Error("expected key to exist") } if value != "v1" { t.Errorf("expected: %s, but got: %s", "v1", value) } value, ok = cache.Get("k2") if !ok { t.Error("expected key to exist") } if value != "v2" { t.Errorf("expected: %s, but got: %s", "v2", value) } cache.SetAll(map[string]any{"k1": "updated"}) value, ok = cache.Get("k1") if !ok { t.Error("expected key to exist") } if value != "updated" { t.Errorf("expected: %s, but got: %s", "updated", value) } } func TestCache_SetWithTTL(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.SetWithTTL("key", "value", NoExpiration) value, ok := cache.Get("key") if !ok { t.Error("expected key to exist") } if value != "value" { t.Errorf("expected: %s, but got: %s", "value", value) } } func TestCache_SetWithTTLWhenTTLIsNegative(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.SetWithTTL("key", "value", -12345) _, ok := cache.Get("key") if ok { t.Error("expected key to not exist, because there's no point in creating a cache entry that has a negative TTL") } } func TestCache_SetWithTTLWhenTTLIsZero(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.SetWithTTL("key", "value", 0) _, ok := cache.Get("key") if ok { t.Error("expected key to not exist, because there's no point in creating a cache entry that has a TTL of 0") } } func TestCache_SetWithTTLWhenTTLIsZeroAndEntryAlreadyExists(t *testing.T) { cache := NewCache().WithMaxSize(NoMaxSize) cache.SetWithTTL("key", "value", NoExpiration) cache.SetWithTTL("key", "value", 0) _, ok := cache.Get("key") if ok { t.Error("expected key to not exist, because there's the entry was created with a TTL of 0, so it should have been deleted immediately") } } func TestCache_EvictionsRespectMaxSize(t *testing.T) { cache := NewCache().WithMaxSize(5) for n := 0; n < 10; n++ { cache.Set(fmt.Sprintf("test_%d", n), []byte("value")) } count := cache.Count() if count > 5 { t.Error("Max size was set to 5, but the cache size reached a size of", count) } } func TestCache_EvictionsWithFIFO(t *testing.T) { cache := NewCache().WithMaxSize(3).WithEvictionPolicy(FirstInFirstOut) cache.Set("1", []byte("value")) cache.Set("2", []byte("value")) cache.Set("3", []byte("value")) _, _ = cache.Get("1") cache.Set("4", []byte("value")) _, ok := cache.Get("1") if ok { t.Error("expected key 1 to have been removed, because FIFO") } } func TestCache_EvictionsWithLRU(t *testing.T) { cache := NewCache().WithMaxSize(3).WithEvictionPolicy(LeastRecentlyUsed) cache.Set("1", []byte("value")) cache.Set("2", []byte("value")) cache.Set("3", []byte("value")) _, _ = cache.Get("1") cache.Set("4", []byte("value")) _, ok := cache.Get("1") if !ok { t.Error("expected key 1 to still exist, because LRU") } } func TestCache_HeadToTailSimple(t *testing.T) { cache := NewCache().WithMaxSize(3) cache.Set("1", "1") if cache.tail.Key != "1" && cache.head.Key != "1" { t.Error("expected tail=1 and head=1") } cache.Set("2", "2") if cache.tail.Key != "1" && cache.head.Key != "2" { t.Error("expected tail=1 and head=2") } cache.Set("3", "3") if cache.tail.Key != "1" && cache.head.Key != "3" { t.Error("expected tail=1 and head=4") } cache.Set("4", "4") if cache.tail.Key != "2" && cache.head.Key != "4" { t.Error("expected tail=2 and head=4") } cache.Set("5", "5") if cache.tail.Key != "3" && cache.head.Key != "5" { t.Error("expected tail=3 and head=5") } } func TestCache_HeadTailWorksWithFIFO(t *testing.T) { cache := NewCache().WithMaxSize(3).WithEvictionPolicy(FirstInFirstOut) if cache.tail != nil { t.Error("cache tail should have been nil") } if cache.head != nil { t.Error("cache head should have been nil") } cache.Set("1", []byte("value")) // (head) 1 (tail) if cache.tail == nil || cache.tail.Key != "1" { t.Error("cache tail should have been entry with key 1") } if cache.head == nil || cache.head.Key != "1" { t.Error("cache head should have been entry with key 1") } cache.Set("2", []byte("value")) // (head) 2 - 1 (tail) if cache.tail == nil || cache.tail.Key != "1" { t.Error("cache tail should have been the entry with key 1") } if cache.head == nil || cache.head.Key != "2" { t.Error("cache head should have been the entry with key 2") } if cache.head.next.Key != "1" { t.Error("The entry key next to the cache head should have been 1") } if cache.head.previous != nil { t.Error("The cache head should not have a previous node") } if cache.tail.previous.Key != "2" { t.Error("The entry key previous to the cache tail should have been 2") } if cache.tail.next != nil { t.Error("The cache tail should not have a next node") } cache.Set("3", []byte("value")) // (head) 3 - 2 - 1 (tail) if cache.tail == nil || cache.tail.Key != "1" { t.Error("cache tail should have been the entry with key 1") } if cache.tail.previous.Key != "2" { t.Error("The entry key previous to the cache tail should have been 2") } if cache.tail.next != nil { t.Error("The cache tail should not have a next node") } if cache.head == nil || cache.head.Key != "3" { t.Error("cache head should have been the entry with key 3") } if cache.head.next.Key != "2" { t.Error("The entry key next to the cache head should have been 2") } if cache.head.previous != nil { t.Error("The cache head should not have a previous node") } if cache.head.next.previous.Key != "3" { t.Error("The head's next node should have its previous node pointing to the cache head") } if cache.head.next.next.Key != "1" { t.Error("The head's next node should have its next node pointing to the cache tail") } // Get the first entry. This doesn't change anything for FIFO, but for LRU, it would mean that retrieved entry // wouldn't be evicted since it was recently accessed. Basically, we just want to make sure that FIFO works // as intended (i.e. not like LRU) _, _ = cache.Get("1") cache.Set("4", []byte("value")) // (head) 4 - 3 - 2 (tail) _, ok := cache.Get("1") if ok { t.Error("expected key 1 to have been removed, because FIFO") } if cache.tail == nil || cache.tail.Key != "2" { t.Error("cache tail should have been the entry with key 2") } if cache.tail.previous.Key != "3" { t.Error("The entry key previous to the cache tail should have been 3") } if cache.tail.next != nil { t.Error("The cache tail should not have a next node") } if cache.head == nil || cache.head.Key != "4" { t.Error("cache head should have been the entry with key 4") } if cache.head.next.Key != "3" { t.Error("The entry key next to the cache head should have been 3") } if cache.head.previous != nil { t.Error("The cache head should not have a previous node") } if cache.head.next.previous.Key != "4" { t.Error("The head's next node should have its previous node pointing to the cache head") } if cache.head.next.next.Key != "2" { t.Error("The head's next node should have its next node pointing to the cache tail") } } func TestCache_HeadTailWorksWithLRU(t *testing.T) { cache := NewCache().WithMaxSize(3).WithEvictionPolicy(LeastRecentlyUsed) if cache.tail != nil { t.Error("cache tail should have been nil") } if cache.head != nil { t.Error("cache head should have been nil") } cache.Set("1", []byte("value")) // (head) 1 (tail) if cache.tail == nil || cache.tail.Key != "1" { t.Error("cache tail should have been entry with key 1") } if cache.head == nil || cache.head.Key != "1" { t.Error("cache head should have been entry with key 1") } cache.Set("2", []byte("value")) // (head) 2 - 1 (tail) if cache.tail == nil || cache.tail.Key != "1" { t.Error("cache tail should have been the entry with key 1") } if cache.head == nil || cache.head.Key != "2" { t.Error("cache head should have been the entry with key 2") } if cache.head.next.Key != "1" { t.Error("The entry key next to the cache head should have been 1") } if cache.head.previous != nil { t.Error("The cache head should not have a previous node") } if cache.tail.previous.Key != "2" { t.Error("The entry key previous to the cache tail should have been 2") } if cache.tail.next != nil { t.Error("The cache tail should not have a next node") } cache.Set("3", []byte("value")) // (head) 3 - 2 - 1 (tail) if cache.tail == nil || cache.tail.Key != "1" { t.Error("cache tail should have been the entry with key 1") } if cache.tail.previous.Key != "2" { t.Error("The entry key previous to the cache tail should have been 2") } if cache.tail.next != nil { t.Error("The cache tail should not have a next node") } if cache.head == nil || cache.head.Key != "3" { t.Error("cache head should have been the entry with key 3") } if cache.head.next.Key != "2" { t.Error("The entry key next to the cache head should have been 2") } if cache.head.previous != nil { t.Error("The cache head should not have a previous node") } if cache.head.next.previous.Key != "3" { t.Error("The head's next node should have its previous node pointing to the cache head") } if cache.head.next.next.Key != "1" { t.Error("The head's next node should have its next node pointing to the cache tail") } // Because we're using a LRU cache, this should cause 1 to get moved back to the head, thus // moving it from the tail. // In other words, because we retrieved the key 1 here, this is no longer the least recently used cache entry, // which means it will not be evicted during the next insertion. _, _ = cache.Get("1") // (head) 1 - 3 - 2 (tail) (This updated because LRU) cache.Set("4", []byte("value")) // (head) 4 - 1 - 3 (tail) if cache.tail == nil || cache.tail.Key != "3" { t.Error("cache tail should have been the entry with key 3") } if cache.tail.previous.Key != "1" { t.Error("The entry key previous to the cache tail should have been 1") } if cache.tail.next != nil { t.Error("The cache tail should not have a next node") } if cache.head == nil || cache.head.Key != "4" { t.Error("cache head should have been the entry with key 4") } if cache.head.next.Key != "1" { t.Error("The entry key next to the cache head should have been 1") } if cache.head.previous != nil { t.Error("The cache head should not have a previous node") } if cache.head.next.previous.Key != cache.head.Key { t.Error("The head's next node should have its previous node pointing to the cache head") } if cache.head.next.next.Key != cache.tail.Key { t.Error("Should be able to walk from head to tail") } if cache.tail.previous.previous != cache.head { t.Error("Should be able to walk from tail to head") } _, ok := cache.Get("1") if !ok { t.Error("expected key 1 to still exist, because LRU") } } func TestCache_HeadStaysTheSameIfCallRepeatedly(t *testing.T) { cache := NewCache().WithEvictionPolicy(LeastRecentlyUsed).WithMaxSize(10) cache.Set("1", "1") if cache.tail.Key != "1" && cache.head.Key != "1" { t.Error("expected tail=1 and head=1") } cache.Set("1", "1") if cache.tail.Key != "1" && cache.head.Key != "1" { t.Error("expected tail=1 and head=1") } cache.Get("1") if cache.tail.Key != "1" && cache.head.Key != "1" { t.Error("expected tail=1 and head=1") } cache.Get("1") if cache.tail.Key != "1" && cache.head.Key != "1" { t.Error("expected tail=1 and head=1") } } func TestCache_Delete(t *testing.T) { cache := NewCache() if cache.tail != nil { t.Error("cache tail should have been nil") } if cache.head != nil { t.Error("cache head should have been nil") } cache.Set("1", "hey") cache.Set("2", []byte("sup")) cache.Set("3", 123456) // (head) 3 - 2 - 1 (tail) if cache.tail.Key != "1" { t.Error("cache tail should have been the entry with key 1") } if cache.head.Key != "3" { t.Error("cache head should have been the entry with key 3") } cache.Delete("2") // (head) 3 - 1 (tail) if cache.tail.Key != "1" { t.Error("cache tail should have been the entry with key 1") } if cache.head.Key != "3" { t.Error("cache head should have been the entry with key 3") } if cache.tail.previous.Key != "3" { t.Error("The entry key previous to the cache tail should have been 3") } if cache.head.next.Key != "1" { t.Error("The entry key next to the cache tail should have been 1") } cache.Delete("1") // (head) 3 (tail) if cache.tail.Key != "3" { t.Error("cache tail should have been the entry with key 3") } if cache.head.Key != "3" { t.Error("cache head should have been the entry with key 3") } if cache.head != cache.tail { t.Error("There should only be one entry in the cache") } if cache.head.next != nil || cache.tail.previous != nil { t.Error("Since head == tail, there should be no next/prev") } } func TestCache_DeleteAll(t *testing.T) { cache := NewCache() cache.Set("1", []byte("1")) cache.Set("2", []byte("2")) cache.Set("3", []byte("3")) if len(cache.GetByKeys([]string{"1", "2", "3"})) != 3 { t.Error("Expected keys 1, 2 and 3 to exist") } numberOfDeletedKeys := cache.DeleteAll([]string{"1", "2", "3"}) if numberOfDeletedKeys != 3 { t.Errorf("Expected 3 keys to have been deleted, but only %d were deleted", numberOfDeletedKeys) } } func TestCache_DeleteKeysByPattern(t *testing.T) { cache := NewCache() cache.Set("a1", []byte("v")) cache.Set("a2", []byte("v")) cache.Set("b1", []byte("v")) if len(cache.GetByKeys([]string{"a1", "a2", "b1"})) != 3 { t.Error("Expected keys 1, 2 and 3 to exist") } numberOfDeletedKeys := cache.DeleteKeysByPattern("a*") if numberOfDeletedKeys != 2 { t.Errorf("Expected 2 keys to have been deleted, but only %d were deleted", numberOfDeletedKeys) } if _, exists := cache.Get("b1"); !exists { t.Error("Expected key b1 to still exist") } } func TestCache_TTL(t *testing.T) { cache := NewCache() ttl, err := cache.TTL("key") if err != ErrKeyDoesNotExist { t.Errorf("expected %s, got %s", ErrKeyDoesNotExist, err) } cache.Set("key", "value") _, err = cache.TTL("key") if err != ErrKeyHasNoExpiration { t.Error("Expected TTL on new key created using Set to have no expiration") } cache.SetWithTTL("key", "value", time.Hour) ttl, err = cache.TTL("key") if err != nil { t.Error("Unexpected error") } if ttl.Minutes() < 59 || ttl.Minutes() > 60 { t.Error("Expected the TTL to be almost an hour") } cache.SetWithTTL("key", "value", 5*time.Millisecond) time.Sleep(6 * time.Millisecond) ttl, err = cache.TTL("key") if err != ErrKeyDoesNotExist { t.Error("key should've expired, thus TTL should've returned ") } } func TestCache_Expire(t *testing.T) { cache := NewCache() if cache.Expire("key-that-does-not-exist", time.Minute) { t.Error("Expected Expire to return false, because the key used did not exist") } cache.Set("key", "value") _, err := cache.TTL("key") if err != ErrKeyHasNoExpiration { t.Error("Expected TTL on new key created using Set to have no expiration") } if !cache.Expire("key", time.Hour) { t.Error("Expected Expire to return true") } ttl, err := cache.TTL("key") if err != nil { t.Error("Unexpected error") } if ttl.Minutes() < 59 || ttl.Minutes() > 60 { t.Error("Expected the TTL to be almost an hour") } if !cache.Expire("key", 5*time.Millisecond) { t.Error("Expected Expire to return true") } time.Sleep(6 * time.Millisecond) _, err = cache.TTL("key") if err != ErrKeyDoesNotExist { t.Error("key should've expired, thus TTL should've returned ErrKeyDoesNotExist") } if cache.Expire("key", time.Hour) { t.Error("Expire should've returned false, because the key should've already expired, thus no longer exist") } cache.SetWithTTL("key", "value", time.Hour) if !cache.Expire("key", NoExpiration) { t.Error("Expire should've returned true") } if _, err := cache.TTL("key"); err != ErrKeyHasNoExpiration { t.Error("TTL should've returned ErrKeyHasNoExpiration") } } func TestCache_Clear(t *testing.T) { cache := NewCache().WithMaxSize(10) cache.Set("k1", "v1") cache.Set("k2", "v2") cache.Set("k3", "v3") if cache.Count() != 3 { t.Error("expected cache size to be 3, got", cache.Count()) } cache.Clear() if cache.Count() != 0 { t.Error("expected cache to be empty") } if cache.memoryUsage != 0 { t.Error("expected cache.memoryUsage to be 0") } } func TestCache_WithMaxSize(t *testing.T) { cache := NewCache().WithMaxSize(1234) if cache.MaxSize() != 1234 { t.Error("expected cache to have a maximum size of 1234") } } func TestCache_WithMaxSizeAndNegativeValue(t *testing.T) { cache := NewCache().WithMaxSize(-10) if cache.MaxSize() != NoMaxSize { t.Error("expected cache to have no maximum size") } } func TestCache_WithMaxMemoryUsage(t *testing.T) { const ValueSize = Kilobyte cache := NewCache().WithMaxSize(0).WithMaxMemoryUsage(Kilobyte * 64) for i := 0; i < 100; i++ { cache.Set(fmt.Sprintf("%d", i), strings.Repeat("0", ValueSize)) } if cache.MemoryUsage()/1024 < 63 || cache.MemoryUsage()/1024 > 65 { t.Error("expected memoryUsage to be between 63KB and 64KB") } } func TestCache_WithMaxMemoryUsageWhenAddingAnEntryThatCausesMoreThanOneEviction(t *testing.T) { const ValueSize = Kilobyte cache := NewCache().WithMaxSize(0).WithMaxMemoryUsage(64 * Kilobyte) for i := 0; i < 100; i++ { cache.Set(fmt.Sprintf("%d", i), strings.Repeat("0", ValueSize)) } if cache.MemoryUsage()/1024 < 63 || cache.MemoryUsage()/1024 > 65 { t.Error("expected memoryUsage to be between 63KB and 64KB") } } func TestCache_WithMaxMemoryUsageAndNegativeValue(t *testing.T) { cache := NewCache().WithMaxSize(0).WithMaxMemoryUsage(-1234) if cache.MaxMemoryUsage() != NoMaxMemoryUsage { t.Error("attempting to set a negative max memory usage should force MaxMemoryUsage to NoMaxMemoryUsage") } } func TestCache_MemoryUsageAfterSet10000AndDelete5000(t *testing.T) { const ValueSize = 64 cache := NewCache().WithMaxSize(10000).WithMaxMemoryUsage(Gigabyte) for i := 0; i < cache.maxSize; i++ { cache.Set(fmt.Sprintf("%05d", i), strings.Repeat("0", ValueSize)) } memoryUsageBeforeDeleting := cache.MemoryUsage() for i := 0; i < cache.maxSize/2; i++ { key := fmt.Sprintf("%05d", i) cache.Delete(key) } memoryUsageRatio := float32(cache.MemoryUsage()) / float32(memoryUsageBeforeDeleting) if memoryUsageRatio != 0.5 { t.Error("Since half of the keys were deleted, the memoryUsage should've been half of what the memory usage was before beginning deletion") } } func TestCache_MemoryUsageIsReliable(t *testing.T) { cache := NewCache().WithMaxMemoryUsage(Megabyte) previousCacheMemoryUsage := cache.MemoryUsage() if previousCacheMemoryUsage != 0 { t.Error("cache.MemoryUsage() should've been 0") } cache.Set("1", 1) if cache.MemoryUsage() <= previousCacheMemoryUsage { t.Error("cache.MemoryUsage() should've increased") } previousCacheMemoryUsage = cache.MemoryUsage() cache.SetAll(map[string]any{"2": "2", "3": "3", "4": "4"}) if cache.MemoryUsage() <= previousCacheMemoryUsage { t.Error("cache.MemoryUsage() should've increased") } previousCacheMemoryUsage = cache.MemoryUsage() cache.Delete("2") if cache.MemoryUsage() >= previousCacheMemoryUsage { t.Error("cache.MemoryUsage() should've decreased") } previousCacheMemoryUsage = cache.MemoryUsage() cache.Set("1", 1) if cache.MemoryUsage() != previousCacheMemoryUsage { t.Error("cache.MemoryUsage() shouldn't have changed, because the entry didn't change") } previousCacheMemoryUsage = cache.MemoryUsage() cache.Delete("3") if cache.MemoryUsage() >= previousCacheMemoryUsage { t.Error("cache.MemoryUsage() should've decreased") } previousCacheMemoryUsage = cache.MemoryUsage() cache.Delete("4") if cache.MemoryUsage() >= previousCacheMemoryUsage { t.Error("cache.MemoryUsage() should've decreased") } previousCacheMemoryUsage = cache.MemoryUsage() cache.Delete("1") if cache.MemoryUsage() >= previousCacheMemoryUsage || cache.memoryUsage != 0 { t.Error("cache.MemoryUsage() should've been 0") } previousCacheMemoryUsage = cache.MemoryUsage() cache.Set("1", "v4lu3") if cache.MemoryUsage() <= previousCacheMemoryUsage { t.Error("cache.MemoryUsage() should've increased") } previousCacheMemoryUsage = cache.MemoryUsage() cache.Set("1", "value") if cache.MemoryUsage() != previousCacheMemoryUsage { t.Error("cache.MemoryUsage() shouldn't have changed") } previousCacheMemoryUsage = cache.MemoryUsage() cache.Set("1", true) if cache.MemoryUsage() >= previousCacheMemoryUsage { t.Error("cache.MemoryUsage() should've decreased, because a bool uses less memory than a string") } } func TestCache_MemoryUsageAndMaxSizeIsReliable(t *testing.T) { cache := NewCache().WithMaxMemoryUsage(100 * Kilobyte).WithMaxSize(1) rand.Seed(time.Now().UnixNano()) for i := 0; i < 10000; i++ { if i%5 == 0 { cache.Set(strconv.Itoa(rand.Intn(10)), strings.Repeat("what", rand.Intn(10000))) } else { cache.Set(strconv.Itoa(rand.Intn(10)), struct { m map[string]any s string }{ m: map[string]any{ "a": "b", strings.Repeat("b", rand.Intn(1000)): map[string]string{ strings.Repeat("c", rand.Intn(1000)): "d", }, }, s: strings.Repeat("e", rand.Intn(1000)), }) } cache.Get(strconv.Itoa(rand.Intn(10))) if cache.MemoryUsage() < 0 { t.Fatal("cache.MemoryUsage() should never be negative") } } } func TestCache_WithDefaultTTL(t *testing.T) { cache := NewCache().WithDefaultTTL(5 * time.Millisecond) if cache.defaultTTL != 5*time.Millisecond { t.Error("expected defaultTTL to be 5ms") } cache.Set("1", 1) cache.SetWithTTL("2", 2, time.Hour) if cache.GetValue("1") == nil { t.Error("expected cache entry with key 1 to still exist") } if cache.GetValue("2") == nil { t.Error("expected cache entry with key 2 to still exist") } time.Sleep(10 * time.Millisecond) if cache.GetValue("1") != nil { t.Error("expected cache entry with key 1 to have expired") } if cache.GetValue("2") == nil { t.Error("expected cache entry with key 2 to still exist") } } func TestCache_WithForceNilInterfaceOnNilPointer(t *testing.T) { type Struct struct{} cache := NewCache().WithForceNilInterfaceOnNilPointer(true) cache.Set("key", (*Struct)(nil)) if value, exists := cache.Get("key"); !exists { t.Error("expected key to exist") } else { if value != nil { // the value is not nil, because cache.Get returns an interface{} (any), and the type of that interface is not nil t.Error("value should be nil") } } cache.Clear() cache = cache.WithForceNilInterfaceOnNilPointer(false) cache.Set("key", (*Struct)(nil)) if value, exists := cache.Get("key"); !exists { t.Error("expected key to exist") } else { if value == nil { t.Error("value should be not be nil, because the type of the interface is not nil") } if value.(*Struct) != nil { t.Error("casted value should be nil") } } } func TestEvictionWhenThereIsNothingToEvict(t *testing.T) { cache := NewCache() cache.evict() cache.evict() cache.evict() } func TestCache(t *testing.T) { cache := NewCache().WithMaxSize(3).WithEvictionPolicy(LeastRecentlyUsed) cache.Set("1", 1) cache.Set("2", 2) cache.Set("3", 3) cache.Set("4", 4) if _, ok := cache.Get("4"); !ok { t.Error("expected 4 to exist") } if _, ok := cache.Get("3"); !ok { t.Error("expected 3 to exist") } if _, ok := cache.Get("2"); !ok { t.Error("expected 2 to exist") } if _, ok := cache.Get("1"); ok { t.Error("expected 1 to have been evicted") } cache.Set("5", 5) if _, ok := cache.Get("1"); ok { t.Error("expected 1 to have been evicted") } if _, ok := cache.Get("2"); !ok { t.Error("expected 2 to exist") } if _, ok := cache.Get("3"); !ok { t.Error("expected 3 to exist") } if _, ok := cache.Get("4"); ok { t.Error("expected 4 to have been evicted") } if _, ok := cache.Get("5"); !ok { t.Error("expected 5 to exist") } }