Files
cache_manager/manager/cache_manager_test.go
T
2026-05-16 19:54:12 +08:00

445 lines
10 KiB
Go

package manager
import (
"context"
"encoding/json"
"fmt"
"os"
"testing"
"time"
"wallet-pay-api/common/global"
"wallet-pay-api/common/model"
"wallet-pay-api/pkg/loggerx"
"github.com/redis/go-redis/v9"
)
func newTestRedisCache() *CacheRedis {
// 支持通过环境变量覆盖 redis 地址
addr := os.Getenv("TEST_REDIS_ADDR")
if addr == "" {
addr = "10.40.92.54:30379"
}
client := redis.NewClient(&redis.Options{
Addr: addr,
Password: "123000",
DB: 0,
})
return NewCacheRedis(context.Background(), client, "test")
}
func TestBatchGetRedis(t *testing.T) {
ctx := context.Background()
cache := newTestRedisCache()
keys := []string{"k1", "k2", "k3"}
resp, err := BatchGetRedis[model.YuebaoUserAsset](cache, ctx, keys)
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", resp)
}
func TestCacheRedis_Types(t *testing.T) {
ctx := context.Background()
cache := newTestRedisCache()
// int
err := cache.Set(ctx, "int", 123, time.Second*10, []int64{123})
if err != nil {
t.Fatal(err)
}
var i int
err = cache.Get(ctx, "int", &i)
if err != nil || i != 123 {
t.Errorf("int get failed: %v, %d", err, i)
}
// string
err = cache.Set(ctx, "str", "abc", time.Second*10, []int64{124})
if err != nil {
t.Fatal(err)
}
var s string
err = cache.Get(ctx, "str", &s)
if err != nil || s != "abc" {
t.Errorf("string get failed: %v, %s", err, s)
}
// struct
st := TestStruct{Name: "foo"}
err = cache.Set(ctx, "struct", st, time.Second*10, []int64{125})
if err != nil {
t.Fatal(err)
}
var st2 TestStruct
err = cache.Get(ctx, "struct", &st2)
if err != nil || st2.Name != "foo" {
t.Errorf("struct get failed: %v, %+v", err, st2)
}
// map
m := map[string]int{"a": 1}
err = cache.Set(ctx, "map", m, time.Second*10, []int64{126})
if err != nil {
t.Fatal(err)
}
var m2 map[string]int
err = cache.Get(ctx, "map", &m2)
if err != nil || m2["a"] != 1 {
t.Errorf("map get failed: %v, %+v", err, m2)
}
// slice
sl := []string{"x", "y"}
err = cache.Set(ctx, "slice", sl, time.Second*10, []int64{127})
if err != nil {
t.Fatal(err)
}
var sl2 []string
err = cache.Get(ctx, "slice", &sl2)
if err != nil || len(sl2) != 2 || sl2[0] != "x" {
t.Errorf("slice get failed: %v, %+v", err, sl2)
}
}
func TestCacheRedis_Expire(t *testing.T) {
ctx := context.Background()
cache := newTestRedisCache()
err := cache.Set(ctx, "expire", "v", time.Millisecond*200, []int64{128})
if err != nil {
t.Fatal(err)
}
var s string
err = cache.Get(ctx, "expire", &s)
if err != nil || s != "v" {
t.Errorf("expire get failed: %v, %s", err, s)
}
time.Sleep(time.Millisecond * 250)
err = cache.Get(ctx, "expire", &s)
if err == nil {
t.Errorf("should expire, got: %v", s)
}
}
func TestCacheRedis_Del(t *testing.T) {
ctx := context.Background()
cache := newTestRedisCache()
err := cache.Set(ctx, "del", 1, time.Second*10, []int64{129})
if err != nil {
t.Fatal(err)
}
err = cache.Del(ctx, "del")
if err != nil {
t.Fatal(err)
}
var i int
err = cache.Get(ctx, "del", &i)
if err == nil {
t.Errorf("should not get deleted key")
}
}
func TestCacheRedis_TypeMismatch(t *testing.T) {
ctx := context.Background()
cache := newTestRedisCache()
err := cache.Set(ctx, "mismatch", 123, time.Second*10, []int64{123})
if err != nil {
t.Fatal(err)
}
var s string
err = cache.Get(ctx, "mismatch", &s)
if err == nil {
t.Errorf("should type mismatch")
}
}
func TestCacheRedis_BatchGet(t *testing.T) {
ctx := context.Background()
cache := newTestRedisCache()
cache.Set(ctx, "ba", 1, time.Second*10, []int64{1})
cache.Set(ctx, "bb", 2, time.Second*10, []int64{2})
cache.Set(ctx, "bc", 3, time.Second*10, []int64{3})
keys := []string{"bj", "ba", "bb", "bx"}
m, err := BatchGetRedis[int](cache, ctx, keys)
if err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(m)
fmt.Println(string(b))
if len(m) != 2 || *m["ba"] != 1 || *m["bb"] != 2 {
t.Errorf("batch get failed: %+v", m)
}
}
func TestCacheManager_Redis_Generic(t *testing.T) {
ctx := context.Background()
cache := newTestRedisCache()
m := NewCacheManager[TestStruct](cache, "pref", func(t *TestStruct) int64 { return 0 })
data := TestStruct{Name: "bar"}
err := m.Set(ctx, "k1", &data, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
got, err := m.Get(ctx, "k1")
if err != nil || got.Name != "bar" {
t.Errorf("manager redis get failed: %v, %+v", err, got)
}
err = m.Del(ctx, "k1")
if err != nil {
t.Fatal(err)
}
got, err = m.Get(ctx, "k1")
if err == nil {
t.Errorf("manager redis del failed")
}
}
func TestICacheInterface(t *testing.T) {
ctx := context.Background()
var cache ICache = NewCacheLocal(time.Second * 10)
err := cache.Set(ctx, "iface", "val", time.Second*10, []int64{130})
if err != nil {
t.Fatal(err)
}
var s string
err = cache.Get(ctx, "iface", &s)
if err != nil || s != "val" {
t.Errorf("iface get failed: %v, %s", err, s)
}
err = cache.Del(ctx, "iface")
if err != nil {
t.Fatal(err)
}
err = cache.Get(ctx, "iface", &s)
if err == nil {
t.Errorf("iface del failed")
}
}
func TestCacheManager_Generic(t *testing.T) {
ctx := context.Background()
cache := NewCacheLocal(time.Second * 10)
m := NewCacheManager[TestStruct](cache, "pref", func(t *TestStruct) int64 { return 0 })
data := TestStruct{Name: "bar"}
err := m.Set(ctx, "k1", &data, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
got, err := m.Get(ctx, "k1")
if err != nil || got.Name != "bar" {
t.Errorf("manager get failed: %v, %+v", err, got)
}
err = m.Del(ctx, "k1")
if err != nil {
t.Fatal(err)
}
got, err = m.Get(ctx, "k1")
if err == nil {
t.Errorf("manager del failed")
}
}
func TestCacheLocal_Boundary(t *testing.T) {
ctx := context.Background()
cache := NewCacheLocal(time.Second * 10)
// 空 key
err := cache.Set(ctx, "", "v", time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
var s string
err = cache.Get(ctx, "", &s)
if err != nil || s != "v" {
t.Errorf("empty key failed: %v, %s", err, s)
}
// nil value
err = cache.Set(ctx, "nil", nil, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
var x any
err = cache.Get(ctx, "nil", &x)
if err != nil || x != nil {
t.Errorf("nil value failed: %v, %v", err, x)
}
// 极短 ttl
err = cache.Set(ctx, "short", "v", time.Millisecond*1, []int64{0})
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 5)
err = cache.Get(ctx, "short", &s)
if err == nil {
t.Errorf("short ttl should expire")
}
// 重复 set
err = cache.Set(ctx, "dup", "v1", time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
err = cache.Set(ctx, "dup", "v2", time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
err = cache.Get(ctx, "dup", &s)
if err != nil || s != "v2" {
t.Errorf("dup set failed: %v, %s", err, s)
}
// Stop 后行为
cache.Stop()
// Stop 后再次 Stop 不应 panic
cache.Stop()
}
func TestCacheLocal_Concurrent(t *testing.T) {
ctx := context.Background()
cache := NewCacheLocal(time.Second * 10)
n := 100
done := make(chan struct{}, n*2)
for i := 0; i < n; i++ {
go func(i int) {
key := "k" + string(rune(i))
cache.Set(ctx, key, i, time.Second*10, []int64{int64(i)})
done <- struct{}{}
}(i)
go func(i int) {
key := "k" + string(rune(i))
var v int
cache.Get(ctx, key, &v)
done <- struct{}{}
}(i)
}
for i := 0; i < n*2; i++ {
<-done
}
}
func TestCacheLocal_Types(t *testing.T) {
ctx := context.Background()
cache := NewCacheLocal(time.Second * 10)
// int
err := cache.Set(ctx, "int", 123, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
var i int
err = cache.Get(ctx, "int", &i)
if err != nil || i != 123 {
t.Errorf("int get failed: %v, %d", err, i)
}
// string
err = cache.Set(ctx, "str", "abc", time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
var s string
err = cache.Get(ctx, "str", &s)
if err != nil || s != "abc" {
t.Errorf("string get failed: %v, %s", err, s)
}
// struct
st := TestStruct{Name: "foo"}
err = cache.Set(ctx, "struct", st, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
var st2 TestStruct
err = cache.Get(ctx, "struct", &st2)
if err != nil || st2.Name != "foo" {
t.Errorf("struct get failed: %v, %+v", err, st2)
}
// map
m := map[string]int{"a": 1}
err = cache.Set(ctx, "map", m, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
var m2 map[string]int
err = cache.Get(ctx, "map", &m2)
if err != nil || m2["a"] != 1 {
t.Errorf("map get failed: %v, %+v", err, m2)
}
// slice
sl := []string{"x", "y"}
err = cache.Set(ctx, "slice", sl, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
var sl2 []string
err = cache.Get(ctx, "slice", &sl2)
if err != nil || len(sl2) != 2 || sl2[0] != "x" {
t.Errorf("slice get failed: %v, %+v", err, sl2)
}
}
func TestCacheLocal_Expire(t *testing.T) {
ctx := context.Background()
cache := NewCacheLocal(time.Millisecond * 50)
err := cache.Set(ctx, "expire", "v", time.Millisecond*100, []int64{0})
if err != nil {
t.Fatal(err)
}
var s string
err = cache.Get(ctx, "expire", &s)
if err != nil || s != "v" {
t.Errorf("expire get failed: %v, %s", err, s)
}
time.Sleep(time.Millisecond * 120)
err = cache.Get(ctx, "expire", &s)
if err == nil {
t.Errorf("should expire, got: %v", s)
}
}
func TestCacheLocal_Del(t *testing.T) {
ctx := context.Background()
cache := NewCacheLocal(time.Second * 10)
err := cache.Set(ctx, "del", 1, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
err = cache.Del(ctx, "del")
if err != nil {
t.Fatal(err)
}
var i int
err = cache.Get(ctx, "del", &i)
if err == nil {
t.Errorf("should not get deleted key")
}
}
func TestCacheLocal_TypeMismatch(t *testing.T) {
ctx := context.Background()
cache := NewCacheLocal(time.Second * 10)
err := cache.Set(ctx, "mismatch", 123, time.Second*10, []int64{0})
if err != nil {
t.Fatal(err)
}
var s string
err = cache.Get(ctx, "mismatch", &s)
if err == nil {
t.Errorf("should type mismatch")
}
}
func TestCacheLocal_BatchGet(t *testing.T) {
ctx := context.Background()
cache := NewCacheLocal(time.Second * 10)
cache.Set(ctx, "a", 1, time.Second*10, []int64{0})
cache.Set(ctx, "b", 2, time.Second*10, []int64{0})
cache.Set(ctx, "c", 3, time.Second*10, []int64{0})
keys := []string{"a", "b", "x"}
m, err := BatchGetLocal[int](cache, ctx, keys)
if err != nil {
t.Fatal(err)
}
if len(m) != 2 || *m["a"] != 1 || *m["b"] != 2 {
t.Errorf("batch get failed: %+v", m)
}
}
type TestStruct struct {
Name string `json:"name"`
}