[Python-checkins] bpo-36829: Add test.support.catch_unraisable_exception() (GH-13490)

Victor Stinner webhook-mailer at python.org
Wed May 22 17:44:06 EDT 2019


https://github.com/python/cpython/commit/e4d300e07c33a9a77549c62d8687d8fe130c53d5
commit: e4d300e07c33a9a77549c62d8687d8fe130c53d5
branch: master
author: Victor Stinner <vstinner at redhat.com>
committer: GitHub <noreply at github.com>
date: 2019-05-22T23:44:02+02:00
summary:

bpo-36829: Add test.support.catch_unraisable_exception() (GH-13490)

* Copy test_exceptions.test_unraisable() to
  test_sys.UnraisableHookTest().
* Use catch_unraisable_exception() in test_coroutines,
  test_exceptions, test_generators.

files:
A Misc/NEWS.d/next/Tests/2019-05-22-12-57-15.bpo-36829.e9mRWC.rst
M Lib/test/support/__init__.py
M Lib/test/test_coroutines.py
M Lib/test/test_exceptions.py
M Lib/test/test_generators.py
M Lib/test/test_sys.py

diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 9e60d960ab12..2fe9d9dc8099 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -3034,3 +3034,36 @@ def collision_stats(nbins, nballs):
         collisions = k - occupied
         var = dn*(dn-1)*((dn-2)/dn)**k + meanempty * (1 - meanempty)
         return float(collisions), float(var.sqrt())
+
+
+class catch_unraisable_exception:
+    """
+    Context manager catching unraisable exception using sys.unraisablehook.
+
+    Usage:
+
+        with support.catch_unraisable_exception() as cm:
+            ...
+
+            # check the expected unraisable exception: use cm.unraisable
+            ...
+
+        # cm.unraisable is None here (to break a reference cycle)
+    """
+
+    def __init__(self):
+        self.unraisable = None
+        self._old_hook = None
+
+    def _hook(self, unraisable):
+        self.unraisable = unraisable
+
+    def __enter__(self):
+        self._old_hook = sys.unraisablehook
+        sys.unraisablehook = self._hook
+        return self
+
+    def __exit__(self, *exc_info):
+        # Clear the unraisable exception to explicitly break a reference cycle
+        self.unraisable = None
+        sys.unraisablehook = self._old_hook
diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py
index 8443e658a620..036f13fa50e9 100644
--- a/Lib/test/test_coroutines.py
+++ b/Lib/test/test_coroutines.py
@@ -2342,12 +2342,19 @@ def test_unawaited_warning_when_module_broken(self):
         orig_wuc = warnings._warn_unawaited_coroutine
         try:
             warnings._warn_unawaited_coroutine = lambda coro: 1/0
-            with support.captured_stderr() as stream:
-                corofn()
+            with support.catch_unraisable_exception() as cm, \
+                 support.captured_stderr() as stream:
+                # only store repr() to avoid keeping the coroutine alive
+                coro = corofn()
+                coro_repr = repr(coro)
+
+                # clear reference to the coroutine without awaiting for it
+                del coro
                 support.gc_collect()
-            self.assertIn("Exception ignored in", stream.getvalue())
-            self.assertIn("ZeroDivisionError", stream.getvalue())
-            self.assertIn("was never awaited", stream.getvalue())
+
+                self.assertEqual(repr(cm.unraisable.object), coro_repr)
+                self.assertEqual(cm.unraisable.exc_type, ZeroDivisionError)
+                self.assertIn("was never awaited", stream.getvalue())
 
             del warnings._warn_unawaited_coroutine
             with support.captured_stderr() as stream:
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index 6ef529e2b015..d7e11d2d30a8 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -12,6 +12,9 @@
                           check_warnings, cpython_only, gc_collect, run_unittest,
                           no_tracing, unlink, import_module, script_helper,
                           SuppressCrashReport)
+from test import support
+
+
 class NaiveException(Exception):
     def __init__(self, x):
         self.x = x
@@ -1181,29 +1184,12 @@ def __del__(self):
                 # The following line is included in the traceback report:
                 raise exc
 
-        class BrokenExceptionDel:
-            def __del__(self):
-                exc = BrokenStrException()
-                # The following line is included in the traceback report:
-                raise exc
+        obj = BrokenDel()
+        with support.catch_unraisable_exception() as cm:
+            del obj
 
-        for test_class in (BrokenDel, BrokenExceptionDel):
-            with self.subTest(test_class):
-                obj = test_class()
-                with captured_stderr() as stderr:
-                    del obj
-                report = stderr.getvalue()
-                self.assertIn("Exception ignored", report)
-                self.assertIn(test_class.__del__.__qualname__, report)
-                self.assertIn("test_exceptions.py", report)
-                self.assertIn("raise exc", report)
-                if test_class is BrokenExceptionDel:
-                    self.assertIn("BrokenStrException", report)
-                    self.assertIn("<exception str() failed>", report)
-                else:
-                    self.assertIn("ValueError", report)
-                    self.assertIn("del is broken", report)
-                self.assertTrue(report.endswith("\n"))
+            self.assertEqual(cm.unraisable.object, BrokenDel.__del__)
+            self.assertIsNotNone(cm.unraisable.exc_traceback)
 
     def test_unhandled(self):
         # Check for sensible reporting of unhandled exceptions
diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py
index 320793c7dab6..7f1472fa03ac 100644
--- a/Lib/test/test_generators.py
+++ b/Lib/test/test_generators.py
@@ -2156,25 +2156,21 @@ def printsolution(self, x):
 printing warnings and to doublecheck that we actually tested what we wanted
 to test.
 
->>> import sys, io
->>> old = sys.stderr
->>> try:
-...     sys.stderr = io.StringIO()
-...     class Leaker:
-...         def __del__(self):
-...             def invoke(message):
-...                 raise RuntimeError(message)
-...             invoke("test")
+>>> from test import support
+>>> class Leaker:
+...     def __del__(self):
+...         def invoke(message):
+...             raise RuntimeError(message)
+...         invoke("del failed")
 ...
+>>> with support.catch_unraisable_exception() as cm:
 ...     l = Leaker()
 ...     del l
-...     err = sys.stderr.getvalue().strip()
-...     "Exception ignored in" in err
-...     "RuntimeError: test" in err
-...     "Traceback" in err
-...     "in invoke" in err
-... finally:
-...     sys.stderr = old
+...
+...     cm.unraisable.object == Leaker.__del__
+...     cm.unraisable.exc_type == RuntimeError
+...     str(cm.unraisable.exc_value) == "del failed"
+...     cm.unraisable.exc_traceback is not None
 True
 True
 True
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 2b358ca0466e..67a952d9b454 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -909,6 +909,47 @@ def test_original_unraisablehook(self):
         self.assertIn('Traceback (most recent call last):\n', err)
         self.assertIn('ValueError: 42\n', err)
 
+    def test_original_unraisablehook_err(self):
+        # bpo-22836: PyErr_WriteUnraisable() should give sensible reports
+        class BrokenDel:
+            def __del__(self):
+                exc = ValueError("del is broken")
+                # The following line is included in the traceback report:
+                raise exc
+
+        class BrokenStrException(Exception):
+            def __str__(self):
+                raise Exception("str() is broken")
+
+        class BrokenExceptionDel:
+            def __del__(self):
+                exc = BrokenStrException()
+                # The following line is included in the traceback report:
+                raise exc
+
+        for test_class in (BrokenDel, BrokenExceptionDel):
+            with self.subTest(test_class):
+                obj = test_class()
+                with test.support.captured_stderr() as stderr, \
+                     test.support.swap_attr(sys, 'unraisablehook',
+                                            sys.__unraisablehook__):
+                    # Trigger obj.__del__()
+                    del obj
+
+                report = stderr.getvalue()
+                self.assertIn("Exception ignored", report)
+                self.assertIn(test_class.__del__.__qualname__, report)
+                self.assertIn("test_sys.py", report)
+                self.assertIn("raise exc", report)
+                if test_class is BrokenExceptionDel:
+                    self.assertIn("BrokenStrException", report)
+                    self.assertIn("<exception str() failed>", report)
+                else:
+                    self.assertIn("ValueError", report)
+                    self.assertIn("del is broken", report)
+                self.assertTrue(report.endswith("\n"))
+
+
     def test_original_unraisablehook_wrong_type(self):
         exc = ValueError(42)
         with test.support.swap_attr(sys, 'unraisablehook',
diff --git a/Misc/NEWS.d/next/Tests/2019-05-22-12-57-15.bpo-36829.e9mRWC.rst b/Misc/NEWS.d/next/Tests/2019-05-22-12-57-15.bpo-36829.e9mRWC.rst
new file mode 100644
index 000000000000..4ab342b8a2b3
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2019-05-22-12-57-15.bpo-36829.e9mRWC.rst
@@ -0,0 +1,2 @@
+Add :func:`test.support.catch_unraisable_exception`: context manager
+catching unraisable exception using :func:`sys.unraisablehook`.



More information about the Python-checkins mailing list