bpo-37531: Fix regrtest timeout for subprocesses (GH-15072) · python/cpython@b0c8369

@@ -13,7 +13,7 @@

13131414

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):

103103104104105105

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)

127128128129

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

130137131138

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)

139153140154

def _runtest(self, test_name):

141155

try:

@@ -154,7 +168,19 @@ def _runtest(self, test_name):

154168

raise ExitThread

155169156170

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

168193169194

retcode = popen.wait()

@@ -191,8 +216,7 @@ def _runtest(self, test_name):

191216

err_msg = "Failed to parse worker JSON: %s" % exc

192217193218

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)

196220197221

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

243269244270

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

275302276303

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)

279306280307

# 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()

348375349376

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