11package main
22
33import (
4+ "context"
5+ "sync"
46 "testing"
57 "time"
68
79 "github.com/stretchr/testify/assert"
810)
911
1012type metric struct {
11- delete func ()
13+ expired bool
14+ delete func ()
15+ deleted bool
1216}
1317
1418func (m * metric ) Expired (i time.Duration ) bool {
15- return true
19+ return m . expired
1620}
1721
1822func (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
2329func 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