package cache import ( "context" "encoding/json" "fmt" "io/ioutil" "net/http" "os" "strings" "sync" "sync/atomic" "testing" "time" "go-common/library/cache/memcache" "go-common/library/container/pool" "go-common/library/ecode" "go-common/library/log" bm "go-common/library/net/http/blademaster" "go-common/library/net/http/blademaster/middleware/cache/store" xtime "go-common/library/time" "github.com/stretchr/testify/assert" ) const ( SockAddr = "127.0.0.1:18080" McSockAddr = "172.16.33.54:11211" ) func uri(base, path string) string { return fmt.Sprintf("%s://%s%s", "http", base, path) } func init() { log.Init(nil) } func newMemcache() (*Cache, func()) { s := store.NewMemcache(&memcache.Config{ Config: &pool.Config{ Active: 10, Idle: 2, IdleTimeout: xtime.Duration(time.Second), }, Name: "test", Proto: "tcp", Addr: McSockAddr, DialTimeout: xtime.Duration(time.Second), ReadTimeout: xtime.Duration(time.Second), WriteTimeout: xtime.Duration(time.Second), }) return New(s), func() {} } func newFile() (*Cache, func()) { path, err := ioutil.TempDir("", "cache-test") if err != nil { panic("Failed to create cache directory") } s := store.NewFile(&store.FileConfig{ RootDir: path, }) remove := func() { os.RemoveAll(path) } return New(s), remove } func TestPage(t *testing.T) { memcache, remove1 := newMemcache() filestore, remove2 := newFile() defer func() { remove1() remove2() }() t.Run("Memcache Store", pageCase(memcache, true)) t.Run("File Store", pageCase(filestore, false)) } func TestControl(t *testing.T) { memcache, remove1 := newMemcache() filestore, remove2 := newFile() defer func() { remove1() remove2() }() t.Run("Memcache Store", controlCase(memcache, true)) t.Run("File Store", controlCase(filestore, false)) } func TestPageCacheMultiWrite(t *testing.T) { memcache, remove1 := newMemcache() filestore, remove2 := newFile() defer func() { remove1() remove2() }() t.Run("Memcache Store", pageMultiWriteCase(memcache)) t.Run("File Store", pageMultiWriteCase(filestore)) } func TestDegrade(t *testing.T) { memcache, remove1 := newMemcache() filestore, remove2 := newFile() defer func() { remove1() remove2() }() t.Run("Memcache Store", degradeCase(memcache)) t.Run("File Store", degradeCase(filestore)) } func pageCase(cache *Cache, testExpire bool) func(t *testing.T) { return func(t *testing.T) { expire := int32(3) pc := NewPage(expire) engine := bm.Default() engine.GET("/page-cache", cache.Cache(pc, nil), func(ctx *bm.Context) { ctx.Writer.Header().Set("X-Hello", "World") ctx.String(203, "%s\n", time.Now().String()) }) go engine.Run(SockAddr) defer func() { engine.Server().Shutdown(context.Background()) }() time.Sleep(time.Second) code1, content1, headers1, err1 := httpGet(uri(SockAddr, "/page-cache")) code2, content2, headers2, err2 := httpGet(uri(SockAddr, "/page-cache")) assert.Nil(t, err1) assert.Nil(t, err2) assert.Equal(t, code1, 203) assert.Equal(t, code2, 203) assert.NotNil(t, content1) assert.NotNil(t, content2) assert.Equal(t, headers1["X-Hello"], []string{"World"}) assert.Equal(t, headers2["X-Hello"], []string{"World"}) assert.Equal(t, string(content1), string(content2)) if !testExpire { return } // test if the last caching is expired t.Logf("Waiting %d seconds for caching expire test", expire+1) time.Sleep(time.Second * time.Duration(expire+1)) _, content3, _, err3 := httpGet(uri(SockAddr, "/page-cache")) _, content4, _, err4 := httpGet(uri(SockAddr, "/page-cache")) assert.Nil(t, err3) assert.Nil(t, err4) assert.NotNil(t, content1) assert.NotNil(t, content2) assert.NotEqual(t, string(content1), string(content3)) assert.Equal(t, string(content3), string(content4)) } } func pageMultiWriteCase(cache *Cache) func(t *testing.T) { return func(t *testing.T) { chunks := []string{ "Hello", "World", "Hello", "World", "Hello", "World", "Hello", "World", } pc := NewPage(3) engine := bm.Default() engine.GET("/page-cache-write", cache.Cache(pc, nil), func(ctx *bm.Context) { ctx.Writer.Header().Set("X-Hello", "World") ctx.Writer.WriteHeader(203) for _, chunk := range chunks { ctx.Writer.Write([]byte(chunk)) } }) go engine.Run(SockAddr) defer func() { engine.Server().Shutdown(context.Background()) }() time.Sleep(time.Second) code1, content1, headers1, err1 := httpGet(uri(SockAddr, "/page-cache-write")) code2, content2, headers2, err2 := httpGet(uri(SockAddr, "/page-cache-write")) assert.Nil(t, err1) assert.Nil(t, err2) assert.Equal(t, code1, 203) assert.Equal(t, code2, 203) assert.NotNil(t, content1) assert.NotNil(t, content2) assert.Equal(t, headers1["X-Hello"], []string{"World"}) assert.Equal(t, headers2["X-Hello"], []string{"World"}) assert.Equal(t, strings.Join(chunks, ""), string(content1)) assert.Equal(t, strings.Join(chunks, ""), string(content2)) assert.Equal(t, string(content1), string(content2)) } } func degradeCase(cache *Cache) func(t *testing.T) { return func(t *testing.T) { wg := sync.WaitGroup{} i := int32(0) degrade := NewDegrader(10) engine := bm.Default() engine.GET("/scheduled/error", cache.Cache(degrade.Args("name", "age"), nil), func(c *bm.Context) { code := atomic.AddInt32(&i, 1) if code == 5 { c.JSON("succeed", nil) return } if code%2 == 0 { c.JSON("", ecode.Degrade) return } c.JSON(fmt.Sprintf("Code: %d", code), ecode.Int(int(code))) }) wg.Add(1) go func() { engine.Run(":18080") wg.Done() }() defer func() { engine.Server().Shutdown(context.TODO()) wg.Wait() }() time.Sleep(time.Second) for index := 1; index < 10; index++ { _, content, _, _ := httpGet(uri(SockAddr, "/scheduled/error?name=degrader&age=26")) t.Log(index, string(content)) var res struct { Data string `json:"data"` } err := json.Unmarshal(content, &res) assert.Nil(t, err) if index == 5 { // ensure response is write to cache time.Sleep(time.Second) } if index > 5 && index%2 == 0 { if res.Data != "succeed" { t.Fatalf("Failed to degrade at index: %d", index) } else { t.Logf("This request is degraded at index: %d", index) } } } } } func controlCase(cache *Cache, testExpire bool) func(t *testing.T) { return func(t *testing.T) { wg := sync.WaitGroup{} i := int32(0) expire := int32(30) control := NewControl(expire) filter := func(ctx *bm.Context) bool { if ctx.Request.Form.Get("cache") == "false" { return false } return true } engine := bm.Default() engine.GET("/large/response", cache.Cache(control, filter), func(c *bm.Context) { c.JSON(map[string]interface{}{ "index": atomic.AddInt32(&i, 1), "Hello0": "World", "Hello1": "World", "Hello2": "World", "Hello3": "World", "Hello4": "World", "Hello5": "World", "Hello6": "World", "Hello7": "World", "Hello8": "World", }, nil) }) engine.GET("/large/response/error", cache.Cache(control, filter), func(c *bm.Context) { c.JSON(nil, ecode.RequestErr) }) wg.Add(1) go func() { engine.Run(":18080") wg.Done() }() defer func() { engine.Server().Shutdown(context.TODO()) wg.Wait() }() time.Sleep(time.Second) code, content, headers, err := httpGet(uri(SockAddr, "/large/response?name=hello&age=1")) assert.NoError(t, err) assert.Equal(t, 200, code) assert.NotEmpty(t, content) assert.Equal(t, "max-age=30", headers.Get("Cache-Control")) exp, err := http.ParseTime(headers.Get("Expires")) assert.NoError(t, err) assert.InDelta(t, 30, exp.Unix()-time.Now().Unix(), 5) code, content, headers, err = httpGet(uri(SockAddr, "/large/response/error?name=hello&age=1&cache=false")) assert.NoError(t, err) assert.Equal(t, 200, code) assert.NotEmpty(t, content) assert.Empty(t, headers.Get("Expires")) assert.Empty(t, headers.Get("Cache-Control")) code, content, headers, err = httpGet(uri(SockAddr, "/large/response/error?name=hello&age=1")) assert.NoError(t, err) assert.Equal(t, 200, code) assert.NotEmpty(t, content) assert.Empty(t, headers.Get("Expires")) assert.Empty(t, headers.Get("Cache-Control")) } } func httpGet(url string) (code int, content []byte, headers http.Header, err error) { resp, err := http.Get(url) if err != nil { return } defer resp.Body.Close() if content, err = ioutil.ReadAll(resp.Body); err != nil { return } code = resp.StatusCode headers = resp.Header return }