added utils (Logged and Retry) and test_utils
Change-Id: Ia0e47636409a82cdd6f1e7af7b16b772d3ce16dd
diff --git a/test_utils.py b/test_utils.py
new file mode 100644
index 0000000..fadeab5
--- /dev/null
+++ b/test_utils.py
@@ -0,0 +1,31 @@
+#-*- coding: utf-8 -*-
+
+import unittest
+
+import utils
+
+
+logger = utils.logger.getChild('TestUtils')
+
+
+class TestUtils(unittest.TestCase):
+
+ def setUp(self):
+ self.gen10 = self.get_generator(10)
+
+ def get_generator(self, max):
+ for i in xrange(max):
+ yield i
+
+ def test_retry_3_from_5(self):
+ res = utils.Retry(timeout=1,
+ attempts=5).wait_result(self.gen10.next, 3)
+ self.assertEqual(res, 3)
+
+ def test_retry_failed(self):
+ retryer = utils.Retry(timeout=1, attempts=3)
+ self.assertRaises(utils.ResultNotProduced,
+ retryer.wait_result, self.gen10.next, 5)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..f92b6e6
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,104 @@
+#-*- coding: utf-8 -*-
+
+
+import logging
+import os
+import time
+
+
+logging.basicConfig()
+logger = logging.getLogger('safe_rsync')
+
+loglevel = os.environ.get('LOGLEVEL', 'INFO')
+if loglevel not in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'):
+ logger.warn('LOGLEVEL environment variable has wrong value=={}. Using '
+ '"INFO" by default.'.format(loglevel))
+ loglevel = 'INFO'
+logger.setLevel(loglevel)
+
+
+def logged(logger=None):
+ if logger is None:
+ logger = globals().get('logger')
+
+ def wrap(f):
+ def wrapped_f(*args, **kwargs):
+ logger.debug('Starting {}({}, {}) (defaults: {})'
+ ''.format(f.__name__,
+ str(args),
+ str(kwargs),
+ str(f.__defaults__))
+ )
+ r = f(*args, **kwargs)
+ logger.debug('{} done with result "{}".'.
+ format(f.__name__, str(r)))
+ return r
+ return wrapped_f
+ return wrap
+
+
+# retry decorator
+def retry(expected_status, timeout=None, attempts=None):
+
+ def wrap(f):
+ def wrapped_f(*args, **kwargs):
+ r = f(*args, **kwargs)
+ return r
+ return wrapped_f
+ return wrap
+
+
+class ResultNotProduced(Exception):
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return repr(self.value)
+
+
+class Retry(object):
+ """
+ Waits while the function reaches the specified status.
+
+ :param function: function that returns some status
+ :param expected_status: status the machine should turn to
+ :param attempts: how many times to check status
+ :param timeout: timeout in seconds before attempts
+ :return: True if node moves to the specified status, False otherwise
+ :Examples:
+ Retry(timeout=3, attempts=10).wait(function, result, param1, param2)
+ Retry().wait_result(function, result, param1, param2)
+ """
+
+ def __init__(self, timeout=5, attempts=10):
+ self.timeout = timeout
+ self.attempts = attempts
+ self.logger = globals().get('logger').getChild('Retry')
+
+ def wait_result(self, function, expected_result, *args, **kwargs):
+
+ self.logger.debug('Wait for {}() == {}...'
+ ''.format(function.__name__, str(expected_result)))
+
+ @logged(self.logger)
+ def f():
+ return function(*args, **kwargs)
+
+ attempt = 1
+ while attempt <= self.attempts:
+ try:
+ result = f()
+ except Exception as e:
+ self.logger.error('Exception on function {}: {}'
+ ''.format(function.__name__, str(e)))
+ raise
+ else:
+ if result == expected_result:
+ self.logger.debug('Got on attempt #{}:'.format(attempt))
+ return result
+ attempt += 1
+ time.sleep(self.timeout)
+
+ raise ResultNotProduced('Result "{}" was not produced during '
+ '{} attempts.'
+ ''.format(expected_result, attempt - 1))