Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent crashing from UnicodeDecodeError #288

Merged
merged 1 commit into from
Mar 23, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions nose2/plugins/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""

import sys
import traceback

from six import StringIO

Expand Down Expand Up @@ -97,13 +98,39 @@ def outcomeDetail(self, event):
"""Add buffered output to event.extraDetail"""
for stream in ('stdout', 'stderr'):
if stream in event.outcomeEvent.metadata:
buf = event.outcomeEvent.metadata[stream].getvalue()
if not buf:
buf = ''
stream_buffer_exc_info = None
try:
buf = event.outcomeEvent.metadata[stream].getvalue()
except UnicodeError:
# python2's StringIO.StringIO [1] class has this warning:
#
# The StringIO object can accept either Unicode or 8-bit strings,
# but mixing the two may take some care. If both are used, 8-bit
# strings that cannot be interpreted as 7-bit ASCII (that use the
# 8th bit) will cause a UnicodeError to be raised when getvalue()
# is called.
#
# This exception handler is a protection against crashes
# caused by this exception (such as [2] in the original
# nose application). Capturing the exception info allows us
# to display it back to the user.
#
# [1] <https://github.com/python/cpython/blob/2.7/Lib/StringIO.py#L258>
# [2] <https://github.com/nose-devs/nose/issues/816>
stream_buffer_exc_info = sys.exc_info()
if (not buf) and (not stream_buffer_exc_info):
continue
event.extraDetail.append(
ln('>> begin captured %s <<' % stream))
event.extraDetail.append(buf)
event.extraDetail.append(ln('>> end captured %s <<' % stream))
if stream_buffer_exc_info:
event.extraDetail.append('OUTPUT ERROR: Could not get captured %s output.' % stream)
event.extraDetail.append('The test may have output both non-ASCII Unicode strings and non-ASCII 8-bit strings.')
event.extraDetail.append(ln('>> begin captured %s exception traceback <<' % stream))
event.extraDetail.append(''.join(traceback.format_exception(*stream_buffer_exc_info)))
event.extraDetail.append(ln('>> end captured %s exception traceback <<' % stream))

def beforeInteraction(self, event):
"""Stop buffering so users can see stdout"""
Expand Down
28 changes: 27 additions & 1 deletion nose2/tests/unit/test_buffer_plugin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-

import sys

import six

from nose2.plugins import buffer
from nose2 import events, result, session
from nose2 import events, result, session, util
from nose2.tests._common import TestCase


Expand All @@ -18,12 +20,22 @@ def setUp(self):

class Test(TestCase):

unicode_string = util.safe_decode("test 日本")

def test_out(self):
six.print_("hello")
raise {}["oops"]

def test_err(self):
six.print_("goodbye", file=sys.stderr)

def test_nonascii_mixed_unicode_and_str(self):
six.print_(self.unicode_string)
six.print_(self.unicode_string.encode('utf-8'))
six.print_(self.unicode_string, file=sys.stderr)
six.print_(self.unicode_string.encode('utf-8'), file=sys.stderr)
raise {}["oops"]

self.case = Test

class Watcher(events.Plugin):
Expand Down Expand Up @@ -63,6 +75,20 @@ def test_captures_stderr_when_configured(self):
finally:
sys.stderr = err

def test_does_not_crash_with_nonascii_mixed_unicode_and_str(self):
self.plugin.captureStderr = True
test = self.case('test_nonascii_mixed_unicode_and_str')
test(self.result)
evt = events.OutcomeDetailEvent(self.watcher.events[0])
self.session.hooks.outcomeDetail(evt)
extraDetail = "".join(evt.extraDetail)
if six.PY2:
assert self.case.unicode_string not in extraDetail, "Output unexpectedly found in error message"
assert "OUTPUT ERROR" in extraDetail
assert "UnicodeDecodeError" in extraDetail
else:
assert self.case.unicode_string in extraDetail, "Output not found in error message"

def test_decorates_outcome_detail(self):
test = self.case('test_out')
test(self.result)
Expand Down