Skip to content

Commit bbac015

Browse files
vkmcclaude
andcommitted
Improve test coverage for expiry.go (#164)
Add tests for check() edge cases, run() function, and concurrent access. Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9220851 commit bbac015

1 file changed

Lines changed: 236 additions & 4 deletions

File tree

Lines changed: 236 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
package main
22

33
import (
4+
"context"
5+
"sync"
46
"testing"
57
"time"
68

79
"github.com/stretchr/testify/assert"
810
)
911

1012
type metric struct {
11-
delete func()
13+
expired bool
14+
delete func()
15+
deleted bool
1216
}
1317

1418
func (m *metric) Expired(i time.Duration) bool {
15-
return true
19+
return m.expired
1620
}
1721

1822
func (m *metric) Delete() bool {
19-
m.delete()
20-
return true
23+
if m.delete != nil {
24+
m.delete()
25+
}
26+
return m.deleted
2127
}
2228

2329
func TestExpiry(t *testing.T) {
@@ -26,13 +32,239 @@ func TestExpiry(t *testing.T) {
2632
t.Run("single entry", func(t *testing.T) {
2733
deleted := false
2834
ep.register(&metric{
35+
expired: true,
2936
delete: func() {
3037
deleted = true
3138
},
39+
deleted: true,
3240
})
3341
assert.Equal(t, 1, ep.entries.Len(), "entry not registered")
3442
ep.check()
3543
assert.Equal(t, true, deleted, "expiry.delete() not called")
3644
assert.Equal(t, 0, ep.entries.Len(), "entry not removed after expiration")
3745
})
46+
47+
t.Run("multiple entries", func(t *testing.T) {
48+
ep := newExpiryProc(1)
49+
deleteCount := 0
50+
51+
// Register 3 expired entries
52+
for i := 0; i < 3; i++ {
53+
ep.register(&metric{
54+
expired: true,
55+
delete: func() {
56+
deleteCount++
57+
},
58+
deleted: true,
59+
})
60+
}
61+
62+
assert.Equal(t, 3, ep.entries.Len(), "entries not registered")
63+
ep.check()
64+
assert.Equal(t, 3, deleteCount, "not all delete() called")
65+
assert.Equal(t, 0, ep.entries.Len(), "entries not removed after expiration")
66+
})
67+
68+
t.Run("entry not expired", func(t *testing.T) {
69+
ep := newExpiryProc(1)
70+
deleted := false
71+
72+
ep.register(&metric{
73+
expired: false,
74+
delete: func() {
75+
deleted = true
76+
},
77+
deleted: true,
78+
})
79+
80+
assert.Equal(t, 1, ep.entries.Len(), "entry not registered")
81+
ep.check()
82+
assert.Equal(t, false, deleted, "delete() should not be called for non-expired entry")
83+
assert.Equal(t, 1, ep.entries.Len(), "non-expired entry should remain in list")
84+
})
85+
86+
t.Run("entry expired but delete returns false", func(t *testing.T) {
87+
ep := newExpiryProc(1)
88+
deleted := false
89+
90+
ep.register(&metric{
91+
expired: true,
92+
delete: func() {
93+
deleted = true
94+
},
95+
deleted: false, // Delete returns false
96+
})
97+
98+
assert.Equal(t, 1, ep.entries.Len(), "entry not registered")
99+
ep.check()
100+
assert.Equal(t, true, deleted, "delete() should be called")
101+
assert.Equal(t, 1, ep.entries.Len(), "entry should remain if Delete() returns false")
102+
})
103+
104+
t.Run("mixed expired and non-expired entries", func(t *testing.T) {
105+
ep := newExpiryProc(1)
106+
deleteCount := 0
107+
108+
// Register expired entry
109+
ep.register(&metric{
110+
expired: true,
111+
delete: func() {
112+
deleteCount++
113+
},
114+
deleted: true,
115+
})
116+
117+
// Register non-expired entry
118+
ep.register(&metric{
119+
expired: false,
120+
delete: func() {
121+
deleteCount++
122+
},
123+
deleted: true,
124+
})
125+
126+
// Register another expired entry
127+
ep.register(&metric{
128+
expired: true,
129+
delete: func() {
130+
deleteCount++
131+
},
132+
deleted: true,
133+
})
134+
135+
assert.Equal(t, 3, ep.entries.Len(), "entries not registered")
136+
ep.check()
137+
assert.Equal(t, 2, deleteCount, "only expired entries should be deleted")
138+
assert.Equal(t, 1, ep.entries.Len(), "only non-expired entry should remain")
139+
})
140+
141+
t.Run("nil value entry", func(t *testing.T) {
142+
ep := newExpiryProc(1)
143+
144+
// Manually add a nil entry to test the nil check
145+
ep.Lock()
146+
ep.entries.PushBack(nil)
147+
ep.Unlock()
148+
149+
assert.Equal(t, 1, ep.entries.Len(), "nil entry not added")
150+
ep.check()
151+
assert.Equal(t, 0, ep.entries.Len(), "nil entry should be removed")
152+
})
153+
}
154+
155+
func TestExpiryProc_run(t *testing.T) {
156+
t.Run("run with zero interval returns immediately", func(t *testing.T) {
157+
ep := newExpiryProc(0)
158+
ctx := context.Background()
159+
160+
// This should return immediately without blocking
161+
done := make(chan bool)
162+
go func() {
163+
ep.run(ctx)
164+
done <- true
165+
}()
166+
167+
select {
168+
case <-done:
169+
// Success - run returned immediately
170+
case <-time.After(100 * time.Millisecond):
171+
t.Fatal("run() should return immediately when interval is 0")
172+
}
173+
})
174+
175+
t.Run("run with context cancellation", func(t *testing.T) {
176+
ep := newExpiryProc(100 * time.Millisecond)
177+
ctx, cancel := context.WithCancel(context.Background())
178+
179+
done := make(chan bool)
180+
go func() {
181+
ep.run(ctx)
182+
done <- true
183+
}()
184+
185+
// Give it a moment to start
186+
time.Sleep(10 * time.Millisecond)
187+
188+
// Cancel the context
189+
cancel()
190+
191+
// Should exit quickly after cancellation
192+
select {
193+
case <-done:
194+
// Success - run exited after context cancellation
195+
case <-time.After(200 * time.Millisecond):
196+
t.Fatal("run() should exit when context is cancelled")
197+
}
198+
})
199+
200+
t.Run("run performs periodic checks", func(t *testing.T) {
201+
ep := newExpiryProc(50 * time.Millisecond)
202+
ctx, cancel := context.WithCancel(context.Background())
203+
defer cancel()
204+
205+
deleteCount := 0
206+
mu := sync.Mutex{}
207+
208+
// Register an expired metric
209+
ep.register(&metric{
210+
expired: true,
211+
delete: func() {
212+
mu.Lock()
213+
deleteCount++
214+
mu.Unlock()
215+
},
216+
deleted: true,
217+
})
218+
219+
// Start the run loop
220+
go ep.run(ctx)
221+
222+
// Wait for at least one check cycle (interval + 1 second as per run() implementation)
223+
time.Sleep(1200 * time.Millisecond)
224+
225+
// Cancel to stop the run loop
226+
cancel()
227+
228+
// The metric should have been deleted
229+
mu.Lock()
230+
assert.Greater(t, deleteCount, 0, "check() should have been called at least once")
231+
mu.Unlock()
232+
})
233+
}
234+
235+
func TestExpiryProc_concurrent_access(t *testing.T) {
236+
t.Run("concurrent register and check", func(t *testing.T) {
237+
ep := newExpiryProc(10 * time.Millisecond)
238+
ctx, cancel := context.WithCancel(context.Background())
239+
defer cancel()
240+
241+
// Start the run loop
242+
go ep.run(ctx)
243+
244+
var wg sync.WaitGroup
245+
246+
// Concurrently register entries
247+
for i := 0; i < 10; i++ {
248+
wg.Add(1)
249+
go func() {
250+
defer wg.Done()
251+
ep.register(&metric{
252+
expired: true,
253+
deleted: true,
254+
})
255+
}()
256+
}
257+
258+
wg.Wait()
259+
260+
// Give time for checks to process (interval + 1 second as per run() implementation)
261+
time.Sleep(1100 * time.Millisecond)
262+
263+
// All should be processed and removed
264+
ep.Lock()
265+
finalLen := ep.entries.Len()
266+
ep.Unlock()
267+
268+
assert.Equal(t, 0, finalLen, "all entries should have been processed")
269+
})
38270
}

0 commit comments

Comments
 (0)