Merge "Move the `related_bug` decorator from test.py to tempest/lib"
diff --git a/releasenotes/notes/move-related_bug-decorator-to-lib-dbfd5c543bbb2805.yaml b/releasenotes/notes/move-related_bug-decorator-to-lib-dbfd5c543bbb2805.yaml
new file mode 100644
index 0000000..8c420c8
--- /dev/null
+++ b/releasenotes/notes/move-related_bug-decorator-to-lib-dbfd5c543bbb2805.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    A new ``related_bug`` decorator has been added to
+    ``tempest.lib.decorators``. Use it to decorate and tag a test that was
+    added in relation to a launchpad bug report.
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index 4360586..aff61bf 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -18,7 +18,6 @@
 from tempest.common import waiters
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
-from tempest import test
 
 
 class ServersAdminTestJSON(base.BaseV2ComputeAdminTest):
@@ -93,7 +92,7 @@
         self.assertIn(self.s1_name, servers_name)
         self.assertIn(self.s2_name, servers_name)
 
-    @test.related_bug('1659811')
+    @decorators.related_bug('1659811')
     @decorators.idempotent_id('7e5d6b8f-454a-4ba1-8ae2-da857af8338b')
     def test_list_servers_by_admin_with_specified_tenant(self):
         # In nova v2, tenant_id is ignored unless all_tenants is specified
diff --git a/tempest/api/compute/admin/test_volumes_negative.py b/tempest/api/compute/admin/test_volumes_negative.py
index 06b0893..7ebd074 100644
--- a/tempest/api/compute/admin/test_volumes_negative.py
+++ b/tempest/api/compute/admin/test_volumes_negative.py
@@ -46,7 +46,7 @@
                           self.server['id'], nonexistent_volume,
                           volumeId=volume['id'])
 
-    @test.related_bug('1629110', status_code=400)
+    @decorators.related_bug('1629110', status_code=400)
     @test.attr(type=['negative'])
     @decorators.idempotent_id('7dcac15a-b107-46d3-a5f6-cb863f4e454a')
     def test_update_attached_volume_with_nonexistent_volume_in_body(self):
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index c6b3b40..40a4289 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -178,7 +178,7 @@
                           self.client.rebuild_server,
                           server['id'], self.image_ref)
 
-    @test.related_bug('1660878', status_code=409)
+    @decorators.related_bug('1660878', status_code=409)
     @test.attr(type=['negative'])
     @decorators.idempotent_id('581a397d-5eab-486f-9cf9-1014bbd4c984')
     def test_reboot_deleted_server(self):
@@ -219,7 +219,7 @@
                           name=server_name)
 
     @test.attr(type=['negative'])
-    @test.related_bug('1651064', status_code=500)
+    @decorators.related_bug('1651064', status_code=500)
     @decorators.idempotent_id('12146ac1-d7df-4928-ad25-b1f99e5286cd')
     def test_create_server_invalid_bdm_in_2nd_dict(self):
         volume = self.create_volume()
diff --git a/tempest/api/compute/volumes/test_attach_volume_negative.py b/tempest/api/compute/volumes/test_attach_volume_negative.py
index c017690..c178a87 100644
--- a/tempest/api/compute/volumes/test_attach_volume_negative.py
+++ b/tempest/api/compute/volumes/test_attach_volume_negative.py
@@ -31,7 +31,7 @@
             raise cls.skipException(skip_msg)
 
     @test.attr(type=['negative'])
-    @test.related_bug('1630783', status_code=500)
+    @decorators.related_bug('1630783', status_code=500)
     @decorators.idempotent_id('a313b5cd-fbd0-49cc-94de-870e99f763c7')
     def test_delete_attached_volume(self):
         server = self.create_test_server(wait_until='ACTIVE')
diff --git a/tempest/lib/decorators.py b/tempest/lib/decorators.py
index 92f9698..c2ee212 100644
--- a/tempest/lib/decorators.py
+++ b/tempest/lib/decorators.py
@@ -16,9 +16,12 @@
 import uuid
 
 import debtcollector.removals
+from oslo_log import log as logging
 import six
 import testtools
 
+LOG = logging.getLogger(__name__)
+
 
 def skip_because(*args, **kwargs):
     """A decorator useful to skip tests hitting known bugs
@@ -45,6 +48,28 @@
     return decorator
 
 
+def related_bug(bug, status_code=None):
+    """A decorator useful to know solutions from launchpad bug reports
+
+    @param bug: The launchpad bug number causing the test
+    @param status_code: The status code related to the bug report
+    """
+    def decorator(f):
+        @functools.wraps(f)
+        def wrapper(self, *func_args, **func_kwargs):
+            try:
+                return f(self, *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:
+                    LOG.error('Hints: This test was made for the bug %s. '
+                              'The failure could be related to '
+                              'https://launchpad.net/bugs/%s', bug, bug)
+                raise exc
+        return wrapper
+    return decorator
+
+
 def idempotent_id(id):
     """Stub for metadata decorator"""
     if not isinstance(id, six.string_types):
diff --git a/tempest/test.py b/tempest/test.py
index 4eecbd6..052033d 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -45,6 +45,11 @@
     version='Mitaka', removal_version='?')
 
 
+related_bug = debtcollector.moves.moved_function(
+    decorators.related_bug, 'related_bug', __name__,
+    version='Pike', removal_version='?')
+
+
 def attr(**kwargs):
     """A decorator which applies the testtools attr decorator
 
@@ -143,28 +148,6 @@
     return False
 
 
-def related_bug(bug, status_code=None):
-    """A decorator useful to know solutions from launchpad bug reports
-
-    @param bug: The launchpad bug number causing the test
-    @param status_code: The status code related to the bug report
-    """
-    def decorator(f):
-        @functools.wraps(f)
-        def wrapper(self, *func_args, **func_kwargs):
-            try:
-                return f(self, *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:
-                    LOG.error('Hints: This test was made for the bug %s. '
-                              'The failure could be related to '
-                              'https://launchpad.net/bugs/%s', bug, bug)
-                raise exc
-        return wrapper
-    return decorator
-
-
 def is_scheduler_filter_enabled(filter_name):
     """Check the list of enabled compute scheduler filters from config.
 
diff --git a/tempest/tests/lib/test_decorators.py b/tempest/tests/lib/test_decorators.py
index f3a4e9c..ea38e08 100644
--- a/tempest/tests/lib/test_decorators.py
+++ b/tempest/tests/lib/test_decorators.py
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import mock
 import testtools
 
 from tempest.lib import base as test
@@ -123,3 +124,33 @@
 
     def test_no_skip_for_attr_exist_and_true(self):
         self._test_skip_unless_attr('expected_attr', expected_to_skip=False)
+
+
+class TestRelatedBugDecorator(base.TestCase):
+    def test_relatedbug_when_no_exception(self):
+        f = mock.Mock()
+        sentinel = object()
+
+        @decorators.related_bug(bug="1234", status_code=500)
+        def test_foo(self):
+            f(self)
+
+        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 f(self):
+            raise MyException(status_code=500)
+
+        @decorators.related_bug(bug="1234", status_code=500)
+        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', '1234')