Skip to content

Commit b9b6900

Browse files
authored
bpo-31234: Add support.join_thread() helper (#3587)
join_thread() joins a thread but raises an AssertionError if the thread is still alive after timeout seconds.
1 parent 167cbde commit b9b6900

File tree

9 files changed

+50
-65
lines changed

9 files changed

+50
-65
lines changed

Lib/test/_test_multiprocessing.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import weakref
2222
import test.support
2323
import test.support.script_helper
24+
from test import support
2425

2526

2627
# Skip tests if _multiprocessing wasn't built.
@@ -72,6 +73,12 @@ def close_queue(queue):
7273
queue.join_thread()
7374

7475

76+
def join_process(process, timeout):
77+
# Since multiprocessing.Process has the same API than threading.Thread
78+
# (join() and is_alive(), the support function can be reused
79+
support.join_thread(process, timeout)
80+
81+
7582
#
7683
# Constants
7784
#
@@ -477,7 +484,7 @@ def test_many_processes(self):
477484
for p in procs:
478485
p.start()
479486
for p in procs:
480-
p.join(timeout=10)
487+
join_process(p, timeout=10)
481488
for p in procs:
482489
self.assertEqual(p.exitcode, 0)
483490

@@ -489,7 +496,7 @@ def test_many_processes(self):
489496
for p in procs:
490497
p.terminate()
491498
for p in procs:
492-
p.join(timeout=10)
499+
join_process(p, timeout=10)
493500
if os.name != 'nt':
494501
for p in procs:
495502
self.assertEqual(p.exitcode, -signal.SIGTERM)
@@ -652,7 +659,7 @@ def test_sys_exit(self):
652659
p = self.Process(target=self._test_sys_exit, args=(reason, testfn))
653660
p.daemon = True
654661
p.start()
655-
p.join(5)
662+
join_process(p, timeout=5)
656663
self.assertEqual(p.exitcode, 1)
657664

658665
with open(testfn, 'r') as f:
@@ -665,7 +672,7 @@ def test_sys_exit(self):
665672
p = self.Process(target=sys.exit, args=(reason,))
666673
p.daemon = True
667674
p.start()
668-
p.join(5)
675+
join_process(p, timeout=5)
669676
self.assertEqual(p.exitcode, reason)
670677

671678
#
@@ -1254,8 +1261,7 @@ def test_waitfor(self):
12541261
state.value += 1
12551262
cond.notify()
12561263

1257-
p.join(5)
1258-
self.assertFalse(p.is_alive())
1264+
join_process(p, timeout=5)
12591265
self.assertEqual(p.exitcode, 0)
12601266

12611267
@classmethod
@@ -1291,7 +1297,7 @@ def test_waitfor_timeout(self):
12911297
state.value += 1
12921298
cond.notify()
12931299

1294-
p.join(5)
1300+
join_process(p, timeout=5)
12951301
self.assertTrue(success.value)
12961302

12971303
@classmethod
@@ -4005,7 +4011,7 @@ def test_timeout(self):
40054011
self.assertEqual(conn.recv(), 456)
40064012
conn.close()
40074013
l.close()
4008-
p.join(10)
4014+
join_process(p, timeout=10)
40094015
finally:
40104016
socket.setdefaulttimeout(old_timeout)
40114017

@@ -4041,7 +4047,7 @@ def child(cls, n, conn):
40414047
p = multiprocessing.Process(target=cls.child, args=(n-1, conn))
40424048
p.start()
40434049
conn.close()
4044-
p.join(timeout=5)
4050+
join_process(p, timeout=5)
40454051
else:
40464052
conn.send(len(util._afterfork_registry))
40474053
conn.close()
@@ -4054,7 +4060,7 @@ def test_lock(self):
40544060
p.start()
40554061
w.close()
40564062
new_size = r.recv()
4057-
p.join(timeout=5)
4063+
join_process(p, timeout=5)
40584064
self.assertLessEqual(new_size, old_size)
40594065

40604066
#
@@ -4109,7 +4115,7 @@ def test_closefd(self):
41094115
p.start()
41104116
writer.close()
41114117
e = reader.recv()
4112-
p.join(timeout=5)
4118+
join_process(p, timeout=5)
41134119
finally:
41144120
self.close(fd)
41154121
writer.close()

Lib/test/support/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2107,6 +2107,16 @@ def wait_threads_exit(timeout=60.0):
21072107
gc_collect()
21082108

21092109

2110+
def join_thread(thread, timeout=30.0):
2111+
"""Join a thread. Raise an AssertionError if the thread is still alive
2112+
after timeout seconds.
2113+
"""
2114+
thread.join(timeout)
2115+
if thread.is_alive():
2116+
msg = f"failed to join the thread in {timeout:.1f} seconds"
2117+
raise AssertionError(msg)
2118+
2119+
21102120
def reap_children():
21112121
"""Use this function at the end of test_main() whenever sub-processes
21122122
are started. This will help ensure that no extra children (zombies)

Lib/test/test_asynchat.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,7 @@ def line_terminator_check(self, term, server_chunk):
123123
c.push(b"I'm not dead yet!" + term)
124124
c.push(SERVER_QUIT)
125125
asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01)
126-
s.join(timeout=TIMEOUT)
127-
if s.is_alive():
128-
self.fail("join() timed out")
126+
support.join_thread(s, timeout=TIMEOUT)
129127

130128
self.assertEqual(c.contents, [b"hello world", b"I'm not dead yet!"])
131129

@@ -156,9 +154,7 @@ def numeric_terminator_check(self, termlen):
156154
c.push(data)
157155
c.push(SERVER_QUIT)
158156
asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01)
159-
s.join(timeout=TIMEOUT)
160-
if s.is_alive():
161-
self.fail("join() timed out")
157+
support.join_thread(s, timeout=TIMEOUT)
162158

163159
self.assertEqual(c.contents, [data[:termlen]])
164160

@@ -178,9 +174,7 @@ def test_none_terminator(self):
178174
c.push(data)
179175
c.push(SERVER_QUIT)
180176
asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01)
181-
s.join(timeout=TIMEOUT)
182-
if s.is_alive():
183-
self.fail("join() timed out")
177+
support.join_thread(s, timeout=TIMEOUT)
184178

185179
self.assertEqual(c.contents, [])
186180
self.assertEqual(c.buffer, data)
@@ -192,9 +186,7 @@ def test_simple_producer(self):
192186
p = asynchat.simple_producer(data+SERVER_QUIT, buffer_size=8)
193187
c.push_with_producer(p)
194188
asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01)
195-
s.join(timeout=TIMEOUT)
196-
if s.is_alive():
197-
self.fail("join() timed out")
189+
support.join_thread(s, timeout=TIMEOUT)
198190

199191
self.assertEqual(c.contents, [b"hello world", b"I'm not dead yet!"])
200192

@@ -204,9 +196,7 @@ def test_string_producer(self):
204196
data = b"hello world\nI'm not dead yet!\n"
205197
c.push_with_producer(data+SERVER_QUIT)
206198
asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01)
207-
s.join(timeout=TIMEOUT)
208-
if s.is_alive():
209-
self.fail("join() timed out")
199+
support.join_thread(s, timeout=TIMEOUT)
210200

211201
self.assertEqual(c.contents, [b"hello world", b"I'm not dead yet!"])
212202

@@ -217,9 +207,7 @@ def test_empty_line(self):
217207
c.push(b"hello world\n\nI'm not dead yet!\n")
218208
c.push(SERVER_QUIT)
219209
asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01)
220-
s.join(timeout=TIMEOUT)
221-
if s.is_alive():
222-
self.fail("join() timed out")
210+
support.join_thread(s, timeout=TIMEOUT)
223211

224212
self.assertEqual(c.contents,
225213
[b"hello world", b"", b"I'm not dead yet!"])
@@ -238,9 +226,7 @@ def test_close_when_done(self):
238226
# where the server echoes all of its data before we can check that it
239227
# got any down below.
240228
s.start_resend_event.set()
241-
s.join(timeout=TIMEOUT)
242-
if s.is_alive():
243-
self.fail("join() timed out")
229+
support.join_thread(s, timeout=TIMEOUT)
244230

245231
self.assertEqual(c.contents, [])
246232
# the server might have been able to send a byte or two back, but this
@@ -261,7 +247,7 @@ def test_push(self):
261247
self.assertRaises(TypeError, c.push, 'unicode')
262248
c.push(SERVER_QUIT)
263249
asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01)
264-
s.join(timeout=TIMEOUT)
250+
support.join_thread(s, timeout=TIMEOUT)
265251
self.assertEqual(c.contents, [b'bytes', b'bytes', b'bytes'])
266252

267253

Lib/test/test_asyncio/test_events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ def client():
808808
proto.transport.close()
809809
lsock.close()
810810

811-
thread.join(1)
811+
support.join_thread(thread, timeout=1)
812812
self.assertFalse(thread.is_alive())
813813
self.assertEqual(proto.state, 'CLOSED')
814814
self.assertEqual(proto.nbytes, len(message))

Lib/test/test_asyncore.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,7 @@ def test_send(self):
360360

361361
self.assertEqual(cap.getvalue(), data*2)
362362
finally:
363-
t.join(timeout=TIMEOUT)
364-
if t.is_alive():
365-
self.fail("join() timed out")
363+
support.join_thread(t, timeout=TIMEOUT)
366364

367365

368366
@unittest.skipUnless(hasattr(asyncore, 'file_wrapper'),
@@ -794,9 +792,7 @@ def test_quick_connect(self):
794792
except OSError:
795793
pass
796794
finally:
797-
t.join(timeout=TIMEOUT)
798-
if t.is_alive():
799-
self.fail("join() timed out")
795+
support.join_thread(t, timeout=TIMEOUT)
800796

801797
class TestAPI_UseIPv4Sockets(BaseTestAPI):
802798
family = socket.AF_INET

Lib/test/test_imaplib.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@ def _cleanup(self):
220220
# cleanup the server
221221
self.server.shutdown()
222222
self.server.server_close()
223-
self.thread.join(3.0)
223+
support.join_thread(self.thread, 3.0)
224+
# Explicitly clear the attribute to prevent dangling thread
225+
self.thread = None
224226

225227
def test_EOF_without_complete_welcome_message(self):
226228
# http://bugs.python.org/issue5949

Lib/test/test_logging.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -791,13 +791,10 @@ def stop(self, timeout=None):
791791
to terminate.
792792
"""
793793
self.close()
794-
self._thread.join(timeout)
794+
support.join_thread(self._thread, timeout)
795+
self._thread = None
795796
asyncore.close_all(map=self._map, ignore_all=True)
796797

797-
alive = self._thread.is_alive()
798-
self._thread = None
799-
if alive:
800-
self.fail("join() timed out")
801798

802799
class ControlMixin(object):
803800
"""
@@ -847,11 +844,8 @@ def stop(self, timeout=None):
847844
"""
848845
self.shutdown()
849846
if self._thread is not None:
850-
self._thread.join(timeout)
851-
alive = self._thread.is_alive()
847+
support.join_thread(self._thread, timeout)
852848
self._thread = None
853-
if alive:
854-
self.fail("join() timed out")
855849
self.server_close()
856850
self.ready.clear()
857851

@@ -2892,9 +2886,7 @@ def setup_via_listener(self, text, verify=None):
28922886
finally:
28932887
t.ready.wait(2.0)
28942888
logging.config.stopListening()
2895-
t.join(2.0)
2896-
if t.is_alive():
2897-
self.fail("join() timed out")
2889+
support.join_thread(t, 2.0)
28982890

28992891
def test_listen_config_10_ok(self):
29002892
with support.captured_stdout() as output:

Lib/test/test_queue.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ def do_blocking_test(self, block_func, block_args, trigger_func, trigger_args):
5858
block_func)
5959
return self.result
6060
finally:
61-
thread.join(10) # make sure the thread terminates
62-
if thread.is_alive():
63-
self.fail("trigger function '%r' appeared to not return" %
64-
trigger_func)
61+
support.join_thread(thread, 10) # make sure the thread terminates
6562

6663
# Call this instead if block_func is supposed to raise an exception.
6764
def do_exceptional_blocking_test(self,block_func, block_args, trigger_func,
@@ -77,10 +74,7 @@ def do_exceptional_blocking_test(self,block_func, block_args, trigger_func,
7774
self.fail("expected exception of kind %r" %
7875
expected_exception_class)
7976
finally:
80-
thread.join(10) # make sure the thread terminates
81-
if thread.is_alive():
82-
self.fail("trigger function '%r' appeared to not return" %
83-
trigger_func)
77+
support.join_thread(thread, 10) # make sure the thread terminates
8478
if not thread.startedEvent.is_set():
8579
self.fail("trigger thread ended but event never set")
8680

Lib/test/test_sched.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import threading
44
import time
55
import unittest
6+
from test import support
67

78

89
TIMEOUT = 10
@@ -81,8 +82,7 @@ def test_enter_concurrent(self):
8182
self.assertEqual(q.get(timeout=TIMEOUT), 5)
8283
self.assertTrue(q.empty())
8384
timer.advance(1000)
84-
t.join(timeout=TIMEOUT)
85-
self.assertFalse(t.is_alive())
85+
support.join_thread(t, timeout=TIMEOUT)
8686
self.assertTrue(q.empty())
8787
self.assertEqual(timer.time(), 5)
8888

@@ -137,8 +137,7 @@ def test_cancel_concurrent(self):
137137
self.assertEqual(q.get(timeout=TIMEOUT), 4)
138138
self.assertTrue(q.empty())
139139
timer.advance(1000)
140-
t.join(timeout=TIMEOUT)
141-
self.assertFalse(t.is_alive())
140+
support.join_thread(t, timeout=TIMEOUT)
142141
self.assertTrue(q.empty())
143142
self.assertEqual(timer.time(), 4)
144143

0 commit comments

Comments
 (0)