@@ -162,6 +162,7 @@ def test_constructor_defaults(self):
162162 assert retry_ ._multiplier == 2
163163 assert retry_ ._deadline == 120
164164 assert retry_ ._on_error is None
165+ assert retry_ ._strict_deadline is False
165166
166167 def test_constructor_options (self ):
167168 _some_function = mock .Mock ()
@@ -173,49 +174,118 @@ def test_constructor_options(self):
173174 multiplier = 3 ,
174175 deadline = 4 ,
175176 on_error = _some_function ,
177+ strict_deadline = True ,
176178 )
177179 assert retry_ ._predicate == mock .sentinel .predicate
178180 assert retry_ ._initial == 1
179181 assert retry_ ._maximum == 2
180182 assert retry_ ._multiplier == 3
181183 assert retry_ ._deadline == 4
182184 assert retry_ ._on_error is _some_function
185+ assert retry_ ._strict_deadline is True
183186
184187 def test_with_deadline (self ):
185- retry_ = retry .Retry ()
186- new_retry = retry_ .with_deadline (42 )
188+ retry_ = retry .Retry (
189+ predicate = mock .sentinel .predicate ,
190+ initial = 1 ,
191+ maximum = 2 ,
192+ multiplier = 3 ,
193+ deadline = 4 ,
194+ on_error = mock .sentinel .on_error ,
195+ strict_deadline = True ,
196+ )
197+ new_retry = retry_ .with_deadline (42 , strict_deadline = True )
187198 assert retry_ is not new_retry
188199 assert new_retry ._deadline == 42
200+ assert new_retry ._strict_deadline is True
201+
202+ # the rest of the attributes should remain the same
203+ assert new_retry ._predicate is retry_ ._predicate
204+ assert new_retry ._initial == retry_ ._initial
205+ assert new_retry ._maximum == retry_ ._maximum
206+ assert new_retry ._multiplier == retry_ ._multiplier
207+ assert new_retry ._on_error is retry_ ._on_error
189208
190209 def test_with_predicate (self ):
191- retry_ = retry .Retry ()
210+ retry_ = retry .Retry (
211+ predicate = mock .sentinel .predicate ,
212+ initial = 1 ,
213+ maximum = 2 ,
214+ multiplier = 3 ,
215+ deadline = 4 ,
216+ on_error = mock .sentinel .on_error ,
217+ strict_deadline = True ,
218+ )
192219 new_retry = retry_ .with_predicate (mock .sentinel .predicate )
193220 assert retry_ is not new_retry
194221 assert new_retry ._predicate == mock .sentinel .predicate
195222
223+ # the rest of the attributes should remain the same
224+ assert new_retry ._deadline == retry_ ._deadline
225+ assert new_retry ._strict_deadline == retry_ ._strict_deadline
226+ assert new_retry ._initial == retry_ ._initial
227+ assert new_retry ._maximum == retry_ ._maximum
228+ assert new_retry ._multiplier == retry_ ._multiplier
229+ assert new_retry ._on_error is retry_ ._on_error
230+
196231 def test_with_delay_noop (self ):
197- retry_ = retry .Retry ()
232+ retry_ = retry .Retry (
233+ predicate = mock .sentinel .predicate ,
234+ initial = 1 ,
235+ maximum = 2 ,
236+ multiplier = 3 ,
237+ deadline = 4 ,
238+ on_error = mock .sentinel .on_error ,
239+ strict_deadline = True ,
240+ )
198241 new_retry = retry_ .with_delay ()
199242 assert retry_ is not new_retry
200243 assert new_retry ._initial == retry_ ._initial
201244 assert new_retry ._maximum == retry_ ._maximum
202245 assert new_retry ._multiplier == retry_ ._multiplier
203246
204247 def test_with_delay (self ):
205- retry_ = retry .Retry ()
248+ retry_ = retry .Retry (
249+ predicate = mock .sentinel .predicate ,
250+ initial = 1 ,
251+ maximum = 2 ,
252+ multiplier = 3 ,
253+ deadline = 4 ,
254+ on_error = mock .sentinel .on_error ,
255+ strict_deadline = True ,
256+ )
206257 new_retry = retry_ .with_delay (initial = 1 , maximum = 2 , multiplier = 3 )
207258 assert retry_ is not new_retry
208259 assert new_retry ._initial == 1
209260 assert new_retry ._maximum == 2
210261 assert new_retry ._multiplier == 3
211262
263+ # the rest of the attributes should remain the same
264+ assert new_retry ._deadline == retry_ ._deadline
265+ assert new_retry ._strict_deadline == retry_ ._strict_deadline
266+ assert new_retry ._predicate is retry_ ._predicate
267+ assert new_retry ._on_error is retry_ ._on_error
268+
212269 def test___str__ (self ):
213- retry_ = retry .Retry ()
270+ def if_exception_type (exc ):
271+ return bool (exc ) # the exact logic is irrelevant
272+
273+ # Explicitly set all attributes as changed Retry defaults should not
274+ # cause this test to start failing.
275+ retry_ = retry .Retry (
276+ predicate = if_exception_type ,
277+ initial = 1.0 ,
278+ maximum = 60.0 ,
279+ multiplier = 2.0 ,
280+ deadline = 120.0 ,
281+ on_error = None ,
282+ strict_deadline = False ,
283+ )
214284 assert re .match (
215285 (
216286 r"<Retry predicate=<function.*?if_exception_type.*?>, "
217287 r"initial=1.0, maximum=60.0, multiplier=2.0, deadline=120.0, "
218- r"on_error=None>"
288+ r"on_error=None, strict_deadline=False >"
219289 ),
220290 str (retry_ ),
221291 )
@@ -259,6 +329,55 @@ def test___call___and_execute_retry(self, sleep, uniform):
259329 sleep .assert_called_once_with (retry_ ._initial )
260330 assert on_error .call_count == 1
261331
332+ # Make uniform return half of its maximum, which is the calculated sleep time.
333+ @mock .patch ("random.uniform" , autospec = True , side_effect = lambda m , n : n / 2.0 )
334+ @mock .patch ("time.sleep" , autospec = True )
335+ def test___call___and_execute_retry_strict_deadline (self , sleep , uniform ):
336+
337+ on_error = mock .Mock (spec = ["__call__" ], side_effect = [None ] * 10 )
338+ retry_ = retry .Retry (
339+ predicate = retry .if_exception_type (ValueError ),
340+ initial = 1.0 ,
341+ maximum = 1024.0 ,
342+ multiplier = 2.0 ,
343+ deadline = 9.9 ,
344+ strict_deadline = True ,
345+ )
346+
347+ utcnow = datetime .datetime .utcnow ()
348+ utcnow_patcher = mock .patch (
349+ "google.api_core.datetime_helpers.utcnow" , return_value = utcnow
350+ )
351+
352+ target = mock .Mock (spec = ["__call__" ], side_effect = [ValueError ()] * 10 )
353+ # __name__ is needed by functools.partial.
354+ target .__name__ = "target"
355+
356+ decorated = retry_ (target , on_error = on_error )
357+ target .assert_not_called ()
358+
359+ with utcnow_patcher as patched_utcnow :
360+ # Make sure that calls to fake time.sleep() also advance the mocked
361+ # time clock.
362+ def increase_time (sleep_delay ):
363+ patched_utcnow .return_value += datetime .timedelta (seconds = sleep_delay )
364+ sleep .side_effect = increase_time
365+
366+ with pytest .raises (exceptions .RetryError ):
367+ decorated ("meep" )
368+
369+ assert target .call_count == 5
370+ target .assert_has_calls ([mock .call ("meep" )] * 5 )
371+ assert on_error .call_count == 5
372+
373+ # check the delays
374+ assert sleep .call_count == 4 # once between each successive target calls
375+ last_wait = sleep .call_args .args [0 ]
376+ total_wait = sum (call_args .args [0 ] for call_args in sleep .call_args_list )
377+
378+ assert last_wait == 2.9 # and not 8.0, because the last delay was shortened
379+ assert total_wait == 9.9 # the same as the (strict) deadline
380+
262381 @mock .patch ("time.sleep" , autospec = True )
263382 def test___init___without_retry_executed (self , sleep ):
264383 _some_function = mock .Mock ()
0 commit comments