Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b0c8369

Browse files
vstinnerJoannah Nanjekye
and
Joannah Nanjekye
authored
bpo-37531: Fix regrtest timeout for subprocesses (GH-15072)
Co-Authored-By: Joannah Nanjekye <[email protected]>
1 parent 6bccbe7 commit b0c8369

File tree

5 files changed

+78
-14
lines changed

5 files changed

+78
-14
lines changed

Lib/test/libregrtest/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from test.libregrtest.runtest import (
1515
findtests, runtest, get_abs_module,
1616
STDTESTS, NOTTESTS, PASSED, FAILED, ENV_CHANGED, SKIPPED, RESOURCE_DENIED,
17-
INTERRUPTED, CHILD_ERROR, TEST_DID_NOT_RUN,
17+
INTERRUPTED, CHILD_ERROR, TEST_DID_NOT_RUN, TIMEOUT,
1818
PROGRESS_MIN_TIME, format_test_result, is_failed)
1919
from test.libregrtest.setup import setup_tests
2020
from test.libregrtest.pgo import setup_pgo_tests
@@ -115,6 +115,8 @@ def accumulate_result(self, result, rerun=False):
115115
self.run_no_tests.append(test_name)
116116
elif ok == INTERRUPTED:
117117
self.interrupted = True
118+
elif ok == TIMEOUT:
119+
self.bad.append(test_name)
118120
else:
119121
raise ValueError("invalid test result: %r" % ok)
120122

Lib/test/libregrtest/runtest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from test import support
1414
from test.libregrtest.refleak import dash_R, clear_caches
1515
from test.libregrtest.save_env import saved_test_environment
16-
from test.libregrtest.utils import print_warning
16+
from test.libregrtest.utils import format_duration, print_warning
1717

1818

1919
# Test result constants.
@@ -25,6 +25,7 @@
2525
INTERRUPTED = -4
2626
CHILD_ERROR = -5 # error in a child process
2727
TEST_DID_NOT_RUN = -6
28+
TIMEOUT = -7
2829

2930
_FORMAT_TEST_RESULT = {
3031
PASSED: '%s passed',
@@ -35,6 +36,7 @@
3536
INTERRUPTED: '%s interrupted',
3637
CHILD_ERROR: '%s crashed',
3738
TEST_DID_NOT_RUN: '%s run no tests',
39+
TIMEOUT: '%s timed out',
3840
}
3941

4042
# Minimum duration of a test to display its duration or to mention that
@@ -75,7 +77,10 @@ def is_failed(result, ns):
7577

7678
def format_test_result(result):
7779
fmt = _FORMAT_TEST_RESULT.get(result.result, "%s")
78-
return fmt % result.test_name
80+
text = fmt % result.test_name
81+
if result.result == TIMEOUT:
82+
text = '%s (%s)' % (text, format_duration(result.test_time))
83+
return text
7984

8085

8186
def findtestdir(path=None):
@@ -179,6 +184,7 @@ def runtest(ns, test_name):
179184
FAILED test failed
180185
PASSED test passed
181186
EMPTY_TEST_SUITE test ran no subtests.
187+
TIMEOUT test timed out.
182188
183189
If ns.xmlpath is not None, xml_data is a list containing each
184190
generated testsuite element.

Lib/test/libregrtest/runtest_mp.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from test.libregrtest.runtest import (
1515
runtest, INTERRUPTED, CHILD_ERROR, PROGRESS_MIN_TIME,
16-
format_test_result, TestResult, is_failed)
16+
format_test_result, TestResult, is_failed, TIMEOUT)
1717
from test.libregrtest.setup import setup_tests
1818
from test.libregrtest.utils import format_duration
1919

@@ -103,11 +103,12 @@ class ExitThread(Exception):
103103

104104

105105
class MultiprocessThread(threading.Thread):
106-
def __init__(self, pending, output, ns):
106+
def __init__(self, pending, output, ns, timeout):
107107
super().__init__()
108108
self.pending = pending
109109
self.output = output
110110
self.ns = ns
111+
self.timeout = timeout
111112
self.current_test_name = None
112113
self.start_time = None
113114
self._popen = None
@@ -126,6 +127,12 @@ def __repr__(self):
126127
return '<%s>' % ' '.join(info)
127128

128129
def kill(self):
130+
"""
131+
Kill the current process (if any).
132+
133+
This method can be called by the thread running the process,
134+
or by another thread.
135+
"""
129136
self._killed = True
130137

131138
popen = self._popen
@@ -136,6 +143,13 @@ def kill(self):
136143
# does not hang
137144
popen.stdout.close()
138145
popen.stderr.close()
146+
popen.wait()
147+
148+
def mp_result_error(self, test_name, error_type, stdout='', stderr='',
149+
err_msg=None):
150+
test_time = time.monotonic() - self.start_time
151+
result = TestResult(test_name, error_type, test_time, None)
152+
return MultiprocessResult(result, stdout, stderr, err_msg)
139153

140154
def _runtest(self, test_name):
141155
try:
@@ -154,7 +168,19 @@ def _runtest(self, test_name):
154168
raise ExitThread
155169

156170
try:
171+
stdout, stderr = popen.communicate(timeout=self.timeout)
172+
except subprocess.TimeoutExpired:
173+
if self._killed:
174+
# kill() has been called: communicate() fails
175+
# on reading closed stdout/stderr
176+
raise ExitThread
177+
178+
popen.kill()
157179
stdout, stderr = popen.communicate()
180+
self.kill()
181+
182+
return self.mp_result_error(test_name, TIMEOUT,
183+
stdout, stderr)
158184
except OSError:
159185
if self._killed:
160186
# kill() has been called: communicate() fails
@@ -163,7 +189,6 @@ def _runtest(self, test_name):
163189
raise
164190
except:
165191
self.kill()
166-
popen.wait()
167192
raise
168193

169194
retcode = popen.wait()
@@ -191,8 +216,7 @@ def _runtest(self, test_name):
191216
err_msg = "Failed to parse worker JSON: %s" % exc
192217

193218
if err_msg is not None:
194-
test_time = time.monotonic() - self.start_time
195-
result = TestResult(test_name, CHILD_ERROR, test_time, None)
219+
return self.mp_result_error(test_name, CHILD_ERROR, stdout, stderr, err_msg)
196220

197221
return MultiprocessResult(result, stdout, stderr, err_msg)
198222

@@ -236,13 +260,16 @@ def __init__(self, regrtest):
236260
self.output = queue.Queue()
237261
self.pending = MultiprocessIterator(self.regrtest.tests)
238262
if self.ns.timeout is not None:
239-
self.test_timeout = self.ns.timeout * 1.5
263+
self.worker_timeout = self.ns.timeout * 1.5
264+
self.main_timeout = self.ns.timeout * 2.0
240265
else:
241-
self.test_timeout = None
266+
self.worker_timeout = None
267+
self.main_timeout = None
242268
self.workers = None
243269

244270
def start_workers(self):
245-
self.workers = [MultiprocessThread(self.pending, self.output, self.ns)
271+
self.workers = [MultiprocessThread(self.pending, self.output,
272+
self.ns, self.worker_timeout)
246273
for _ in range(self.ns.use_mp)]
247274
print("Run tests in parallel using %s child processes"
248275
% len(self.workers))
@@ -274,8 +301,8 @@ def _get_result(self):
274301
return None
275302

276303
while True:
277-
if self.test_timeout is not None:
278-
faulthandler.dump_traceback_later(self.test_timeout, exit=True)
304+
if self.main_timeout is not None:
305+
faulthandler.dump_traceback_later(self.main_timeout, exit=True)
279306

280307
# wait for a thread
281308
timeout = max(PROGRESS_UPDATE, PROGRESS_MIN_TIME)
@@ -343,7 +370,7 @@ def run_tests(self):
343370
print()
344371
self.regrtest.interrupted = True
345372
finally:
346-
if self.test_timeout is not None:
373+
if self.main_timeout is not None:
347374
faulthandler.cancel_dump_traceback_later()
348375

349376
# a test failed (and --failfast is set) or all tests completed

Lib/test/test_regrtest.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,33 @@ def test_garbage(self):
11541154
env_changed=[testname],
11551155
fail_env_changed=True)
11561156

1157+
def test_multiprocessing_timeout(self):
1158+
code = textwrap.dedent(r"""
1159+
import time
1160+
import unittest
1161+
try:
1162+
import faulthandler
1163+
except ImportError:
1164+
faulthandler = None
1165+
1166+
class Tests(unittest.TestCase):
1167+
# test hangs and so should be stopped by the timeout
1168+
def test_sleep(self):
1169+
# we want to test regrtest multiprocessing timeout,
1170+
# not faulthandler timeout
1171+
if faulthandler is not None:
1172+
faulthandler.cancel_dump_traceback_later()
1173+
1174+
time.sleep(60 * 5)
1175+
""")
1176+
testname = self.create_test(code=code)
1177+
1178+
output = self.run_tests("-j2", "--timeout=1.0", testname, exitcode=2)
1179+
self.check_executed_tests(output, [testname],
1180+
failed=testname)
1181+
self.assertRegex(output,
1182+
re.compile('%s timed out' % testname, re.MULTILINE))
1183+
11571184
def test_unraisable_exc(self):
11581185
# --fail-env-changed must catch unraisable exception
11591186
code = textwrap.dedent(r"""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"python3 -m test -jN --timeout=TIMEOUT" now kills a worker process if it runs
2+
longer than *TIMEOUT* seconds.

0 commit comments

Comments
 (0)