tailscale/util/cache/cache_test.go
Andrew Dunham 9fd29f15c7 util/cache: add package for general-purpose caching
This package allows caching arbitrary key/value pairs in-memory, along
with an interface implemented by the cache types.

Extracted from #7493

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ic8ca820927c456721cf324a0c8f3882a57752cc9
2023-12-07 18:19:38 -05:00

200 lines
4.3 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cache
import (
"errors"
"testing"
"time"
)
var startTime = time.Date(2023, time.March, 1, 0, 0, 0, 0, time.UTC)
func TestSingleCache(t *testing.T) {
testTime := startTime
timeNow := func() time.Time { return testTime }
c := &Single[string, int]{
timeNow: timeNow,
}
t.Run("NoServeExpired", func(t *testing.T) {
testCacheImpl(t, c, &testTime, false)
})
t.Run("ServeExpired", func(t *testing.T) {
c.Empty()
c.ServeExpired = true
testTime = startTime
testCacheImpl(t, c, &testTime, true)
})
}
func TestLocking(t *testing.T) {
testTime := startTime
timeNow := func() time.Time { return testTime }
c := NewLocking(&Single[string, int]{
timeNow: timeNow,
})
// Just verify that the inner cache's behaviour hasn't changed.
testCacheImpl(t, c, &testTime, false)
}
func testCacheImpl(t *testing.T, c Cache[string, int], testTime *time.Time, serveExpired bool) {
var fillTime time.Time
t.Run("InitialFill", func(t *testing.T) {
fillTime = testTime.Add(time.Hour)
val, err := c.Get("key", func() (int, time.Time, error) {
return 123, fillTime, nil
})
if err != nil {
t.Fatal(err)
}
if val != 123 {
t.Fatalf("got val=%d; want 123", val)
}
})
// Fetching again won't call our fill function
t.Run("SecondFetch", func(t *testing.T) {
*testTime = fillTime.Add(-1 * time.Second)
called := false
val, err := c.Get("key", func() (int, time.Time, error) {
called = true
return -1, fillTime, nil
})
if called {
t.Fatal("wanted no call to fill function")
}
if err != nil {
t.Fatal(err)
}
if val != 123 {
t.Fatalf("got val=%d; want 123", val)
}
})
// Fetching after the expiry time will re-fill
t.Run("ReFill", func(t *testing.T) {
*testTime = fillTime.Add(1)
fillTime = fillTime.Add(time.Hour)
val, err := c.Get("key", func() (int, time.Time, error) {
return 999, fillTime, nil
})
if err != nil {
t.Fatal(err)
}
if val != 999 {
t.Fatalf("got val=%d; want 999", val)
}
})
// An error on fetch will serve the expired value.
t.Run("FetchError", func(t *testing.T) {
if !serveExpired {
t.Skipf("not testing ServeExpired")
}
*testTime = fillTime.Add(time.Hour + 1)
val, err := c.Get("key", func() (int, time.Time, error) {
return 0, time.Time{}, errors.New("some error")
})
if err != nil {
t.Fatal(err)
}
if val != 999 {
t.Fatalf("got val=%d; want 999", val)
}
})
// Fetching a different key re-fills
t.Run("DifferentKey", func(t *testing.T) {
*testTime = fillTime.Add(time.Hour + 1)
var calls int
val, err := c.Get("key1", func() (int, time.Time, error) {
calls++
return 123, fillTime, nil
})
if err != nil {
t.Fatal(err)
}
if val != 123 {
t.Fatalf("got val=%d; want 123", val)
}
if calls != 1 {
t.Errorf("got %d, want 1 call", calls)
}
val, err = c.Get("key2", func() (int, time.Time, error) {
calls++
return 456, fillTime, nil
})
if err != nil {
t.Fatal(err)
}
if val != 456 {
t.Fatalf("got val=%d; want 456", val)
}
if calls != 2 {
t.Errorf("got %d, want 2 call", calls)
}
})
// Calling Forget with the wrong key does nothing, and with the correct
// key will drop the cache.
t.Run("Forget", func(t *testing.T) {
// Add some time so that previously-cached values don't matter.
fillTime = testTime.Add(2 * time.Hour)
*testTime = fillTime.Add(-1 * time.Second)
const key = "key"
var calls int
val, err := c.Get(key, func() (int, time.Time, error) {
calls++
return 123, fillTime, nil
})
if err != nil {
t.Fatal(err)
}
if val != 123 {
t.Fatalf("got val=%d; want 123", val)
}
if calls != 1 {
t.Errorf("got %d, want 1 call", calls)
}
// Forgetting the wrong key does nothing
c.Forget("other")
val, err = c.Get(key, func() (int, time.Time, error) {
t.Fatal("should not be called")
panic("unreachable")
})
if err != nil {
t.Fatal(err)
}
if val != 123 {
t.Fatalf("got val=%d; want 123", val)
}
// Forgetting the correct key re-fills
c.Forget(key)
val, err = c.Get("key2", func() (int, time.Time, error) {
calls++
return 456, fillTime, nil
})
if err != nil {
t.Fatal(err)
}
if val != 456 {
t.Fatalf("got val=%d; want 456", val)
}
if calls != 2 {
t.Errorf("got %d, want 2 call", calls)
}
})
}