Source code for tempest.lib.decorators

# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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.

import functools
from types import MethodType
import uuid

from oslo_log import log as logging
import testtools

from tempest.lib import exceptions as lib_exc

LOG = logging.getLogger(__name__)

_SUPPORTED_BUG_TYPES = {
    'launchpad': 'https://launchpad.net/bugs/%s',
    'storyboard': 'https://storyboard.openstack.org/#!/story/%s',
}


def _validate_bug_and_bug_type(bug, bug_type):
    """Validates ``bug`` and ``bug_type`` values.

    :param bug: bug number causing the test to skip (launchpad or storyboard)
    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
    :raises: InvalidParam if ``bug`` is not a digit or ``bug_type`` is not
        a valid value
    """
    if not bug.isdigit():
        invalid_param = '%s must be a valid %s number' % (bug, bug_type)
        raise lib_exc.InvalidParam(invalid_param=invalid_param)
    if bug_type not in _SUPPORTED_BUG_TYPES:
        invalid_param = 'bug_type "%s" must be one of: %s' % (
            bug_type, ', '.join(_SUPPORTED_BUG_TYPES.keys()))
        raise lib_exc.InvalidParam(invalid_param=invalid_param)


def _get_bug_url(bug, bug_type='launchpad'):
    """Get the bug URL based on the ``bug_type`` and ``bug``

    :param bug: The launchpad/storyboard bug number causing the test
    :param bug_type: 'launchpad' or 'storyboard', default 'launchpad'
    :returns: Bug URL corresponding to ``bug_type`` value
    """
    _validate_bug_and_bug_type(bug, bug_type)
    return _SUPPORTED_BUG_TYPES[bug_type] % bug


[docs] def skip_because(*args, **kwargs): """A decorator useful to skip tests hitting known bugs ``bug`` must be a number and ``condition`` must be true for the test to skip. :param bug: bug number causing the test to skip (launchpad or storyboard) :param bug_type: 'launchpad' or 'storyboard', default 'launchpad' :param condition: optional condition to be True for the skip to have place :raises: testtools.TestCase.skipException if ``condition`` is True and ``bug`` is included """ def decorator(f): @functools.wraps(f) def wrapper(*func_args, **func_kwargs): condition = kwargs.get('condition', True) bug = kwargs.get('bug', None) if bug and condition: bug_type = kwargs.get('bug_type', 'launchpad') bug_url = _get_bug_url(bug, bug_type) raise testtools.TestCase.skipException( "Skipped until bug: %s is resolved." % bug_url) return f(*func_args, **func_kwargs) return wrapper return decorator
[docs] def idempotent_id(id): """Stub for metadata decorator""" if not isinstance(id, str): raise TypeError('Test idempotent_id must be string not %s' '' % type(id).__name__) uuid.UUID(id) def decorator(f): f = testtools.testcase.attr('id-%s' % id)(f) if f.__doc__: f.__doc__ = 'Test idempotent id: %s\n\n%s' % (id, f.__doc__) else: f.__doc__ = 'Test idempotent id: %s' % id return f return decorator
[docs] def attr(**kwargs): """A decorator which applies the testtools attr decorator This decorator applies the testtools.testcase.attr if it is in the list of attributes to testtools we want to apply. :param condition: Optional condition which if true will apply the attr. If a condition is specified which is false the attr will not be applied to the test function. If not specified, the attr is always applied. """ def decorator(f): # Check to see if the attr should be conditional applied. if 'condition' in kwargs and not kwargs.get('condition'): return f if 'type' in kwargs and isinstance(kwargs['type'], str): f = testtools.testcase.attr(kwargs['type'])(f) elif 'type' in kwargs and isinstance(kwargs['type'], list): for attr in kwargs['type']: f = testtools.testcase.attr(attr)(f) return f return decorator
[docs] def unstable_test(*args, **kwargs): """A decorator useful to run tests hitting known bugs and skip it if fails This decorator can be used in cases like: * We have skipped tests with some bug and now bug is claimed to be fixed. Now we want to check the test stability so we use this decorator. The number of skipped cases with that bug can be counted to mark test stable again. * There is test which is failing often, but not always. If there is known bug related to it, and someone is working on fix, this decorator can be used instead of "skip_because". That will ensure that test is still run so new debug data can be collected from jobs' logs but it will not make life of other developers harder by forcing them to recheck jobs more often. ``bug`` must be a number for the test to skip. :param bug: bug number causing the test to skip (launchpad or storyboard) :param bug_type: 'launchpad' or 'storyboard', default 'launchpad' :raises: testtools.TestCase.skipException if test actually fails, and ``bug`` is included """ def decor(f): @functools.wraps(f) def inner(self, *func_args, **func_kwargs): try: return f(self, *func_args, **func_kwargs) except Exception as e: if "bug" in kwargs: bug = kwargs['bug'] bug_type = kwargs.get('bug_type', 'launchpad') bug_url = _get_bug_url(bug, bug_type) msg = ("Marked as unstable and skipped because of bug: " "%s, failure was: %s") % (bug_url, e) raise testtools.TestCase.skipException(msg) else: raise e return inner return decor
[docs] class cleanup_order: """Descriptor for base create function to cleanup based on caller. There are functions created as classmethod and the cleanup was managed by the class with addClassResourceCleanup, In case the function called from a class level (resource_setup) its ok But when it is called from testcase level there is no reason to delete the resource when class tears down. The testcase results will not reflect the resources cleanup because test may pass but the class cleanup fails. if the resources were created by testcase its better to let the testcase delete them and report failure part of the testcase """ def __init__(self, func): self.func = func functools.update_wrapper(self, func) def __get__(self, instance, owner): if instance: # instance is the caller instance.cleanup = instance.addCleanup instance.__name__ = owner.__name__ return MethodType(self.func, instance) elif owner: # class is the caller owner.cleanup = owner.addClassResourceCleanup return MethodType(self.func, owner)
[docs] def serial(cls): """A decorator to mark a test class for serial execution""" cls._serial = True LOG.debug('marked %s for serial execution', cls.__name__) return cls