diff options
author | Irit Katriel <1055913+iritkatriel@users.noreply.github.com> | 2022-03-08 21:43:49 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-08 21:43:49 (GMT) |
commit | 88b7d86a73da9388aa65c96401c2984c8c16f8db (patch) | |
tree | 3dd2dc7d23ffbef10f514821c92ebccd191dd7e9 /Lib/unittest | |
parent | da80d6b2f3beff519cb1457d5e055168c89f7224 (diff) | |
download | cpython-88b7d86a73da9388aa65c96401c2984c8c16f8db.zip cpython-88b7d86a73da9388aa65c96401c2984c8c16f8db.tar.gz cpython-88b7d86a73da9388aa65c96401c2984c8c16f8db.tar.bz2 |
bpo-24959: fix unittest.assertRaises bug where traceback entries are dropped from chained exceptions (GH-23688)
Diffstat (limited to 'Lib/unittest')
-rw-r--r-- | Lib/unittest/result.py | 53 | ||||
-rw-r--r-- | Lib/unittest/test/test_result.py | 55 |
2 files changed, 94 insertions, 14 deletions
diff --git a/Lib/unittest/result.py b/Lib/unittest/result.py index ce7468e..3da7005 100644 --- a/Lib/unittest/result.py +++ b/Lib/unittest/result.py @@ -173,18 +173,10 @@ class TestResult(object): 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 - length = self._count_relevant_tb_levels(tb) - else: - length = None + tb = self._clean_tracebacks(exctype, value, tb, test) 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: @@ -200,16 +192,49 @@ class TestResult(object): 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 - 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 224a784..c616f28 100644 --- a/Lib/unittest/test/test_result.py +++ b/Lib/unittest/test/test_result.py @@ -220,6 +220,61 @@ class Test_TestResult(unittest.TestCase): 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]) + + 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 |