diff --git a/.golangci.yml b/.golangci.yml index 5be2cefe44..27fee20f75 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -86,6 +86,8 @@ linters-settings: desc: do not use the internal package, use AddXxx function instead - pkg: gopkg.in/ini.v1 desc: do not use the ini package, use gitea's config system instead + - pkg: gitea.com/go-chi/cache + desc: do not use the go-chi cache package, use gitea's cache system issues: max-issues-per-linter: 0 diff --git a/modules/cache/cache.go b/modules/cache/cache.go index 09afc8b7f7..2ca77bdb29 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -4,149 +4,75 @@ package cache import ( - "fmt" "strconv" + "time" "code.gitea.io/gitea/modules/setting" - - mc "gitea.com/go-chi/cache" - - _ "gitea.com/go-chi/cache/memcache" // memcache plugin for cache ) -var conn mc.Cache - -func newCache(cacheConfig setting.Cache) (mc.Cache, error) { - return mc.NewCacher(mc.Options{ - Adapter: cacheConfig.Adapter, - AdapterConfig: cacheConfig.Conn, - Interval: cacheConfig.Interval, - }) -} +var defaultCache StringCache // Init start cache service func Init() error { - var err error - - if conn == nil { - if conn, err = newCache(setting.CacheService.Cache); err != nil { + if defaultCache == nil { + c, err := NewStringCache(setting.CacheService.Cache) + if err != nil { return err } - if err = conn.Ping(); err != nil { + for i := 0; i < 10; i++ { + if err = c.Ping(); err == nil { + break + } + time.Sleep(time.Second) + } + if err != nil { return err } + defaultCache = c } - - return err + return nil } // GetCache returns the currently configured cache -func GetCache() mc.Cache { - return conn +func GetCache() StringCache { + return defaultCache } // GetString returns the key value from cache with callback when no key exists in cache func GetString(key string, getFunc func() (string, error)) (string, error) { - if conn == nil || setting.CacheService.TTL == 0 { + if defaultCache == nil || setting.CacheService.TTL == 0 { return getFunc() } - - cached := conn.Get(key) - - if cached == nil { + cached, exist := defaultCache.Get(key) + if !exist { value, err := getFunc() if err != nil { return value, err } - return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) - } - - if value, ok := cached.(string); ok { - return value, nil - } - - if stringer, ok := cached.(fmt.Stringer); ok { - return stringer.String(), nil - } - - return fmt.Sprintf("%s", cached), nil -} - -// GetInt returns key value from cache with callback when no key exists in cache -func GetInt(key string, getFunc func() (int, error)) (int, error) { - if conn == nil || setting.CacheService.TTL == 0 { - return getFunc() - } - - cached := conn.Get(key) - - if cached == nil { - value, err := getFunc() - if err != nil { - return value, err - } - - return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) - } - - switch v := cached.(type) { - case int: - return v, nil - case string: - value, err := strconv.Atoi(v) - if err != nil { - return 0, err - } - return value, nil - default: - value, err := getFunc() - if err != nil { - return value, err - } - return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) + return value, defaultCache.Put(key, value, setting.CacheService.TTLSeconds()) } + return cached, nil } // GetInt64 returns key value from cache with callback when no key exists in cache func GetInt64(key string, getFunc func() (int64, error)) (int64, error) { - if conn == nil || setting.CacheService.TTL == 0 { - return getFunc() + s, err := GetString(key, func() (string, error) { + v, err := getFunc() + return strconv.FormatInt(v, 10), err + }) + if err != nil { + return 0, err } - - cached := conn.Get(key) - - if cached == nil { - value, err := getFunc() - if err != nil { - return value, err - } - - return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) - } - - switch v := conn.Get(key).(type) { - case int64: - return v, nil - case string: - value, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return 0, err - } - return value, nil - default: - value, err := getFunc() - if err != nil { - return value, err - } - - return value, conn.Put(key, value, setting.CacheService.TTLSeconds()) + if s == "" { + return 0, nil } + return strconv.ParseInt(s, 10, 64) } // Remove key from cache func Remove(key string) { - if conn == nil { + if defaultCache == nil { return } - _ = conn.Delete(key) + _ = defaultCache.Delete(key) } diff --git a/modules/cache/cache_redis.go b/modules/cache/cache_redis.go index 6c358b0a78..c5b52a2086 100644 --- a/modules/cache/cache_redis.go +++ b/modules/cache/cache_redis.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/nosql" - "gitea.com/go-chi/cache" + "gitea.com/go-chi/cache" //nolint:depguard "github.com/redis/go-redis/v9" ) diff --git a/modules/cache/cache_test.go b/modules/cache/cache_test.go index 3f65040924..0c68cc26ee 100644 --- a/modules/cache/cache_test.go +++ b/modules/cache/cache_test.go @@ -14,7 +14,7 @@ import ( ) func createTestCache() { - conn, _ = newCache(setting.Cache{ + defaultCache, _ = NewStringCache(setting.Cache{ Adapter: "memory", TTL: time.Minute, }) @@ -25,7 +25,7 @@ func TestNewContext(t *testing.T) { assert.NoError(t, Init()) setting.CacheService.Cache = setting.Cache{Adapter: "redis", Conn: "some random string"} - con, err := newCache(setting.Cache{ + con, err := NewStringCache(setting.Cache{ Adapter: "rand", Conn: "false conf", Interval: 100, @@ -76,42 +76,6 @@ func TestGetString(t *testing.T) { Remove("key") } -func TestGetInt(t *testing.T) { - createTestCache() - - data, err := GetInt("key", func() (int, error) { - return 0, fmt.Errorf("some error") - }) - assert.Error(t, err) - assert.Equal(t, 0, data) - - data, err = GetInt("key", func() (int, error) { - return 0, nil - }) - assert.NoError(t, err) - assert.Equal(t, 0, data) - - data, err = GetInt("key", func() (int, error) { - return 100, nil - }) - assert.NoError(t, err) - assert.Equal(t, 0, data) - Remove("key") - - data, err = GetInt("key", func() (int, error) { - return 100, nil - }) - assert.NoError(t, err) - assert.Equal(t, 100, data) - - data, err = GetInt("key", func() (int, error) { - return 0, fmt.Errorf("some error") - }) - assert.NoError(t, err) - assert.Equal(t, 100, data) - Remove("key") -} - func TestGetInt64(t *testing.T) { createTestCache() diff --git a/modules/cache/cache_twoqueue.go b/modules/cache/cache_twoqueue.go index f9de2563ec..1eda2debc4 100644 --- a/modules/cache/cache_twoqueue.go +++ b/modules/cache/cache_twoqueue.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/json" - mc "gitea.com/go-chi/cache" + mc "gitea.com/go-chi/cache" //nolint:depguard lru "github.com/hashicorp/golang-lru/v2" ) diff --git a/modules/cache/string_cache.go b/modules/cache/string_cache.go new file mode 100644 index 0000000000..4f659616f5 --- /dev/null +++ b/modules/cache/string_cache.go @@ -0,0 +1,120 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cache + +import ( + "errors" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + chi_cache "gitea.com/go-chi/cache" //nolint:depguard +) + +type GetJSONError struct { + err error + cachedError string // Golang error can't be stored in cache, only the string message could be stored +} + +func (e *GetJSONError) ToError() error { + if e.err != nil { + return e.err + } + return errors.New("cached error: " + e.cachedError) +} + +type StringCache interface { + Ping() error + + Get(key string) (string, bool) + Put(key, value string, ttl int64) error + Delete(key string) error + IsExist(key string) bool + + PutJSON(key string, v any, ttl int64) error + GetJSON(key string, ptr any) (exist bool, err *GetJSONError) + + ChiCache() chi_cache.Cache +} + +type stringCache struct { + chiCache chi_cache.Cache +} + +func NewStringCache(cacheConfig setting.Cache) (StringCache, error) { + adapter := util.IfZero(cacheConfig.Adapter, "memory") + interval := util.IfZero(cacheConfig.Interval, 60) + cc, err := chi_cache.NewCacher(chi_cache.Options{ + Adapter: adapter, + AdapterConfig: cacheConfig.Conn, + Interval: interval, + }) + if err != nil { + return nil, err + } + return &stringCache{chiCache: cc}, nil +} + +func (sc *stringCache) Ping() error { + return sc.chiCache.Ping() +} + +func (sc *stringCache) Get(key string) (string, bool) { + v := sc.chiCache.Get(key) + if v == nil { + return "", false + } + s, ok := v.(string) + return s, ok +} + +func (sc *stringCache) Put(key, value string, ttl int64) error { + return sc.chiCache.Put(key, value, ttl) +} + +func (sc *stringCache) Delete(key string) error { + return sc.chiCache.Delete(key) +} + +func (sc *stringCache) IsExist(key string) bool { + return sc.chiCache.IsExist(key) +} + +const cachedErrorPrefix = ":" + +func (sc *stringCache) PutJSON(key string, v any, ttl int64) error { + var s string + switch v := v.(type) { + case error: + s = cachedErrorPrefix + v.Error() + default: + b, err := json.Marshal(v) + if err != nil { + return err + } + s = util.UnsafeBytesToString(b) + } + return sc.chiCache.Put(key, s, ttl) +} + +func (sc *stringCache) GetJSON(key string, ptr any) (exist bool, getErr *GetJSONError) { + s, ok := sc.Get(key) + if !ok || s == "" { + return false, nil + } + s, isCachedError := strings.CutPrefix(s, cachedErrorPrefix) + if isCachedError { + return true, &GetJSONError{cachedError: s} + } + if err := json.Unmarshal(util.UnsafeStringToBytes(s), ptr); err != nil { + return false, &GetJSONError{err: err} + } + return true, nil +} + +func (sc *stringCache) ChiCache() chi_cache.Cache { + return sc.chiCache +} diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index 5b62b90b27..cf9c10d7b4 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -7,18 +7,11 @@ import ( "crypto/sha256" "fmt" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) -// Cache represents a caching interface -type Cache interface { - // Put puts value into cache with key and expire time. - Put(key string, val any, timeout int64) error - // Get gets cached value by given key. - Get(key string) any -} - func getCacheKey(repoPath, commitID, entryPath string) string { hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath))) return fmt.Sprintf("last_commit:%x", hashBytes) @@ -30,11 +23,11 @@ type LastCommitCache struct { ttl func() int64 repo *Repository commitCache map[string]*Commit - cache Cache + cache cache.StringCache } // NewLastCommitCache creates a new last commit cache for repo -func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache Cache) *LastCommitCache { +func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache cache.StringCache) *LastCommitCache { if cache == nil { return nil } @@ -65,7 +58,7 @@ func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) { return nil, nil } - commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)).(string) + commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)) if !ok || commitID == "" { return nil, nil } diff --git a/routers/api/v1/misc/nodeinfo.go b/routers/api/v1/misc/nodeinfo.go index 3bd80de5c1..5973724782 100644 --- a/routers/api/v1/misc/nodeinfo.go +++ b/routers/api/v1/misc/nodeinfo.go @@ -29,9 +29,7 @@ func NodeInfo(ctx *context.APIContext) { nodeInfoUsage := structs.NodeInfoUsage{} if setting.Federation.ShareUserStatistics { - var cached bool - nodeInfoUsage, cached = ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage) - + cached, _ := ctx.Cache.GetJSON(cacheKeyNodeInfoUsage, &nodeInfoUsage) if !cached { usersTotal := int(user_model.CountUsers(ctx, nil)) now := time.Now() @@ -53,7 +51,7 @@ func NodeInfo(ctx *context.APIContext) { LocalComments: int(allComments), } - if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil { + if err := ctx.Cache.PutJSON(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil { ctx.InternalServerError(err) return } diff --git a/services/context/api.go b/services/context/api.go index b18a206b5e..c684add297 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -13,7 +13,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - mc "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httpcache" @@ -21,15 +21,13 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" web_types "code.gitea.io/gitea/modules/web/types" - - "gitea.com/go-chi/cache" ) // APIContext is a specific context for API service type APIContext struct { *Base - Cache cache.Cache + Cache cache.StringCache Doer *user_model.User // current signed-in user IsSigned bool @@ -217,7 +215,7 @@ func APIContexter() func(http.Handler) http.Handler { base, baseCleanUp := NewBaseContext(w, req) ctx := &APIContext{ Base: base, - Cache: mc.GetCache(), + Cache: cache.GetCache(), Repo: &Repository{PullRequest: &PullRequest{}}, Org: &APIOrganization{}, } diff --git a/services/context/captcha.go b/services/context/captcha.go index fa8d779f56..41afe0e7d2 100644 --- a/services/context/captcha.go +++ b/services/context/captcha.go @@ -30,7 +30,7 @@ func GetImageCaptcha() *captcha.Captcha { cpt = captcha.NewCaptcha(captcha.Options{ SubURL: setting.AppSubURL, }) - cpt.Store = cache.GetCache() + cpt.Store = cache.GetCache().ChiCache() }) return cpt } diff --git a/services/context/context.go b/services/context/context.go index 4b318f7e33..7ab48afb73 100644 --- a/services/context/context.go +++ b/services/context/context.go @@ -17,7 +17,7 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - mc "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/setting" @@ -27,7 +27,6 @@ import ( "code.gitea.io/gitea/modules/web/middleware" web_types "code.gitea.io/gitea/modules/web/types" - "gitea.com/go-chi/cache" "gitea.com/go-chi/session" ) @@ -46,7 +45,7 @@ type Context struct { Render Render PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData` - Cache cache.Cache + Cache cache.StringCache Csrf CSRFProtector Flash *middleware.Flash Session session.Store @@ -111,7 +110,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context { Render: render, Session: session, - Cache: mc.GetCache(), + Cache: cache.GetCache(), Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"), Repo: &Repository{PullRequest: &PullRequest{}}, Org: &Organization{}, diff --git a/services/repository/branch.go b/services/repository/branch.go index 229ac54f30..d74e5819a1 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/queue" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" notify_service "code.gitea.io/gitea/services/notify" files_service "code.gitea.io/gitea/services/repository/files" @@ -119,17 +120,15 @@ func getDivergenceCacheKey(repoID int64, branchName string) string { // getDivergenceFromCache gets the divergence from cache func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject, bool) { - data := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName)) + data, ok := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName)) res := git.DivergeObject{ Ahead: -1, Behind: -1, } - s, ok := data.([]byte) - if !ok || len(s) == 0 { + if !ok || data == "" { return &res, false } - - if err := json.Unmarshal(s, &res); err != nil { + if err := json.Unmarshal(util.UnsafeStringToBytes(data), &res); err != nil { log.Error("json.UnMarshal failed: %v", err) return &res, false } @@ -141,7 +140,7 @@ func putDivergenceFromCache(repoID int64, branchName string, divergence *git.Div if err != nil { return err } - return cache.GetCache().Put(getDivergenceCacheKey(repoID, branchName), bs, 30*24*60*60) + return cache.GetCache().Put(getDivergenceCacheKey(repoID, branchName), util.UnsafeBytesToString(bs), 30*24*60*60) } func DelDivergenceFromCache(repoID int64, branchName string) error { diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go index 7c1c6c2609..8a62a603d4 100644 --- a/services/repository/commitstatus/commitstatus.go +++ b/services/repository/commitstatus/commitstatus.go @@ -34,7 +34,7 @@ type commitStatusCacheValue struct { func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheValue { c := cache.GetCache() - statusStr, ok := c.Get(getCacheKey(repoID, branchName)).(string) + statusStr, ok := c.Get(getCacheKey(repoID, branchName)) if ok && statusStr != "" { var cv commitStatusCacheValue err := json.Unmarshal([]byte(statusStr), &cv) diff --git a/services/repository/contributors_graph.go b/services/repository/contributors_graph.go index 7c9f535ae0..b0d6de99ca 100644 --- a/services/repository/contributors_graph.go +++ b/services/repository/contributors_graph.go @@ -17,13 +17,12 @@ import ( "code.gitea.io/gitea/models/avatars" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" - - "gitea.com/go-chi/cache" ) const ( @@ -79,13 +78,13 @@ func findLastSundayBeforeDate(dateStr string) (string, error) { } // GetContributorStats returns contributors stats for git commits for given revision or default branch -func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) { +func GetContributorStats(ctx context.Context, cache cache.StringCache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) { // as GetContributorStats is resource intensive we cache the result cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision) if !cache.IsExist(cacheKey) { genReady := make(chan struct{}) - // dont start multible async generations + // dont start multiple async generations _, run := generateLock.Load(cacheKey) if run { return nil, ErrAwaitGeneration @@ -104,15 +103,11 @@ func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_mode } } // TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout) - - switch v := cache.Get(cacheKey).(type) { - case error: - return nil, v - case map[string]*ContributorData: - return v, nil - default: - return nil, fmt.Errorf("unexpected type in cache detected") + var res map[string]*ContributorData + if _, cacheErr := cache.GetJSON(cacheKey, &res); cacheErr != nil { + return nil, fmt.Errorf("cached error: %w", cacheErr.ToError()) } + return res, nil } // getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision @@ -205,13 +200,12 @@ func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int return extendedCommitStats, nil } -func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey string, repo *repo_model.Repository, revision string) { +func generateContributorStats(genDone chan struct{}, cache cache.StringCache, cacheKey string, repo *repo_model.Repository, revision string) { ctx := graceful.GetManager().HammerContext() gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) if err != nil { - err := fmt.Errorf("OpenRepository: %w", err) - _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) + _ = cache.PutJSON(cacheKey, fmt.Errorf("OpenRepository: %w", err), contributorStatsCacheTimeout) return } defer closer.Close() @@ -221,13 +215,11 @@ func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey } extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision) if err != nil { - err := fmt.Errorf("ExtendedCommitStats: %w", err) - _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) + _ = cache.PutJSON(cacheKey, fmt.Errorf("ExtendedCommitStats: %w", err), contributorStatsCacheTimeout) return } if len(extendedCommitStats) == 0 { - err := fmt.Errorf("no commit stats returned for revision '%s'", revision) - _ = cache.Put(cacheKey, err, contributorStatsCacheTimeout) + _ = cache.PutJSON(cacheKey, fmt.Errorf("no commit stats returned for revision '%s'", revision), contributorStatsCacheTimeout) return } @@ -309,7 +301,7 @@ func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey total.TotalCommits++ } - _ = cache.Put(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout) + _ = cache.PutJSON(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout) generateLock.Delete(cacheKey) if genDone != nil { genDone <- struct{}{} diff --git a/services/repository/contributors_graph_test.go b/services/repository/contributors_graph_test.go index 3801a5eee4..f22c115276 100644 --- a/services/repository/contributors_graph_test.go +++ b/services/repository/contributors_graph_test.go @@ -10,9 +10,9 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/setting" - "gitea.com/go-chi/cache" "github.com/stretchr/testify/assert" ) @@ -20,20 +20,18 @@ func TestRepository_ContributorsGraph(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) assert.NoError(t, repo.LoadOwner(db.DefaultContext)) - mockCache, err := cache.NewCacher(cache.Options{ - Adapter: "memory", - Interval: 24 * 60, - }) + mockCache, err := cache.NewStringCache(setting.Cache{}) assert.NoError(t, err) generateContributorStats(nil, mockCache, "key", repo, "404ref") - err, isErr := mockCache.Get("key").(error) - assert.True(t, isErr) - assert.ErrorAs(t, err, &git.ErrNotExist{}) + var data map[string]*ContributorData + _, getErr := mockCache.GetJSON("key", &data) + assert.NotNil(t, getErr) + assert.ErrorContains(t, getErr.ToError(), "object does not exist") generateContributorStats(nil, mockCache, "key2", repo, "master") - data, isData := mockCache.Get("key2").(map[string]*ContributorData) - assert.True(t, isData) + exist, _ := mockCache.GetJSON("key2", &data) + assert.True(t, exist) var keys []string for k := range data { keys = append(keys, k)