package cache import ( "errors" "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( called = 0 errSentinel = errors.New("an error") errCached = errors.New("a cached error") ) func setup(t *testing.T) (*Cache, CreateFunc) { called = 0 create := func(path string) (interface{}, bool, error) { assert.Equal(t, 0, called) called++ switch path { case "/": return "/", true, nil case "/file.txt": return "/file.txt", true, errCached case "/error": return nil, false, errSentinel case "/err": return nil, false, errSentinel } panic(fmt.Sprintf("Unknown path %q", path)) } c := New() return c, create } func TestGet(t *testing.T) { c, create := setup(t) assert.Equal(t, 0, len(c.cache)) f, err := c.Get("/", create) require.NoError(t, err) assert.Equal(t, 1, len(c.cache)) f2, err := c.Get("/", create) require.NoError(t, err) assert.Equal(t, f, f2) } func TestGetFile(t *testing.T) { c, create := setup(t) assert.Equal(t, 0, len(c.cache)) f, err := c.Get("/file.txt", create) require.Equal(t, errCached, err) assert.Equal(t, 1, len(c.cache)) f2, err := c.Get("/file.txt", create) require.Equal(t, errCached, err) assert.Equal(t, f, f2) } func TestGetError(t *testing.T) { c, create := setup(t) assert.Equal(t, 0, len(c.cache)) f, err := c.Get("/error", create) require.Equal(t, errSentinel, err) require.Equal(t, nil, f) assert.Equal(t, 0, len(c.cache)) } func TestPutErr(t *testing.T) { c, create := setup(t) assert.Equal(t, 0, len(c.cache)) c.PutErr("/alien", "slime", errSentinel) assert.Equal(t, 1, len(c.cache)) fNew, err := c.Get("/alien", create) require.Equal(t, errSentinel, err) require.Equal(t, "slime", fNew) assert.Equal(t, 1, len(c.cache)) } func TestPut(t *testing.T) { c, create := setup(t) assert.Equal(t, 0, len(c.cache)) c.Put("/alien", "slime") assert.Equal(t, 1, len(c.cache)) fNew, err := c.Get("/alien", create) require.NoError(t, err) require.Equal(t, "slime", fNew) assert.Equal(t, 1, len(c.cache)) } func TestCacheExpire(t *testing.T) { c, create := setup(t) c.SetExpireInterval(time.Millisecond) assert.Equal(t, false, c.expireRunning) _, err := c.Get("/", create) require.NoError(t, err) c.mu.Lock() entry := c.cache["/"] assert.Equal(t, 1, len(c.cache)) c.mu.Unlock() c.cacheExpire() c.mu.Lock() assert.Equal(t, 1, len(c.cache)) entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second) assert.Equal(t, true, c.expireRunning) c.mu.Unlock() time.Sleep(250 * time.Millisecond) c.mu.Lock() assert.Equal(t, false, c.expireRunning) assert.Equal(t, 0, len(c.cache)) c.mu.Unlock() } func TestCacheNoExpire(t *testing.T) { c, create := setup(t) assert.False(t, c.noCache()) c.SetExpireDuration(0) assert.Equal(t, false, c.expireRunning) assert.True(t, c.noCache()) f, err := c.Get("/", create) require.NoError(t, err) require.NotNil(t, f) c.mu.Lock() assert.Equal(t, 0, len(c.cache)) c.mu.Unlock() c.Put("/alien", "slime") c.mu.Lock() assert.Equal(t, 0, len(c.cache)) c.mu.Unlock() } func TestCachePin(t *testing.T) { c, create := setup(t) _, err := c.Get("/", create) require.NoError(t, err) // Pin a nonexistent item to show nothing happens c.Pin("notfound") c.mu.Lock() entry := c.cache["/"] assert.Equal(t, 1, len(c.cache)) c.mu.Unlock() c.cacheExpire() c.mu.Lock() assert.Equal(t, 1, len(c.cache)) c.mu.Unlock() // Pin the entry and check it does not get expired c.Pin("/") // Reset last used to make the item expirable c.mu.Lock() entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second) c.mu.Unlock() c.cacheExpire() c.mu.Lock() assert.Equal(t, 1, len(c.cache)) c.mu.Unlock() // Unpin the entry and check it does get expired now c.Unpin("/") // Reset last used c.mu.Lock() entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second) c.mu.Unlock() c.cacheExpire() c.mu.Lock() assert.Equal(t, 0, len(c.cache)) c.mu.Unlock() } func TestClear(t *testing.T) { c, create := setup(t) assert.Equal(t, 0, len(c.cache)) _, err := c.Get("/", create) require.NoError(t, err) assert.Equal(t, 1, len(c.cache)) c.Clear() assert.Equal(t, 0, len(c.cache)) } func TestEntries(t *testing.T) { c, create := setup(t) assert.Equal(t, 0, c.Entries()) _, err := c.Get("/", create) require.NoError(t, err) assert.Equal(t, 1, c.Entries()) c.Clear() assert.Equal(t, 0, c.Entries()) } func TestGetMaybe(t *testing.T) { c, create := setup(t) value, found := c.GetMaybe("/") assert.Equal(t, false, found) assert.Nil(t, value) f, err := c.Get("/", create) require.NoError(t, err) value, found = c.GetMaybe("/") assert.Equal(t, true, found) assert.Equal(t, f, value) c.Clear() value, found = c.GetMaybe("/") assert.Equal(t, false, found) assert.Nil(t, value) } func TestDelete(t *testing.T) { c, create := setup(t) assert.Equal(t, 0, len(c.cache)) _, err := c.Get("/", create) require.NoError(t, err) assert.Equal(t, 1, len(c.cache)) assert.Equal(t, false, c.Delete("notfound")) assert.Equal(t, 1, len(c.cache)) assert.Equal(t, true, c.Delete("/")) assert.Equal(t, 0, len(c.cache)) assert.Equal(t, false, c.Delete("/")) assert.Equal(t, 0, len(c.cache)) } func TestDeletePrefix(t *testing.T) { create := func(path string) (interface{}, bool, error) { return path, true, nil } c := New() _, err := c.Get("remote:path", create) require.NoError(t, err) _, err = c.Get("remote:path2", create) require.NoError(t, err) _, err = c.Get("remote:", create) require.NoError(t, err) _, err = c.Get("remote", create) require.NoError(t, err) assert.Equal(t, 4, len(c.cache)) assert.Equal(t, 3, c.DeletePrefix("remote:")) assert.Equal(t, 1, len(c.cache)) assert.Equal(t, 1, c.DeletePrefix("")) assert.Equal(t, 0, len(c.cache)) assert.Equal(t, 0, c.DeletePrefix("")) assert.Equal(t, 0, len(c.cache)) } func TestCacheRename(t *testing.T) { c := New() create := func(path string) (interface{}, bool, error) { return path, true, nil } existing1, err := c.Get("existing1", create) require.NoError(t, err) _, err = c.Get("existing2", create) require.NoError(t, err) assert.Equal(t, 2, c.Entries()) // rename to nonexistent value, found := c.Rename("existing1", "EXISTING1") assert.Equal(t, true, found) assert.Equal(t, existing1, value) assert.Equal(t, 2, c.Entries()) // rename to existent and check existing value is returned value, found = c.Rename("existing2", "EXISTING1") assert.Equal(t, true, found) assert.Equal(t, existing1, value) assert.Equal(t, 1, c.Entries()) // rename nonexistent value, found = c.Rename("notfound", "NOTFOUND") assert.Equal(t, false, found) assert.Nil(t, value) assert.Equal(t, 1, c.Entries()) } func TestCacheFinalize(t *testing.T) { c := New() numCalled := 0 c.SetFinalizer(func(v interface{}) { numCalled++ }) create := func(path string) (interface{}, bool, error) { return path, true, nil } _, _ = c.Get("ok", create) assert.Equal(t, 0, numCalled) c.Clear() assert.Equal(t, 1, numCalled) _, _ = c.Get("ok", create) c.Delete("ok") assert.Equal(t, 2, numCalled) _, _ = c.Get("ok", create) c.DeletePrefix("ok") assert.Equal(t, 3, numCalled) _, _ = c.Get("old", create) _, _ = c.Get("new", create) c.Rename("old", "new") assert.Equal(t, 4, numCalled) c.expireDuration = 1 * time.Millisecond _, _ = c.Get("ok", create) time.Sleep(2 * time.Millisecond) c.cacheExpire() // "ok" and "new" fall out of cache assert.Equal(t, 6, numCalled) }