From b136b1aac4b7d07e3120ee59e41c02bc86032162 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 16 Apr 2021 14:33:10 +0200 Subject: 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__. --- Lib/test/libregrtest/setup.py | 4 ++- Lib/test/libregrtest/utils.py | 33 ++++++++++++++++++++++ Lib/test/test_regrtest.py | 33 +++++++++++++++++++++- Lib/test/test_socketserver.py | 7 +++-- Lib/test/test_threading.py | 13 +++++++++ .../Tests/2021-04-14-13-22-44.bpo-43843.ruIQKD.rst | 5 ++++ 6 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2021-04-14-13-22-44.bpo-43843.ruIQKD.rst diff --git a/Lib/test/libregrtest/setup.py b/Lib/test/libregrtest/setup.py index 715d4b9..83ce2f7 100644 --- a/Lib/test/libregrtest/setup.py +++ b/Lib/test/libregrtest/setup.py @@ -10,7 +10,8 @@ try: 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 setup_tests(ns): 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 13efdb4..89d7e7e 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 38321e0..054776c 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -1236,7 +1236,7 @@ class ArgsTestCase(BaseTestCase): 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 @@ class ArgsTestCase(BaseTestCase): 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 954e033..211321f 100644 --- a/Lib/test/test_socketserver.py +++ b/Lib/test/test_socketserver.py @@ -323,8 +323,11 @@ class ErrorHandlerTest(unittest.TestCase): 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 49a4af8..f44f17f 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -32,6 +32,11 @@ from test import support 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 @@ class ThreadTests(BaseTestCase): 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 @@ class ThreadRunFail(threading.Thread): 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 0000000..d1085ec --- /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. -- cgit v0.12