[Python-checkins] bpo-43843: libregrtest uses threading.excepthook (GH-25400)

vstinner webhook-mailer at python.org
Fri Apr 16 08:33:14 EDT 2021


https://github.com/python/cpython/commit/b136b1aac4b7d07e3120ee59e41c02bc86032162
commit: b136b1aac4b7d07e3120ee59e41c02bc86032162
branch: master
author: Victor Stinner <vstinner at python.org>
committer: vstinner <vstinner at python.org>
date: 2021-04-16T14:33:10+02:00
summary:

bpo-43843: libregrtest uses threading.excepthook (GH-25400)

test.libregrtest now marks a test as ENV_CHANGED (altered the
execution environment) if a thread raises an exception but does not
catch it. It sets a hook on threading.excepthook. Use
--fail-env-changed option to mark the test as failed.

libregrtest regrtest_unraisable_hook() explicitly flushs
sys.stdout, sys.stderr and sys.__stderr__.

files:
A Misc/NEWS.d/next/Tests/2021-04-14-13-22-44.bpo-43843.ruIQKD.rst
M Lib/test/libregrtest/setup.py
M Lib/test/libregrtest/utils.py
M Lib/test/test_regrtest.py
M Lib/test/test_socketserver.py
M Lib/test/test_threading.py

diff --git a/Lib/test/libregrtest/setup.py b/Lib/test/libregrtest/setup.py
index 715d4b96cf91d..83ce2f73f4e5a 100644
--- a/Lib/test/libregrtest/setup.py
+++ b/Lib/test/libregrtest/setup.py
@@ -10,7 +10,8 @@
 except ImportError:
     gc = None
 
-from test.libregrtest.utils import setup_unraisable_hook
+from test.libregrtest.utils import (setup_unraisable_hook,
+                                    setup_threading_excepthook)
 
 
 def setup_tests(ns):
@@ -81,6 +82,7 @@ def _test_audit_hook(name, args):
         sys.addaudithook(_test_audit_hook)
 
     setup_unraisable_hook()
+    setup_threading_excepthook()
 
     if ns.timeout is not None:
         # For a slow buildbot worker, increase SHORT_TIMEOUT and LONG_TIMEOUT
diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py
index 13efdb45818b7..89d7e7e535405 100644
--- a/Lib/test/libregrtest/utils.py
+++ b/Lib/test/libregrtest/utils.py
@@ -68,14 +68,23 @@ def print_warning(msg):
 orig_unraisablehook = None
 
 
+def flush_std_streams():
+    if sys.stdout is not None:
+        sys.stdout.flush()
+    if sys.stderr is not None:
+        sys.stderr.flush()
+
+
 def regrtest_unraisable_hook(unraisable):
     global orig_unraisablehook
     support.environment_altered = True
     print_warning("Unraisable exception")
     old_stderr = sys.stderr
     try:
+        flush_std_streams()
         sys.stderr = sys.__stderr__
         orig_unraisablehook(unraisable)
+        sys.stderr.flush()
     finally:
         sys.stderr = old_stderr
 
@@ -86,6 +95,30 @@ def setup_unraisable_hook():
     sys.unraisablehook = regrtest_unraisable_hook
 
 
+orig_threading_excepthook = None
+
+
+def regrtest_threading_excepthook(args):
+    global orig_threading_excepthook
+    support.environment_altered = True
+    print_warning(f"Uncaught thread exception: {args.exc_type.__name__}")
+    old_stderr = sys.stderr
+    try:
+        flush_std_streams()
+        sys.stderr = sys.__stderr__
+        orig_threading_excepthook(args)
+        sys.stderr.flush()
+    finally:
+        sys.stderr = old_stderr
+
+
+def setup_threading_excepthook():
+    global orig_threading_excepthook
+    import threading
+    orig_threading_excepthook = threading.excepthook
+    threading.excepthook = regrtest_threading_excepthook
+
+
 def clear_caches():
     # Clear the warnings registry, so they can be displayed again
     for mod in sys.modules.values():
diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py
index 38321e04b54a9..054776ccf4808 100644
--- a/Lib/test/test_regrtest.py
+++ b/Lib/test/test_regrtest.py
@@ -1236,7 +1236,7 @@ def test_sleep(self):
 
     def test_unraisable_exc(self):
         # --fail-env-changed must catch unraisable exception.
-        # The exceptioin must be displayed even if sys.stderr is redirected.
+        # The exception must be displayed even if sys.stderr is redirected.
         code = textwrap.dedent(r"""
             import unittest
             import weakref
@@ -1267,6 +1267,37 @@ def test_unraisable_exc(self):
         self.assertIn("Warning -- Unraisable exception", output)
         self.assertIn("Exception: weakref callback bug", output)
 
+    def test_threading_excepthook(self):
+        # --fail-env-changed must catch uncaught thread exception.
+        # The exception must be displayed even if sys.stderr is redirected.
+        code = textwrap.dedent(r"""
+            import threading
+            import unittest
+            from test.support import captured_stderr
+
+            class MyObject:
+                pass
+
+            def func_bug():
+                raise Exception("bug in thread")
+
+            class Tests(unittest.TestCase):
+                def test_threading_excepthook(self):
+                    with captured_stderr() as stderr:
+                        thread = threading.Thread(target=func_bug)
+                        thread.start()
+                        thread.join()
+                    self.assertEqual(stderr.getvalue(), '')
+        """)
+        testname = self.create_test(code=code)
+
+        output = self.run_tests("--fail-env-changed", "-v", testname, exitcode=3)
+        self.check_executed_tests(output, [testname],
+                                  env_changed=[testname],
+                                  fail_env_changed=True)
+        self.assertIn("Warning -- Uncaught thread exception", output)
+        self.assertIn("Exception: bug in thread", output)
+
     def test_cleanup(self):
         dirname = os.path.join(self.tmptestdir, "test_python_123")
         os.mkdir(dirname)
diff --git a/Lib/test/test_socketserver.py b/Lib/test/test_socketserver.py
index 954e0331352fb..211321f37617e 100644
--- a/Lib/test/test_socketserver.py
+++ b/Lib/test/test_socketserver.py
@@ -323,8 +323,11 @@ def test_threading_handled(self):
         self.check_result(handled=True)
 
     def test_threading_not_handled(self):
-        ThreadingErrorTestServer(SystemExit)
-        self.check_result(handled=False)
+        with threading_helper.catch_threading_exception() as cm:
+            ThreadingErrorTestServer(SystemExit)
+            self.check_result(handled=False)
+
+            self.assertIs(cm.exc_type, SystemExit)
 
     @requires_forking
     def test_forking_handled(self):
diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py
index 49a4af8365afc..f44f17f2978f7 100644
--- a/Lib/test/test_threading.py
+++ b/Lib/test/test_threading.py
@@ -32,6 +32,11 @@
 platforms_to_skip = ('netbsd5', 'hp-ux11')
 
 
+def restore_default_excepthook(testcase):
+    testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook)
+    threading.excepthook = threading.__excepthook__
+
+
 # A trivial mutable counter.
 class Counter(object):
     def __init__(self):
@@ -427,6 +432,8 @@ def _run(self, other_ref, yet_another):
                 if self.should_raise:
                     raise SystemExit
 
+        restore_default_excepthook(self)
+
         cyclic_object = RunSelfFunction(should_raise=False)
         weak_cyclic_object = weakref.ref(cyclic_object)
         cyclic_object.thread.join()
@@ -1331,6 +1338,10 @@ def run(self):
 
 
 class ExceptHookTests(BaseTestCase):
+    def setUp(self):
+        restore_default_excepthook(self)
+        super().setUp()
+
     def test_excepthook(self):
         with support.captured_output("stderr") as stderr:
             thread = ThreadRunFail(name="excepthook thread")
@@ -1501,6 +1512,8 @@ class BarrierTests(lock_tests.BarrierTests):
 
 class MiscTestCase(unittest.TestCase):
     def test__all__(self):
+        restore_default_excepthook(self)
+
         extra = {"ThreadError"}
         not_exported = {'currentThread', 'activeCount'}
         support.check__all__(self, threading, ('threading', '_thread'),
diff --git a/Misc/NEWS.d/next/Tests/2021-04-14-13-22-44.bpo-43843.ruIQKD.rst b/Misc/NEWS.d/next/Tests/2021-04-14-13-22-44.bpo-43843.ruIQKD.rst
new file mode 100644
index 0000000000000..d1085ec2395b8
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2021-04-14-13-22-44.bpo-43843.ruIQKD.rst
@@ -0,0 +1,5 @@
+:mod:`test.libregrtest` now marks a test as ENV_CHANGED (altered the execution
+environment) if a thread raises an exception but does not catch it. It sets a
+hook on :func:`threading.excepthook`. Use ``--fail-env-changed`` option to mark
+the test as failed.
+Patch by Victor Stinner.



More information about the Python-checkins mailing list