Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 1 | # Copyright 2015 Hewlett-Packard Development Company, L.P. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 4 | # not use this file except in compliance with the License. You may obtain |
| 5 | # a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 12 | # License for the specific language governing permissions and limitations |
| 13 | # under the License. |
| 14 | |
| 15 | import functools |
| 16 | import uuid |
| 17 | |
Jordan Pittier | c5665a6 | 2017-04-12 16:42:53 +0200 | [diff] [blame] | 18 | from oslo_log import log as logging |
Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 19 | import six |
| 20 | import testtools |
| 21 | |
Chandan Kumar | 7d8c281 | 2018-02-08 14:26:56 +0530 | [diff] [blame] | 22 | from tempest.lib import exceptions as lib_exc |
| 23 | |
Jordan Pittier | c5665a6 | 2017-04-12 16:42:53 +0200 | [diff] [blame] | 24 | LOG = logging.getLogger(__name__) |
| 25 | |
Chandan Kumar | 7d8c281 | 2018-02-08 14:26:56 +0530 | [diff] [blame] | 26 | _SUPPORTED_BUG_TYPES = { |
| 27 | 'launchpad': 'https://launchpad.net/bugs/%s', |
| 28 | 'storyboard': 'https://storyboard.openstack.org/#!/story/%s', |
| 29 | } |
| 30 | |
| 31 | |
| 32 | def _validate_bug_and_bug_type(bug, bug_type): |
| 33 | """Validates ``bug`` and ``bug_type`` values. |
| 34 | |
| 35 | :param bug: bug number causing the test to skip (launchpad or storyboard) |
| 36 | :param bug_type: 'launchpad' or 'storyboard', default 'launchpad' |
| 37 | :raises: InvalidParam if ``bug`` is not a digit or ``bug_type`` is not |
| 38 | a valid value |
| 39 | """ |
| 40 | if not bug.isdigit(): |
| 41 | invalid_param = '%s must be a valid %s number' % (bug, bug_type) |
| 42 | raise lib_exc.InvalidParam(invalid_param=invalid_param) |
| 43 | if bug_type not in _SUPPORTED_BUG_TYPES: |
| 44 | invalid_param = 'bug_type "%s" must be one of: %s' % ( |
| 45 | bug_type, ', '.join(_SUPPORTED_BUG_TYPES.keys())) |
| 46 | raise lib_exc.InvalidParam(invalid_param=invalid_param) |
| 47 | |
| 48 | |
| 49 | def _get_bug_url(bug, bug_type='launchpad'): |
| 50 | """Get the bug URL based on the ``bug_type`` and ``bug`` |
| 51 | |
| 52 | :param bug: The launchpad/storyboard bug number causing the test |
| 53 | :param bug_type: 'launchpad' or 'storyboard', default 'launchpad' |
| 54 | :returns: Bug URL corresponding to ``bug_type`` value |
| 55 | """ |
| 56 | _validate_bug_and_bug_type(bug, bug_type) |
| 57 | return _SUPPORTED_BUG_TYPES[bug_type] % bug |
| 58 | |
Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 59 | |
| 60 | def skip_because(*args, **kwargs): |
| 61 | """A decorator useful to skip tests hitting known bugs |
| 62 | |
Chandan Kumar | 7d8c281 | 2018-02-08 14:26:56 +0530 | [diff] [blame] | 63 | ``bug`` must be a number and ``condition`` must be true for the test to |
| 64 | skip. |
| 65 | |
| 66 | :param bug: bug number causing the test to skip (launchpad or storyboard) |
| 67 | :param bug_type: 'launchpad' or 'storyboard', default 'launchpad' |
| 68 | :param condition: optional condition to be True for the skip to have place |
| 69 | :raises: testtools.TestCase.skipException if ``condition`` is True and |
| 70 | ``bug`` is included |
Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 71 | """ |
| 72 | def decorator(f): |
| 73 | @functools.wraps(f) |
lkuchlan | a2addfe | 2017-11-13 12:06:34 +0200 | [diff] [blame] | 74 | def wrapper(*func_args, **func_kwargs): |
Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 75 | skip = False |
Chandan Kumar | 7d8c281 | 2018-02-08 14:26:56 +0530 | [diff] [blame] | 76 | msg = '' |
Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 77 | if "condition" in kwargs: |
| 78 | if kwargs["condition"] is True: |
| 79 | skip = True |
| 80 | else: |
| 81 | skip = True |
| 82 | if "bug" in kwargs and skip is True: |
Chandan Kumar | 7d8c281 | 2018-02-08 14:26:56 +0530 | [diff] [blame] | 83 | bug = kwargs['bug'] |
| 84 | bug_type = kwargs.get('bug_type', 'launchpad') |
| 85 | bug_url = _get_bug_url(bug, bug_type) |
| 86 | msg = "Skipped until bug: %s is resolved." % bug_url |
Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 87 | raise testtools.TestCase.skipException(msg) |
lkuchlan | a2addfe | 2017-11-13 12:06:34 +0200 | [diff] [blame] | 88 | return f(*func_args, **func_kwargs) |
Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 89 | return wrapper |
| 90 | return decorator |
| 91 | |
| 92 | |
Chandan Kumar | 7d8c281 | 2018-02-08 14:26:56 +0530 | [diff] [blame] | 93 | def related_bug(bug, status_code=None, bug_type='launchpad'): |
| 94 | """A decorator useful to know solutions from launchpad/storyboard reports |
Jordan Pittier | c5665a6 | 2017-04-12 16:42:53 +0200 | [diff] [blame] | 95 | |
Chandan Kumar | 7d8c281 | 2018-02-08 14:26:56 +0530 | [diff] [blame] | 96 | :param bug: The launchpad/storyboard bug number causing the test bug |
| 97 | :param bug_type: 'launchpad' or 'storyboard', default 'launchpad' |
| 98 | :param status_code: The status code related to the bug report |
Jordan Pittier | c5665a6 | 2017-04-12 16:42:53 +0200 | [diff] [blame] | 99 | """ |
| 100 | def decorator(f): |
| 101 | @functools.wraps(f) |
lkuchlan | f5c1905 | 2017-11-23 09:26:55 +0200 | [diff] [blame] | 102 | def wrapper(*func_args, **func_kwargs): |
Jordan Pittier | c5665a6 | 2017-04-12 16:42:53 +0200 | [diff] [blame] | 103 | try: |
lkuchlan | f5c1905 | 2017-11-23 09:26:55 +0200 | [diff] [blame] | 104 | return f(*func_args, **func_kwargs) |
Jordan Pittier | c5665a6 | 2017-04-12 16:42:53 +0200 | [diff] [blame] | 105 | except Exception as exc: |
| 106 | exc_status_code = getattr(exc, 'status_code', None) |
| 107 | if status_code is None or status_code == exc_status_code: |
Chandan Kumar | 7d8c281 | 2018-02-08 14:26:56 +0530 | [diff] [blame] | 108 | if bug: |
| 109 | LOG.error('Hints: This test was made for the bug_type ' |
| 110 | '%s. The failure could be related to ' |
| 111 | '%s', bug, _get_bug_url(bug, bug_type)) |
Jordan Pittier | c5665a6 | 2017-04-12 16:42:53 +0200 | [diff] [blame] | 112 | raise exc |
| 113 | return wrapper |
| 114 | return decorator |
| 115 | |
| 116 | |
Matthew Treinish | 9e26ca8 | 2016-02-23 11:43:20 -0500 | [diff] [blame] | 117 | def idempotent_id(id): |
| 118 | """Stub for metadata decorator""" |
| 119 | if not isinstance(id, six.string_types): |
| 120 | raise TypeError('Test idempotent_id must be string not %s' |
| 121 | '' % type(id).__name__) |
| 122 | uuid.UUID(id) |
| 123 | |
| 124 | def decorator(f): |
| 125 | f = testtools.testcase.attr('id-%s' % id)(f) |
| 126 | if f.__doc__: |
| 127 | f.__doc__ = 'Test idempotent id: %s\n%s' % (id, f.__doc__) |
| 128 | else: |
| 129 | f.__doc__ = 'Test idempotent id: %s' % id |
| 130 | return f |
| 131 | return decorator |
| 132 | |
| 133 | |
Jordan Pittier | 3b46d27 | 2017-04-12 16:17:28 +0200 | [diff] [blame] | 134 | def attr(**kwargs): |
| 135 | """A decorator which applies the testtools attr decorator |
| 136 | |
| 137 | This decorator applies the testtools.testcase.attr if it is in the list of |
| 138 | attributes to testtools we want to apply. |
Matt Riedemann | 2999963 | 2019-02-14 14:32:20 -0500 | [diff] [blame] | 139 | |
| 140 | :param condition: Optional condition which if true will apply the attr. If |
| 141 | a condition is specified which is false the attr will not be applied to |
| 142 | the test function. If not specified, the attr is always applied. |
Jordan Pittier | 3b46d27 | 2017-04-12 16:17:28 +0200 | [diff] [blame] | 143 | """ |
| 144 | |
| 145 | def decorator(f): |
Matt Riedemann | 2999963 | 2019-02-14 14:32:20 -0500 | [diff] [blame] | 146 | # Check to see if the attr should be conditional applied. |
| 147 | if 'condition' in kwargs and not kwargs.get('condition'): |
| 148 | return f |
Alfredo Moralejo | 3e7752c | 2019-03-13 11:12:29 +0100 | [diff] [blame] | 149 | if 'type' in kwargs and isinstance(kwargs['type'], six.string_types): |
Jordan Pittier | 3b46d27 | 2017-04-12 16:17:28 +0200 | [diff] [blame] | 150 | f = testtools.testcase.attr(kwargs['type'])(f) |
| 151 | elif 'type' in kwargs and isinstance(kwargs['type'], list): |
| 152 | for attr in kwargs['type']: |
| 153 | f = testtools.testcase.attr(attr)(f) |
| 154 | return f |
| 155 | |
| 156 | return decorator |
Slawek Kaplonski | 21f5301 | 2019-01-29 12:52:01 +0100 | [diff] [blame] | 157 | |
| 158 | |
| 159 | def unstable_test(*args, **kwargs): |
| 160 | """A decorator useful to run tests hitting known bugs and skip it if fails |
| 161 | |
| 162 | This decorator can be used in cases like: |
| 163 | |
| 164 | * We have skipped tests with some bug and now bug is claimed to be fixed. |
| 165 | Now we want to check the test stability so we use this decorator. |
| 166 | The number of skipped cases with that bug can be counted to mark test |
| 167 | stable again. |
| 168 | * There is test which is failing often, but not always. If there is known |
| 169 | bug related to it, and someone is working on fix, this decorator can be |
| 170 | used instead of "skip_because". That will ensure that test is still run |
| 171 | so new debug data can be collected from jobs' logs but it will not make |
| 172 | life of other developers harder by forcing them to recheck jobs more |
| 173 | often. |
| 174 | |
| 175 | ``bug`` must be a number for the test to skip. |
| 176 | |
| 177 | :param bug: bug number causing the test to skip (launchpad or storyboard) |
| 178 | :param bug_type: 'launchpad' or 'storyboard', default 'launchpad' |
| 179 | :raises: testtools.TestCase.skipException if test actually fails, |
| 180 | and ``bug`` is included |
| 181 | """ |
| 182 | def decor(f): |
| 183 | @functools.wraps(f) |
| 184 | def inner(self, *func_args, **func_kwargs): |
| 185 | try: |
| 186 | return f(self, *func_args, **func_kwargs) |
| 187 | except Exception as e: |
| 188 | if "bug" in kwargs: |
| 189 | bug = kwargs['bug'] |
| 190 | bug_type = kwargs.get('bug_type', 'launchpad') |
| 191 | bug_url = _get_bug_url(bug, bug_type) |
| 192 | msg = ("Marked as unstable and skipped because of bug: " |
| 193 | "%s, failure was: %s") % (bug_url, e) |
| 194 | raise testtools.TestCase.skipException(msg) |
| 195 | else: |
| 196 | raise e |
| 197 | return inner |
| 198 | return decor |