88"""
99
1010import logging
11+ import math
1112import random
1213from contextlib import contextmanager
1314from unittest import mock
1718from ldclient .impl import retry
1819from ldclient .impl .retry import (
1920 DEFAULT_INITIAL_RECONNECT_DELAY ,
21+ DEFAULT_POLL_INTERVAL ,
2022 EXTENDED_INITIAL_DELAY ,
2123 EXTENDED_MAX_DELAY ,
2224 POLLING_RESET_SUCCESSES ,
@@ -107,24 +109,51 @@ def test_non_error_statuses_are_normal(self, status):
107109 assert classify_http_status (status ) is NORMAL
108110
109111
110- class TestStreamingInitialDelayGuard :
111- """``Config`` does not check ``initial_reconnect_delay``, and a value of
112- zero would reconnect with no wait at all."""
112+ class TestFactoryInputGuards :
113+ """``Config`` does not check ``initial_reconnect_delay`` at all, and only
114+ clamps ``poll_interval``. A non-positive value would retry with no wait; a
115+ non-finite one makes the jitter arithmetic produce NaN."""
113116
114- @pytest .mark .parametrize ("configured" , [0 , - 1 , - 0.5 ])
115- def test_a_non_positive_delay_falls_back_to_the_default (self , configured , caplog ):
117+ @pytest .mark .parametrize (
118+ "configured" ,
119+ [0 , - 1 , - 0.5 , float ('inf' ), float ('-inf' ), float ('nan' )],
120+ ids = ["zero" , "negative" , "negative-fraction" , "inf" , "-inf" , "nan" ],
121+ )
122+ def test_streaming_falls_back_to_the_default (self , configured , caplog ):
116123 caplog .set_level (logging .WARNING )
117124
118125 state = for_streaming (configured )
126+ delay = failure_delay (state , NORMAL )
119127
120128 assert state .min_delay == DEFAULT_INITIAL_RECONNECT_DELAY
121- assert failure_delay (state , NORMAL ) == DEFAULT_INITIAL_RECONNECT_DELAY
129+ assert delay == DEFAULT_INITIAL_RECONNECT_DELAY
130+ assert math .isfinite (delay ) and delay > 0
122131 assert caplog .records [0 ].getMessage () == (
123- "initial_reconnect_delay must be greater than zero; using the default of 1s"
132+ "initial_reconnect_delay must be a positive, finite number of seconds; "
133+ "using the default of 1s"
134+ )
135+
136+ @pytest .mark .parametrize (
137+ "configured" ,
138+ [0 , - 5 , float ('inf' ), float ('-inf' ), float ('nan' )],
139+ ids = ["zero" , "negative" , "inf" , "-inf" , "nan" ],
140+ )
141+ def test_polling_falls_back_to_the_default (self , configured , caplog ):
142+ caplog .set_level (logging .WARNING )
143+
144+ state = for_polling (configured )
145+ delay = failure_delay (state , NORMAL )
146+
147+ assert state .operating_cadence == DEFAULT_POLL_INTERVAL
148+ assert delay == DEFAULT_POLL_INTERVAL
149+ assert math .isfinite (delay ) and delay > 0
150+ assert caplog .records [0 ].getMessage () == (
151+ "poll_interval must be a positive, finite number of seconds; "
152+ "using the default of 30s"
124153 )
125154
126155 @pytest .mark .parametrize ("configured" , [0.001 , 0.5 , 1 , 5 , 45 ])
127- def test_a_positive_delay_is_left_alone (self , configured , caplog ):
156+ def test_a_positive_streaming_delay_is_left_alone (self , configured , caplog ):
128157 caplog .set_level (logging .WARNING )
129158
130159 state = for_streaming (configured )
@@ -133,6 +162,16 @@ def test_a_positive_delay_is_left_alone(self, configured, caplog):
133162 assert failure_delay (state , NORMAL ) == configured
134163 assert caplog .records == []
135164
165+ @pytest .mark .parametrize ("configured" , [0.001 , 1 , 30 , 300 , 2 * 60 * 60 ])
166+ def test_a_positive_poll_interval_is_left_alone (self , configured , caplog ):
167+ caplog .set_level (logging .WARNING )
168+
169+ state = for_polling (configured )
170+
171+ assert state .operating_cadence == configured
172+ assert failure_delay (state , NORMAL ) == configured
173+ assert caplog .records == []
174+
136175
137176class TestStreamingExtendedDelayFloor :
138177 """A delay that applies after an unexpected failure must not be below the
@@ -233,6 +272,32 @@ def test_every_delay_stays_within_the_jitter_bounds(self):
233272 assert base / 2 <= delay <= base
234273
235274
275+ class TestWaitBetweenOperations :
276+ def test_a_streaming_success_does_not_schedule_a_zero_wait (self ):
277+ """Streaming has no cadence, so a success falls back to the initial
278+ delay. Zero would tell a scheduler to run again immediately."""
279+ state = for_streaming (1 )
280+ state .record_failure (NORMAL )
281+ state .record_success ()
282+
283+ assert state .next_delay == 1
284+
285+ def test_a_polling_success_schedules_the_cadence (self ):
286+ state = for_polling (30 )
287+ state .record_failure (NORMAL )
288+ state .record_success ()
289+
290+ assert state .next_delay == 30
291+
292+ def test_a_fresh_state_and_a_success_agree (self ):
293+ """The constructor and record_success share one expression, so the two
294+ cannot drift apart."""
295+ for state in (for_streaming (5 ), for_polling (45 )):
296+ fresh = state .next_delay
297+ state .record_success ()
298+ assert state .next_delay == fresh
299+
300+
236301class TestStreamingReset :
237302 def test_a_minute_of_healthy_operation_resets_the_state (self ):
238303 # The whole minute passes instantly.
0 commit comments