"""Test cases for traceback module""" from collections import namedtuple from io import StringIO import linecache import sys import unittest import re from test.support import TESTFN, Error, captured_output, unlink, cpython_only from test.script_helper import assert_python_ok import textwrap import traceback test_code = namedtuple('code', ['co_filename', 'co_name']) test_frame = namedtuple('frame', ['f_code', 'f_globals', 'f_locals']) test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next']) 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 syntax_error_with_caret_non_ascii(self): compile('Python = "\u1e54\xfd\u0163\u0125\xf2\xf1" +', "?", "exec") def syntax_error_bad_indentation2(self): compile(" 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.assertEqual(err[2].count('\n'), 1) # and no additional newline self.assertEqual(err[1].find("+"), err[2].find("^")) # in the right place err = self.get_exception_format(self.syntax_error_with_caret_non_ascii, SyntaxError) self.assertIn("^", err[2]) # third line has caret self.assertEqual(err[2].count('\n'), 1) # and no additional newline self.assertEqual(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("^")) err = self.get_exception_format(self.syntax_error_bad_indentation2, IndentationError) self.assertEqual(len(err), 4) self.assertEqual(err[1].strip(), "print(2)") self.assertIn("^", err[2]) self.assertEqual(err[1].find("p"), 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 = '' % X.__name__ if X.__module__ in ('__main__', 'builtins'): str_name = X.__qualname__ else: str_name = '.'.join([X.__module__, X.__qualname__]) 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) do_test(" \t\f\n# coding: {0}\n".format(charset), text, charset, 5) # Issue #18960: coding spec should has no effect do_test("0\n# coding: GBK\n", "h\xe9 ho", 'utf-8', 5) def test_print_traceback_at_exit(self): # Issue #22599: Ensure that it is possible to use the traceback module # to display an exception at Python exit code = textwrap.dedent(""" import sys import traceback class PrintExceptionAtExit(object): def __init__(self): try: x = 1 / 0 except Exception: self.exc_info = sys.exc_info() # self.exc_info[1] (traceback) contains frames: # explicitly clear the reference to self in the current # frame to break a reference cycle self = None def __del__(self): traceback.print_exception(*self.exc_info) # Keep a reference in the module namespace to call the destructor # when the module is unloaded obj = PrintExceptionAtExit() """) rc, stdout, stderr = assert_python_ok('-c', code) expected = [b'Traceback (most recent call last):', b' File "", line 8, in __init__', b'ZeroDivisionError: division by zero'] self.assertEqual(stderr.splitlines(), expected) class TracebackFormatTests(unittest.TestCase): def some_exception(self): raise KeyError('blah') @cpython_only def check_traceback_format(self, cleanup_func=None): from _testcapi import traceback_print try: self.some_exception() except KeyError: type_, value, tb = sys.exc_info() if cleanup_func is not None: # Clear the inner frames, not this one cleanup_func(tb.tb_next) traceback_fmt = 'Traceback (most recent call last):\n' + \ ''.join(traceback.format_tb(tb)) file_ = StringIO() traceback_print(tb, file_) python_fmt = file_.getvalue() # Call all _tb and _exc functions with captured_output("stderr") as tbstderr: traceback.print_tb(tb) tbfile = StringIO() traceback.print_tb(tb, file=tbfile) with captured_output("stderr") as excstderr: traceback.print_exc() excfmt = traceback.format_exc() excfile = StringIO() traceback.print_exc(file=excfile) 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) # Now verify the _tb func output self.assertEqual(tbstderr.getvalue(), tbfile.getvalue()) # Now verify the _exc func output self.assertEqual(excstderr.getvalue(), excfile.getvalue()) self.assertEqual(excfmt, excfile.getvalue()) # Make sure that the traceback is properly indented. tb_lines = python_fmt.splitlines() self.assertEqual(len(tb_lines), 5) banner = tb_lines[0] location, source_line = tb_lines[-2:] self.assertTrue(banner.startswith('Traceback')) self.assertTrue(location.startswith(' File')) self.assertTrue(source_line.startswith(' raise')) def test_traceback_format(self): self.check_traceback_format() def test_traceback_format_with_cleared_frames(self): # Check that traceback formatting also works with a clear()ed frame def cleanup_tb(tb): tb.tb_frame.clear() self.check_traceback_format(cleanup_tb) def test_stack_format(self): # Verify _stack functions. Note we have to use _getframe(1) to # compare them without this frame appearing in the output with captured_output("stderr") as ststderr: traceback.print_stack(sys._getframe(1)) stfile = StringIO() traceback.print_stack(sys._getframe(1), file=stfile) self.assertEqual(ststderr.getvalue(), stfile.getvalue()) stfmt = traceback.format_stack(sys._getframe(1)) self.assertEqual(ststderr.getvalue(), "".join(stfmt)) 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_context_suppression(self): try: try: raise Exception except: raise ZeroDivisionError from None 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('ZeroDivisionError from None', lines[2]) self.assertTrue(lines[3].startswith('ZeroDivisionError')) 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. # @cpython_only def get_report(self, e): from _testcapi import exception_print e = self.get_exception(e) with captured_output("stderr") as s: exception_print(e) return s.getvalue() class MiscTracebackCases(unittest.TestCase): # # Check non-printing functions in traceback module # def test_clear(self): def outer(): middle() def middle(): inner() def inner(): i = 1 1/0 try: outer() except: type_, value, tb = sys.exc_info() # Initial assertion: there's one local in the inner frame. inner_frame = tb.tb_next.tb_next.tb_next.tb_frame self.assertEqual(len(inner_frame.f_locals), 1) # Clear traceback frames traceback.clear_frames(tb) # Local variable dict should now be empty. self.assertEqual(len(inner_frame.f_locals), 0) class TestFrame(unittest.TestCase): def test_basics(self): linecache.clearcache() linecache.lazycache("f", globals()) f = traceback.FrameSummary("f", 1, "dummy") self.assertEqual( ("f", 1, "dummy", '"""Test cases for traceback module"""'), tuple(f)) self.assertEqual(None, f.locals) def test_lazy_lines(self): linecache.clearcache() f = traceback.FrameSummary("f", 1, "dummy", lookup_line=False) self.assertEqual(None, f._line) linecache.lazycache("f", globals()) self.assertEqual( '"""Test cases for traceback module"""', f.line) def test_explicit_line(self): f = traceback.FrameSummary("f", 1, "dummy", line="line") self.assertEqual("line", f.line) class TestStack(unittest.TestCase): def test_walk_stack(self): s = list(traceback.walk_stack(None)) self.assertGreater(len(s), 10) def test_walk_tb(self): try: 1/0 except Exception: _, _, tb = sys.exc_info() s = list(traceback.walk_tb(tb)) self.assertEqual(len(s), 1) def test_extract_stack(self): s = traceback.StackSummary.extract(traceback.walk_stack(None)) self.assertIsInstance(s, traceback.StackSummary) def test_extract_stack_limit(self): s = traceback.StackSummary.extract(traceback.walk_stack(None), limit=5) self.assertEqual(len(s), 5) def test_extract_stack_lookup_lines(self): linecache.clearcache() linecache.updatecache('/foo.py', globals()) c = test_code('/foo.py', 'method') f = test_frame(c, None, None) s = traceback.StackSummary.extract(iter([(f, 6)]), lookup_lines=True) linecache.clearcache() self.assertEqual(s[0].line, "import sys") def test_extract_stackup_deferred_lookup_lines(self): linecache.clearcache() c = test_code('/foo.py', 'method') f = test_frame(c, None, None) s = traceback.StackSummary.extract(iter([(f, 6)]), lookup_lines=False) self.assertEqual({}, linecache.cache) linecache.updatecache('/foo.py', globals()) self.assertEqual(s[0].line, "import sys") def test_from_list(self): s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) self.assertEqual( [' File "foo.py", line 1, in fred\n line\n'], s.format()) def test_from_list_edited_stack(self): s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) s[0] = ('foo.py', 2, 'fred', 'line') s2 = traceback.StackSummary.from_list(s) self.assertEqual( [' File "foo.py", line 2, in fred\n line\n'], s2.format()) def test_format_smoke(self): # For detailed tests see the format_list tests, which consume the same # code. s = traceback.StackSummary.from_list([('foo.py', 1, 'fred', 'line')]) self.assertEqual( [' File "foo.py", line 1, in fred\n line\n'], s.format()) def test_locals(self): linecache.updatecache('/foo.py', globals()) c = test_code('/foo.py', 'method') f = test_frame(c, globals(), {'something': 1}) s = traceback.StackSummary.extract(iter([(f, 6)]), capture_locals=True) self.assertEqual(s[0].locals, {'something': '1'}) def test_no_locals(self): linecache.updatecache('/foo.py', globals()) c = test_code('/foo.py', 'method') f = test_frame(c, globals(), {'something': 1}) s = traceback.StackSummary.extract(iter([(f, 6)])) self.assertEqual(s[0].locals, None) def test_format_locals(self): def some_inner(k, v): a = 1 b = 2 return traceback.StackSummary.extract( traceback.walk_stack(None), capture_locals=True, limit=1) s = some_inner(3, 4) self.assertEqual( [' File "' + __file__ + '", line 592, ' 'in some_inner\n' ' traceback.walk_stack(None), capture_locals=True, limit=1)\n' ' a = 1\n' ' b = 2\n' ' k = 3\n' ' v = 4\n' ], s.format()) class TestTracebackException(unittest.TestCase): def test_smoke(self): try: 1/0 except Exception: exc_info = sys.exc_info() exc = traceback.TracebackException(*exc_info) expected_stack = traceback.StackSummary.extract( traceback.walk_tb(exc_info[2])) self.assertEqual(None, exc.__cause__) self.assertEqual(None, exc.__context__) self.assertEqual(False, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) self.assertEqual(exc_info[0], exc.exc_type) self.assertEqual(str(exc_info[1]), str(exc)) def test_from_exception(self): # Check all the parameters are accepted. def foo(): 1/0 try: foo() except Exception as e: exc_info = sys.exc_info() self.expected_stack = traceback.StackSummary.extract( traceback.walk_tb(exc_info[2]), limit=1, lookup_lines=False, capture_locals=True) self.exc = traceback.TracebackException.from_exception( e, limit=1, lookup_lines=False, capture_locals=True) expected_stack = self.expected_stack exc = self.exc self.assertEqual(None, exc.__cause__) self.assertEqual(None, exc.__context__) self.assertEqual(False, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) self.assertEqual(exc_info[0], exc.exc_type) self.assertEqual(str(exc_info[1]), str(exc)) def test_cause(self): try: try: 1/0 finally: exc_info_context = sys.exc_info() exc_context = traceback.TracebackException(*exc_info_context) cause = Exception("cause") raise Exception("uh oh") from cause except Exception: exc_info = sys.exc_info() exc = traceback.TracebackException(*exc_info) expected_stack = traceback.StackSummary.extract( traceback.walk_tb(exc_info[2])) exc_cause = traceback.TracebackException(Exception, cause, None) self.assertEqual(exc_cause, exc.__cause__) self.assertEqual(exc_context, exc.__context__) self.assertEqual(True, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) self.assertEqual(exc_info[0], exc.exc_type) self.assertEqual(str(exc_info[1]), str(exc)) def test_context(self): try: try: 1/0 finally: exc_info_context = sys.exc_info() exc_context = traceback.TracebackException(*exc_info_context) raise Exception("uh oh") except Exception: exc_info = sys.exc_info() exc = traceback.TracebackException(*exc_info) expected_stack = traceback.StackSummary.extract( traceback.walk_tb(exc_info[2])) self.assertEqual(None, exc.__cause__) self.assertEqual(exc_context, exc.__context__) self.assertEqual(False, exc.__suppress_context__) self.assertEqual(expected_stack, exc.stack) self.assertEqual(exc_info[0], exc.exc_type) self.assertEqual(str(exc_info[1]), str(exc)) def test_limit(self): def recurse(n): if n: recurse(n-1) else: 1/0 try: recurse(10) except Exception: exc_info = sys.exc_info() exc = traceback.TracebackException(*exc_info, limit=5) expected_stack = traceback.StackSummary.extract( traceback.walk_tb(exc_info[2]), limit=5) self.assertEqual(expected_stack, exc.stack) def test_lookup_lines(self): linecache.clearcache() e = Exception("uh oh") c = test_code('/foo.py', 'method') f = test_frame(c, None, None) tb = test_tb(f, 6, None) exc = traceback.TracebackException(Exception, e, tb, lookup_lines=False) self.assertEqual({}, linecache.cache) linecache.updatecache('/foo.py', globals()) self.assertEqual(exc.stack[0].line, "import sys") def test_locals(self): linecache.updatecache('/foo.py', globals()) e = Exception("uh oh") c = test_code('/foo.py', 'method') f = test_frame(c, globals(), {'something': 1, 'other': 'string'}) tb = test_tb(f, 6, None) exc = traceback.TracebackException( Exception, e, tb, capture_locals=True) self.assertEqual( exc.stack[0].locals, {'something': '1', 'other': "'string'"}) def test_no_locals(self): linecache.updatecache('/foo.py', globals()) e = Exception("uh oh") c = test_code('/foo.py', 'method') f = test_frame(c, globals(), {'something': 1}) tb = test_tb(f, 6, None) exc = traceback.TracebackException(Exception, e, tb) self.assertEqual(exc.stack[0].locals, None) class MiscTest(unittest.TestCase): def test_all(self): expected = set() blacklist = {'print_list'} for name in dir(traceback): if name.startswith('_') or name in blacklist: continue module_object = getattr(traceback, name) if getattr(module_object, '__module__', None) == 'traceback': expected.add(name) self.assertCountEqual(traceback.__all__, expected) if __name__ == "__main__": unittest.main()