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
This commit is contained in:
Ivan A. Melnikov
2014-02-04 15:07:16 +04:00
committed by Joshua Harlow
parent de0398c457
commit ec908db1fc
3 changed files with 69 additions and 8 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.