# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Unit tests for decorators.py.
"""

# pylint: disable=W0613

import time
import traceback
import unittest

from devil.android import decorators
from devil.android import device_errors
from devil.utils import reraiser_thread

_DEFAULT_TIMEOUT = 30
_DEFAULT_RETRIES = 3


class DecoratorsTest(unittest.TestCase):
  _decorated_function_called_count = 0

  def testFunctionDecoratorDoesTimeouts(self):
    """Tests that the base decorator handles the timeout logic."""
    DecoratorsTest._decorated_function_called_count = 0

    @decorators.WithTimeoutAndRetries
    def alwaysTimesOut(timeout=None, retries=None):
      DecoratorsTest._decorated_function_called_count += 1
      time.sleep(100)

    start_time = time.time()
    with self.assertRaises(device_errors.CommandTimeoutError):
      alwaysTimesOut(timeout=1, retries=0)
    elapsed_time = time.time() - start_time
    self.assertTrue(elapsed_time >= 1)
    self.assertEquals(1, DecoratorsTest._decorated_function_called_count)

  def testFunctionDecoratorDoesRetries(self):
    """Tests that the base decorator handles the retries logic."""
    DecoratorsTest._decorated_function_called_count = 0

    @decorators.WithTimeoutAndRetries
    def alwaysRaisesCommandFailedError(timeout=None, retries=None):
      DecoratorsTest._decorated_function_called_count += 1
      raise device_errors.CommandFailedError('testCommand failed')

    with self.assertRaises(device_errors.CommandFailedError):
      alwaysRaisesCommandFailedError(timeout=30, retries=10)
    self.assertEquals(11, DecoratorsTest._decorated_function_called_count)

  def testFunctionDecoratorRequiresParams(self):
    """Tests that the base decorator requires timeout and retries params."""

    @decorators.WithTimeoutAndRetries
    def requiresExplicitTimeoutAndRetries(timeout=None, retries=None):
      return (timeout, retries)

    with self.assertRaises(KeyError):
      requiresExplicitTimeoutAndRetries()
    with self.assertRaises(KeyError):
      requiresExplicitTimeoutAndRetries(timeout=10)
    with self.assertRaises(KeyError):
      requiresExplicitTimeoutAndRetries(retries=0)
    expected_timeout = 10
    expected_retries = 1
    (actual_timeout, actual_retries) = (requiresExplicitTimeoutAndRetries(
        timeout=expected_timeout, retries=expected_retries))
    self.assertEquals(expected_timeout, actual_timeout)
    self.assertEquals(expected_retries, actual_retries)

  def testFunctionDecoratorTranslatesReraiserExceptions(self):
    """Tests that the explicit decorator translates reraiser exceptions."""

    @decorators.WithTimeoutAndRetries
    def alwaysRaisesProvidedException(exception, timeout=None, retries=None):
      raise exception

    exception_desc = 'Reraiser thread timeout error'
    with self.assertRaises(device_errors.CommandTimeoutError) as e:
      alwaysRaisesProvidedException(
          reraiser_thread.TimeoutError(exception_desc), timeout=10, retries=1)
    self.assertEquals(exception_desc, str(e.exception))

  def testConditionalRetriesDecoratorRetries(self):
    def do_not_retry_no_adb_error(exc):
      return not isinstance(exc, device_errors.NoAdbError)

    actual_tries = [0]

    @decorators.WithTimeoutAndConditionalRetries(do_not_retry_no_adb_error)
    def alwaysRaisesCommandFailedError(timeout=None, retries=None):
      actual_tries[0] += 1
      raise device_errors.CommandFailedError('Command failed :(')

    with self.assertRaises(device_errors.CommandFailedError):
      alwaysRaisesCommandFailedError(timeout=10, retries=10)
    self.assertEquals(11, actual_tries[0])

  def testConditionalRetriesDecoratorDoesntRetry(self):
    def do_not_retry_no_adb_error(exc):
      return not isinstance(exc, device_errors.NoAdbError)

    actual_tries = [0]

    @decorators.WithTimeoutAndConditionalRetries(do_not_retry_no_adb_error)
    def alwaysRaisesNoAdbError(timeout=None, retries=None):
      actual_tries[0] += 1
      raise device_errors.NoAdbError()

    with self.assertRaises(device_errors.NoAdbError):
      alwaysRaisesNoAdbError(timeout=10, retries=10)
    self.assertEquals(1, actual_tries[0])

  def testDefaultsFunctionDecoratorDoesTimeouts(self):
    """Tests that the defaults decorator handles timeout logic."""
    DecoratorsTest._decorated_function_called_count = 0

    @decorators.WithTimeoutAndRetriesDefaults(1, 0)
    def alwaysTimesOut(timeout=None, retries=None):
      DecoratorsTest._decorated_function_called_count += 1
      time.sleep(100)

    start_time = time.time()
    with self.assertRaises(device_errors.CommandTimeoutError):
      alwaysTimesOut()
    elapsed_time = time.time() - start_time
    self.assertTrue(elapsed_time >= 1)
    self.assertEquals(1, DecoratorsTest._decorated_function_called_count)

    DecoratorsTest._decorated_function_called_count = 0
    with self.assertRaises(device_errors.CommandTimeoutError):
      alwaysTimesOut(timeout=2)
    elapsed_time = time.time() - start_time
    self.assertTrue(elapsed_time >= 2)
    self.assertEquals(1, DecoratorsTest._decorated_function_called_count)

  def testDefaultsFunctionDecoratorDoesRetries(self):
    """Tests that the defaults decorator handles retries logic."""
    DecoratorsTest._decorated_function_called_count = 0

    @decorators.WithTimeoutAndRetriesDefaults(30, 10)
    def alwaysRaisesCommandFailedError(timeout=None, retries=None):
      DecoratorsTest._decorated_function_called_count += 1
      raise device_errors.CommandFailedError('testCommand failed')

    with self.assertRaises(device_errors.CommandFailedError):
      alwaysRaisesCommandFailedError()
    self.assertEquals(11, DecoratorsTest._decorated_function_called_count)

    DecoratorsTest._decorated_function_called_count = 0
    with self.assertRaises(device_errors.CommandFailedError):
      alwaysRaisesCommandFailedError(retries=5)
    self.assertEquals(6, DecoratorsTest._decorated_function_called_count)

  def testDefaultsFunctionDecoratorPassesValues(self):
    """Tests that the defaults decorator passes timeout and retries kwargs."""

    @decorators.WithTimeoutAndRetriesDefaults(30, 10)
    def alwaysReturnsTimeouts(timeout=None, retries=None):
      return timeout

    self.assertEquals(30, alwaysReturnsTimeouts())
    self.assertEquals(120, alwaysReturnsTimeouts(timeout=120))

    @decorators.WithTimeoutAndRetriesDefaults(30, 10)
    def alwaysReturnsRetries(timeout=None, retries=None):
      return retries

    self.assertEquals(10, alwaysReturnsRetries())
    self.assertEquals(1, alwaysReturnsRetries(retries=1))

  def testDefaultsFunctionDecoratorTranslatesReraiserExceptions(self):
    """Tests that the explicit decorator translates reraiser exceptions."""

    @decorators.WithTimeoutAndRetriesDefaults(30, 10)
    def alwaysRaisesProvidedException(exception, timeout=None, retries=None):
      raise exception

    exception_desc = 'Reraiser thread timeout error'
    with self.assertRaises(device_errors.CommandTimeoutError) as e:
      alwaysRaisesProvidedException(
          reraiser_thread.TimeoutError(exception_desc))
    self.assertEquals(exception_desc, str(e.exception))

  def testExplicitFunctionDecoratorDoesTimeouts(self):
    """Tests that the explicit decorator handles timeout logic."""
    DecoratorsTest._decorated_function_called_count = 0

    @decorators.WithExplicitTimeoutAndRetries(1, 0)
    def alwaysTimesOut():
      DecoratorsTest._decorated_function_called_count += 1
      time.sleep(100)

    start_time = time.time()
    with self.assertRaises(device_errors.CommandTimeoutError):
      alwaysTimesOut()
    elapsed_time = time.time() - start_time
    self.assertTrue(elapsed_time >= 1)
    self.assertEquals(1, DecoratorsTest._decorated_function_called_count)

  def testExplicitFunctionDecoratorDoesRetries(self):
    """Tests that the explicit decorator handles retries logic."""
    DecoratorsTest._decorated_function_called_count = 0

    @decorators.WithExplicitTimeoutAndRetries(30, 10)
    def alwaysRaisesCommandFailedError():
      DecoratorsTest._decorated_function_called_count += 1
      raise device_errors.CommandFailedError('testCommand failed')

    with self.assertRaises(device_errors.CommandFailedError):
      alwaysRaisesCommandFailedError()
    self.assertEquals(11, DecoratorsTest._decorated_function_called_count)

  def testExplicitDecoratorTranslatesReraiserExceptions(self):
    """Tests that the explicit decorator translates reraiser exceptions."""

    @decorators.WithExplicitTimeoutAndRetries(30, 10)
    def alwaysRaisesProvidedException(exception):
      raise exception

    exception_desc = 'Reraiser thread timeout error'
    with self.assertRaises(device_errors.CommandTimeoutError) as e:
      alwaysRaisesProvidedException(
          reraiser_thread.TimeoutError(exception_desc))
    self.assertEquals(exception_desc, str(e.exception))

  class _MethodDecoratorTestObject(object):
    """An object suitable for testing the method decorator."""

    def __init__(self,
                 test_case,
                 default_timeout=_DEFAULT_TIMEOUT,
                 default_retries=_DEFAULT_RETRIES):
      self._test_case = test_case
      self.default_timeout = default_timeout
      self.default_retries = default_retries
      self.function_call_counters = {
          'alwaysRaisesCommandFailedError': 0,
          'alwaysTimesOut': 0,
          'requiresExplicitTimeoutAndRetries': 0,
      }

    @decorators.WithTimeoutAndRetriesFromInstance('default_timeout',
                                                  'default_retries')
    def alwaysTimesOut(self, timeout=None, retries=None):
      self.function_call_counters['alwaysTimesOut'] += 1
      time.sleep(100)
      self._test_case.assertFalse(True, msg='Failed to time out?')

    @decorators.WithTimeoutAndRetriesFromInstance('default_timeout',
                                                  'default_retries')
    def alwaysRaisesCommandFailedError(self, timeout=None, retries=None):
      self.function_call_counters['alwaysRaisesCommandFailedError'] += 1
      raise device_errors.CommandFailedError('testCommand failed')

    # pylint: disable=no-self-use

    @decorators.WithTimeoutAndRetriesFromInstance('default_timeout',
                                                  'default_retries')
    def alwaysReturnsTimeout(self, timeout=None, retries=None):
      return timeout

    @decorators.WithTimeoutAndRetriesFromInstance(
        'default_timeout', 'default_retries', min_default_timeout=100)
    def alwaysReturnsTimeoutWithMin(self, timeout=None, retries=None):
      return timeout

    @decorators.WithTimeoutAndRetriesFromInstance('default_timeout',
                                                  'default_retries')
    def alwaysReturnsRetries(self, timeout=None, retries=None):
      return retries

    @decorators.WithTimeoutAndRetriesFromInstance('default_timeout',
                                                  'default_retries')
    def alwaysRaisesProvidedException(self,
                                      exception,
                                      timeout=None,
                                      retries=None):
      raise exception

    # pylint: enable=no-self-use

  def testMethodDecoratorDoesTimeout(self):
    """Tests that the method decorator handles timeout logic."""
    test_obj = self._MethodDecoratorTestObject(self)
    start_time = time.time()
    with self.assertRaises(device_errors.CommandTimeoutError):
      try:
        test_obj.alwaysTimesOut(timeout=1, retries=0)
      except:
        traceback.print_exc()
        raise
    elapsed_time = time.time() - start_time
    self.assertTrue(elapsed_time >= 1)
    self.assertEquals(1, test_obj.function_call_counters['alwaysTimesOut'])

  def testMethodDecoratorDoesRetries(self):
    """Tests that the method decorator handles retries logic."""
    test_obj = self._MethodDecoratorTestObject(self)
    with self.assertRaises(device_errors.CommandFailedError):
      try:
        test_obj.alwaysRaisesCommandFailedError(retries=10)
      except:
        traceback.print_exc()
        raise
    self.assertEquals(
        11, test_obj.function_call_counters['alwaysRaisesCommandFailedError'])

  def testMethodDecoratorPassesValues(self):
    """Tests that the method decorator passes timeout and retries kwargs."""
    test_obj = self._MethodDecoratorTestObject(
        self, default_timeout=42, default_retries=31)
    self.assertEquals(42, test_obj.alwaysReturnsTimeout())
    self.assertEquals(41, test_obj.alwaysReturnsTimeout(timeout=41))
    self.assertEquals(31, test_obj.alwaysReturnsRetries())
    self.assertEquals(32, test_obj.alwaysReturnsRetries(retries=32))

  def testMethodDecoratorUsesMiniumumTimeout(self):
    test_obj = self._MethodDecoratorTestObject(
        self, default_timeout=42, default_retries=31)
    self.assertEquals(100, test_obj.alwaysReturnsTimeoutWithMin())
    self.assertEquals(41, test_obj.alwaysReturnsTimeoutWithMin(timeout=41))

  def testMethodDecoratorTranslatesReraiserExceptions(self):
    test_obj = self._MethodDecoratorTestObject(self)

    exception_desc = 'Reraiser thread timeout error'
    with self.assertRaises(device_errors.CommandTimeoutError) as e:
      test_obj.alwaysRaisesProvidedException(
          reraiser_thread.TimeoutError(exception_desc))
    self.assertEquals(exception_desc, str(e.exception))


if __name__ == '__main__':
  unittest.main(verbosity=2)
