Go · 3541 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package lru
4
5 import (
6 "context"
7 "errors"
8 "strconv"
9 "sync"
10 "sync/atomic"
11 "testing"
12 "time"
13 )
14
15 func TestCache_GetSetEviction(t *testing.T) {
16 t.Parallel()
17 c := New[string, int](2)
18 c.Set("a", 1)
19 c.Set("b", 2)
20 if v, ok := c.Get("a"); !ok || v != 1 {
21 t.Fatalf("Get(a) = %d,%v; want 1,true", v, ok)
22 }
23 // Touching "a" makes "b" the LRU.
24 c.Set("c", 3)
25 if _, ok := c.Get("b"); ok {
26 t.Errorf("b should have been evicted")
27 }
28 if v, ok := c.Get("c"); !ok || v != 3 {
29 t.Errorf("c = %d,%v; want 3,true", v, ok)
30 }
31 if s := c.Stats(); s.Evictions != 1 {
32 t.Errorf("Evictions = %d; want 1", s.Evictions)
33 }
34 }
35
36 func TestCache_TTLExpiry(t *testing.T) {
37 t.Parallel()
38 c := NewWithTTL[string, int](4, 50*time.Millisecond)
39 now := time.Now()
40 c.now = func() time.Time { return now }
41 c.Set("k", 42)
42 if v, ok := c.Get("k"); !ok || v != 42 {
43 t.Fatalf("fresh hit: got %d,%v; want 42,true", v, ok)
44 }
45 now = now.Add(60 * time.Millisecond)
46 if _, ok := c.Get("k"); ok {
47 t.Errorf("expired entry should be a miss")
48 }
49 }
50
51 func TestCache_DeleteAndStats(t *testing.T) {
52 t.Parallel()
53 c := New[string, int](2)
54 c.Set("a", 1)
55 c.Get("a")
56 c.Get("missing")
57 c.Delete("a")
58 if c.Len() != 0 {
59 t.Errorf("Len after Delete = %d; want 0", c.Len())
60 }
61 s := c.Stats()
62 if s.Hits != 1 || s.Misses != 1 {
63 t.Errorf("stats = %+v; want hits=1 misses=1", s)
64 }
65 }
66
67 func TestSizedCache_BytesBounded(t *testing.T) {
68 t.Parallel()
69 c := NewSized[string](100)
70 c.Set("a", make([]byte, 60))
71 c.Set("b", make([]byte, 60)) // forces eviction of "a"
72 if _, ok := c.Get("a"); ok {
73 t.Errorf("a should have been evicted to fit b")
74 }
75 if c.Bytes() != 60 {
76 t.Errorf("Bytes = %d; want 60", c.Bytes())
77 }
78 }
79
80 func TestSizedCache_ReplaceShrinks(t *testing.T) {
81 t.Parallel()
82 c := NewSized[string](100)
83 c.Set("a", make([]byte, 80))
84 c.Set("a", make([]byte, 10)) // smaller replacement
85 if c.Bytes() != 10 {
86 t.Errorf("Bytes after shrink = %d; want 10", c.Bytes())
87 }
88 }
89
90 func TestGroup_SingleFlightCollapsesConcurrentMisses(t *testing.T) {
91 t.Parallel()
92 c := New[string, int](16)
93 g := NewGroup(c, func(s string) string { return s })
94
95 var calls atomic.Int64
96 fetch := func(ctx context.Context) (int, error) {
97 calls.Add(1)
98 time.Sleep(20 * time.Millisecond)
99 return 99, nil
100 }
101
102 const N = 50
103 var wg sync.WaitGroup
104 wg.Add(N)
105 for i := 0; i < N; i++ {
106 go func() {
107 defer wg.Done()
108 v, err := g.Do(context.Background(), "k", fetch)
109 if err != nil || v != 99 {
110 t.Errorf("Do = %d,%v; want 99,nil", v, err)
111 }
112 }()
113 }
114 wg.Wait()
115
116 if calls.Load() != 1 {
117 t.Errorf("upstream called %d times; want 1 (singleflight collapse failed)", calls.Load())
118 }
119 }
120
121 func TestGroup_ErrorNotCached(t *testing.T) {
122 t.Parallel()
123 c := New[string, int](4)
124 g := NewGroup(c, func(s string) string { return s })
125
126 var attempt atomic.Int64
127 fetch := func(ctx context.Context) (int, error) {
128 n := attempt.Add(1)
129 if n == 1 {
130 return 0, errors.New("transient")
131 }
132 return 7, nil
133 }
134 if _, err := g.Do(context.Background(), "k", fetch); err == nil {
135 t.Fatalf("expected error on first call")
136 }
137 v, err := g.Do(context.Background(), "k", fetch)
138 if err != nil {
139 t.Fatalf("second call err: %v", err)
140 }
141 if v != 7 {
142 t.Errorf("v = %d; want 7", v)
143 }
144 }
145
146 func BenchmarkCacheSetGet(b *testing.B) {
147 c := New[int, int](1024)
148 for i := 0; i < 1024; i++ {
149 c.Set(i, i)
150 }
151 b.ResetTimer()
152 for i := 0; i < b.N; i++ {
153 _, _ = c.Get(i & 1023)
154 }
155 }
156
157 // Reference for keyer construction in the test above.
158 var _ = strconv.Itoa
159