| # 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 |
| |
| |
| 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 |
| |
| |
| def related_bug(bug, status_code=None, bug_type='launchpad'): |
| """A decorator useful to know solutions from launchpad/storyboard reports |
| |
| :param bug: The launchpad/storyboard bug number causing the test bug |
| :param bug_type: 'launchpad' or 'storyboard', default 'launchpad' |
| :param status_code: The status code related to the bug report |
| """ |
| def decorator(f): |
| @functools.wraps(f) |
| def wrapper(*func_args, **func_kwargs): |
| try: |
| return f(*func_args, **func_kwargs) |
| except Exception as exc: |
| exc_status_code = getattr(exc, 'status_code', None) |
| if status_code is None or status_code == exc_status_code: |
| if bug: |
| LOG.error('Hints: This test was made for the bug_type ' |
| '%s. The failure could be related to ' |
| '%s', bug, _get_bug_url(bug, bug_type)) |
| raise exc |
| return wrapper |
| return decorator |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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 |