# ***** BEGIN LICENSE BLOCK *****
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
# ***** END LICENSE BLOCK *****

import mock
import unittest

from redo import retry, retriable, retrying, retrier

ATTEMPT_N = 1


def _succeedOnSecondAttempt(foo=None, exception=Exception):
    global ATTEMPT_N
    if ATTEMPT_N == 2:
        ATTEMPT_N += 1
        return
    ATTEMPT_N += 1
    raise exception("Fail")


def _alwaysPass():
    global ATTEMPT_N
    ATTEMPT_N += 1
    return True


def _mirrorArgs(*args, **kwargs):
    return args, kwargs


def _alwaysFail():
    raise Exception("Fail")


class NewError(Exception):
    pass


class OtherError(Exception):
    pass


def _raiseCustomException():
    return _succeedOnSecondAttempt(exception=NewError)


class TestRetry(unittest.TestCase):
    def setUp(self):
        global ATTEMPT_N
        ATTEMPT_N = 1
        self.sleep_patcher = mock.patch('time.sleep')
        self.sleep_patcher.start()

    def tearDown(self):
        self.sleep_patcher.stop()

    def testRetrySucceed(self):
        # Will raise if anything goes wrong
        retry(_succeedOnSecondAttempt, attempts=2, sleeptime=0, jitter=0)

    def testRetriableSucceed(self):
        func = retriable(attempts=2, sleeptime=0, jitter=0)(_succeedOnSecondAttempt)
        func()

    def testRetryFailWithoutCatching(self):
        self.assertRaises(Exception, retry, _alwaysFail, sleeptime=0, jitter=0,
                          exceptions=())

    def testRetriableFailWithoutCatching(self):
        func = retriable(sleeptime=0)(_alwaysFail)
        self.assertRaises(Exception, func, retry_exceptions=())

    def testRetryFailEnsureRaisesLastException(self):
        self.assertRaises(Exception, retry, _alwaysFail, sleeptime=0, jitter=0)

    def testRetriableFailEnsureRaisesLastException(self):
        func = retriable(sleeptime=0)(_alwaysFail)
        self.assertRaises(Exception, func)

    def testRetrySelectiveExceptionSucceed(self):
        retry(_raiseCustomException, attempts=2, sleeptime=0, jitter=0,
              retry_exceptions=(NewError,))

    def testRetriableSelectiveExceptionSucceed(self):
        func = retriable(attempts=2, sleeptime=0, jitter=0,
                         retry_exceptions=(NewError,))(_raiseCustomException)
        func()

    def testRetrySelectiveExceptionFail(self):
        self.assertRaises(NewError, retry, _raiseCustomException, attempts=2,
                          sleeptime=0, jitter=0, retry_exceptions=(OtherError,))

    def testRetriableSelectiveExceptionFail(self):
        func = retriable(attempts=2, sleeptime=0, jitter=0,
                         retry_exceptions=(OtherError,))(_raiseCustomException)
        self.assertRaises(NewError, func)

    def testRetryWithSleep(self):
        retry(_succeedOnSecondAttempt, attempts=2, sleeptime=1)

    def testRetriableWithSleep(self):
        func = retriable(attempts=2, sleeptime=1)(_succeedOnSecondAttempt)
        func()

    def testRetryOnlyRunOnce(self):
        """Tests that retry() doesn't call the action again after success"""
        global ATTEMPT_N
        retry(_alwaysPass, attempts=3, sleeptime=0, jitter=0)
        # ATTEMPT_N gets increased regardless of pass/fail
        self.assertEquals(2, ATTEMPT_N)

    def testRetriableOnlyRunOnce(self):
        global ATTEMPT_N
        func = retriable(attempts=3, sleeptime=0, jitter=0)(_alwaysPass)
        func()
        # ATTEMPT_N gets increased regardless of pass/fail
        self.assertEquals(2, ATTEMPT_N)

    def testRetryReturns(self):
        ret = retry(_alwaysPass, sleeptime=0, jitter=0)
        self.assertEquals(ret, True)

    def testRetriableReturns(self):
        func = retriable(sleeptime=0, jitter=0)(_alwaysPass)
        ret = func()
        self.assertEquals(ret, True)

    def testRetryCleanupIsCalled(self):
        cleanup = mock.Mock()
        retry(_succeedOnSecondAttempt, cleanup=cleanup, sleeptime=0, jitter=0)
        self.assertEquals(cleanup.call_count, 1)

    def testRetriableCleanupIsCalled(self):
        cleanup = mock.Mock()
        func = retriable(cleanup=cleanup, sleeptime=0, jitter=0)(_succeedOnSecondAttempt)
        func()
        self.assertEquals(cleanup.call_count, 1)

    def testRetryArgsPassed(self):
        args = (1, 'two', 3)
        kwargs = dict(foo='a', bar=7)
        ret = retry(_mirrorArgs, args=args, kwargs=kwargs.copy(), sleeptime=0, jitter=0)
        self.assertEqual(ret[0], args)
        self.assertEqual(ret[1], kwargs)

    def testRetriableArgsPassed(self):
        args = (1, 'two', 3)
        kwargs = dict(foo='a', bar=7)
        func = retriable(sleeptime=0, jitter=0)(_mirrorArgs)
        ret = func(*args, **kwargs)
        self.assertEqual(ret[0], args)
        self.assertEqual(ret[1], kwargs)

    def test_retrying_id(self):
        """Make sure that the context manager doesn't change the original
        callable"""
        def wrapped():
            pass
        before = id(wrapped)
        with retrying(wrapped) as w:
            within = id(w)
        after = id(wrapped)
        self.assertEqual(before, after)
        self.assertNotEqual(before, within)

    def test_retrying_call_retry(self):
        """Make sure to distribute retry and function args/kwargs properly"""
        def wrapped(*args, **kwargs):
            pass
        with mock.patch("redo.retry") as mocked_retry:
            with retrying(wrapped, 1, x="y") as w:
                w("a", b=1, c="a")
            mocked_retry.assert_called_once_with(
                wrapped, 1, x='y', args=('a',), kwargs={'c': 'a', 'b': 1})

    def test_retrier(self):
        """Make sure retrier behaves properly"""
        n = 0
        for _ in retrier(attempts=5, sleeptime=0, jitter=0):
            n += 1
        self.assertEquals(n, 5)

    def test_retrier_sleep(self):
        """Make sure retrier sleep is behaving"""
        with mock.patch("time.sleep") as sleep:
            # Test that normal sleep scaling works
            for _ in retrier(attempts=5, sleeptime=10, max_sleeptime=300, sleepscale=2, jitter=0):
                pass
            expected = [mock.call(x) for x in (10, 20, 40, 80)]
            self.assertEquals(sleep.call_args_list, expected)

    def test_retrier_sleep_no_jitter(self):
        """Make sure retrier sleep is behaving"""
        with mock.patch("time.sleep") as sleep:
            # Test that normal sleep scaling works without a jitter
            for _ in retrier(attempts=5, sleeptime=10, max_sleeptime=300, sleepscale=2, jitter=None):
                pass
            expected = [mock.call(x) for x in (10, 20, 40, 80)]
            self.assertEquals(sleep.call_args_list, expected)

    def test_retrier_maxsleep(self):
        with mock.patch("time.sleep") as sleep:
            # Test that max sleep time works
            for _ in retrier(attempts=5, sleeptime=10, max_sleeptime=30, sleepscale=2, jitter=0):
                pass
            expected = [mock.call(x) for x in (10, 20, 30, 30)]
            self.assertEquals(sleep.call_args_list, expected)

    def test_jitter_bounds(self):
        self.assertRaises(Exception, retrier(sleeptime=1, jitter=2))

    def test_retrier_jitter(self):
        with mock.patch("time.sleep") as sleep:
            # Test that jitter works
            with mock.patch("random.randint") as randint:
                randint.return_value = 3
                for _ in retrier(attempts=5, sleeptime=10, max_sleeptime=300,
                                 sleepscale=2, jitter=3):
                    randint.return_value *= -1
                expected = [mock.call(x) for x in (7, 23, 37, 83)]
                self.assertEquals(sleep.call_args_list, expected)
                self.assertEquals(randint.call_args, mock.call(-48, 48))

    def test_retrier_yields(self):
        """Ensure that retrier yields the sleep time"""
        for real_sleeptime in retrier(attempts=3, sleeptime=1, sleepscale=1, jitter=0):
            self.assertEquals(real_sleeptime, 1)
