package cache import ( "context" "errors" "sync/atomic" "testing" "time" ) func TestMemoryOwnershipCapacityExpiryAndClose(t *testing.T) { m, err := NewMemory(MemoryConfig{MaxEntries: 2, MaxBytes: 10}) if err != nil { t.Fatal(err) } now := time.Now() m.now = func() time.Time { return now } ctx := context.Background() value := []byte("aa") if err := m.Set(ctx, "a", value, time.Second); err != nil { t.Fatal(err) } value[0] = 'x' got, _ := m.Get(ctx, "a") if string(got) != "aa" { t.Fatal("retained caller memory") } got[0] = 'z' got, _ = m.Get(ctx, "a") if string(got) != "aa" { t.Fatal("returned mutable cache memory") } m.Set(ctx, "b", []byte("bb"), 0) m.Get(ctx, "a") m.Set(ctx, "c", []byte("cc"), 0) if _, err := m.Get(ctx, "b"); !errors.Is(err, ErrMiss) { t.Fatal("LRU eviction failed") } now = now.Add(time.Second) if _, err := m.Get(ctx, "a"); !errors.Is(err, ErrMiss) { t.Fatal("expiration boundary ignored") } if err := m.Set(ctx, "large", []byte("123456"), 0); !errors.Is(err, ErrTooLarge) { t.Fatal(err) } if _, err := m.Get(ctx, "c"); err != nil { t.Fatal("oversized write evicted valid data") } canceled, cancel := context.WithCancel(ctx) cancel() if err := m.Delete(canceled, "c"); !errors.Is(err, context.Canceled) { t.Fatal(err) } m.Close() m.Close() if _, err := m.Get(ctx, "c"); !errors.Is(err, ErrClosed) { t.Fatal(err) } } func TestLoaderCoalescesAndSeparatesCallerCancellation(t *testing.T) { m, _ := NewMemory(MemoryConfig{}) loader, _ := NewLoader(m, LoaderConfig{MaxLoads: 1, Timeout: time.Second}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() started, release := make(chan struct{}), make(chan struct{}) var calls atomic.Int32 load := func(ctx context.Context) ([]byte, error) { calls.Add(1) close(started) select { case <-release: return []byte("value"), nil case <-ctx.Done(): return nil, ctx.Err() } } first := make(chan error, 1) go func() { _, err := loader.GetOrLoad(ctx, "key", time.Minute, load); first <- err }() <-started second := make(chan error, 1) go func() { v, err := loader.GetOrLoad(context.Background(), "key", time.Minute, load) if err == nil && string(v) != "value" { err = errors.New("wrong value") } second <- err }() if _, err := loader.GetOrLoad(context.Background(), "other", 0, load); !errors.Is(err, ErrBusy) { t.Fatal(err) } cancel() if err := <-first; !errors.Is(err, context.Canceled) { t.Fatal(err) } close(release) if err := <-second; err != nil { t.Fatal(err) } if calls.Load() != 1 { t.Fatalf("loads=%d", calls.Load()) } } func TestLoaderPanicDeadlineAndStoreFailure(t *testing.T) { m, _ := NewMemory(MemoryConfig{}) l, _ := NewLoader(m, LoaderConfig{Timeout: 20 * time.Millisecond}) if _, err := l.GetOrLoad(context.Background(), "panic", 0, func(context.Context) ([]byte, error) { panic("failure") }); err == nil { t.Fatal("panic escaped") } if _, err := l.GetOrLoad(context.Background(), "panic", 0, func(ctx context.Context) ([]byte, error) { <-ctx.Done(); return nil, ctx.Err() }); !errors.Is(err, context.DeadlineExceeded) { t.Fatal(err) } m.Close() if _, err := l.GetOrLoad(context.Background(), "closed", 0, func(context.Context) ([]byte, error) { t.Fatal("store outage triggered load"); return nil, nil }); !errors.Is(err, ErrClosed) { t.Fatal(err) } }