package cache
import (
"bytes"
"io/ioutil"
"runtime"
"strconv"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type TestStruct struct {
Num int
Children [ ] * TestStruct
}
func NewTestStruct ( num int ) TestStruct {
v := TestStruct { Num : num }
return v
}
func TestCache ( t * testing . T ) {
tc := New [ string , TestStruct ] ( DefaultExpiration , 0 )
a , found := tc . Get ( "a" )
if found {
t . Error ( "Getting A found value that shouldn't exist:" , a )
}
b , found := tc . Get ( "b" )
if found {
t . Error ( "Getting B found value that shouldn't exist:" , b )
}
c , found := tc . Get ( "c" )
if found {
t . Error ( "Getting C found value that shouldn't exist:" , c )
}
tc . Set ( "a" , TestStruct { Num : 1 } , DefaultExpiration )
tc . Set ( "b" , TestStruct { Num : 2 } , DefaultExpiration )
x , found := tc . Get ( "a" )
if ! found {
t . Error ( "a was not found while getting a2" )
}
assert . Equal ( t , 1 , x . Num )
x , found = tc . Get ( "b" )
assert . True ( t , found , "b was not found while getting b2" )
assert . Equal ( t , 2 , x . Num )
}
func TestCacheTimes ( t * testing . T ) {
var found bool
tc := New [ string , TestStruct ] ( 50 * time . Millisecond , 1 * time . Millisecond )
tc . Set ( "a" , NewTestStruct ( 1 ) , DefaultExpiration )
tc . Set ( "b" , NewTestStruct ( 2 ) , NoExpiration )
tc . Set ( "c" , NewTestStruct ( 3 ) , 20 * time . Millisecond )
tc . Set ( "d" , NewTestStruct ( 4 ) , 70 * time . Millisecond )
<- time . After ( 25 * time . Millisecond )
_ , found = tc . Get ( "c" )
if found {
t . Error ( "Found c when it should have been automatically deleted" )
}
<- time . After ( 30 * time . Millisecond )
_ , found = tc . Get ( "a" )
if found {
t . Error ( "Found a when it should have been automatically deleted" )
}
_ , found = tc . Get ( "b" )
if ! found {
t . Error ( "Did not find b even though it was set to never expire" )
}
_ , found = tc . Get ( "d" )
if ! found {
t . Error ( "Did not find d even though it was set to expire later than the default" )
}
<- time . After ( 20 * time . Millisecond )
_ , found = tc . Get ( "d" )
if found {
t . Error ( "Found d when it should have been automatically deleted (later than the default)" )
}
}
func TestNewFrom ( t * testing . T ) {
m := map [ string ] Item [ int ] {
"a" : Item [ int ] {
Object : 1 ,
Expiration : 0 ,
} ,
"b" : Item [ int ] {
Object : 2 ,
Expiration : 0 ,
} ,
}
tc := NewFrom ( DefaultExpiration , 0 , m )
a , found := tc . Get ( "a" )
if ! found {
t . Fatal ( "Did not find a" )
}
if a != 1 {
t . Fatal ( "a is not 1" )
}
b , found := tc . Get ( "b" )
if ! found {
t . Fatal ( "Did not find b" )
}
if b != 2 {
t . Fatal ( "b is not 2" )
}
}
func TestStorePointerToStruct ( t * testing . T ) {
tc := New [ string , * TestStruct ] ( DefaultExpiration , 0 )
tc . Set ( "foo" , & TestStruct { Num : 1 } , DefaultExpiration )
x , found := tc . Get ( "foo" )
if ! found {
t . Fatal ( "*TestStruct was not found for foo" )
}
foo := x
foo . Num ++
y , found := tc . Get ( "foo" )
if ! found {
t . Fatal ( "*TestStruct was not found for foo (second time)" )
}
bar := y
if bar . Num != 2 {
t . Fatal ( "TestStruct.Num is not 2" )
}
}
func TestAdd ( t * testing . T ) {
tc := New [ string , string ] ( DefaultExpiration , 0 )
err := tc . Add ( "foo" , "bar" , DefaultExpiration )
if err != nil {
t . Error ( "Couldn't add foo even though it shouldn't exist" )
}
err = tc . Add ( "foo" , "baz" , DefaultExpiration )
if err == nil {
t . Error ( "Successfully added another foo when it should have returned an error" )
}
}
func TestReplace ( t * testing . T ) {
tc := New [ string , string ] ( DefaultExpiration , 0 )
err := tc . Replace ( "foo" , "bar" , DefaultExpiration )
if err == nil {
t . Error ( "Replaced foo when it shouldn't exist" )
}
tc . Set ( "foo" , "bar" , DefaultExpiration )
err = tc . Replace ( "foo" , "bar" , DefaultExpiration )
if err != nil {
t . Error ( "Couldn't replace existing key foo" )
}
}
func TestDelete ( t * testing . T ) {
tc := New [ string , string ] ( DefaultExpiration , 0 )
tc . Set ( "foo" , "bar" , DefaultExpiration )
tc . Delete ( "foo" )
_ , found := tc . Get ( "foo" )
if found {
t . Error ( "foo was found, but it should have been deleted" )
}
}
func TestItemCount ( t * testing . T ) {
tc := New [ string , string ] ( DefaultExpiration , 0 )
tc . Set ( "foo" , "1" , DefaultExpiration )
tc . Set ( "bar" , "2" , DefaultExpiration )
tc . Set ( "baz" , "3" , DefaultExpiration )
if n := tc . ItemCount ( ) ; n != 3 {
t . Errorf ( "Item count is not 3: %d" , n )
}
}
func TestFlush ( t * testing . T ) {
tc := New [ string , string ] ( DefaultExpiration , 0 )
tc . Set ( "foo" , "bar" , DefaultExpiration )
tc . Set ( "baz" , "yes" , DefaultExpiration )
tc . Flush ( )
_ , found := tc . Get ( "foo" )
if found {
t . Error ( "foo was found, but it should have been deleted" )
}
_ , found = tc . Get ( "baz" )
if found {
t . Error ( "baz was found, but it should have been deleted" )
}
}
func TestOnEvicted ( t * testing . T ) {
tc := New [ string , int ] ( DefaultExpiration , time . Millisecond )
tc . Set ( "foo" , 3 , DefaultExpiration )
if tc . onEvicted != nil {
t . Fatal ( "tc.onEvicted is not nil" )
}
works := false
tc . OnEvicted ( func ( k string , v int ) {
if k == "foo" && v == 3 {
works = true
}
tc . Set ( "bar" , 4 , DefaultExpiration )
} )
tc . Delete ( "foo" )
x , _ := tc . Get ( "bar" )
if ! works {
t . Error ( "works bool not true" )
}
if x != 4 {
t . Error ( "bar was not 4" )
}
}
func TestCacheSerialization ( t * testing . T ) {
tc := New [ string , TestStruct ] ( DefaultExpiration , 0 )
testFillAndSerialize ( t , tc )
// Check if gob.Register behaves properly even after multiple gob.Register
// on c.Items (many of which will be the same type)
testFillAndSerialize ( t , tc )
}
func testFillAndSerialize ( t * testing . T , tc * Cache [ string , TestStruct ] ) {
tc . Set ( "*struct" , TestStruct { Num : 1 } , DefaultExpiration )
tc . Set ( "structception" , TestStruct {
Num : 42 ,
Children : [ ] * TestStruct {
& TestStruct { Num : 6174 } ,
& TestStruct { Num : 4716 } ,
} ,
} , DefaultExpiration )
tc . Set ( "structceptionexpire" , TestStruct {
Num : 42 ,
Children : [ ] * TestStruct {
& TestStruct { Num : 6174 } ,
& TestStruct { Num : 4716 } ,
} ,
} , 1 * time . Millisecond )
fp := & bytes . Buffer { }
err := tc . Save ( fp )
if err != nil {
t . Fatal ( "Couldn't save cache to fp:" , err )
}
oc := New [ string , TestStruct ] ( DefaultExpiration , 0 )
err = oc . Load ( fp )
if err != nil {
t . Fatal ( "Couldn't load cache from fp:" , err )
}
<- time . After ( 5 * time . Millisecond )
_ , found := oc . Get ( "structceptionexpire" )
if found {
t . Error ( "expired was found" )
}
s1 , found := oc . Get ( "*struct" )
if ! found {
t . Error ( "*struct was not found" )
}
if s1 . Num != 1 {
t . Error ( "*struct.Num is not 1" )
}
s4 , found := oc . get ( "structception" )
if ! found {
t . Error ( "structception was not found" )
}
s4r := s4
if len ( s4r . Children ) != 2 {
t . Error ( "Length of s4r.Children is not 2" )
}
if s4r . Children [ 0 ] . Num != 6174 {
t . Error ( "s4r.Children[0].Num is not 6174" )
}
if s4r . Children [ 1 ] . Num != 4716 {
t . Error ( "s4r.Children[1].Num is not 4716" )
}
}
func TestFileSerialization ( t * testing . T ) {
tc := New [ string , string ] ( DefaultExpiration , 0 )
tc . Add ( "a" , "a" , DefaultExpiration )
tc . Add ( "b" , "b" , DefaultExpiration )
f , err := ioutil . TempFile ( "" , "go-cache-cache.dat" )
if err != nil {
t . Fatal ( "Couldn't create cache file:" , err )
}
fname := f . Name ( )
f . Close ( )
tc . SaveFile ( fname )
oc := New [ string , string ] ( DefaultExpiration , 0 )
oc . Add ( "a" , "aa" , 0 ) // this should not be overwritten
err = oc . LoadFile ( fname )
if err != nil {
t . Error ( err )
}
a , found := oc . Get ( "a" )
if ! found {
t . Error ( "a was not found" )
}
astr := a
if astr != "aa" {
if astr == "a" {
t . Error ( "a was overwritten" )
} else {
t . Error ( "a is not aa" )
}
}
b , found := oc . Get ( "b" )
if ! found {
t . Error ( "b was not found" )
}
if b != "b" {
t . Error ( "b is not b" )
}
}
func TestSerializeUnserializable ( t * testing . T ) {
tc := New [ string , chan bool ] ( DefaultExpiration , 0 )
ch := make ( chan bool , 1 )
ch <- true
tc . Set ( "chan" , ch , DefaultExpiration )
fp := & bytes . Buffer { }
err := tc . Save ( fp ) // this should fail gracefully
if assert . Error ( t , err ) {
assert . NotEqual ( t , err . Error ( ) , "gob NewTypeObject can't handle type: chan bool" , "Error from Save was not gob NewTypeObject can't handle type chan bool:" , err )
}
}
func BenchmarkCacheGetStringExpiring ( b * testing . B ) {
benchmarkCacheGetString ( b , 5 * time . Minute )
}
func BenchmarkCacheGetStringNotExpiring ( b * testing . B ) {
benchmarkCacheGetString ( b , NoExpiration )
}
func benchmarkCacheGetString ( b * testing . B , exp time . Duration ) {
b . StopTimer ( )
tc := New [ string , string ] ( exp , 0 )
tc . Set ( "foo" , "bar" , DefaultExpiration )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
tc . Get ( "foo" )
}
}
func BenchmarkRWMutexMapGet ( b * testing . B ) {
b . StopTimer ( )
m := map [ string ] string {
"foo" : "bar" ,
}
mu := sync . RWMutex { }
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
mu . RLock ( )
_ = m [ "foo" ]
mu . RUnlock ( )
}
}
func BenchmarkRWMutexInterfaceMapGetStruct ( b * testing . B ) {
b . StopTimer ( )
s := struct { name string } { name : "foo" }
m := map [ interface { } ] string {
s : "bar" ,
}
mu := sync . RWMutex { }
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
mu . RLock ( )
_ = m [ s ]
mu . RUnlock ( )
}
}
func BenchmarkRWMutexInterfaceMapGetString ( b * testing . B ) {
b . StopTimer ( )
m := map [ interface { } ] string {
"foo" : "bar" ,
}
mu := sync . RWMutex { }
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
mu . RLock ( )
_ = m [ "foo" ]
mu . RUnlock ( )
}
}
func BenchmarkCacheGetConcurrentExpiring ( b * testing . B ) {
benchmarkCacheGetConcurrent ( b , 5 * time . Minute )
}
func BenchmarkCacheGetConcurrentNotExpiring ( b * testing . B ) {
benchmarkCacheGetConcurrent ( b , NoExpiration )
}
func benchmarkCacheGetConcurrent ( b * testing . B , exp time . Duration ) {
b . StopTimer ( )
tc := New [ string , string ] ( exp , 0 )
tc . Set ( "foo" , "bar" , DefaultExpiration )
wg := new ( sync . WaitGroup )
workers := runtime . NumCPU ( )
each := b . N / workers
wg . Add ( workers )
b . StartTimer ( )
for i := 0 ; i < workers ; i ++ {
go func ( ) {
for j := 0 ; j < each ; j ++ {
tc . Get ( "foo" )
}
wg . Done ( )
} ( )
}
wg . Wait ( )
}
func BenchmarkRWMutexMapGetConcurrent ( b * testing . B ) {
b . StopTimer ( )
m := map [ string ] string {
"foo" : "bar" ,
}
mu := sync . RWMutex { }
wg := new ( sync . WaitGroup )
workers := runtime . NumCPU ( )
each := b . N / workers
wg . Add ( workers )
b . StartTimer ( )
for i := 0 ; i < workers ; i ++ {
go func ( ) {
for j := 0 ; j < each ; j ++ {
mu . RLock ( )
_ = m [ "foo" ]
mu . RUnlock ( )
}
wg . Done ( )
} ( )
}
wg . Wait ( )
}
func BenchmarkCacheGetManyConcurrentExpiring ( b * testing . B ) {
benchmarkCacheGetManyConcurrent ( b , 5 * time . Minute )
}
func BenchmarkCacheGetManyConcurrentNotExpiring ( b * testing . B ) {
benchmarkCacheGetManyConcurrent ( b , NoExpiration )
}
func benchmarkCacheGetManyConcurrent ( b * testing . B , exp time . Duration ) {
// This is the same as BenchmarkCacheGetConcurrent, but its result
// can be compared against BenchmarkShardedCacheGetManyConcurrent
// in sharded_test.go.
b . StopTimer ( )
n := 10000
tc := New [ string , string ] ( exp , 0 )
keys := make ( [ ] string , n )
for i := 0 ; i < n ; i ++ {
k := "foo" + strconv . Itoa ( i )
keys [ i ] = k
tc . Set ( k , "bar" , DefaultExpiration )
}
each := b . N / n
wg := new ( sync . WaitGroup )
wg . Add ( n )
for _ , v := range keys {
go func ( k string ) {
for j := 0 ; j < each ; j ++ {
tc . Get ( k )
}
wg . Done ( )
} ( v )
}
b . StartTimer ( )
wg . Wait ( )
}
func BenchmarkCacheSetStringExpiring ( b * testing . B ) {
benchmarkCacheSetString ( b , 5 * time . Minute )
}
func BenchmarkCacheSetStringNotExpiring ( b * testing . B ) {
benchmarkCacheSetString ( b , NoExpiration )
}
func benchmarkCacheSetString ( b * testing . B , exp time . Duration ) {
b . StopTimer ( )
tc := New [ string , string ] ( exp , 0 )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
tc . Set ( "foo" , "bar" , DefaultExpiration )
}
}
func BenchmarkRWMutexMapSet ( b * testing . B ) {
b . StopTimer ( )
m := map [ string ] string { }
mu := sync . RWMutex { }
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
mu . Lock ( )
m [ "foo" ] = "bar"
mu . Unlock ( )
}
}
func BenchmarkCacheSetDelete ( b * testing . B ) {
b . StopTimer ( )
tc := New [ string , string ] ( DefaultExpiration , 0 )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
tc . Set ( "foo" , "bar" , DefaultExpiration )
tc . Delete ( "foo" )
}
}
func BenchmarkRWMutexMapSetDelete ( b * testing . B ) {
b . StopTimer ( )
m := map [ string ] string { }
mu := sync . RWMutex { }
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
mu . Lock ( )
m [ "foo" ] = "bar"
mu . Unlock ( )
mu . Lock ( )
delete ( m , "foo" )
mu . Unlock ( )
}
}
func BenchmarkCacheSetDeleteSingleLock ( b * testing . B ) {
b . StopTimer ( )
tc := New [ string , string ] ( DefaultExpiration , 0 )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
tc . mu . Lock ( )
tc . set ( "foo" , "bar" , DefaultExpiration )
tc . delete ( "foo" )
tc . mu . Unlock ( )
}
}
func BenchmarkRWMutexMapSetDeleteSingleLock ( b * testing . B ) {
b . StopTimer ( )
m := map [ string ] string { }
mu := sync . RWMutex { }
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
mu . Lock ( )
m [ "foo" ] = "bar"
delete ( m , "foo" )
mu . Unlock ( )
}
}
func BenchmarkDeleteExpiredLoop ( b * testing . B ) {
b . StopTimer ( )
tc := New [ string , string ] ( 5 * time . Minute , 0 )
tc . mu . Lock ( )
for i := 0 ; i < 100000 ; i ++ {
tc . set ( strconv . Itoa ( i ) , "bar" , DefaultExpiration )
}
tc . mu . Unlock ( )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
tc . DeleteExpired ( )
}
}
func TestGetWithExpiration ( t * testing . T ) {
tc := New [ string , int ] ( DefaultExpiration , 0 )
a , expiration , found := tc . GetWithExpiration ( "a" )
if found || ! expiration . IsZero ( ) {
t . Error ( "Getting A found value that shouldn't exist:" , a )
}
b , expiration , found := tc . GetWithExpiration ( "b" )
if found || ! expiration . IsZero ( ) {
t . Error ( "Getting B found value that shouldn't exist:" , b )
}
c , expiration , found := tc . GetWithExpiration ( "c" )
if found || ! expiration . IsZero ( ) {
t . Error ( "Getting C found value that shouldn't exist:" , c )
}
tc . Set ( "a" , 1 , DefaultExpiration )
tc . Set ( "b" , 2 , DefaultExpiration )
tc . Set ( "c" , 3 , DefaultExpiration )
tc . Set ( "d" , 4 , NoExpiration )
tc . Set ( "e" , 5 , 50 * time . Millisecond )
x , expiration , found := tc . GetWithExpiration ( "a" )
assert . True ( t , found , "Didn't find a" )
assert . Equal ( t , 1 , x )
assert . True ( t , expiration . IsZero ( ) , "expiration for a is not a zeroed time" )
x , expiration , found = tc . GetWithExpiration ( "b" )
assert . True ( t , found , "Didn't find b" )
assert . Equal ( t , 2 , x )
assert . True ( t , expiration . IsZero ( ) , "expiration for b is not a zeroed time" )
x , expiration , found = tc . GetWithExpiration ( "c" )
assert . True ( t , found , "Didn't find c" )
assert . Equal ( t , 3 , x )
assert . True ( t , expiration . IsZero ( ) , "expiration for c is not a zeroed time" )
x , expiration , found = tc . GetWithExpiration ( "d" )
assert . True ( t , found , "Didn't find d" )
assert . Equal ( t , 4 , x )
assert . True ( t , expiration . IsZero ( ) , "expiration for d is not a zeroed time" )
x , expiration , found = tc . GetWithExpiration ( "e" )
assert . True ( t , found , "Didn't find e" )
assert . Equal ( t , 5 , x )
assert . Equal ( t , expiration . UnixNano ( ) , tc . items [ "e" ] . Expiration , "expiration for e is not the correct time" )
assert . Greater ( t , expiration . UnixNano ( ) , time . Now ( ) . UnixNano ( ) , "expiration for e is in the past" )
}
// Benchmark struct
func BenchmarkCacheGetStructExpiring ( b * testing . B ) {
benchmarkCacheGetStruct ( b , 5 * time . Minute )
}
func BenchmarkCacheGetStructNotExpiring ( b * testing . B ) {
benchmarkCacheGetStruct ( b , NoExpiration )
}
func benchmarkCacheGetStruct ( b * testing . B , exp time . Duration ) {
b . StopTimer ( )
tc := New [ string , * TestStruct ] ( exp , 0 )
tc . Set ( "foo" , & TestStruct { Num : 1 } , DefaultExpiration )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
st , _ := tc . Get ( "foo" )
// just do something
st . Num ++
}
}
func BenchmarkCacheSetStructExpiring ( b * testing . B ) {
benchmarkCacheSetStruct ( b , 5 * time . Minute )
}
func BenchmarkCacheSetStructNotExpiring ( b * testing . B ) {
benchmarkCacheSetStruct ( b , NoExpiration )
}
func benchmarkCacheSetStruct ( b * testing . B , exp time . Duration ) {
b . StopTimer ( )
tc := New [ string , * TestStruct ] ( exp , 0 )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
tc . Set ( "foo" , & TestStruct { Num : 1 } , DefaultExpiration )
}
}
func BenchmarkCacheSetFatStructExpiring ( b * testing . B ) {
b . StopTimer ( )
tc := New [ string , * TestStruct ] ( NoExpiration , 0 )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
tc . Set ( "foo" , & TestStruct { Num : 1 , Children : [ ] * TestStruct {
& TestStruct { Num : 2 , Children : [ ] * TestStruct { } } } } , DefaultExpiration )
}
}
func BenchmarkCacheGetFatStructNotExpiring ( b * testing . B ) {
b . StopTimer ( )
tc := New [ string , * TestStruct ] ( NoExpiration , 0 )
tc . Set ( "foo" , & TestStruct { Num : 1 , Children : [ ] * TestStruct {
& TestStruct { Num : 2 , Children : [ ] * TestStruct { } } } } , DefaultExpiration )
b . StartTimer ( )
for i := 0 ; i < b . N ; i ++ {
st , _ := tc . Get ( "foo" )
// just do something
st . Num ++
}
}