mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
Implements the cache.Cache interface with in-memory backend (TTL, maxSize/maxBytes bounds, oldest/LRU eviction, background cleanup). Adds httpcache layer for HTTP response policy (per-status TTL, serve-stale, method/body filtering). Wires into config parser, loader, registry, and all four sniffing handlers (forward/local, forward/remote, sni, http) so HTTP responses are cached before dialing upstream. Bumps core dependency to v0.6.0 for the new cache.Cache interface.
206 lines
5.3 KiB
Go
206 lines
5.3 KiB
Go
package cache
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/cache"
|
|
)
|
|
|
|
func TestSetGet(t *testing.T) {
|
|
c := NewMemoryCache(Options{DefaultTTL: 10 * time.Second})
|
|
ctx := context.Background()
|
|
|
|
err := c.Set(ctx, "k", []byte("hello"))
|
|
if err != nil {
|
|
t.Fatalf("Set: %v", err)
|
|
}
|
|
|
|
e, err := c.Get(ctx, "k")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if string(e.Data) != "hello" {
|
|
t.Fatalf("data = %q, want %q", e.Data, "hello")
|
|
}
|
|
if e.Expired() {
|
|
t.Fatal("fresh entry reported expired")
|
|
}
|
|
if e.Expiration.IsZero() {
|
|
t.Fatal("expiration is zero")
|
|
}
|
|
}
|
|
|
|
func TestGetNotFound(t *testing.T) {
|
|
c := NewMemoryCache(Options{})
|
|
_, err := c.Get(context.Background(), "nosuch")
|
|
if err != cache.ErrNotFound {
|
|
t.Fatalf("err = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestGetExpiredStillReturned(t *testing.T) {
|
|
c := NewMemoryCache(Options{})
|
|
ctx := context.Background()
|
|
if err := c.Set(ctx, "k", []byte("data"), cache.WithTTL(1*time.Nanosecond)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
e, err := c.Get(ctx, "k")
|
|
if err != nil {
|
|
t.Fatal("expired entry should still be returned for serve-stale")
|
|
}
|
|
if !e.Expired() {
|
|
t.Fatal("expected expired")
|
|
}
|
|
}
|
|
|
|
func TestWithTTLOverride(t *testing.T) {
|
|
c := NewMemoryCache(Options{DefaultTTL: 1 * time.Hour})
|
|
ctx := context.Background()
|
|
if err := c.Set(ctx, "k", []byte("x"), cache.WithTTL(50*time.Millisecond)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(60 * time.Millisecond)
|
|
e, _ := c.Get(ctx, "k")
|
|
if !e.Expired() {
|
|
t.Fatal("WithTTL should override default")
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
c := NewMemoryCache(Options{DefaultTTL: 10 * time.Second})
|
|
ctx := context.Background()
|
|
c.Set(ctx, "k", []byte("x"))
|
|
if err := c.Delete(ctx, "k"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := c.Get(ctx, "k"); err != cache.ErrNotFound {
|
|
t.Fatalf("err = %v, want ErrNotFound after delete", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteAbsentKey(t *testing.T) {
|
|
c := NewMemoryCache(Options{})
|
|
if err := c.Delete(context.Background(), "nosuch"); err != nil {
|
|
t.Fatalf("Delete absent key: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNeverExpires(t *testing.T) {
|
|
c := NewMemoryCache(Options{}) // no default TTL
|
|
ctx := context.Background()
|
|
c.Set(ctx, "k", []byte("data"))
|
|
time.Sleep(10 * time.Millisecond)
|
|
e, _ := c.Get(ctx, "k")
|
|
if e.Expired() {
|
|
t.Fatal("entry with no TTL should never expire")
|
|
}
|
|
if e.TTL() != -1 {
|
|
t.Fatalf("TTL = %v, want -1", e.TTL())
|
|
}
|
|
}
|
|
|
|
func TestMaxSizeEviction(t *testing.T) {
|
|
c := NewMemoryCache(Options{MaxSize: 2, Eviction: EvictOldest})
|
|
ctx := context.Background()
|
|
c.Set(ctx, "a", []byte("1"))
|
|
c.Set(ctx, "b", []byte("2"))
|
|
c.Set(ctx, "c", []byte("3")) // evicts oldest (a)
|
|
|
|
if _, err := c.Get(ctx, "a"); err != cache.ErrNotFound {
|
|
t.Fatal("a should have been evicted")
|
|
}
|
|
if _, err := c.Get(ctx, "b"); err == cache.ErrNotFound {
|
|
t.Fatal("b should remain")
|
|
}
|
|
if _, err := c.Get(ctx, "c"); err == cache.ErrNotFound {
|
|
t.Fatal("c should remain")
|
|
}
|
|
}
|
|
|
|
func TestMaxSizeLRU(t *testing.T) {
|
|
c := NewMemoryCache(Options{MaxSize: 2, Eviction: EvictLRU})
|
|
ctx := context.Background()
|
|
c.Set(ctx, "a", []byte("1"))
|
|
c.Set(ctx, "b", []byte("2"))
|
|
c.Get(ctx, "a") // touch a
|
|
c.Set(ctx, "c", []byte("3")) // LRU-evicts b (a's lastAccess is newer)
|
|
|
|
if _, err := c.Get(ctx, "b"); err != cache.ErrNotFound {
|
|
t.Fatal("b should have been evicted (LRU)")
|
|
}
|
|
if _, err := c.Get(ctx, "a"); err == cache.ErrNotFound {
|
|
t.Fatal("a should remain (touched)")
|
|
}
|
|
}
|
|
|
|
func TestMaxBytesEviction(t *testing.T) {
|
|
c := NewMemoryCache(Options{MaxBytes: 4})
|
|
ctx := context.Background()
|
|
c.Set(ctx, "a", []byte{1, 2}) // 2 bytes, total=2
|
|
c.Set(ctx, "b", []byte{3, 4}) // 2 bytes, total=4
|
|
c.Set(ctx, "c", []byte{5}) // 1 byte, total=5 > 4 → evicts oldest (a)
|
|
|
|
if _, err := c.Get(ctx, "a"); err != cache.ErrNotFound {
|
|
t.Fatal("a should have been evicted (maxBytes)")
|
|
}
|
|
if _, err := c.Get(ctx, "b"); err == cache.ErrNotFound {
|
|
t.Fatal("b should remain")
|
|
}
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
c := NewMemoryCache(Options{CleanupInterval: 10 * time.Millisecond})
|
|
closer := c.(io.Closer)
|
|
if err := closer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := closer.Close(); err != nil {
|
|
t.Fatal("second Close should be a no-op")
|
|
}
|
|
}
|
|
|
|
func TestCloseStopsBackgroundScan(t *testing.T) {
|
|
c := NewMemoryCache(Options{
|
|
DefaultTTL: 5 * time.Millisecond,
|
|
CleanupInterval: 10 * time.Millisecond,
|
|
})
|
|
ctx := context.Background()
|
|
c.Set(ctx, "k", []byte("data"))
|
|
time.Sleep(20 * time.Millisecond)
|
|
e, _ := c.Get(ctx, "k")
|
|
// Without Close, background scanner cleared expired entries. With Close
|
|
// before sleep, the entry stays. We test that Close means the goroutine
|
|
// stops and does NOT panic another background prune. Just exercising the
|
|
// code path.
|
|
if e.Expired() {
|
|
t.Log("expired (expected if scanner ran)")
|
|
}
|
|
c.(io.Closer).Close()
|
|
time.Sleep(30 * time.Millisecond) // goroutine should have stopped
|
|
// No panic = pass
|
|
}
|
|
|
|
func TestDataCopied(t *testing.T) {
|
|
c := NewMemoryCache(Options{DefaultTTL: 1 * time.Hour})
|
|
ctx := context.Background()
|
|
data := []byte("mutable")
|
|
c.Set(ctx, "k", data)
|
|
data[0] = 'X'
|
|
|
|
e, _ := c.Get(ctx, "k")
|
|
if string(e.Data) != "mutable" {
|
|
t.Fatal("Get returns internal data, not a copy")
|
|
}
|
|
// Mutate Get's returned slice — original should be unchanged
|
|
e.Data[0] = 'Y'
|
|
e2, _ := c.Get(ctx, "k")
|
|
if string(e2.Data) != "mutable" {
|
|
t.Fatal("Get returns shared slice")
|
|
}
|
|
}
|