package kvmemcache import ( "errors" "fmt" "sync" "testing" "time" ) type State[K comparable] struct { Keys []K Stats Stats } func (c *Cache[K,V]) assert(state State[K]) error { c.lock.Lock() defer c.lock.Unlock() if len(c.cache) != len(state.Keys) { return fmt.Errorf( "Expected %d keys but found %d.", len(state.Keys), len(c.cache)) } for _, k := range state.Keys { if _, ok := c.cache[k]; !ok { return fmt.Errorf( "Expected key %v not found.", k) } } if c.stats.Hits != state.Stats.Hits { return fmt.Errorf( "Expected %d hits, but found %d.", state.Stats.Hits, c.stats.Hits) } if c.stats.Misses != state.Stats.Misses { return fmt.Errorf( "Expected %d misses, but found %d.", state.Stats.Misses, c.stats.Misses) } return nil } var ErrTest = errors.New("Hello") func TestCache_basic(t *testing.T) { c := New(Config[string, string]{ MaxSize: 4, TTL: 50 * time.Millisecond, Src: func(key string) (string, error) { if key == "err" { return "", ErrTest } return key, nil }, }) type testCase struct { name string sleep time.Duration key string evict bool state State[string] } cases := []testCase{ { name: "get a", key: "a", state: State[string]{ Keys: []string{"a"}, Stats: Stats{Hits: 0, Misses: 1}, }, }, { name: "get a again", key: "a", state: State[string]{ Keys: []string{"a"}, Stats: Stats{Hits: 1, Misses: 1}, }, }, { name: "sleep, then get a again", sleep: 55 * time.Millisecond, key: "a", state: State[string]{ Keys: []string{"a"}, Stats: Stats{Hits: 1, Misses: 2}, }, }, { name: "get b", key: "b", state: State[string]{ Keys: []string{"a", "b"}, Stats: Stats{Hits: 1, Misses: 3}, }, }, { name: "get c", key: "c", state: State[string]{ Keys: []string{"a", "b", "c"}, Stats: Stats{Hits: 1, Misses: 4}, }, }, { name: "get d", key: "d", state: State[string]{ Keys: []string{"a", "b", "c", "d"}, Stats: Stats{Hits: 1, Misses: 5}, }, }, { name: "get e", key: "e", state: State[string]{ Keys: []string{"b", "c", "d", "e"}, Stats: Stats{Hits: 1, Misses: 6}, }, }, { name: "get c again", key: "c", state: State[string]{ Keys: []string{"b", "c", "d", "e"}, Stats: Stats{Hits: 2, Misses: 6}, }, }, { name: "get err", key: "err", state: State[string]{ Keys: []string{"c", "d", "e", "err"}, Stats: Stats{Hits: 2, Misses: 7}, }, }, { name: "get err again", key: "err", state: State[string]{ Keys: []string{"c", "d", "e", "err"}, Stats: Stats{Hits: 3, Misses: 7}, }, }, { name: "evict c", key: "c", evict: true, state: State[string]{ Keys: []string{"d", "e", "err"}, Stats: Stats{Hits: 3, Misses: 7}, }, }, { name: "reload-all a", key: "a", state: State[string]{ Keys: []string{"a", "d", "e", "err"}, Stats: Stats{Hits: 3, Misses: 8}, }, }, { name: "reload-all b", key: "b", state: State[string]{ Keys: []string{"a", "b", "e", "err"}, Stats: Stats{Hits: 3, Misses: 9}, }, }, { name: "reload-all c", key: "c", state: State[string]{ Keys: []string{"a", "b", "c", "err"}, Stats: Stats{Hits: 3, Misses: 10}, }, }, { name: "reload-all d", key: "d", state: State[string]{ Keys: []string{"a", "b", "c", "d"}, Stats: Stats{Hits: 3, Misses: 11}, }, }, { name: "read a again", key: "a", state: State[string]{ Keys: []string{"b", "c", "d", "a"}, Stats: Stats{Hits: 4, Misses: 11}, }, }, { name: "read e, evicting b", key: "e", state: State[string]{ Keys: []string{"c", "d", "a", "e"}, Stats: Stats{Hits: 4, Misses: 12}, }, }, } for _, tc := range cases { time.Sleep(tc.sleep) if !tc.evict { val, err := c.Get(tc.key) if tc.key == "err" && err != ErrTest { t.Fatal(tc.name, val) } if tc.key != "err" && val != tc.key { t.Fatal(tc.name, tc.key, val) } } else { c.Evict(tc.key) } if err := c.assert(tc.state); err != nil { t.Fatal(err) } } } func TestCache_thunderingHerd(t *testing.T) { c := New(Config[string,string]{ MaxSize: 4, Src: func(key string) (string, error) { time.Sleep(time.Second) return key, nil }, }) wg := sync.WaitGroup{} for i := 0; i < 16384; i++ { wg.Add(1) go func() { defer wg.Done() val, err := c.Get("a") if err != nil { panic(err) } if val != "a" { panic(err) } }() } wg.Wait() stats := c.Stats() if stats.Hits != 16383 || stats.Misses != 1 { t.Fatal(stats) } }