Skip to content

Commit b136b1a

Browse files
authored
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__.
1 parent 75ec103 commit b136b1a

File tree

6 files changed

+91
-4
lines changed

6 files changed

+91
-4
lines changed

Lib/test/libregrtest/setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
except ImportError:
1111
gc = None
1212

13-
from test.libregrtest.utils import setup_unraisable_hook
13+
from test.libregrtest.utils import (setup_unraisable_hook,
14+
setup_threading_excepthook)
1415

1516

1617
def setup_tests(ns):
@@ -81,6 +82,7 @@ def _test_audit_hook(name, args):
8182
sys.addaudithook(_test_audit_hook)
8283

8384
setup_unraisable_hook()
85+
setup_threading_excepthook()
8486

8587
if ns.timeout is not None:
8688
# For a slow buildbot worker, increase SHORT_TIMEOUT and LONG_TIMEOUT

Lib/test/libregrtest/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,23 @@ def print_warning(msg):
6868
orig_unraisablehook = None
6969

7070

71+
def flush_std_streams():
72+
if sys.stdout is not None:
73+
sys.stdout.flush()
74+
if sys.stderr is not None:
75+
sys.stderr.flush()
76+
77+
7178
def regrtest_unraisable_hook(unraisable):
7279
global orig_unraisablehook
7380
support.environment_altered = True
7481
print_warning("Unraisable exception")
7582
old_stderr = sys.stderr
7683
try:
84+
flush_std_streams()
7785
sys.stderr = sys.__stderr__
7886
orig_unraisablehook(unraisable)
87+
sys.stderr.flush()
7988
finally:
8089
sys.stderr = old_stderr
8190

@@ -86,6 +95,30 @@ def setup_unraisable_hook():
8695
sys.unraisablehook = regrtest_unraisable_hook
8796

8897

98+
orig_threading_excepthook = None
99+
100+
101+
def regrtest_threading_excepthook(args):
102+
global orig_threading_excepthook
103+
support.environment_altered = True
104+
print_warning(f"Uncaught thread exception: {args.exc_type.__name__}")
105+
old_stderr = sys.stderr
106+
try:
107+
flush_std_streams()
108+
sys.stderr = sys.__stderr__
109+
orig_threading_excepthook(args)
110+
sys.stderr.flush()
111+
finally:
112+
sys.stderr = old_stderr
113+
114+
115+
def setup_threading_excepthook():
116+
global orig_threading_excepthook
117+
import threading
118+
orig_threading_excepthook = threading.excepthook
119+
threading.excepthook = regrtest_threading_excepthook
120+
121+
89122
def clear_caches():
90123
# Clear the warnings registry, so they can be displayed again
91124
for mod in sys.modules.values():

Lib/test/test_regrtest.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1236,7 +1236,7 @@ def test_sleep(self):
12361236

12371237
def test_unraisable_exc(self):
12381238
# --fail-env-changed must catch unraisable exception.
1239-
# The exceptioin must be displayed even if sys.stderr is redirected.
1239+
# The exception must be displayed even if sys.stderr is redirected.
12401240
code = textwrap.dedent(r"""
12411241
import unittest
12421242
import weakref
@@ -1267,6 +1267,37 @@ def test_unraisable_exc(self):
12671267
self.assertIn("Warning -- Unraisable exception", output)
12681268
self.assertIn("Exception: weakref callback bug", output)
12691269

1270+
def test_threading_excepthook(self):
1271+
# --fail-env-changed must catch uncaught thread exception.
1272+
# The exception must be displayed even if sys.stderr is redirected.
1273+
code = textwrap.dedent(r"""
1274+
import threading
1275+
import unittest
1276+
from test.support import captured_stderr
1277+
1278+
class MyObject:
1279+
pass
1280+
1281+
def func_bug():
1282+
raise Exception("bug in thread")
1283+
1284+
class Tests(unittest.TestCase):
1285+
def test_threading_excepthook(self):
1286+
with captured_stderr() as stderr:
1287+
thread = threading.Thread(target=func_bug)
1288+
thread.start()
1289+
thread.join()
1290+
self.assertEqual(stderr.getvalue(), '')
1291+
""")
1292+
testname = self.create_test(code=code)
1293+
1294+
output = self.run_tests("--fail-env-changed", "-v", testname, exitcode=3)
1295+
self.check_executed_tests(output, [testname],
1296+
env_changed=[testname],
1297+
fail_env_changed=True)
1298+
self.assertIn("Warning -- Uncaught thread exception", output)
1299+
self.assertIn("Exception: bug in thread", output)
1300+
12701301
def test_cleanup(self):
12711302
dirname = os.path.join(self.tmptestdir, "test_python_123")
12721303
os.mkdir(dirname)

Lib/test/test_socketserver.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,11 @@ def test_threading_handled(self):
323323
self.check_result(handled=True)
324324

325325
def test_threading_not_handled(self):
326-
ThreadingErrorTestServer(SystemExit)
327-
self.check_result(handled=False)
326+
with threading_helper.catch_threading_exception() as cm:
327+
ThreadingErrorTestServer(SystemExit)
328+
self.check_result(handled=False)
329+
330+
self.assertIs(cm.exc_type, SystemExit)
328331

329332
@requires_forking
330333
def test_forking_handled(self):

Lib/test/test_threading.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
platforms_to_skip = ('netbsd5', 'hp-ux11')
3333

3434

35+
def restore_default_excepthook(testcase):
36+
testcase.addCleanup(setattr, threading, 'excepthook', threading.excepthook)
37+
threading.excepthook = threading.__excepthook__
38+
39+
3540
# A trivial mutable counter.
3641
class Counter(object):
3742
def __init__(self):
@@ -427,6 +432,8 @@ def _run(self, other_ref, yet_another):
427432
if self.should_raise:
428433
raise SystemExit
429434

435+
restore_default_excepthook(self)
436+
430437
cyclic_object = RunSelfFunction(should_raise=False)
431438
weak_cyclic_object = weakref.ref(cyclic_object)
432439
cyclic_object.thread.join()
@@ -1331,6 +1338,10 @@ def run(self):
13311338

13321339

13331340
class ExceptHookTests(BaseTestCase):
1341+
def setUp(self):
1342+
restore_default_excepthook(self)
1343+
super().setUp()
1344+
13341345
def test_excepthook(self):
13351346
with support.captured_output("stderr") as stderr:
13361347
thread = ThreadRunFail(name="excepthook thread")
@@ -1501,6 +1512,8 @@ class BarrierTests(lock_tests.BarrierTests):
15011512

15021513
class MiscTestCase(unittest.TestCase):
15031514
def test__all__(self):
1515+
restore_default_excepthook(self)
1516+
15041517
extra = {"ThreadError"}
15051518
not_exported = {'currentThread', 'activeCount'}
15061519
support.check__all__(self, threading, ('threading', '_thread'),
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:mod:`test.libregrtest` now marks a test as ENV_CHANGED (altered the execution
2+
environment) if a thread raises an exception but does not catch it. It sets a
3+
hook on :func:`threading.excepthook`. Use ``--fail-env-changed`` option to mark
4+
the test as failed.
5+
Patch by Victor Stinner.

0 commit comments

Comments
 (0)