<gh_stars>10-100
# Copyright (c) 2011 Rackspace
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Decorators useful to the tests."""

from functools import wraps

from proboscis.asserts import assert_raises_instance
from proboscis import compatability
from proboscis.core import TestRegistry


DEFAULT_REGISTRY = TestRegistry()


def expect_exception(exception_type):
    """Decorates a test method to show it expects an exception to be raised."""
    def return_method(method):
        @wraps(method)
        def new_method(*args, **kwargs):
            assert_raises_instance(exception_type, method, *args, **kwargs)
        return new_method
    return return_method


class TimeoutError(RuntimeError):
    """Thrown when a method has exceeded the time allowed."""
    pass


def time_out(time):
    """Raises TimeoutError if the decorated method does not finish in time."""
    if not compatability.supports_time_out():
        raise ImportError("time_out not supported for this version of Python.")
    import signal

    def cb_timeout(signum, frame):
        raise TimeoutError("Time out after waiting " + str(time) + " seconds.")

    def return_method(func):
        """Turns function into decorated function."""
        @wraps(func)
        def new_method(*kargs, **kwargs):
            previous_handler = signal.signal(signal.SIGALRM, cb_timeout)
            try:
                signal.alarm(time)
                return func(*kargs, **kwargs)
            finally:
                signal.alarm(0)
                signal.signal(signal.SIGALRM, previous_handler)
        return new_method
    return return_method


def register(**kwargs):
    """Registers a test in proboscis's default registry.

    :param home: The target class or function.

    This also allows all of the parameters used by the @test decorator.

    This function works differently than a decorator as it allows the class or
    function which is being registered to appear in the same call as all of the
    options.

    Its designed to make it easier to register class or functions with
    Proboscis after they're defined.

    """
    DEFAULT_REGISTRY.register(**kwargs)


def test(home=None, **kwargs):
    """Decorates a test class or function to cause Proboscis to run it.

    The behavior differs depending the target:

        - If put on a stand-alone function, the function will run by itself.

        - If put on a class inheriting unittest.TestCase, then the class will
          run just like a normal unittest class by using the method names and
          instantiate a new instance of the class for each test method.

        - If the class does not inherit from unittest.TestCase, the class will
          be instantiated once and this instance will be passed to each method
          decorated with @test (this increases encapsulation over using class
          fields as the instance can not be accessed outside of its methods).

          Note that due to how decorators work its impossible to know if a
          function is or is not part of a class; thus if a class method is
          decorated with test but its class is not then
          ProboscisTestMethodNotDecorated will be raised.

    :param groups: A list of strings representing the groups this test method
                   or class belongs to. By default this is an empty list.
    :param depends_on: A list of test functions or classes which must run
                       before this test. By default this is an empty list.
    :param depends_on_groups: A list of strings each naming a group that must
                              run before this test. By default this is an empty
                              list.
    :param enabled: By default, true. If set to false this test will not run.
    :param always_run: If true this test will run even if the tests listed in
                       depends_on or depends_on_groups have failed.
    """
    if home:
        return DEFAULT_REGISTRY.register(home, **kwargs)
    else:
        def cb_method(home_2):
            return DEFAULT_REGISTRY.register(home_2, **kwargs)
        return cb_method


def before_class(home=None, **kwargs):
    """Like @test but indicates this should run before other class methods.

    All of the arguments sent to @test work with this decorator as well.

    """
    kwargs.update({'run_before_class':True})
    return test(home=home, **kwargs)


def after_class(home=None, **kwargs):
    """Like @test but indicates this should run after other class methods.

    All of the arguments sent to @test work with this decorator as well.

    This will be skipped if a class method test fails;
    set always_run if that is not desired. See `issue #5
    <https://github.com/rackspace/python-proboscis/issues/5>`__.

    """
    kwargs.update({'run_after_class':True})
    return test(home=home, **kwargs)


def factory(func=None, **kwargs):
    """Decorates a function which returns new instances of Test classes."""
    if func:
        return DEFAULT_REGISTRY.register_factory(func)
    else:
        raise ValueError("Arguments not supported on factories.")

