From f642752b725ea1867f22be6bfa2e3f951070d707 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Tue, 8 Dec 2020 01:29:58 +0000 Subject: [PATCH 1/8] bpo-42247: fix unittest.assertRaises bug where traceback entries were dropped from the chained exception --- Lib/unittest/result.py | 22 ++++++++++------- Lib/unittest/test/test_result.py | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index ce7468e31481f07..d6cbc01777b9af7 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -179,12 +179,10 @@ 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, compact=True) + capture_locals=self.tb_locals, compact=True) msgLines = list(tb_e.format()) if self.buffer: @@ -204,12 +202,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 d6efc7ef0662a42..c0e3cabdbde318c 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -424,6 +424,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 *_: '' From e0c80194044fbd1e86c0709ea582b70c30e3b6aa Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Sun, 5 Sep 2021 22:01:35 +0100 Subject: [PATCH 2/8] Update Lib/unittest/test/test_result.py Co-authored-by: Serhiy Storchaka --- Lib/unittest/test/test_result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py index c0e3cabdbde318c..c164fe650542d81 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -443,7 +443,7 @@ def raiseAnException(): raiseAnException() except: self.fail('too bad') - except: + except self.failureException: exc_info = sys.exc_info() class ResultWithoutFilter(unittest.TestResult): From 6bca38a83740bc2be17d6d0405de10de092e3967 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Sun, 5 Sep 2021 22:01:41 +0100 Subject: [PATCH 3/8] Update Lib/unittest/test/test_result.py Co-authored-by: Serhiy Storchaka --- Lib/unittest/test/test_result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py index c164fe650542d81..b7b5584158d817a 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -441,7 +441,7 @@ def raiseAnException(): try: try: raiseAnException() - except: + except ValueError: self.fail('too bad') except self.failureException: exc_info = sys.exc_info() From b60a82958bc9cc4fd8117cacc12153a80b8ce334 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Sun, 5 Sep 2021 22:01:45 +0100 Subject: [PATCH 4/8] Update Lib/unittest/test/test_result.py Co-authored-by: Serhiy Storchaka --- Lib/unittest/test/test_result.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py index b7b5584158d817a..5ec13cdb9a588f0 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -427,7 +427,7 @@ class tb_frame(object): def testRemoveUnittestTbFrames(self): try: self.fail('too bad') - except: + except self.failureException: exc_info = sys.exc_info() tb = exc_info[2] self.assertEqual(len(list(traceback.walk_tb(tb))), 2) From 7274a04656e0e98a0d1c1201f8a4c3e79d6d29e9 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Sep 2021 11:54:17 +0100 Subject: [PATCH 5/8] rewrite test --- Lib/unittest/test/test_result.py | 67 +++++++++++++------------------- 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py index 5ec13cdb9a588f0..ded880e9813cb2b 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -204,6 +204,32 @@ def test_1(self): self.assertIs(test_case, test) self.assertIsInstance(formatted_exc, str) + def test_addFailure_filter_traceback_frames(self): + class Foo(unittest.TestCase): + def test_1(self): + pass + + test = Foo('test_1') + def get_exc_info(): + try: + test.fail("foo") + except: + return sys.exc_info() + + exc_info_tuple = get_exc_info() + + full_exc = traceback.format_exception(*exc_info_tuple) + + result = unittest.TestResult() + result.startTest(test) + result.addFailure(test, exc_info_tuple) + result.stopTest(test) + + formatted_exc = result.failures[0][1] + dropped = [l for l in full_exc if l not in formatted_exc] + self.assertEqual(len(dropped), 1) + self.assertIn("raise self.failureException(msg)", dropped[0]) + # "addError(test, err)" # ... # "Called when the test case test raises an unexpected exception err @@ -424,47 +450,6 @@ 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 self.failureException: - 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 ValueError: - self.fail('too bad') - except self.failureException: - 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 *_: '' From d067f50de716699b7994835fab2c579e8edef281 Mon Sep 17 00:00:00 2001 From: Irit Katriel Date: Mon, 6 Sep 2021 16:42:31 +0100 Subject: [PATCH 6/8] clean tracebacks for chained exceptions as well --- Lib/unittest/result.py | 33 +++++++++++++++++++++++++------- Lib/unittest/test/test_result.py | 29 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index d6cbc01777b9af7..3da7005e603f4a9 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -173,13 +173,7 @@ def stop(self): def _exc_info_to_string(self, err, test): """Converts a sys.exc_info()-style tuple of values into a string.""" exctype, value, tb = err - # Skip test runner traceback levels - while tb and self._is_relevant_tb_level(tb): - tb = tb.tb_next - - if exctype is test.failureException: - # Skip assert*() traceback levels - self._remove_unittest_tb_frames(tb) + tb = self._clean_tracebacks(exctype, value, tb, test) tb_e = traceback.TracebackException( exctype, value, tb, capture_locals=self.tb_locals, compact=True) @@ -198,6 +192,31 @@ def _exc_info_to_string(self, err, test): msgLines.append(STDERR_LINE % error) return ''.join(msgLines) + def _clean_tracebacks(self, exctype, value, tb, test): + ret = None + first = True + excs = [(exctype, value, tb)] + while excs: + (exctype, value, tb) = excs.pop() + # Skip test runner traceback levels + while tb and self._is_relevant_tb_level(tb): + tb = tb.tb_next + + # Skip assert*() traceback levels + if exctype is test.failureException: + self._remove_unittest_tb_frames(tb) + + if first: + ret = tb + first = False + else: + value.__traceback__ = tb + + if value is not None: + for c in (value.__cause__, value.__context__): + if c is not None: + excs.append((type(c), c, c.__traceback__)) + return ret def _is_relevant_tb_level(self, tb): return '__unittest' in tb.tb_frame.f_globals diff --git a/Lib/unittest/test/test_result.py b/Lib/unittest/test/test_result.py index ded880e9813cb2b..13f22c310d5d7e5 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -230,6 +230,35 @@ def get_exc_info(): self.assertEqual(len(dropped), 1) self.assertIn("raise self.failureException(msg)", dropped[0]) + def test_addFailure_filter_traceback_frames_context(self): + class Foo(unittest.TestCase): + def test_1(self): + pass + + test = Foo('test_1') + def get_exc_info(): + try: + try: + test.fail("foo") + except: + raise ValueError(42) + except: + return sys.exc_info() + + exc_info_tuple = get_exc_info() + + full_exc = traceback.format_exception(*exc_info_tuple) + + result = unittest.TestResult() + result.startTest(test) + result.addFailure(test, exc_info_tuple) + result.stopTest(test) + + formatted_exc = result.failures[0][1] + dropped = [l for l in full_exc if l not in formatted_exc] + self.assertEqual(len(dropped), 1) + self.assertIn("raise self.failureException(msg)", dropped[0]) + # "addError(test, err)" # ... # "Called when the test case test raises an unexpected exception err From 4ae0bf07b13ba787eb7efc65fd2e8725d7702ff3 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 6 Sep 2021 15:46:54 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst diff --git a/Misc/NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst b/Misc/NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst new file mode 100644 index 000000000000000..357f24dd5f3cf42 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst @@ -0,0 +1 @@ +Fix bug where :mod:`unittest` sometimes drops frames from tracebacks of exceptions raised in tests. \ No newline at end of file From 92909672e488820933f2095069bcab1efc924b7b Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Tue, 8 Mar 2022 21:12:14 +0000 Subject: [PATCH 8/8] newline --- .../next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst b/Misc/NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst index 357f24dd5f3cf42..b702986f9468a65 100644 --- a/Misc/NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst +++ b/Misc/NEWS.d/next/Library/2021-09-06-15-46-53.bpo-24959.UVFgiO.rst @@ -1 +1 @@ -Fix bug where :mod:`unittest` sometimes drops frames from tracebacks of exceptions raised in tests. \ No newline at end of file +Fix bug where :mod:`unittest` sometimes drops frames from tracebacks of exceptions raised in tests.