1 # Copyright 2013 Red Hat, Inc.
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
20 def get_exception_handler(debugger_name):
21 debugger = _get_debugger(debugger_name)
22 return functools.partial(_exception_handler, debugger)
25 def _get_debugger(debugger_name):
27 debugger = __import__(debugger_name)
29 raise ValueError("can't import %s module as a post mortem debugger" %
31 if 'post_mortem' in dir(debugger):
34 raise ValueError("%s is not a supported post mortem debugger" %
38 def _exception_handler(debugger, exc_info):
39 """Exception handler enabling post-mortem debugging.
41 A class extending testtools.TestCase can add this handler in setUp():
43 self.addOnException(post_mortem_debug.exception_handler)
45 When an exception occurs, the user will be dropped into a debugger
46 session in the execution environment of the failure.
48 Frames associated with the testing framework are excluded so that
49 the post-mortem session for an assertion failure will start at the
50 assertion call (e.g. self.assertTrue) rather than the framework code
51 that raises the failure exception (e.g. the assertTrue method).
54 ignored_traceback = get_ignored_traceback(tb)
56 tb = FilteredTraceback(tb, ignored_traceback)
57 traceback.print_exception(exc_info[0], exc_info[1], tb)
58 debugger.post_mortem(tb)
61 def get_ignored_traceback(tb):
62 """Retrieve the first traceback of an ignored trailing chain.
64 Given an initial traceback, find the first traceback of a trailing
65 chain of tracebacks that should be ignored. The criteria for
66 whether a traceback should be ignored is whether its frame's
67 globals include the __unittest marker variable. This criteria is
70 unittest.TestResult._is_relevant_tb_level
74 tb.tb_next => tb0.tb_next => tb1.tb_next
76 - If no tracebacks were to be ignored, None would be returned.
77 - If only tb1 was to be ignored, tb1 would be returned.
78 - If tb0 and tb1 were to be ignored, tb0 would be returned.
79 - If either of only tb or only tb0 was to be ignored, None would
80 be returned because neither tb or tb0 would be part of a
81 trailing chain of ignored tracebacks.
83 # Turn the traceback chain into a list
89 # Find all members of an ignored trailing chain
90 ignored_tracebacks = []
91 for tb in reversed(tb_list):
92 if '__unittest' in tb.tb_frame.f_globals:
93 ignored_tracebacks.append(tb)
97 # Return the first member of the ignored trailing chain
98 if ignored_tracebacks:
99 return ignored_tracebacks[-1]
102 class FilteredTraceback(object):
103 """Wraps a traceback to filter unwanted frames."""
105 def __init__(self, tb, filtered_traceback):
108 :param tb: The start of the traceback chain to filter.
109 :param filtered_traceback: The first traceback of a trailing
110 chain that is to be filtered.
113 self.tb_lasti = self._tb.tb_lasti
114 self.tb_lineno = self._tb.tb_lineno
115 self.tb_frame = self._tb.tb_frame
116 self._filtered_traceback = filtered_traceback
120 tb_next = self._tb.tb_next
121 if tb_next and tb_next != self._filtered_traceback:
122 return FilteredTraceback(tb_next, self._filtered_traceback)