diff --git a/tobiko/__init__.py b/tobiko/__init__.py index 6a98083a6..f9b320c8e 100644 --- a/tobiko/__init__.py +++ b/tobiko/__init__.py @@ -33,6 +33,8 @@ fail = _asserts.fail TobikoException = _exception.TobikoException check_valid_type = _exception.check_valid_type exc_info = _exception.exc_info +handle_multiple_exceptions = _exception.handle_multiple_exceptions +list_exc_infos = _exception.list_exc_infos is_fixture = _fixture.is_fixture get_fixture = _fixture.get_fixture diff --git a/tobiko/common/_exception.py b/tobiko/common/_exception.py index 550dca6e7..c2ef36e2e 100644 --- a/tobiko/common/_exception.py +++ b/tobiko/common/_exception.py @@ -13,11 +13,13 @@ # under the License. from __future__ import absolute_import +import contextlib import collections import sys from oslo_log import log import six +import testtools LOG = log.getLogger(__name__) @@ -107,3 +109,36 @@ def exc_info(reraise=True): info = ExceptionInfo(*sys.exc_info()) info.reraise_on_exit = reraise return info + + +@contextlib.contextmanager +def handle_multiple_exceptions(): + try: + yield + except testtools.MultipleExceptions as exc: + exc_infos = list_exc_infos() + if exc_infos: + for info in exc_infos[1:]: + LOG.exception("Unhandled exception:", exc_info=info) + six.reraise(*exc_infos[0]) + else: + LOG.debug('empty MultipleExceptions: %s', str(exc)) + + +def list_exc_infos(exc_info=None): + # pylint: disable=redefined-outer-name + exc_info = exc_info or sys.exc_info() + result = [] + if exc_info[0]: + visited = set() + visiting = [exc_info] + while visiting: + exc_info = visiting.pop() + _, exc, _ = exc_info + if exc not in visited: + visited.add(exc) + if isinstance(exc, testtools.MultipleExceptions): + visiting.extend(reversed(exc.args)) + else: + result.append(exc_info) + return result diff --git a/tobiko/common/_fixture.py b/tobiko/common/_fixture.py index 641030386..a2a72409b 100644 --- a/tobiko/common/_fixture.py +++ b/tobiko/common/_fixture.py @@ -23,6 +23,7 @@ import testtools import tobiko from tobiko.common import _detail +from tobiko.common import _exception LOG = log.getLogger(__name__) @@ -98,33 +99,24 @@ def remove_fixture(obj, manager=None): def setup_fixture(obj, manager=None): '''Get registered fixture and setup it up''' fixture = get_fixture(obj, manager=manager) - try: + with _exception.handle_multiple_exceptions(): fixture.setUp() - except testtools.MultipleExceptions as ex: - for exc_info in ex.args[1:]: - LOG.exception("Error setting up fixture %r", - fixture.fixture_name, exc_info=exc_info) - six.reraise(*ex.args[0]) return fixture def reset_fixture(obj, manager=None): - '''Get registered fixture and setup it up''' + '''Get registered fixture and reset it''' fixture = get_fixture(obj, manager=manager) - try: + with _exception.handle_multiple_exceptions(): fixture.reset() - except testtools.MultipleExceptions as ex: - for exc_info in ex.args[1:]: - LOG.exception("Error reseting fixture %r", - fixture.fixture_name, exc_info=exc_info) - six.reraise(*ex.args[0]) return fixture def cleanup_fixture(obj, manager=None): '''Get registered fixture and clean it up''' fixture = get_fixture(obj, manager=manager) - fixture.cleanUp() + with _exception.handle_multiple_exceptions(): + fixture.cleanUp() return fixture diff --git a/tobiko/tests/unit/test_exception.py b/tobiko/tests/unit/test_exception.py index 3a86b05c2..7a721d601 100644 --- a/tobiko/tests/unit/test_exception.py +++ b/tobiko/tests/unit/test_exception.py @@ -17,6 +17,7 @@ import sys import tobiko from tobiko.tests import unit +import testtools class SomeException(tobiko.TobikoException): @@ -122,3 +123,95 @@ class TestExcInfo(unit.TobikoUnitTest): reraised = self.assertRaises(RuntimeError, exc_info.reraise) self.assertIs(exc_value, reraised) + + +class TestListExcInfo(unit.TobikoUnitTest): + + def test_list_exc_info(self): + result = tobiko.list_exc_infos() + self.assertEqual([], result) + + def test_list_exc_info_with_info(self): + error = make_exception(RuntimeError, 'error') + result = tobiko.list_exc_infos(exc_info=error) + self.assertEqual([error], result) + + def test_list_exc_info_handling_exception(self): + try: + raise RuntimeError('error') + except RuntimeError: + result = tobiko.list_exc_infos() + self.assertEqual([sys.exc_info()], result) + + def test_list_exc_info_handling_empty_multiple_exceptions(self): + error = make_exception(testtools.MultipleExceptions) + result = tobiko.list_exc_infos(exc_info=error) + self.assertEqual([], result) + + def test_list_exc_info_handling_multiple_exceptions(self): + a = make_exception(RuntimeError, 'a') + b = make_exception(ValueError, 'b') + c = make_exception(TypeError, 'c') + multi = make_exception(testtools.MultipleExceptions, a, b, c) + result = tobiko.list_exc_infos(exc_info=multi) + self.assertEqual([a, b, c], result) + + def test_list_exc_info_handling_nested_multiple_exceptions(self): + a = make_exception(RuntimeError, 'a') + b = make_exception(ValueError, 'b') + c = make_exception(TypeError, 'c') + d = make_exception(IndexError, 'd') + inner = make_exception(testtools.MultipleExceptions, b, c) + multi = make_exception(testtools.MultipleExceptions, a, inner, d) + result = tobiko.list_exc_infos(exc_info=multi) + self.assertEqual([a, b, c, d], result) + + +class TestHandleMultipleExceptions(unit.TobikoUnitTest): + + def test_handle_multiple_exceptions(self): + with tobiko.handle_multiple_exceptions(): + pass + + def test_handle_multiple_exceptions_with_exception(self): + def run(): + with tobiko.handle_multiple_exceptions(): + raise RuntimeError('error') + self.assertRaises(RuntimeError, run) + + def test_handle_multiple_exceptions_with_empty_multiple_exception(self): + with tobiko.handle_multiple_exceptions(): + raise testtools.MultipleExceptions() + + def test_handle_multiple_exceptions_with_multiple_exceptions(self): + a = make_exception(TypeError, 'a') + b = make_exception(ValueError, 'b') + c = make_exception(RuntimeError, 'c') + + def run(): + with tobiko.handle_multiple_exceptions(): + raise testtools.MultipleExceptions(a, b, c) + + ex = self.assertRaises(TypeError, run) + self.assertEqual(a[1], ex) + + def test_handle_multiple_exceptions_with_nested_multiple_exceptions(self): + a = make_exception(RuntimeError, 'a') + b = make_exception(ValueError, 'b') + c = make_exception(TypeError, 'c') + d = make_exception(IndexError, 'd') + inner = make_exception(testtools.MultipleExceptions, b, c) + + def run(): + with tobiko.handle_multiple_exceptions(): + raise testtools.MultipleExceptions(a, inner, d) + + ex = self.assertRaises(RuntimeError, run) + self.assertEqual(a[1], ex) + + +def make_exception(cls, *args, **kwargs): + try: + raise cls(*args, **kwargs) + except cls: + return sys.exc_info()