From ec908db1fc33fc113e8861ae3925064c5410caed Mon Sep 17 00:00:00 2001 From: "Ivan A. Melnikov" Date: Tue, 4 Feb 2014 15:07:16 +0400 Subject: [PATCH] Be really careful with non-ascii data in exceptions/failures When exception message is Python 2 str with non-ascii symbols, six.text_type(exc) raises UnicodeError. This change handles this case gracefully by calling str() instead of string.text_type() on such exceptions when retrieving messages for exceptions/failures. Closes-bug: 1276053 Change-Id: I2eb7318a7a5cd5dd687390a65abc0a45bd47de40 --- taskflow/exceptions.py | 16 +++++++- taskflow/tests/unit/test_utils_failure.py | 47 +++++++++++++++++++++++ taskflow/utils/misc.py | 14 +++---- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/taskflow/exceptions.py b/taskflow/exceptions.py index 698a4465..1fa17675 100644 --- a/taskflow/exceptions.py +++ b/taskflow/exceptions.py @@ -16,6 +16,8 @@ # License for the specific language governing permissions and limitations # under the License. +import six + class TaskFlowException(Exception): """Base class for exceptions emitted from this library.""" @@ -136,4 +138,16 @@ class WrappedFailure(TaskFlowException): return None def __str__(self): - return 'WrappedFailure: %s' % [str(cause) for cause in self._causes] + causes = [exception_message(cause) for cause in self._causes] + return 'WrappedFailure: %s' % causes + + +def exception_message(exc): + """Return the string representation of exception.""" + # NOTE(imelnikov): Dealing with non-ascii data in python is difficult: + # https://bugs.launchpad.net/taskflow/+bug/1275895 + # https://bugs.launchpad.net/taskflow/+bug/1276053 + try: + return six.text_type(exc) + except UnicodeError: + return str(exc) diff --git a/taskflow/tests/unit/test_utils_failure.py b/taskflow/tests/unit/test_utils_failure.py index ef90a150..44cd4b4a 100644 --- a/taskflow/tests/unit/test_utils_failure.py +++ b/taskflow/tests/unit/test_utils_failure.py @@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +import six from taskflow import exceptions from taskflow import test @@ -225,3 +226,49 @@ class WrappedFailureTestCase(test.TestCase): wf = exceptions.WrappedFailure([fail_obj, f3]) self.assertEqual(list(wf), [f1, f2, f3]) + + +class NonAsciiExceptionsTestCase(test.TestCase): + + def test_exception_with_non_ascii_str(self): + bad_string = chr(200) + fail = misc.Failure.from_exception(ValueError(bad_string)) + self.assertEqual(fail.exception_str, bad_string) + self.assertEqual(str(fail), 'Failure: ValueError: %s' % bad_string) + + def test_exception_non_ascii_unicode(self): + hi_ru = u'привет' + fail = misc.Failure.from_exception(ValueError(hi_ru)) + self.assertEqual(fail.exception_str, hi_ru) + self.assertIsInstance(fail.exception_str, six.text_type) + self.assertEqual(six.text_type(fail), + u'Failure: ValueError: %s' % hi_ru) + + def test_wrapped_failure_non_ascii_unicode(self): + hi_cn = u'嗨' + fail = ValueError(hi_cn) + self.assertEqual(hi_cn, exceptions.exception_message(fail)) + fail = misc.Failure.from_exception(fail) + wrapped_fail = exceptions.WrappedFailure([fail]) + if six.PY2: + # Python 2.x will unicode escape it, while python 3.3+ will not, + # so we sadly have to differentiate between these two... + expected_result = (u"WrappedFailure: " + "[u'Failure: ValueError: %s']" + % (hi_cn.encode("unicode-escape"))) + else: + expected_result = (u"WrappedFailure: " + "['Failure: ValueError: %s']" % (hi_cn)) + self.assertEqual(expected_result, six.text_type(wrapped_fail)) + + def test_failure_equality_with_non_ascii_str(self): + bad_string = chr(200) + fail = misc.Failure.from_exception(ValueError(bad_string)) + copied = fail.copy() + self.assertEqual(fail, copied) + + def test_failure_equality_non_ascii_unicode(self): + hi_ru = u'привет' + fail = misc.Failure.from_exception(ValueError(hi_ru)) + copied = fail.copy() + self.assertEqual(fail, copied) diff --git a/taskflow/utils/misc.py b/taskflow/utils/misc.py index e14bc86f..0f50ad4b 100644 --- a/taskflow/utils/misc.py +++ b/taskflow/utils/misc.py @@ -31,7 +31,7 @@ import traceback import six -from taskflow import exceptions +from taskflow import exceptions as exc from taskflow.openstack.common import jsonutils from taskflow.utils import reflection @@ -136,8 +136,8 @@ def item_from(container, index, name=None): # unsubscriptable type is being requested (type error). if name is None: name = index - raise exceptions.NotFound("Unable to find %r in container %s" - % (name, container)) + raise exc.NotFound("Unable to find %r in container %s" + % (name, container)) def get_duplicate_keys(iterable, key=None): @@ -444,7 +444,7 @@ def are_equal_exc_info_tuples(ei1, ei2): if ei1[0] is not ei2[0]: return False if not all((type(ei1[1]) == type(ei2[1]), - six.text_type(ei1[1]) == six.text_type(ei2[1]), + exc.exception_message(ei1[1]) == exc.exception_message(ei2[1]), repr(ei1[1]) == repr(ei2[1]))): return False if ei1[2] == ei2[2]: @@ -470,7 +470,7 @@ class Failure(object): reflection.get_all_class_names(exc_info[0], up_to=Exception)) if not self._exc_type_names: raise TypeError('Invalid exception type: %r' % exc_info[0]) - self._exception_str = six.text_type(self._exc_info[1]) + self._exception_str = exc.exception_message(self._exc_info[1]) self._traceback_str = ''.join( traceback.format_tb(self._exc_info[2])) else: @@ -557,14 +557,14 @@ class Failure(object): if len(failures) == 1: failures[0].reraise() elif len(failures) > 1: - raise exceptions.WrappedFailure(failures) + raise exc.WrappedFailure(failures) def reraise(self): """Re-raise captured exception.""" if self._exc_info: six.reraise(*self._exc_info) else: - raise exceptions.WrappedFailure([self]) + raise exc.WrappedFailure([self]) def check(self, *exc_classes): """Check if any of exc_classes caused the failure.