94d0aa
diff -up nose-1.3.7/AUTHORS.unicode nose-1.3.7/AUTHORS
94d0aa
diff -up nose-1.3.7/CHANGELOG.unicode nose-1.3.7/CHANGELOG
94d0aa
diff -up nose-1.3.7/nose/plugins/capture.py.unicode nose-1.3.7/nose/plugins/capture.py
94d0aa
--- nose-1.3.7/nose/plugins/capture.py.unicode	2015-04-04 02:52:52.000000000 -0600
94d0aa
+++ nose-1.3.7/nose/plugins/capture.py	2016-11-15 13:58:18.713025335 -0700
94d0aa
@@ -12,6 +12,7 @@ the options ``-s`` or ``--nocapture``.
94d0aa
 import logging
94d0aa
 import os
94d0aa
 import sys
94d0aa
+import traceback
94d0aa
 from nose.plugins.base import Plugin
94d0aa
 from nose.pyversion import exc_to_unicode, force_unicode
94d0aa
 from nose.util import ln
94d0aa
@@ -71,26 +72,56 @@ class Capture(Plugin):
94d0aa
     def formatError(self, test, err):
94d0aa
         """Add captured output to error report.
94d0aa
         """
94d0aa
-        test.capturedOutput = output = self.buffer
94d0aa
+        test.capturedOutput = output = ''
94d0aa
+        output_exc_info = None
94d0aa
+        try:
94d0aa
+            test.capturedOutput = output = self.buffer
94d0aa
+        except UnicodeError:
94d0aa
+            # python2's StringIO.StringIO [1] class has this warning:
94d0aa
+            #
94d0aa
+            #     The StringIO object can accept either Unicode or 8-bit strings,
94d0aa
+            #     but mixing the two may take some care. If both are used, 8-bit
94d0aa
+            #     strings that cannot be interpreted as 7-bit ASCII (that use the
94d0aa
+            #     8th bit) will cause a UnicodeError to be raised when getvalue()
94d0aa
+            #     is called.
94d0aa
+            #
94d0aa
+            # This exception handler is a protection against issue #816 [2].
94d0aa
+            # Capturing the exception info allows us to display it back to the
94d0aa
+            # user.
94d0aa
+            #
94d0aa
+            # [1] <https://github.com/python/cpython/blob/2.7/Lib/StringIO.py#L258>
94d0aa
+            # [2] <https://github.com/nose-devs/nose/issues/816>
94d0aa
+            output_exc_info = sys.exc_info()
94d0aa
         self._buf = None
94d0aa
-        if not output:
94d0aa
+        if (not output) and (not output_exc_info):
94d0aa
             # Don't return None as that will prevent other
94d0aa
             # formatters from formatting and remove earlier formatters
94d0aa
             # formats, instead return the err we got
94d0aa
             return err
94d0aa
         ec, ev, tb = err
94d0aa
-        return (ec, self.addCaptureToErr(ev, output), tb)
94d0aa
+        return (ec, self.addCaptureToErr(ev, output, output_exc_info=output_exc_info), tb)
94d0aa
 
94d0aa
     def formatFailure(self, test, err):
94d0aa
         """Add captured output to failure report.
94d0aa
         """
94d0aa
         return self.formatError(test, err)
94d0aa
 
94d0aa
-    def addCaptureToErr(self, ev, output):
94d0aa
+    def addCaptureToErr(self, ev, output, output_exc_info=None):
94d0aa
+        # If given, output_exc_info should be a 3-tuple from sys.exc_info(),
94d0aa
+        # from an exception raised while trying to get the captured output.
94d0aa
         ev = exc_to_unicode(ev)
94d0aa
         output = force_unicode(output)
94d0aa
-        return u'\n'.join([ev, ln(u'>> begin captured stdout <<'),
94d0aa
-                           output, ln(u'>> end captured stdout <<')])
94d0aa
+        error_text = [ev, ln(u'>> begin captured stdout <<'),
94d0aa
+                      output, ln(u'>> end captured stdout <<')]
94d0aa
+        if output_exc_info:
94d0aa
+            error_text.extend([u'OUTPUT ERROR: Could not get captured output.',
94d0aa
+                               # <https://github.com/python/cpython/blob/2.7/Lib/StringIO.py#L258>
94d0aa
+                               # <https://github.com/nose-devs/nose/issues/816>
94d0aa
+                               u"The test might've printed both 'unicode' strings and non-ASCII 8-bit 'str' strings.",
94d0aa
+                               ln(u'>> begin captured stdout exception traceback <<'),
94d0aa
+                               u''.join(traceback.format_exception(*output_exc_info)),
94d0aa
+                               ln(u'>> end captured stdout exception traceback <<')])
94d0aa
+        return u'\n'.join(error_text)
94d0aa
 
94d0aa
     def start(self):
94d0aa
         self.stdout.append(sys.stdout)
94d0aa
diff -up nose-1.3.7/unit_tests/test_capture_plugin.py.unicode nose-1.3.7/unit_tests/test_capture_plugin.py
94d0aa
--- nose-1.3.7/unit_tests/test_capture_plugin.py.unicode	2012-09-29 02:18:54.000000000 -0600
94d0aa
+++ nose-1.3.7/unit_tests/test_capture_plugin.py	2016-11-15 13:58:18.714025330 -0700
94d0aa
@@ -4,6 +4,12 @@ import unittest
94d0aa
 from optparse import OptionParser
94d0aa
 from nose.config import Config
94d0aa
 from nose.plugins.capture import Capture
94d0aa
+from nose.pyversion import force_unicode
94d0aa
+
94d0aa
+if sys.version_info[0] == 2:
94d0aa
+    py2 = True
94d0aa
+else:
94d0aa
+    py2 = False
94d0aa
 
94d0aa
 class TestCapturePlugin(unittest.TestCase):
94d0aa
 
94d0aa
@@ -62,6 +68,35 @@ class TestCapturePlugin(unittest.TestCas
94d0aa
         c.end()
94d0aa
         self.assertEqual(c.buffer, "test 日本\n")
94d0aa
 
94d0aa
+    def test_does_not_crash_with_mixed_unicode_and_nonascii_str(self):
94d0aa
+        class Dummy:
94d0aa
+            pass
94d0aa
+        d = Dummy()
94d0aa
+        c = Capture()
94d0aa
+        c.start()
94d0aa
+        printed_nonascii_str = force_unicode("test 日本").encode('utf-8')
94d0aa
+        printed_unicode = force_unicode("Hello")
94d0aa
+        print printed_nonascii_str
94d0aa
+        print printed_unicode
94d0aa
+        try:
94d0aa
+            raise Exception("boom")
94d0aa
+        except:
94d0aa
+            err = sys.exc_info()
94d0aa
+        formatted = c.formatError(d, err)
94d0aa
+        _, fev, _ = formatted
94d0aa
+
94d0aa
+        if py2:
94d0aa
+            for string in [force_unicode(printed_nonascii_str, encoding='utf-8'), printed_unicode]:
94d0aa
+                assert string not in fev, "Output unexpectedly found in error message"
94d0aa
+            assert d.capturedOutput == '', "capturedOutput unexpectedly non-empty"
94d0aa
+            assert "OUTPUT ERROR" in fev
94d0aa
+            assert "captured stdout exception traceback" in fev
94d0aa
+            assert "UnicodeDecodeError" in fev
94d0aa
+        else:
94d0aa
+            for string in [repr(printed_nonascii_str), printed_unicode]:
94d0aa
+                assert string in fev, "Output not found in error message"
94d0aa
+                assert string in d.capturedOutput, "Output not attached to test"
94d0aa
+
94d0aa
     def test_format_error(self):
94d0aa
         class Dummy:
94d0aa
             pass