Merge "Added storyboard integration to tempest.lib decorators"
diff --git a/releasenotes/notes/add-storyboard-in-skip-because-decorator-3e139aa8a4f7970f.yaml b/releasenotes/notes/add-storyboard-in-skip-because-decorator-3e139aa8a4f7970f.yaml
new file mode 100644
index 0000000..dd4a90b
--- /dev/null
+++ b/releasenotes/notes/add-storyboard-in-skip-because-decorator-3e139aa8a4f7970f.yaml
@@ -0,0 +1,17 @@
+---
+features:
+ - |
+ Add a new parameter called ``bug_type`` to
+ ``tempest.lib.decorators.related_bug`` and
+ ``tempest.lib.decorators.skip_because`` decorators, which accepts
+ 2 values:
+
+ * launchpad
+ * storyboard
+
+ This offers the possibility of tracking bugs related to tests using
+ launchpad or storyboard references. The default value is launchpad
+ for backward compatibility.
+
+ Passing in a non-digit ``bug`` value to either decorator will raise
+ a ``InvalidParam`` exception (previously ``ValueError``).
diff --git a/tempest/lib/decorators.py b/tempest/lib/decorators.py
index e99dd24..b399aa0 100644
--- a/tempest/lib/decorators.py
+++ b/tempest/lib/decorators.py
@@ -19,39 +19,83 @@
import six
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
- @param bug: bug number causing the test to skip
- @param condition: optional condition to be True for the skip to have place
+ ``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):
skip = False
+ msg = ''
if "condition" in kwargs:
if kwargs["condition"] is True:
skip = True
else:
skip = True
if "bug" in kwargs and skip is True:
- if not kwargs['bug'].isdigit():
- raise ValueError('bug must be a valid bug number')
- msg = "Skipped until Bug: %s is resolved." % kwargs["bug"]
+ bug = kwargs['bug']
+ bug_type = kwargs.get('bug_type', 'launchpad')
+ bug_url = _get_bug_url(bug, bug_type)
+ msg = "Skipped until bug: %s is resolved." % bug_url
raise testtools.TestCase.skipException(msg)
return f(*func_args, **func_kwargs)
return wrapper
return decorator
-def related_bug(bug, status_code=None):
- """A decorator useful to know solutions from launchpad bug reports
+def related_bug(bug, status_code=None, bug_type='launchpad'):
+ """A decorator useful to know solutions from launchpad/storyboard reports
- @param bug: The launchpad bug number causing the test
- @param status_code: The status code related to the bug report
+ :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)
@@ -61,9 +105,10 @@
except Exception as exc:
exc_status_code = getattr(exc, 'status_code', None)
if status_code is None or status_code == exc_status_code:
- LOG.error('Hints: This test was made for the bug %s. '
- 'The failure could be related to '
- 'https://launchpad.net/bugs/%s', bug, bug)
+ 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
diff --git a/tempest/tests/lib/test_decorators.py b/tempest/tests/lib/test_decorators.py
index ed0eea3..0b1a599 100644
--- a/tempest/tests/lib/test_decorators.py
+++ b/tempest/tests/lib/test_decorators.py
@@ -19,6 +19,7 @@
from tempest.lib import base as test
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
from tempest.tests import base
@@ -62,21 +63,40 @@
t = TestFoo('test_bar')
if expected_to_skip:
- self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+ e = self.assertRaises(testtools.TestCase.skipException, t.test_bar)
+ bug = decorator_args['bug']
+ bug_type = decorator_args.get('bug_type', 'launchpad')
+ self.assertRegex(
+ str(e),
+ r'Skipped until bug\: %s.*' % decorators._get_bug_url(
+ bug, bug_type)
+ )
else:
# assert that test_bar returned 0
self.assertEqual(TestFoo('test_bar').test_bar(), 0)
- def test_skip_because_bug(self):
+ def test_skip_because_launchpad_bug(self):
self._test_skip_because_helper(bug='12345')
- def test_skip_because_bug_and_condition_true(self):
+ def test_skip_because_launchpad_bug_and_condition_true(self):
self._test_skip_because_helper(bug='12348', condition=True)
- def test_skip_because_bug_and_condition_false(self):
+ def test_skip_because_launchpad_bug_and_condition_false(self):
self._test_skip_because_helper(expected_to_skip=False,
bug='12349', condition=False)
+ def test_skip_because_storyboard_bug(self):
+ self._test_skip_because_helper(bug='1992', bug_type='storyboard')
+
+ def test_skip_because_storyboard_bug_and_condition_true(self):
+ self._test_skip_because_helper(bug='1992', bug_type='storyboard',
+ condition=True)
+
+ def test_skip_because_storyboard_bug_and_condition_false(self):
+ self._test_skip_because_helper(expected_to_skip=False,
+ bug='1992', bug_type='storyboard',
+ condition=False)
+
def test_skip_because_bug_without_bug_never_skips(self):
"""Never skip without a bug parameter."""
self._test_skip_because_helper(expected_to_skip=False,
@@ -84,8 +104,8 @@
self._test_skip_because_helper(expected_to_skip=False)
def test_skip_because_invalid_bug_number(self):
- """Raise ValueError if with an invalid bug number"""
- self.assertRaises(ValueError, self._test_skip_because_helper,
+ """Raise InvalidParam if with an invalid bug number"""
+ self.assertRaises(lib_exc.InvalidParam, self._test_skip_because_helper,
bug='critical_bug')
@@ -126,6 +146,13 @@
class TestRelatedBugDecorator(base.TestCase):
+
+ def _get_my_exception(self):
+ class MyException(Exception):
+ def __init__(self, status_code):
+ self.status_code = status_code
+ return MyException
+
def test_relatedbug_when_no_exception(self):
f = mock.Mock()
sentinel = object()
@@ -137,10 +164,9 @@
test_foo(sentinel)
f.assert_called_once_with(sentinel)
- def test_relatedbug_when_exception(self):
- class MyException(Exception):
- def __init__(self, status_code):
- self.status_code = status_code
+ def test_relatedbug_when_exception_with_launchpad_bug_type(self):
+ """Validate related_bug decorator with bug_type == 'launchpad'"""
+ MyException = self._get_my_exception()
def f(self):
raise MyException(status_code=500)
@@ -152,4 +178,53 @@
with mock.patch.object(decorators.LOG, 'error') as m_error:
self.assertRaises(MyException, test_foo, object())
- m_error.assert_called_once_with(mock.ANY, '1234', '1234')
+ m_error.assert_called_once_with(
+ mock.ANY, '1234', 'https://launchpad.net/bugs/1234')
+
+ def test_relatedbug_when_exception_with_storyboard_bug_type(self):
+ """Validate related_bug decorator with bug_type == 'storyboard'"""
+ MyException = self._get_my_exception()
+
+ def f(self):
+ raise MyException(status_code=500)
+
+ @decorators.related_bug(bug="1234", status_code=500,
+ bug_type='storyboard')
+ def test_foo(self):
+ f(self)
+
+ with mock.patch.object(decorators.LOG, 'error') as m_error:
+ self.assertRaises(MyException, test_foo, object())
+
+ m_error.assert_called_once_with(
+ mock.ANY, '1234', 'https://storyboard.openstack.org/#!/story/1234')
+
+ def test_relatedbug_when_exception_invalid_bug_type(self):
+ """Check related_bug decorator raises exc when bug_type is not valid"""
+ MyException = self._get_my_exception()
+
+ def f(self):
+ raise MyException(status_code=500)
+
+ @decorators.related_bug(bug="1234", status_code=500,
+ bug_type=mock.sentinel.invalid)
+ def test_foo(self):
+ f(self)
+
+ with mock.patch.object(decorators.LOG, 'error'):
+ self.assertRaises(lib_exc.InvalidParam, test_foo, object())
+
+ def test_relatedbug_when_exception_invalid_bug_number(self):
+ """Check related_bug decorator raises exc when bug_number != digit"""
+ MyException = self._get_my_exception()
+
+ def f(self):
+ raise MyException(status_code=500)
+
+ @decorators.related_bug(bug="not a digit", status_code=500,
+ bug_type='launchpad')
+ def test_foo(self):
+ f(self)
+
+ with mock.patch.object(decorators.LOG, 'error'):
+ self.assertRaises(lib_exc.InvalidParam, test_foo, object())