Skip to content

Commit 278c1e1

Browse files
authored
bpo-40094: Add test.support.wait_process() (GH-19254)
Moreover, the following tests now check the child process exit code: * test_os.PtyTests * test_mailbox.test_lock_conflict() * test_tempfile.test_process_awareness() * test_uuid.testIssue8621() * multiprocessing resource tracker tests
1 parent 400e1db commit 278c1e1

19 files changed

+125
-103
lines changed

Doc/library/test.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,21 @@ The :mod:`test.support` module defines the following functions:
825825
target of the "as" clause, if there is one.
826826

827827

828+
.. function:: wait_process(pid, *, exitcode, timeout=None)
829+
830+
Wait until process *pid* completes and check that the process exit code is
831+
*exitcode*.
832+
833+
Raise an :exc:`AssertionError` if the process exit code is not equal to
834+
*exitcode*.
835+
836+
If the process runs longer than *timeout* seconds (:data:`SHORT_TIMEOUT` by
837+
default), kill the process and raise an :exc:`AssertionError`. The timeout
838+
feature is not available on Windows.
839+
840+
.. versionadded:: 3.9
841+
842+
828843
.. function:: wait_threads_exit(timeout=60.0)
829844

830845
Context manager to wait until all threads created in the ``with`` statement

Lib/test/_test_multiprocessing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5124,7 +5124,7 @@ def check_resource_tracker_death(self, signum, should_die):
51245124
pid = _resource_tracker._pid
51255125
if pid is not None:
51265126
os.kill(pid, signal.SIGKILL)
5127-
os.waitpid(pid, 0)
5127+
support.wait_process(pid, exitcode=-signal.SIGKILL)
51285128
with warnings.catch_warnings():
51295129
warnings.simplefilter("ignore")
51305130
_resource_tracker.ensure_running()

Lib/test/fork_wait.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,7 @@ def f(self, id):
4444
pass
4545

4646
def wait_impl(self, cpid):
47-
for i in range(10):
48-
# waitpid() shouldn't hang, but some of the buildbots seem to hang
49-
# in the forking tests. This is an attempt to fix the problem.
50-
spid, status = os.waitpid(cpid, os.WNOHANG)
51-
if spid == cpid:
52-
break
53-
time.sleep(2 * SHORTSLEEP)
54-
55-
self.assertEqual(spid, cpid)
56-
self.assertEqual(status, 0, "cause = %d, exit = %d" % (status&0xff, status>>8))
47+
support.wait_process(cpid, exitcode=0)
5748

5849
def test_wait(self):
5950
for i in range(NUM_THREADS):

Lib/test/support/__init__.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3400,3 +3400,62 @@ def __exit__(self, *exc_info):
34003400
del self.exc_value
34013401
del self.exc_traceback
34023402
del self.thread
3403+
3404+
3405+
def wait_process(pid, *, exitcode, timeout=None):
3406+
"""
3407+
Wait until process pid completes and check that the process exit code is
3408+
exitcode.
3409+
3410+
Raise an AssertionError if the process exit code is not equal to exitcode.
3411+
3412+
If the process runs longer than timeout seconds (SHORT_TIMEOUT by default),
3413+
kill the process (if signal.SIGKILL is available) and raise an
3414+
AssertionError. The timeout feature is not available on Windows.
3415+
"""
3416+
if os.name != "nt":
3417+
if timeout is None:
3418+
timeout = SHORT_TIMEOUT
3419+
t0 = time.monotonic()
3420+
deadline = t0 + timeout
3421+
sleep = 0.001
3422+
max_sleep = 0.1
3423+
while True:
3424+
pid2, status = os.waitpid(pid, os.WNOHANG)
3425+
if pid2 != 0:
3426+
break
3427+
# process is still running
3428+
3429+
dt = time.monotonic() - t0
3430+
if dt > SHORT_TIMEOUT:
3431+
try:
3432+
os.kill(pid, signal.SIGKILL)
3433+
os.waitpid(pid, 0)
3434+
except OSError:
3435+
# Ignore errors like ChildProcessError or PermissionError
3436+
pass
3437+
3438+
raise AssertionError(f"process {pid} is still running "
3439+
f"after {dt:.1f} seconds")
3440+
3441+
sleep = min(sleep * 2, max_sleep)
3442+
time.sleep(sleep)
3443+
3444+
if os.WIFEXITED(status):
3445+
exitcode2 = os.WEXITSTATUS(status)
3446+
elif os.WIFSIGNALED(status):
3447+
exitcode2 = -os.WTERMSIG(status)
3448+
else:
3449+
raise ValueError(f"invalid wait status: {status!r}")
3450+
else:
3451+
# Windows implementation
3452+
pid2, status = os.waitpid(pid, 0)
3453+
exitcode2 = (status >> 8)
3454+
3455+
if exitcode2 != exitcode:
3456+
raise AssertionError(f"process {pid} exited with code {exitcode2}, "
3457+
f"but exit code {exitcode} is expected")
3458+
3459+
# sanity check: it should not fail in practice
3460+
if pid2 != pid:
3461+
raise AssertionError(f"pid {pid2} != pid {pid}")

Lib/test/test_builtin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from textwrap import dedent
2626
from types import AsyncGeneratorType, FunctionType
2727
from operator import neg
28+
from test import support
2829
from test.support import (
2930
EnvironmentVarGuard, TESTFN, check_warnings, swap_attr, unlink,
3031
maybe_get_event_loop_policy)
@@ -1890,7 +1891,7 @@ def run_child(self, child, terminal_input):
18901891
os.close(fd)
18911892

18921893
# Wait until the child process completes
1893-
os.waitpid(pid, 0)
1894+
support.wait_process(pid, exitcode=0)
18941895

18951896
return lines
18961897

Lib/test/test_logging.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -727,30 +727,19 @@ def lock_holder_thread_fn():
727727

728728
locks_held__ready_to_fork.wait()
729729
pid = os.fork()
730-
if pid == 0: # Child.
730+
if pid == 0:
731+
# Child process
731732
try:
732733
test_logger.info(r'Child process did not deadlock. \o/')
733734
finally:
734735
os._exit(0)
735-
else: # Parent.
736+
else:
737+
# Parent process
736738
test_logger.info(r'Parent process returned from fork. \o/')
737739
fork_happened__release_locks_and_end_thread.set()
738740
lock_holder_thread.join()
739-
start_time = time.monotonic()
740-
while True:
741-
test_logger.debug('Waiting for child process.')
742-
waited_pid, status = os.waitpid(pid, os.WNOHANG)
743-
if waited_pid == pid:
744-
break # child process exited.
745-
if time.monotonic() - start_time > 7:
746-
break # so long? implies child deadlock.
747-
time.sleep(0.05)
748-
test_logger.debug('Done waiting.')
749-
if waited_pid != pid:
750-
os.kill(pid, signal.SIGKILL)
751-
waited_pid, status = os.waitpid(pid, 0)
752-
self.fail("child process deadlocked.")
753-
self.assertEqual(status, 0, msg="child process error")
741+
742+
support.wait_process(pid, exitcode=0)
754743

755744

756745
class BadStream(object):

Lib/test/test_mailbox.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,7 @@ def test_lock_conflict(self):
10921092
# Signal the child it can now release the lock and exit.
10931093
p.send(b'p')
10941094
# Wait for child to exit. Locking should now succeed.
1095-
exited_pid, status = os.waitpid(pid, 0)
1095+
support.wait_process(pid, exitcode=0)
10961096

10971097
self._box.lock()
10981098
self._box.unlock()

Lib/test/test_os.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2792,8 +2792,7 @@ def test_waitpid(self):
27922792
args = [sys.executable, '-c', 'pass']
27932793
# Add an implicit test for PyUnicode_FSConverter().
27942794
pid = os.spawnv(os.P_NOWAIT, FakePath(args[0]), args)
2795-
status = os.waitpid(pid, 0)
2796-
self.assertEqual(status, (pid, 0))
2795+
support.wait_process(pid, exitcode=0)
27972796

27982797

27992798
class SpawnTests(unittest.TestCase):
@@ -2877,14 +2876,7 @@ def test_spawnvpe(self):
28772876
def test_nowait(self):
28782877
args = self.create_args()
28792878
pid = os.spawnv(os.P_NOWAIT, args[0], args)
2880-
result = os.waitpid(pid, 0)
2881-
self.assertEqual(result[0], pid)
2882-
status = result[1]
2883-
if hasattr(os, 'WIFEXITED'):
2884-
self.assertTrue(os.WIFEXITED(status))
2885-
self.assertEqual(os.WEXITSTATUS(status), self.exitcode)
2886-
else:
2887-
self.assertEqual(status, self.exitcode << 8)
2879+
support.wait_process(pid, exitcode=self.exitcode)
28882880

28892881
@requires_os_func('spawnve')
28902882
def test_spawnve_bytes(self):

Lib/test/test_platform.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,7 @@ def test_mac_ver_with_fork(self):
236236

237237
else:
238238
# parent
239-
cpid, sts = os.waitpid(pid, 0)
240-
self.assertEqual(cpid, pid)
241-
self.assertEqual(sts, 0)
239+
support.wait_process(pid, exitcode=0)
242240

243241
def test_libc_ver(self):
244242
# check that libc_ver(executable) doesn't raise an exception

0 commit comments

Comments
 (0)