diff options
Diffstat (limited to 'Lib/test/test_traceback.py')
-rw-r--r-- | Lib/test/test_traceback.py | 352 |
1 files changed, 242 insertions, 110 deletions
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 8076be8a785..4752d37e5b3 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1,26 +1,27 @@ """Test cases for traceback module""" -from _testcapi import traceback_print -from StringIO import StringIO +from _testcapi import traceback_print, exception_print +from io import StringIO import sys import unittest -from imp import reload -from test.test_support import run_unittest, is_jython, Error +import re +from test.support import run_unittest, Error, captured_output +from test.support import TESTFN, unlink import traceback -class TracebackCases(unittest.TestCase): +class SyntaxTracebackCases(unittest.TestCase): # For now, a very minimal set of tests. I want to be sure that # formatting of SyntaxErrors works based on changes for 2.1. def get_exception_format(self, func, exc): try: func() - except exc, value: + except exc as value: return traceback.format_exception_only(exc, value) else: - raise ValueError, "call did not raise exception" + raise ValueError("call did not raise exception") def syntax_error_with_caret(self): compile("def fact(x):\n\treturn x!\n", "?", "exec") @@ -28,20 +29,16 @@ class TracebackCases(unittest.TestCase): def syntax_error_with_caret_2(self): compile("1 +\n", "?", "exec") - def syntax_error_without_caret(self): - # XXX why doesn't compile raise the same traceback? - import test.badsyntax_nocaret - def syntax_error_bad_indentation(self): - compile("def spam():\n print 1\n print 2", "?", "exec") + compile("def spam():\n print(1)\n print(2)", "?", "exec") def test_caret(self): err = self.get_exception_format(self.syntax_error_with_caret, SyntaxError) - self.assertTrue(len(err) == 4) + self.assertEqual(len(err), 4) self.assertTrue(err[1].strip() == "return x!") self.assertIn("^", err[2]) # third line has caret - self.assertTrue(err[1].find("!") == err[2].find("^")) # in the right place + self.assertEqual(err[1].find("!"), err[2].find("^")) # in the right place err = self.get_exception_format(self.syntax_error_with_caret_2, SyntaxError) @@ -50,66 +47,18 @@ class TracebackCases(unittest.TestCase): self.assertTrue(err[1].find("+") == err[2].find("^")) # in the right place def test_nocaret(self): - if is_jython: - # jython adds a caret in this case (why shouldn't it?) - return - err = self.get_exception_format(self.syntax_error_without_caret, - SyntaxError) - self.assertTrue(len(err) == 3) - self.assertTrue(err[1].strip() == "[x for x in x] = x") + exc = SyntaxError("error", ("x.py", 23, None, "bad syntax")) + err = traceback.format_exception_only(SyntaxError, exc) + self.assertEqual(len(err), 3) + self.assertEqual(err[1].strip(), "bad syntax") def test_bad_indentation(self): err = self.get_exception_format(self.syntax_error_bad_indentation, IndentationError) - self.assertTrue(len(err) == 4) - self.assertTrue(err[1].strip() == "print 2") + self.assertEqual(len(err), 4) + self.assertEqual(err[1].strip(), "print(2)") self.assertIn("^", err[2]) - self.assertTrue(err[1].find("2") == err[2].find("^")) - - def test_bug737473(self): - import os, tempfile, time - - savedpath = sys.path[:] - testdir = tempfile.mkdtemp() - try: - sys.path.insert(0, testdir) - testfile = os.path.join(testdir, 'test_bug737473.py') - print >> open(testfile, 'w'), """ -def test(): - raise ValueError""" - - if 'test_bug737473' in sys.modules: - del sys.modules['test_bug737473'] - import test_bug737473 - - try: - test_bug737473.test() - except ValueError: - # this loads source code to linecache - traceback.extract_tb(sys.exc_traceback) - - # If this test runs too quickly, test_bug737473.py's mtime - # attribute will remain unchanged even if the file is rewritten. - # Consequently, the file would not reload. So, added a sleep() - # delay to assure that a new, distinct timestamp is written. - # Since WinME with FAT32 has multisecond resolution, more than - # three seconds are needed for this test to pass reliably :-( - time.sleep(4) - - print >> open(testfile, 'w'), """ -def test(): - raise NotImplementedError""" - reload(test_bug737473) - try: - test_bug737473.test() - except NotImplementedError: - src = traceback.extract_tb(sys.exc_traceback)[-1][-1] - self.assertEqual(src, 'raise NotImplementedError') - finally: - sys.path[:] = savedpath - for f in os.listdir(testdir): - os.unlink(os.path.join(testdir, f)) - os.rmdir(testdir) + self.assertEqual(err[1].find(")"), err[2].find("^")) def test_base_exception(self): # Test that exceptions derived from BaseException are formatted right @@ -117,56 +66,86 @@ def test(): lst = traceback.format_exception_only(e.__class__, e) self.assertEqual(lst, ['KeyboardInterrupt\n']) - # String exceptions are deprecated, but legal. The quirky form with - # separate "type" and "value" tends to break things, because - # not isinstance(value, type) - # and a string cannot be the first argument to issubclass. - # - # Note that sys.last_type and sys.last_value do not get set if an - # exception is caught, so we sort of cheat and just emulate them. - # - # test_string_exception1 is equivalent to - # - # >>> raise "String Exception" - # - # test_string_exception2 is equivalent to - # - # >>> raise "String Exception", "String Value" - # - def test_string_exception1(self): - str_type = "String Exception" - err = traceback.format_exception_only(str_type, None) - self.assertEqual(len(err), 1) - self.assertEqual(err[0], str_type + '\n') - - def test_string_exception2(self): - str_type = "String Exception" - str_value = "String Value" - err = traceback.format_exception_only(str_type, str_value) - self.assertEqual(len(err), 1) - self.assertEqual(err[0], str_type + ': ' + str_value + '\n') - def test_format_exception_only_bad__str__(self): class X(Exception): def __str__(self): - 1 // 0 + 1/0 err = traceback.format_exception_only(X, X()) self.assertEqual(len(err), 1) str_value = '<unprintable %s object>' % X.__name__ - self.assertEqual(err[0], X.__name__ + ': ' + str_value + '\n') + if X.__module__ in ('__main__', 'builtins'): + str_name = X.__name__ + else: + str_name = '.'.join([X.__module__, X.__name__]) + self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value)) def test_without_exception(self): err = traceback.format_exception_only(None, None) self.assertEqual(err, ['None\n']) - def test_unicode(self): - err = AssertionError('\xff') - lines = traceback.format_exception_only(type(err), err) - self.assertEqual(lines, ['AssertionError: \xff\n']) - - err = AssertionError(u'\xe9') - lines = traceback.format_exception_only(type(err), err) - self.assertEqual(lines, ['AssertionError: \\xe9\n']) + def test_encoded_file(self): + # Test that tracebacks are correctly printed for encoded source files: + # - correct line number (Issue2384) + # - respect file encoding (Issue3975) + import tempfile, sys, subprocess, os + + # The spawned subprocess has its stdout redirected to a PIPE, and its + # encoding may be different from the current interpreter, on Windows + # at least. + process = subprocess.Popen([sys.executable, "-c", + "import sys; print(sys.stdout.encoding)"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + stdout, stderr = process.communicate() + output_encoding = str(stdout, 'ascii').splitlines()[0] + + def do_test(firstlines, message, charset, lineno): + # Raise the message in a subprocess, and catch the output + try: + output = open(TESTFN, "w", encoding=charset) + output.write("""{0}if 1: + import traceback; + raise RuntimeError('{1}') + """.format(firstlines, message)) + output.close() + process = subprocess.Popen([sys.executable, TESTFN], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdout, stderr = process.communicate() + stdout = stdout.decode(output_encoding).splitlines() + finally: + unlink(TESTFN) + + # The source lines are encoded with the 'backslashreplace' handler + encoded_message = message.encode(output_encoding, + 'backslashreplace') + # and we just decoded them with the output_encoding. + message_ascii = encoded_message.decode(output_encoding) + + err_line = "raise RuntimeError('{0}')".format(message_ascii) + err_msg = "RuntimeError: {0}".format(message_ascii) + + self.assertIn(("line %s" % lineno), stdout[1], + "Invalid line number: {0!r} instead of {1}".format( + stdout[1], lineno)) + self.assertTrue(stdout[2].endswith(err_line), + "Invalid traceback line: {0!r} instead of {1!r}".format( + stdout[2], err_line)) + self.assertTrue(stdout[3] == err_msg, + "Invalid error message: {0!r} instead of {1!r}".format( + stdout[3], err_msg)) + + do_test("", "foo", "ascii", 3) + for charset in ("ascii", "iso-8859-1", "utf-8", "GBK"): + if charset == "ascii": + text = "foo" + elif charset == "GBK": + text = "\u4E02\u5100" + else: + text = "h\xe9 ho" + do_test("# coding: {0}\n".format(charset), + text, charset, 4) + do_test("#!shebang\n# coding: {0}\n".format(charset), + text, charset, 5) class TracebackFormatTests(unittest.TestCase): @@ -196,9 +175,162 @@ class TracebackFormatTests(unittest.TestCase): self.assertTrue(source_line.startswith(' raise')) -def test_main(): - run_unittest(TracebackCases, TracebackFormatTests) +cause_message = ( + "\nThe above exception was the direct cause " + "of the following exception:\n\n") + +context_message = ( + "\nDuring handling of the above exception, " + "another exception occurred:\n\n") +boundaries = re.compile( + '(%s|%s)' % (re.escape(cause_message), re.escape(context_message))) + + +class BaseExceptionReportingTests: + + def get_exception(self, exception_or_callable): + if isinstance(exception_or_callable, Exception): + return exception_or_callable + try: + exception_or_callable() + except Exception as e: + return e + + def zero_div(self): + 1/0 # In zero_div + + def check_zero_div(self, msg): + lines = msg.splitlines() + self.assertTrue(lines[-3].startswith(' File')) + self.assertIn('1/0 # In zero_div', lines[-2]) + self.assertTrue(lines[-1].startswith('ZeroDivisionError'), lines[-1]) + + def test_simple(self): + try: + 1/0 # Marker + except ZeroDivisionError as _: + e = _ + lines = self.get_report(e).splitlines() + self.assertEqual(len(lines), 4) + self.assertTrue(lines[0].startswith('Traceback')) + self.assertTrue(lines[1].startswith(' File')) + self.assertIn('1/0 # Marker', lines[2]) + self.assertTrue(lines[3].startswith('ZeroDivisionError')) + + def test_cause(self): + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError as e: + raise KeyError from e + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEqual(len(blocks), 3) + self.assertEqual(blocks[1], cause_message) + self.check_zero_div(blocks[0]) + self.assertIn('inner_raise() # Marker', blocks[2]) + + def test_context(self): + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError: + raise KeyError + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEqual(len(blocks), 3) + self.assertEqual(blocks[1], context_message) + self.check_zero_div(blocks[0]) + self.assertIn('inner_raise() # Marker', blocks[2]) + + def test_cause_and_context(self): + # When both a cause and a context are set, only the cause should be + # displayed and the context should be muted. + def inner_raise(): + try: + self.zero_div() + except ZeroDivisionError as _e: + e = _e + try: + xyzzy + except NameError: + raise KeyError from e + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEqual(len(blocks), 3) + self.assertEqual(blocks[1], cause_message) + self.check_zero_div(blocks[0]) + self.assertIn('inner_raise() # Marker', blocks[2]) + + def test_cause_recursive(self): + def inner_raise(): + try: + try: + self.zero_div() + except ZeroDivisionError as e: + z = e + raise KeyError from e + except KeyError as e: + raise z from e + def outer_raise(): + inner_raise() # Marker + blocks = boundaries.split(self.get_report(outer_raise)) + self.assertEqual(len(blocks), 3) + self.assertEqual(blocks[1], cause_message) + # The first block is the KeyError raised from the ZeroDivisionError + self.assertIn('raise KeyError from e', blocks[0]) + self.assertNotIn('1/0', blocks[0]) + # The second block (apart from the boundary) is the ZeroDivisionError + # re-raised from the KeyError + self.assertIn('inner_raise() # Marker', blocks[2]) + self.check_zero_div(blocks[2]) + + def test_syntax_error_offset_at_eol(self): + # See #10186. + def e(): + raise SyntaxError('', ('', 0, 5, 'hello')) + msg = self.get_report(e).splitlines() + self.assertEqual(msg[-2], " ^") + def e(): + exec("x = 5 | 4 |") + msg = self.get_report(e).splitlines() + self.assertEqual(msg[-2], ' ^') + + +class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): + # + # This checks reporting through the 'traceback' module, with both + # format_exception() and print_exception(). + # + + def get_report(self, e): + e = self.get_exception(e) + s = ''.join( + traceback.format_exception(type(e), e, e.__traceback__)) + with captured_output("stderr") as sio: + traceback.print_exception(type(e), e, e.__traceback__) + self.assertEqual(sio.getvalue(), s) + return s + + +class CExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): + # + # This checks built-in reporting by the interpreter. + # + + def get_report(self, e): + e = self.get_exception(e) + with captured_output("stderr") as s: + exception_print(e) + return s.getvalue() + + +def test_main(): + run_unittest(__name__) if __name__ == "__main__": test_main() |