Skip to content

bpo-43843: libregrtest uses threading.excepthook #25400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Lib/test/libregrtest/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions Lib/test/libregrtest/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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():
Expand Down
33 changes: 32 additions & 1 deletion Lib/test/test_regrtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 5 additions & 2 deletions Lib/test/test_socketserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
@@ -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.