def test_while_until_true_invoke_inline(mock_time_sleep): """while_until_true with dynamic invocation.""" mock = MagicMock() mock.side_effect = [ 'test string 1', 'test string 2', 'test string 3', 'expected value', 'test string 5' ] actual_counter = 0 def decorate_me(counter, arg1, arg2): """Test static decorator syntax.""" assert arg1 == 'v1' assert arg2 == 'v2' nonlocal actual_counter actual_counter += 1 assert actual_counter == counter if mock(arg1) == 'expected value': return True else: return False assert poll.while_until_true(interval=0.01, max_attempts=10)(decorate_me)('v1', 'v2') assert mock.call_count == 4 mock.assert_called_with('v1') assert mock_time_sleep.call_count == 3 mock_time_sleep.assert_called_with(0.01)
def test_while_until_true_once_found(mock_time_sleep): """wait_until_true max_attempts 1.""" mock = MagicMock() mock.side_effect = [ 'expected value', 'test string 2', ] actual_counter = 0 def decorate_me(counter, arg1, arg2): """Test static decorator syntax.""" assert arg1 == 'v1' assert arg2 == 'v2' nonlocal actual_counter actual_counter += 1 assert actual_counter == counter if mock(arg1) == 'expected value': return True else: return False assert poll.while_until_true(interval=0.01, max_attempts=1)(decorate_me)('v1', 'v2') mock.assert_called_once_with('v1') mock_time_sleep.assert_not_called()
def test_while_until_true_no_max(mock_time_sleep): """while_until_true with dynamic invocation, infinite (max is None).""" mock = MagicMock() mock.side_effect = [ 'test string 1', 'test string 2', 'test string 3', 'test string 4', 'test string 5', 'test string 6', 'test string 7', 'test string 8', 'test string 9', 'test string 10', 'test string 11', ] actual_counter = 0 def decorate_me(counter, arg1, arg2): """Test static decorator syntax""" assert arg1 == 'v1' assert arg2 == 'v2' nonlocal actual_counter actual_counter += 1 assert actual_counter == counter out = mock(arg1) assert out == f'test string {counter}' if out == 'test string 11': return True else: return False logger = logging.getLogger('pypyr.utils.poll') with patch.object(logger, 'debug') as mock_logger_debug: assert (poll.while_until_true(interval=0.01, max_attempts=None)(decorate_me)('v1', 'v2')) assert mock_logger_debug.mock_calls == [ call('started'), call('Looping every 0.01 seconds.'), call('iteration 1. Still waiting. . .'), call('iteration 2. Still waiting. . .'), call('iteration 3. Still waiting. . .'), call('iteration 4. Still waiting. . .'), call('iteration 5. Still waiting. . .'), call('iteration 6. Still waiting. . .'), call('iteration 7. Still waiting. . .'), call('iteration 8. Still waiting. . .'), call('iteration 9. Still waiting. . .'), call('iteration 10. Still waiting. . .'), call('iteration 11. Desired state reached.'), call('done') ] assert mock.call_count == 11 mock.assert_called_with('v1') assert mock_time_sleep.call_count == 10 mock_time_sleep.assert_called_with(0.01)
def retry_loop(self, context, step_method): """Run step inside a retry loop. Args: context: (pypyr.context.Context) The pypyr context. This arg will mutate - after method execution will contain the new updated context. step_method: (method/function) This is the method/function that will execute on every loop iteration. Signature is: function(context) """ logger.debug("starting") context['retryCounter'] = 0 self.retry_counter = 0 sleep = context.get_formatted_value(self.sleep) backoff_name = context.get_formatted_value( self.backoff) if self.backoff else 'fixed' max_sleep = None if self.sleep_max: max_sleep = context.get_formatted_as_type(self.sleep_max, out_type=float) jrc = context.get_formatted_value(self.jrc) backoff_args = context.get_formatted_value(self.backoff_args) backoff_callable = backoff_cache.get_backoff(backoff_name)( sleep=sleep, max_sleep=max_sleep, jrc=jrc, kwargs=backoff_args) if self.max: max = context.get_formatted_as_type(self.max, out_type=int) logger.info( "retry decorator will try %d times with %s backoff starting " "at %ss intervals.", max, backoff_name, sleep) else: max = None logger.info( "retry decorator will try indefinitely with %s backoff " "starting at %ss intervals.", backoff_name, sleep) # this will never be false. because on (counter == max) exec_iteration # raises an exception, breaking out of the loop. is_retry_ok = poll.while_until_true( interval=backoff_callable, max_attempts=max)(self.exec_iteration)(context=context, step_method=step_method, max=max) assert is_retry_ok logger.debug("retry loop complete, reporting success.") logger.debug("done")
def test_while_until_true_callable_with_max_attempts(mock_sleep): """Interval calculated from callable per iteration with max set.""" decorate_me = MagicMock() decorate_me.side_effect = [False, False, True, False] assert poll.while_until_true(interval=arb_callable, max_attempts=3)(decorate_me)('arg1', 'arg2') assert decorate_me.mock_calls == [call(1, 'arg1', 'arg2'), call(2, 'arg1', 'arg2'), call(3, 'arg1', 'arg2')] assert mock_sleep.mock_calls == [call(2), call(4)]
def retry_loop(self, context, step_method): """Run step inside a retry loop. Args: context: (pypyr.context.Context) The pypyr context. This arg will mutate - after method execution will contain the new updated context. step_method: (method/function) This is the method/function that will execute on every loop iteration. Signature is: function(context) """ logger.debug("starting") context['retryCounter'] = 0 self.retry_counter = 0 sleep = context.get_formatted_as_type(self.sleep, out_type=float) if self.max: max = context.get_formatted_as_type(self.max, out_type=int) logger.info( "retry decorator will try %d times at %ss " "intervals.", max, sleep) else: max = None logger.info( "retry decorator will try indefinitely at %ss " "intervals.", sleep) # this will never be false. because on counter == max, # exec_iteration raises an exception, breaking out of the loop. # pragma because cov doesn't know the implied else is impossible. # unit test cov is 100%, though. if poll.while_until_true(interval=sleep, max_attempts=max)( self.exec_iteration)( context=context, step_method=step_method): # pragma: no cover logger.debug("retry loop complete, reporting success.") logger.debug("retry loop done") logger.debug("done")
def test_while_until_true_with_exhaust(mock_time_sleep): """while_until_true with dynamic invocation, exhaust wait attempts.""" mock = MagicMock() mock.side_effect = [ 'test string 1', 'test string 2', 'test string 3', 'test string 4', 'test string 5', 'test string 6', 'test string 7', 'test string 8', 'test string 9', 'test string 10', 'test string 11', ] actual_counter = 0 def decorate_me(counter, arg1, arg2): """Test static decorator syntax.""" assert arg1 == 'v1' assert arg2 == 'v2' nonlocal actual_counter actual_counter += 1 assert actual_counter == counter out = mock(arg1) assert out == f'test string {counter}' if out == 'expected value': return True else: return False assert not poll.while_until_true(interval=0.01, max_attempts=10)(decorate_me)('v1', 'v2') assert mock.call_count == 10 mock.assert_called_with('v1') assert mock_time_sleep.call_count == 9 mock_time_sleep.assert_called_with(0.01)
def test_while_until_true_max_exhaust(mock_time_sleep): """while_until_true with dynamic invocation, exhaust max.""" mock = MagicMock() mock.side_effect = [ 'test string 1', 'test string 2', 'test string 3', ] actual_counter = 0 def decorate_me(counter, arg1, arg2): """Test static decorator syntax""" assert arg1 == 'v1' assert arg2 == 'v2' nonlocal actual_counter actual_counter += 1 assert actual_counter == counter out = mock(arg1) assert out == f'test string {counter}' return False logger = logging.getLogger('pypyr.utils.poll') with patch.object(logger, 'debug') as mock_logger_debug: assert not (poll.while_until_true( interval=0.01, max_attempts=3)(decorate_me)('v1', 'v2')) assert mock_logger_debug.mock_calls == [ call('started'), call('Looping every 0.01 seconds for 3 attempts'), call('iteration 1. Still waiting. . .'), call('iteration 2. Still waiting. . .'), call('iteration 3. Max attempts exhausted.'), call('done') ] assert mock.call_count == 3 mock.assert_called_with('v1') assert mock_time_sleep.call_count == 2 mock_time_sleep.assert_called_with(0.01)
def while_loop(self, context, step_method): """Run step inside a while loop. Args: context: (pypyr.context.Context) The pypyr context. This arg will mutate - after method execution will contain the new updated context. step_method: (method/function) This is the method/function that will execute on every loop iteration. Signature is: function(context) """ logger.debug("starting") context['whileCounter'] = 0 self.while_counter = 0 if self.stop is None and self.max is None: # the ctor already does this check, but guess theoretically # consumer could have messed with the props since ctor logger.error("while decorator missing both max and stop.") raise PipelineDefinitionError("the while decorator must have " "either max or stop, or both. " "But not neither.") error_on_max = context.get_formatted_as_type(self.error_on_max, out_type=bool) sleep = context.get_formatted_as_type(self.sleep, out_type=float) if self.max is None: max = None logger.info( "while decorator will loop until %s " "evaluates to True at %ss intervals.", self.stop, sleep) else: max = context.get_formatted_as_type(self.max, out_type=int) if max < 1: logger.info("max %s is %s. while only runs when max > 0.", self.max, max) logger.debug("done") return if self.stop is None: logger.info( "while decorator will loop %s times at " "%ss intervals.", max, sleep) else: logger.info( "while decorator will loop %s times, or " "until %s evaluates to True at " "%ss intervals.", max, self.stop, sleep) if not poll.while_until_true(interval=sleep, max_attempts=max)( self.exec_iteration)(context=context, step_method=step_method): # False means loop exhausted and stop never eval-ed True. if error_on_max: logger.error( "exhausted %s iterations of while loop, " "and errorOnMax is True.", max) if self.stop and max: raise LoopMaxExhaustedError("while loop reached " f"{max} and {self.stop} " "never evaluated to True.") else: raise LoopMaxExhaustedError(f"while loop reached {max}.") else: if self.stop and max: logger.info( "while decorator looped %s times, " "and %s never evaluated to True.", max, self.stop) logger.debug("while loop done") else: logger.info( "while loop done, stop condition %s " "evaluated True.", self.stop) logger.debug("done")