diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index 111317b329a8521..4164897d081b174 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -179,11 +179,9 @@ def _exc_info_to_string(self, err, test): if exctype is test.failureException: # Skip assert*() traceback levels - length = self._count_relevant_tb_levels(tb) - else: - length = None + self._remove_unittest_tb_frames(tb) tb_e = traceback.TracebackException( - exctype, value, tb, limit=length, capture_locals=self.tb_locals) + exctype, value, tb, capture_locals=self.tb_locals) msgLines = list(tb_e.format()) if self.buffer: @@ -203,12 +201,20 @@ def _exc_info_to_string(self, err, test): def _is_relevant_tb_level(self, tb): return '__unittest' in tb.tb_frame.f_globals - def _count_relevant_tb_levels(self, tb): - length = 0 + def _remove_unittest_tb_frames(self, tb): + '''Truncates usercode tb at the first unittest frame. + + If the first frame of the traceback is in user code, + the prefix up to the first unittest frame is returned. + If the first frame is already in the unittest module, + the traceback is not modified. + ''' + prev = None while tb and not self._is_relevant_tb_level(tb): - length += 1 + prev = tb tb = tb.tb_next - return length + if prev is not None: + prev.tb_next = None def __repr__(self): return ("<%s run=%i errors=%i failures=%i>" % diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py index a4af67bd8d56de2..56e46f526811e64 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -413,6 +413,47 @@ class tb_frame(object): Frame.tb_frame.f_globals['__unittest'] = True self.assertTrue(result._is_relevant_tb_level(Frame)) + def testRemoveUnittestTbFrames(self): + try: + self.fail('too bad') + except: + exc_info = sys.exc_info() + tb = exc_info[2] + self.assertEqual(len(list(traceback.walk_tb(tb))), 2) + result = unittest.TestResult() + result._remove_unittest_tb_frames(tb) + self.assertEqual(len(list(traceback.walk_tb(tb))), 1) + + def testExcInfoStringChained(self): # bpo 49563 + def raiseAnException(): + raise ValueError(42) + try: + try: + raiseAnException() + except: + self.fail('too bad') + except: + exc_info = sys.exc_info() + + class ResultWithoutFilter(unittest.TestResult): + def _remove_unittest_tb_frames(self, tb): + pass + + result = ResultWithoutFilter() + exc_str_no_filter = result._exc_info_to_string(exc_info, self) + + result = unittest.TestResult() + exc_str_filtered = result._exc_info_to_string(exc_info, self) + for exc_str in [exc_str_no_filter, exc_str_filtered]: + self.assertIn('raiseAnException()', exc_str) + self.assertIn('raise ValueError(42)', exc_str) + self.assertIn("self.fail('too bad')", exc_str) + self.assertIn('AssertionError: too bad', exc_str) + + # The unittest call: + self.assertNotIn('raise self.failureException(msg)', exc_str_filtered) + self.assertIn('raise self.failureException(msg)', exc_str_no_filter) + def testFailFast(self): result = unittest.TestResult() result._exc_info_to_string = lambda *_: ''