Skip to content

Commit e8a0aef

Browse files
authored
Merge pull request #25 from go-pkgz/synctest-deterministic-time
Rewrite v3 time-dependent tests with testing/synctest
2 parents 7b2c36f + d93d0b8 commit e8a0aef

5 files changed

Lines changed: 213 additions & 95 deletions

File tree

.github/workflows/ci-v3.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: set up go
2424
uses: actions/setup-go@v5
2525
with:
26-
go-version: "1.20"
26+
go-version: "1.25"
2727
cache-dependency-path: v3/go.sum
2828
id: go
2929

v3/cache_expire_synctest_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//go:build go1.25
2+
3+
package cache
4+
5+
import (
6+
"testing"
7+
"testing/synctest"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestCacheWithDeleteExpired(t *testing.T) {
14+
synctest.Test(t, func(t *testing.T) {
15+
var evicted []string
16+
lc := NewCache[string, string]().WithTTL(time.Second * 15).WithOnEvicted(
17+
func(key string, value string) {
18+
evicted = append(evicted, key, value)
19+
})
20+
21+
lc.Set("key1", "val1", 0)
22+
23+
time.Sleep(time.Second * 10) // not enough to expire
24+
lc.DeleteExpired()
25+
assert.Equal(t, 1, lc.Len())
26+
27+
v, ok := lc.Get("key1")
28+
assert.Equal(t, "val1", v)
29+
assert.True(t, ok)
30+
31+
time.Sleep(time.Second * 20) // expire
32+
lc.DeleteExpired()
33+
v, ok = lc.Get("key1")
34+
assert.False(t, ok)
35+
assert.Equal(t, "", v)
36+
37+
assert.Equal(t, 0, lc.Len())
38+
assert.Equal(t, []string{"key1", "val1"}, evicted)
39+
40+
// add new entry
41+
lc.Set("key2", "val2", 0)
42+
assert.Equal(t, 1, lc.Len())
43+
44+
// nothing deleted
45+
lc.DeleteExpired()
46+
assert.Equal(t, 1, lc.Len())
47+
assert.Equal(t, []string{"key1", "val1"}, evicted)
48+
49+
// Purge, cache should be clean
50+
lc.Purge()
51+
assert.Equal(t, 0, lc.Len())
52+
assert.Equal(t, []string{"key1", "val1", "key2", "val2"}, evicted)
53+
})
54+
}
55+
56+
func TestCacheExpired(t *testing.T) {
57+
synctest.Test(t, func(t *testing.T) {
58+
lc := NewCache[string, string]().WithTTL(time.Second * 5)
59+
60+
lc.Set("key1", "val1", 0)
61+
assert.Equal(t, 1, lc.Len())
62+
63+
v, ok := lc.Peek("key1")
64+
assert.Equal(t, v, "val1")
65+
assert.True(t, ok)
66+
67+
v, ok = lc.Get("key1")
68+
assert.Equal(t, v, "val1")
69+
assert.True(t, ok)
70+
71+
time.Sleep(time.Second * 10) // wait for entry to expire
72+
assert.Equal(t, 1, lc.Len()) // but not purged
73+
74+
v, ok = lc.Peek("key1")
75+
assert.Equal(t, "val1", v, "expired and marked as such, but value is available")
76+
assert.False(t, ok)
77+
78+
v, ok = lc.Get("key1")
79+
assert.Equal(t, "val1", v, "expired and marked as such, but value is available")
80+
assert.False(t, ok)
81+
82+
assert.Empty(t, lc.Values())
83+
})
84+
}
85+
86+
func TestCache_GetExpiration(t *testing.T) {
87+
synctest.Test(t, func(t *testing.T) {
88+
lc := NewCache[string, string]().WithTTL(time.Second * 5)
89+
90+
lc.Set("key1", "val1", time.Second*5)
91+
assert.Equal(t, 1, lc.Len())
92+
93+
exp, ok := lc.GetExpiration("key1")
94+
assert.True(t, ok)
95+
assert.Equal(t, time.Now().Add(time.Second*5), exp)
96+
97+
lc.Set("key2", "val2", time.Second*10)
98+
assert.Equal(t, 2, lc.Len())
99+
100+
exp, ok = lc.GetExpiration("key2")
101+
assert.True(t, ok)
102+
assert.Equal(t, time.Now().Add(time.Second*10), exp)
103+
104+
exp, ok = lc.GetExpiration("non-existing-key")
105+
assert.False(t, ok)
106+
assert.Zero(t, exp)
107+
})
108+
}

v3/cache_expire_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
//go:build !go1.25
2+
3+
package cache
4+
5+
import (
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestCacheWithDeleteExpired(t *testing.T) {
13+
var evicted []string
14+
lc := NewCache[string, string]().WithTTL(150 * time.Millisecond).WithOnEvicted(
15+
func(key string, value string) {
16+
evicted = append(evicted, key, value)
17+
})
18+
19+
lc.Set("key1", "val1", 0)
20+
21+
time.Sleep(100 * time.Millisecond) // not enough to expire
22+
lc.DeleteExpired()
23+
assert.Equal(t, 1, lc.Len())
24+
25+
v, ok := lc.Get("key1")
26+
assert.Equal(t, "val1", v)
27+
assert.True(t, ok)
28+
29+
time.Sleep(200 * time.Millisecond) // expire
30+
lc.DeleteExpired()
31+
v, ok = lc.Get("key1")
32+
assert.False(t, ok)
33+
assert.Equal(t, "", v)
34+
35+
assert.Equal(t, 0, lc.Len())
36+
assert.Equal(t, []string{"key1", "val1"}, evicted)
37+
38+
// add new entry
39+
lc.Set("key2", "val2", 0)
40+
assert.Equal(t, 1, lc.Len())
41+
42+
// nothing deleted
43+
lc.DeleteExpired()
44+
assert.Equal(t, 1, lc.Len())
45+
assert.Equal(t, []string{"key1", "val1"}, evicted)
46+
47+
// Purge, cache should be clean
48+
lc.Purge()
49+
assert.Equal(t, 0, lc.Len())
50+
assert.Equal(t, []string{"key1", "val1", "key2", "val2"}, evicted)
51+
}
52+
53+
func TestCacheExpired(t *testing.T) {
54+
lc := NewCache[string, string]().WithTTL(time.Millisecond * 5)
55+
56+
lc.Set("key1", "val1", 0)
57+
assert.Equal(t, 1, lc.Len())
58+
59+
v, ok := lc.Peek("key1")
60+
assert.Equal(t, v, "val1")
61+
assert.True(t, ok)
62+
63+
v, ok = lc.Get("key1")
64+
assert.Equal(t, v, "val1")
65+
assert.True(t, ok)
66+
67+
time.Sleep(time.Millisecond * 10) // wait for entry to expire
68+
assert.Equal(t, 1, lc.Len()) // but not purged
69+
70+
v, ok = lc.Peek("key1")
71+
assert.Equal(t, "val1", v, "expired and marked as such, but value is available")
72+
assert.False(t, ok)
73+
74+
v, ok = lc.Get("key1")
75+
assert.Equal(t, "val1", v, "expired and marked as such, but value is available")
76+
assert.False(t, ok)
77+
78+
assert.Empty(t, lc.Values())
79+
}
80+
81+
func TestCache_GetExpiration(t *testing.T) {
82+
lc := NewCache[string, string]().WithTTL(time.Second * 5)
83+
84+
lc.Set("key1", "val1", time.Second*5)
85+
assert.Equal(t, 1, lc.Len())
86+
87+
exp, ok := lc.GetExpiration("key1")
88+
assert.True(t, ok)
89+
assert.True(t, exp.After(time.Now().Add(time.Second*4)))
90+
assert.True(t, exp.Before(time.Now().Add(time.Second*6)))
91+
92+
lc.Set("key2", "val2", time.Second*10)
93+
assert.Equal(t, 2, lc.Len())
94+
95+
exp, ok = lc.GetExpiration("key2")
96+
assert.True(t, ok)
97+
assert.True(t, exp.After(time.Now().Add(time.Second*9)))
98+
assert.True(t, exp.Before(time.Now().Add(time.Second*11)))
99+
100+
exp, ok = lc.GetExpiration("non-existing-key")
101+
assert.False(t, ok)
102+
assert.Zero(t, exp)
103+
}

v3/cache_test.go

Lines changed: 0 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -175,47 +175,6 @@ func TestCacheNoPurge(t *testing.T) {
175175
assert.Equal(t, []string{"key3", "key4", "key1"}, lc.Keys())
176176
}
177177

178-
func TestCacheWithDeleteExpired(t *testing.T) {
179-
var evicted []string
180-
lc := NewCache[string, string]().WithTTL(150 * time.Millisecond).WithOnEvicted(
181-
func(key string, value string) {
182-
evicted = append(evicted, key, value)
183-
})
184-
185-
lc.Set("key1", "val1", 0)
186-
187-
time.Sleep(100 * time.Millisecond) // not enough to expire
188-
lc.DeleteExpired()
189-
assert.Equal(t, 1, lc.Len())
190-
191-
v, ok := lc.Get("key1")
192-
assert.Equal(t, "val1", v)
193-
assert.True(t, ok)
194-
195-
time.Sleep(200 * time.Millisecond) // expire
196-
lc.DeleteExpired()
197-
v, ok = lc.Get("key1")
198-
assert.False(t, ok)
199-
assert.Equal(t, "", v)
200-
201-
assert.Equal(t, 0, lc.Len())
202-
assert.Equal(t, []string{"key1", "val1"}, evicted)
203-
204-
// add new entry
205-
lc.Set("key2", "val2", 0)
206-
assert.Equal(t, 1, lc.Len())
207-
208-
// nothing deleted
209-
lc.DeleteExpired()
210-
assert.Equal(t, 1, lc.Len())
211-
assert.Equal(t, []string{"key1", "val1"}, evicted)
212-
213-
// Purge, cache should be clean
214-
lc.Purge()
215-
assert.Equal(t, 0, lc.Len())
216-
assert.Equal(t, []string{"key1", "val1", "key2", "val2"}, evicted)
217-
}
218-
219178
func TestCache_Values(t *testing.T) {
220179
lc := NewCache[string, string]().WithMaxKeys(3)
221180

@@ -303,58 +262,6 @@ func TestCacheInvalidateAndEvict(t *testing.T) {
303262
assert.Zero(t, lc.Len())
304263
}
305264

306-
func TestCacheExpired(t *testing.T) {
307-
lc := NewCache[string, string]().WithTTL(time.Millisecond * 5)
308-
309-
lc.Set("key1", "val1", 0)
310-
assert.Equal(t, 1, lc.Len())
311-
312-
v, ok := lc.Peek("key1")
313-
assert.Equal(t, v, "val1")
314-
assert.True(t, ok)
315-
316-
v, ok = lc.Get("key1")
317-
assert.Equal(t, v, "val1")
318-
assert.True(t, ok)
319-
320-
time.Sleep(time.Millisecond * 10) // wait for entry to expire
321-
assert.Equal(t, 1, lc.Len()) // but not purged
322-
323-
v, ok = lc.Peek("key1")
324-
assert.Equal(t, "val1", v, "expired and marked as such, but value is available")
325-
assert.False(t, ok)
326-
327-
v, ok = lc.Get("key1")
328-
assert.Equal(t, "val1", v, "expired and marked as such, but value is available")
329-
assert.False(t, ok)
330-
331-
assert.Empty(t, lc.Values())
332-
}
333-
334-
func TestCache_GetExpiration(t *testing.T) {
335-
lc := NewCache[string, string]().WithTTL(time.Second * 5)
336-
337-
lc.Set("key1", "val1", time.Second*5)
338-
assert.Equal(t, 1, lc.Len())
339-
340-
exp, ok := lc.GetExpiration("key1")
341-
assert.True(t, ok)
342-
assert.True(t, exp.After(time.Now().Add(time.Second*4)))
343-
assert.True(t, exp.Before(time.Now().Add(time.Second*6)))
344-
345-
lc.Set("key2", "val2", time.Second*10)
346-
assert.Equal(t, 2, lc.Len())
347-
348-
exp, ok = lc.GetExpiration("key2")
349-
assert.True(t, ok)
350-
assert.True(t, exp.After(time.Now().Add(time.Second*9)))
351-
assert.True(t, exp.Before(time.Now().Add(time.Second*11)))
352-
353-
exp, ok = lc.GetExpiration("non-existing-key")
354-
assert.False(t, ok)
355-
assert.Zero(t, exp)
356-
}
357-
358265
func TestCacheRemoveOldest(t *testing.T) {
359266
lc := NewCache[string, string]().WithLRU().WithMaxKeys(2)
360267

v3/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/go-pkgz/expirable-cache/v3
22

3-
go 1.20
3+
go 1.23
44

55
require github.com/stretchr/testify v1.10.0
66

0 commit comments

Comments
 (0)