11package controlplane
22
33import (
4+ "context"
5+ "sync/atomic"
46 "testing"
57 "time"
68
79 "github.com/stretchr/testify/assert"
10+ "github.com/stretchr/testify/require"
811)
912
1013func Test_Poller (t * testing.T ) {
@@ -23,13 +26,150 @@ func Test_Poller(t *testing.T) {
2326 })
2427 })
2528
26- // This is a guarunteed pass because Poll.Stop() always returns nil,
27- // but it's good to have a test for it should there be an error in the future
28- t .Run ("stopping should work correctly" , func (t * testing.T ) {
29- p := NewPoll (1 * time .Second , 0 * time .Second )
29+ t .Run ("interval plus jitter timing should work correctly" , func (t * testing.T ) {
30+ interval := 100 * time .Millisecond
31+ maxJitter := 50 * time .Millisecond
32+ expectedMinInterval := interval
33+ expectedMaxInterval := interval + maxJitter
3034
31- err := p . Stop ( )
35+ p := NewPoll ( interval , maxJitter )
3236
33- assert .NoError (t , err )
37+ // Record execution timestamps
38+ var timestamps []time.Time
39+ executionCount := 0
40+ targetExecutions := 4
41+
42+ ctx , cancel := context .WithTimeout (context .Background (), 2 * time .Second )
43+ defer cancel ()
44+
45+ p .Subscribe (ctx , func () {
46+ timestamps = append (timestamps , time .Now ())
47+ executionCount ++
48+
49+ // Cancel after we have enough executions
50+ if executionCount >= targetExecutions {
51+ cancel ()
52+ }
53+ })
54+
55+ // Wait for context to be cancelled or timeout
56+ <- ctx .Done ()
57+
58+ // We should have at least 2 executions to measure intervals
59+ require .GreaterOrEqual (t , len (timestamps ), 2 , "should have at least 2 executions" )
60+
61+ // Calculate intervals between executions
62+ for i := 1 ; i < len (timestamps ); i ++ {
63+ actualInterval := timestamps [i ].Sub (timestamps [i - 1 ])
64+
65+ // Each interval should be at least the minimum interval
66+ assert .GreaterOrEqual (t , actualInterval , expectedMinInterval ,
67+ "execution %d: actual interval %v should be >= minimum interval %v" ,
68+ i , actualInterval , expectedMinInterval )
69+
70+ // Each interval should be at most interval + maxJitter
71+ assert .LessOrEqual (t , actualInterval , expectedMaxInterval ,
72+ "execution %d: actual interval %v should be <= maximum interval %v" ,
73+ i , actualInterval , expectedMaxInterval )
74+
75+ t .Logf ("execution %d: interval = %v (expected: %v to %v)" ,
76+ i , actualInterval , expectedMinInterval , expectedMaxInterval )
77+ }
78+ })
79+
80+ t .Run ("should not allow concurrent handler invocations" , func (t * testing.T ) {
81+ interval := 50 * time .Millisecond
82+ maxJitter := 0 * time .Millisecond // No jitter for predictable timing
83+
84+ p := NewPoll (interval , maxJitter )
85+
86+ var concurrentInvocations int32
87+ var maxConcurrentInvocations int32
88+ var totalInvocations int32
89+
90+ ctx , cancel := context .WithTimeout (context .Background (), 500 * time .Millisecond )
91+ defer cancel ()
92+
93+ p .Subscribe (ctx , func () {
94+ // Increment concurrent counter
95+ current := atomic .AddInt32 (& concurrentInvocations , 1 )
96+
97+ // Track the maximum concurrent invocations we've seen
98+ for {
99+ max := atomic .LoadInt32 (& maxConcurrentInvocations )
100+ if current <= max || atomic .CompareAndSwapInt32 (& maxConcurrentInvocations , max , current ) {
101+ break
102+ }
103+ }
104+
105+ // Increment total invocations
106+ atomic .AddInt32 (& totalInvocations , 1 )
107+
108+ // Simulate work that takes longer than the interval
109+ // This should cause subsequent timer events to be skipped
110+ time .Sleep (150 * time .Millisecond )
111+
112+ // Decrement concurrent counter
113+ atomic .AddInt32 (& concurrentInvocations , - 1 )
114+ })
115+
116+ // Wait for context timeout
117+ <- ctx .Done ()
118+
119+ // Verify that we never had more than 1 concurrent invocation
120+ maxConcurrent := atomic .LoadInt32 (& maxConcurrentInvocations )
121+ totalInvoked := atomic .LoadInt32 (& totalInvocations )
122+
123+ assert .Equal (t , int32 (1 ), maxConcurrent ,
124+ "should never have more than 1 concurrent handler invocation" )
125+
126+ // We should have fewer invocations than if they were all allowed to run
127+ // (500ms test duration / 50ms interval = 10 possible, but many should be skipped)
128+ assert .Greater (t , int32 (10 ), totalInvoked ,
129+ "some invocations should have been skipped due to handler still running" )
130+
131+ // But we should have at least some invocations
132+ assert .Greater (t , totalInvoked , int32 (0 ),
133+ "should have at least some handler invocations" )
134+
135+ t .Logf ("total invocations: %d, max concurrent: %d" , totalInvoked , maxConcurrent )
136+ })
137+
138+ t .Run ("should stop polling when context is cancelled" , func (t * testing.T ) {
139+ interval := 50 * time .Millisecond
140+ maxJitter := 0 * time .Millisecond // No jitter for predictable timing
141+
142+ p := NewPoll (interval , maxJitter )
143+
144+ var executionCount int32
145+
146+ ctx , cancel := context .WithCancel (context .Background ())
147+
148+ // Start polling
149+ go p .Subscribe (ctx , func () {
150+ atomic .AddInt32 (& executionCount , 1 )
151+ })
152+
153+ // Let it run for a bit to ensure polling starts
154+ time .Sleep (150 * time .Millisecond )
155+ countBeforeCancel := atomic .LoadInt32 (& executionCount )
156+
157+ // Cancel the context
158+ cancel ()
159+
160+ // Wait for any pending executions to complete and verify no new ones occur
161+ time .Sleep (200 * time .Millisecond )
162+ countAfterCancel := atomic .LoadInt32 (& executionCount )
163+
164+ // Verify that we had some executions before cancellation
165+ assert .Greater (t , countBeforeCancel , int32 (0 ),
166+ "should have had executions before context cancellation" )
167+
168+ // Verify that no new executions occurred after cancellation
169+ assert .Equal (t , countBeforeCancel , countAfterCancel ,
170+ "should not have new executions after context cancellation" )
171+
172+ t .Logf ("executions before cancel: %d, executions after cancel: %d" ,
173+ countBeforeCancel , countAfterCancel )
34174 })
35175}
0 commit comments