| """Test cases for traceback module""" |
| |
| from _testcapi import traceback_print, exception_print |
| from io import StringIO |
| import sys |
| import unittest |
| import re |
| from test.support import run_unittest, Error, captured_output |
| from test.support import TESTFN, unlink |
| |
| import traceback |
| |
| |
| 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 as value: |
| return traceback.format_exception_only(exc, value) |
| else: |
| raise ValueError("call did not raise exception") |
| |
| def syntax_error_with_caret(self): |
| compile("def fact(x):\n\treturn x!\n", "?", "exec") |
| |
| def syntax_error_with_caret_2(self): |
| compile("1 +\n", "?", "exec") |
| |
| def syntax_error_bad_indentation(self): |
| 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.assertEqual(len(err), 4) |
| self.assertTrue(err[1].strip() == "return x!") |
| self.assertIn("^", err[2]) # third line has caret |
| self.assertEqual(err[1].find("!"), err[2].find("^")) # in the right place |
| |
| err = self.get_exception_format(self.syntax_error_with_caret_2, |
| SyntaxError) |
| self.assertIn("^", err[2]) # third line has caret |
| self.assertTrue(err[2].count('\n') == 1) # and no additional newline |
| self.assertTrue(err[1].find("+") == err[2].find("^")) # in the right place |
| |
| def test_nocaret(self): |
| 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.assertEqual(len(err), 4) |
| self.assertEqual(err[1].strip(), "print(2)") |
| self.assertIn("^", err[2]) |
| self.assertEqual(err[1].find(")"), err[2].find("^")) |
| |
| def test_base_exception(self): |
| # Test that exceptions derived from BaseException are formatted right |
| e = KeyboardInterrupt() |
| lst = traceback.format_exception_only(e.__class__, e) |
| self.assertEqual(lst, ['KeyboardInterrupt\n']) |
| |
| def test_format_exception_only_bad__str__(self): |
| class X(Exception): |
| def __str__(self): |
| 1/0 |
| err = traceback.format_exception_only(X, X()) |
| self.assertEqual(len(err), 1) |
| str_value = '<unprintable %s object>' % X.__name__ |
| 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_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): |
| |
| def test_traceback_format(self): |
| try: |
| raise KeyError('blah') |
| except KeyError: |
| type_, value, tb = sys.exc_info() |
| traceback_fmt = 'Traceback (most recent call last):\n' + \ |
| ''.join(traceback.format_tb(tb)) |
| file_ = StringIO() |
| traceback_print(tb, file_) |
| python_fmt = file_.getvalue() |
| else: |
| raise Error("unable to create test traceback string") |
| |
| # Make sure that Python and the traceback module format the same thing |
| self.assertEqual(traceback_fmt, python_fmt) |
| |
| # Make sure that the traceback is properly indented. |
| tb_lines = python_fmt.splitlines() |
| self.assertEqual(len(tb_lines), 3) |
| banner, location, source_line = tb_lines |
| self.assertTrue(banner.startswith('Traceback')) |
| self.assertTrue(location.startswith(' File')) |
| self.assertTrue(source_line.startswith(' raise')) |
| |
| |
| 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() |