blob: 808e0fb40cfae51bdf32e07c2bf303e0097bfd79 [file] [log] [blame]
Matthew Treinish9e26ca82016-02-23 11:43:20 -05001# 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
15import functools
16import uuid
17
Jordan Pittierc5665a62017-04-12 16:42:53 +020018from oslo_log import log as logging
Matthew Treinish9e26ca82016-02-23 11:43:20 -050019import six
20import testtools
21
Chandan Kumar7d8c2812018-02-08 14:26:56 +053022from tempest.lib import exceptions as lib_exc
23
Jordan Pittierc5665a62017-04-12 16:42:53 +020024LOG = logging.getLogger(__name__)
25
Chandan Kumar7d8c2812018-02-08 14:26:56 +053026_SUPPORTED_BUG_TYPES = {
27 'launchpad': 'https://launchpad.net/bugs/%s',
28 'storyboard': 'https://storyboard.openstack.org/#!/story/%s',
29}
30
31
32def _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
49def _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 Treinish9e26ca82016-02-23 11:43:20 -050059
60def skip_because(*args, **kwargs):
61 """A decorator useful to skip tests hitting known bugs
62
Chandan Kumar7d8c2812018-02-08 14:26:56 +053063 ``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 Treinish9e26ca82016-02-23 11:43:20 -050071 """
72 def decorator(f):
73 @functools.wraps(f)
lkuchlana2addfe2017-11-13 12:06:34 +020074 def wrapper(*func_args, **func_kwargs):
Matthew Treinish9e26ca82016-02-23 11:43:20 -050075 skip = False
Chandan Kumar7d8c2812018-02-08 14:26:56 +053076 msg = ''
Matthew Treinish9e26ca82016-02-23 11:43:20 -050077 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 Kumar7d8c2812018-02-08 14:26:56 +053083 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 Treinish9e26ca82016-02-23 11:43:20 -050087 raise testtools.TestCase.skipException(msg)
lkuchlana2addfe2017-11-13 12:06:34 +020088 return f(*func_args, **func_kwargs)
Matthew Treinish9e26ca82016-02-23 11:43:20 -050089 return wrapper
90 return decorator
91
92
Chandan Kumar7d8c2812018-02-08 14:26:56 +053093def related_bug(bug, status_code=None, bug_type='launchpad'):
94 """A decorator useful to know solutions from launchpad/storyboard reports
Jordan Pittierc5665a62017-04-12 16:42:53 +020095
Chandan Kumar7d8c2812018-02-08 14:26:56 +053096 :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 Pittierc5665a62017-04-12 16:42:53 +020099 """
100 def decorator(f):
101 @functools.wraps(f)
lkuchlanf5c19052017-11-23 09:26:55 +0200102 def wrapper(*func_args, **func_kwargs):
Jordan Pittierc5665a62017-04-12 16:42:53 +0200103 try:
lkuchlanf5c19052017-11-23 09:26:55 +0200104 return f(*func_args, **func_kwargs)
Jordan Pittierc5665a62017-04-12 16:42:53 +0200105 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 Kumar7d8c2812018-02-08 14:26:56 +0530108 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 Pittierc5665a62017-04-12 16:42:53 +0200112 raise exc
113 return wrapper
114 return decorator
115
116
Matthew Treinish9e26ca82016-02-23 11:43:20 -0500117def 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 Pittier3b46d272017-04-12 16:17:28 +0200134def 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 Riedemann29999632019-02-14 14:32:20 -0500139
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 Pittier3b46d272017-04-12 16:17:28 +0200143 """
144
145 def decorator(f):
Matt Riedemann29999632019-02-14 14:32:20 -0500146 # Check to see if the attr should be conditional applied.
147 if 'condition' in kwargs and not kwargs.get('condition'):
148 return f
Alfredo Moralejo3e7752c2019-03-13 11:12:29 +0100149 if 'type' in kwargs and isinstance(kwargs['type'], six.string_types):
Jordan Pittier3b46d272017-04-12 16:17:28 +0200150 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 Kaplonski21f53012019-01-29 12:52:01 +0100157
158
159def 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