Merge "Skip verifying empty devices in test_tagged_attachment until bug is fixed"
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/api/compute/servers/test_novnc.py b/tempest/api/compute/servers/test_novnc.py
index 1dfd0f9..3e44b56 100644
--- a/tempest/api/compute/servers/test_novnc.py
+++ b/tempest/api/compute/servers/test_novnc.py
@@ -58,6 +58,9 @@
def resource_setup(cls):
super(NoVNCConsoleTestJSON, cls).resource_setup()
cls.server = cls.create_test_server(wait_until="ACTIVE")
+ cls.use_get_remote_console = False
+ if not cls.is_requested_microversion_compatible('2.5'):
+ cls.use_get_remote_console = True
def _validate_novnc_html(self, vnc_url):
"""Verify we can connect to novnc and get back the javascript."""
@@ -170,8 +173,13 @@
@decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
def test_novnc(self):
- body = self.client.get_vnc_console(self.server['id'],
- type='novnc')['console']
+ if self.use_get_remote_console:
+ body = self.client.get_remote_console(
+ self.server['id'], console_type='novnc',
+ protocol='vnc')['remote_console']
+ else:
+ body = self.client.get_vnc_console(self.server['id'],
+ type='novnc')['console']
self.assertEqual('novnc', body['type'])
# Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
self._validate_novnc_html(body['url'])
@@ -184,8 +192,13 @@
@decorators.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
def test_novnc_bad_token(self):
- body = self.client.get_vnc_console(self.server['id'],
- type='novnc')['console']
+ if self.use_get_remote_console:
+ body = self.client.get_remote_console(
+ self.server['id'], console_type='novnc',
+ protocol='vnc')['remote_console']
+ else:
+ body = self.client.get_vnc_console(self.server['id'],
+ type='novnc')['console']
self.assertEqual('novnc', body['type'])
# Do the WebSockify HTTP Request to novncproxy with a bad token
url = body['url'].replace('token=', 'token=bad')
diff --git a/tempest/api/compute/servers/test_server_actions.py b/tempest/api/compute/servers/test_server_actions.py
index 350e8ba..961b2b7 100644
--- a/tempest/api/compute/servers/test_server_actions.py
+++ b/tempest/api/compute/servers/test_server_actions.py
@@ -527,10 +527,10 @@
def _get_output(self):
output = self.client.get_console_output(
- self.server_id, length=10)['output']
+ self.server_id, length=3)['output']
self.assertTrue(output, "Console output was empty.")
lines = len(output.split('\n'))
- self.assertEqual(lines, 10)
+ self.assertEqual(lines, 3)
@decorators.idempotent_id('4b8867e6-fffa-4d54-b1d1-6fdda57be2f3')
@testtools.skipUnless(CONF.compute_feature_enabled.console_output,
@@ -561,8 +561,8 @@
# NOTE: This test tries to get full length console log, and the
# length should be bigger than the one of test_get_console_output.
- self.assertGreater(lines, 10, "Cannot get enough console log "
- "length. (lines: %s)" % lines)
+ self.assertGreater(lines, 3, "Cannot get enough console log "
+ "length. (lines: %s)" % lines)
self.wait_for(_check_full_length_console_log)
@@ -690,8 +690,13 @@
# Get the VNC console of type 'novnc' and 'xvpvnc'
console_types = ['novnc', 'xvpvnc']
for console_type in console_types:
- body = self.client.get_vnc_console(self.server_id,
- type=console_type)['console']
+ if self.is_requested_microversion_compatible('2.5'):
+ body = self.client.get_vnc_console(
+ self.server_id, type=console_type)['console']
+ else:
+ body = self.client.get_remote_console(
+ self.server_id, console_type=console_type,
+ protocol='vnc')['remote_console']
self.assertEqual(console_type, body['type'])
self.assertNotEqual('', body['url'])
self._validate_url(body['url'])
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/services/volume/v2/test_volumes_client.py b/tempest/tests/lib/services/volume/v2/test_volumes_client.py
deleted file mode 100644
index d7b042e..0000000
--- a/tempest/tests/lib/services/volume/v2/test_volumes_client.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# Copyright 2017 FiberHome Telecommunication Technologies CO.,LTD
-# All Rights Reserved.
-#
-# 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.
-
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.services.volume.v2 import volumes_client
-from tempest.tests.lib import fake_auth_provider
-from tempest.tests.lib.services import base
-
-
-class TestVolumesClient(base.BaseServiceTest):
-
- FAKE_VOLUME_METADATA_ITEM = {
- "meta": {
- "key1": "value1"
- }
- }
-
- FAKE_VOLUME_IMAGE_METADATA = {
- "metadata": {
- "container_format": "bare",
- "min_ram": "0",
- "disk_format": "raw",
- "image_name": "xly-ubuntu16-server",
- "image_id": "3e087b0c-10c5-4255-b147-6e8e9dbad6fc",
- "checksum": "008f5d22fe3cb825d714da79607a90f9",
- "min_disk": "0",
- "size": "8589934592"
- }
- }
-
- def setUp(self):
- super(TestVolumesClient, self).setUp()
- fake_auth = fake_auth_provider.FakeAuthProvider()
- self.client = volumes_client.VolumesClient(fake_auth,
- 'volume',
- 'regionOne')
-
- def _test_retype_volume(self, bytes_body=False):
- kwargs = {
- "new_type": "dedup-tier-replication",
- "migration_policy": "never"
- }
-
- self.check_service_client_function(
- self.client.retype_volume,
- 'tempest.lib.common.rest_client.RestClient.post',
- {},
- to_utf=bytes_body,
- status=202,
- volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
- **kwargs
- )
-
- def _test_force_detach_volume(self, bytes_body=False):
- kwargs = {
- 'attachment_id': '6980e295-920f-412e-b189-05c50d605acd',
- 'connector': {
- 'initiator': 'iqn.2017-04.org.fake:01'
- }
- }
-
- self.check_service_client_function(
- self.client.force_detach_volume,
- 'tempest.lib.common.rest_client.RestClient.post',
- {},
- to_utf=bytes_body,
- status=202,
- volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
- **kwargs
- )
-
- def _test_show_volume_metadata_item(self, bytes_body=False):
- self.check_service_client_function(
- self.client.show_volume_metadata_item,
- 'tempest.lib.common.rest_client.RestClient.get',
- self.FAKE_VOLUME_METADATA_ITEM,
- to_utf=bytes_body,
- volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
- id="key1")
-
- def _test_show_volume_image_metadata(self, bytes_body=False):
- fake_volume_id = "a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8"
- self.check_service_client_function(
- self.client.show_volume_image_metadata,
- 'tempest.lib.common.rest_client.RestClient.post',
- self.FAKE_VOLUME_IMAGE_METADATA,
- to_utf=bytes_body,
- mock_args=['volumes/%s/action' % fake_volume_id,
- json.dumps({"os-show_image_metadata": {}})],
- volume_id=fake_volume_id)
-
- def test_force_detach_volume_with_str_body(self):
- self._test_force_detach_volume()
-
- def test_force_detach_volume_with_bytes_body(self):
- self._test_force_detach_volume(bytes_body=True)
-
- def test_show_volume_metadata_item_with_str_body(self):
- self._test_show_volume_metadata_item()
-
- def test_show_volume_metadata_item_with_bytes_body(self):
- self._test_show_volume_metadata_item(bytes_body=True)
-
- def test_show_volume_image_metadata_with_str_body(self):
- self._test_show_volume_image_metadata()
-
- def test_show_volume_image_metadata_with_bytes_body(self):
- self._test_show_volume_image_metadata(bytes_body=True)
-
- def test_retype_volume_with_str_body(self):
- self._test_retype_volume()
-
- def test_retype_volume_with_bytes_body(self):
- self._test_retype_volume(bytes_body=True)
diff --git a/tempest/tests/lib/services/volume/v3/test_volumes_client.py b/tempest/tests/lib/services/volume/v3/test_volumes_client.py
index a515fd3..1250536 100644
--- a/tempest/tests/lib/services/volume/v3/test_volumes_client.py
+++ b/tempest/tests/lib/services/volume/v3/test_volumes_client.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from oslo_serialization import jsonutils as json
+
from tempest.lib.services.volume.v3 import volumes_client
from tempest.tests.lib import fake_auth_provider
from tempest.tests.lib.services import base
@@ -27,6 +29,25 @@
}
}
+ FAKE_VOLUME_METADATA_ITEM = {
+ "meta": {
+ "key1": "value1"
+ }
+ }
+
+ FAKE_VOLUME_IMAGE_METADATA = {
+ "metadata": {
+ "container_format": "bare",
+ "min_ram": "0",
+ "disk_format": "raw",
+ "image_name": "xly-ubuntu16-server",
+ "image_id": "3e087b0c-10c5-4255-b147-6e8e9dbad6fc",
+ "checksum": "008f5d22fe3cb825d714da79607a90f9",
+ "min_disk": "0",
+ "size": "8589934592"
+ }
+ }
+
def setUp(self):
super(TestVolumesClient, self).setUp()
fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -34,6 +55,60 @@
'volume',
'regionOne')
+ def _test_retype_volume(self, bytes_body=False):
+ kwargs = {
+ "new_type": "dedup-tier-replication",
+ "migration_policy": "never"
+ }
+
+ self.check_service_client_function(
+ self.client.retype_volume,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ to_utf=bytes_body,
+ status=202,
+ volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+ **kwargs
+ )
+
+ def _test_force_detach_volume(self, bytes_body=False):
+ kwargs = {
+ 'attachment_id': '6980e295-920f-412e-b189-05c50d605acd',
+ 'connector': {
+ 'initiator': 'iqn.2017-04.org.fake:01'
+ }
+ }
+
+ self.check_service_client_function(
+ self.client.force_detach_volume,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ {},
+ to_utf=bytes_body,
+ status=202,
+ volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+ **kwargs
+ )
+
+ def _test_show_volume_metadata_item(self, bytes_body=False):
+ self.check_service_client_function(
+ self.client.show_volume_metadata_item,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_VOLUME_METADATA_ITEM,
+ to_utf=bytes_body,
+ volume_id="a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8",
+ id="key1")
+
+ def _test_show_volume_image_metadata(self, bytes_body=False):
+ fake_volume_id = "a3be971b-8de5-4bdf-bdb8-3d8eb0fb69f8"
+ self.check_service_client_function(
+ self.client.show_volume_image_metadata,
+ 'tempest.lib.common.rest_client.RestClient.post',
+ self.FAKE_VOLUME_IMAGE_METADATA,
+ to_utf=bytes_body,
+ mock_args=['volumes/%s/action' % fake_volume_id,
+ json.dumps({"os-show_image_metadata": {}})],
+ volume_id=fake_volume_id)
+
def _test_show_volume_summary(self, bytes_body=False):
self.check_service_client_function(
self.client.show_volume_summary,
@@ -41,6 +116,30 @@
self.FAKE_VOLUME_SUMMARY,
bytes_body)
+ def test_force_detach_volume_with_str_body(self):
+ self._test_force_detach_volume()
+
+ def test_force_detach_volume_with_bytes_body(self):
+ self._test_force_detach_volume(bytes_body=True)
+
+ def test_show_volume_metadata_item_with_str_body(self):
+ self._test_show_volume_metadata_item()
+
+ def test_show_volume_metadata_item_with_bytes_body(self):
+ self._test_show_volume_metadata_item(bytes_body=True)
+
+ def test_show_volume_image_metadata_with_str_body(self):
+ self._test_show_volume_image_metadata()
+
+ def test_show_volume_image_metadata_with_bytes_body(self):
+ self._test_show_volume_image_metadata(bytes_body=True)
+
+ def test_retype_volume_with_str_body(self):
+ self._test_retype_volume()
+
+ def test_retype_volume_with_bytes_body(self):
+ self._test_retype_volume(bytes_body=True)
+
def test_show_volume_summary_with_str_body(self):
self._test_show_volume_summary()
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())