Skip to content

Commit b0917df

Browse files
authored
bpo-36719: regrtest -jN no longer stops on crash (GH-13231)
"python3 -m test -jN ..." now continues the execution of next tests when a worker process crash (CHILD_ERROR state). Previously, the test suite stopped immediately. Use --failfast to stop at the first error. Moreover, --forever now also implies --failfast.
1 parent 85c69d5 commit b0917df

File tree

5 files changed

+49
-18
lines changed

5 files changed

+49
-18
lines changed

Lib/test/libregrtest/cmdline.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def _create_parser():
256256
help='suppress error message boxes on Windows')
257257
group.add_argument('-F', '--forever', action='store_true',
258258
help='run the specified tests in a loop, until an '
259-
'error happens')
259+
'error happens; imply --failfast')
260260
group.add_argument('--list-tests', action='store_true',
261261
help="only write the name of tests that will be run, "
262262
"don't execute them")
@@ -389,5 +389,8 @@ def _parse_args(args, **kwargs):
389389
with open(ns.match_filename) as fp:
390390
for line in fp:
391391
ns.match_tests.append(line.strip())
392+
if ns.forever:
393+
# --forever implies --failfast
394+
ns.failfast = True
392395

393396
return ns

Lib/test/libregrtest/main.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
findtests, runtest, get_abs_module,
1717
STDTESTS, NOTTESTS, PASSED, FAILED, ENV_CHANGED, SKIPPED, RESOURCE_DENIED,
1818
INTERRUPTED, CHILD_ERROR, TEST_DID_NOT_RUN,
19-
PROGRESS_MIN_TIME, format_test_result)
19+
PROGRESS_MIN_TIME, format_test_result, is_failed)
2020
from test.libregrtest.setup import setup_tests
2121
from test.libregrtest.utils import removepy, count, format_duration, printlist
2222
from test import support
@@ -404,7 +404,7 @@ def run_tests_sequential(self):
404404
test_time = time.monotonic() - start_time
405405
if test_time >= PROGRESS_MIN_TIME:
406406
previous_test = "%s in %s" % (previous_test, format_duration(test_time))
407-
elif result[0] == PASSED:
407+
elif result.result == PASSED:
408408
# be quiet: say nothing if the test passed shortly
409409
previous_test = None
410410

@@ -413,6 +413,9 @@ def run_tests_sequential(self):
413413
if module not in save_modules and module.startswith("test."):
414414
support.unload(module)
415415

416+
if self.ns.failfast and is_failed(result, self.ns):
417+
break
418+
416419
if previous_test:
417420
print(previous_test)
418421

Lib/test/libregrtest/runtest.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
RESOURCE_DENIED = -3
2525
INTERRUPTED = -4
2626
CHILD_ERROR = -5 # error in a child process
27-
TEST_DID_NOT_RUN = -6 # error in a child process
27+
TEST_DID_NOT_RUN = -6
2828

2929
_FORMAT_TEST_RESULT = {
3030
PASSED: '%s passed',
@@ -64,6 +64,15 @@
6464
FOUND_GARBAGE = []
6565

6666

67+
def is_failed(result, ns):
68+
ok = result.result
69+
if ok in (PASSED, RESOURCE_DENIED, SKIPPED, TEST_DID_NOT_RUN):
70+
return False
71+
if ok == ENV_CHANGED:
72+
return ns.fail_env_changed
73+
return True
74+
75+
6776
def format_test_result(result):
6877
fmt = _FORMAT_TEST_RESULT.get(result.result, "%s")
6978
return fmt % result.test_name

Lib/test/libregrtest/runtest_mp.py

Lines changed: 27 additions & 14 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)
16+
format_test_result, TestResult, is_failed)
1717
from test.libregrtest.setup import setup_tests
1818
from test.libregrtest.utils import format_duration
1919

@@ -22,8 +22,12 @@
2222
PROGRESS_UPDATE = 30.0 # seconds
2323

2424

25-
def must_stop(result):
26-
return result.result in (INTERRUPTED, CHILD_ERROR)
25+
def must_stop(result, ns):
26+
if result.result == INTERRUPTED:
27+
return True
28+
if ns.failfast and is_failed(result, ns):
29+
return True
30+
return False
2731

2832

2933
def run_test_in_subprocess(testname, ns):
@@ -66,16 +70,22 @@ class MultiprocessIterator:
6670

6771
"""A thread-safe iterator over tests for multiprocess mode."""
6872

69-
def __init__(self, tests):
73+
def __init__(self, tests_iter):
7074
self.lock = threading.Lock()
71-
self.tests = tests
75+
self.tests_iter = tests_iter
7276

7377
def __iter__(self):
7478
return self
7579

7680
def __next__(self):
7781
with self.lock:
78-
return next(self.tests)
82+
if self.tests_iter is None:
83+
raise StopIteration
84+
return next(self.tests_iter)
85+
86+
def stop(self):
87+
with self.lock:
88+
self.tests_iter = None
7989

8090

8191
MultiprocessResult = collections.namedtuple('MultiprocessResult',
@@ -92,23 +102,24 @@ def __init__(self, pending, output, ns):
92102
self._popen = None
93103

94104
def kill(self):
95-
if not self.is_alive():
105+
popen = self._popen
106+
if popen is None:
96107
return
97-
if self._popen is not None:
98-
self._popen.kill()
108+
print("Kill regrtest worker process %s" % popen.pid)
109+
popen.kill()
99110

100111
def _runtest(self, test_name):
101112
try:
102113
self.start_time = time.monotonic()
103114
self.current_test_name = test_name
104115

105-
popen = run_test_in_subprocess(test_name, self.ns)
106-
self._popen = popen
116+
self._popen = run_test_in_subprocess(test_name, self.ns)
117+
popen = self._popen
107118
with popen:
108119
try:
109120
stdout, stderr = popen.communicate()
110121
except:
111-
popen.kill()
122+
self.kill()
112123
popen.wait()
113124
raise
114125

@@ -153,7 +164,7 @@ def run(self):
153164
mp_result = self._runtest(test_name)
154165
self.output.put((False, mp_result))
155166

156-
if must_stop(mp_result.result):
167+
if must_stop(mp_result.result, self.ns):
157168
break
158169
except BaseException:
159170
self.output.put((True, traceback.format_exc()))
@@ -255,7 +266,7 @@ def _process_result(self, item):
255266
if mp_result.stderr and not self.ns.pgo:
256267
print(mp_result.stderr, file=sys.stderr, flush=True)
257268

258-
if must_stop(mp_result.result):
269+
if must_stop(mp_result.result, self.ns):
259270
return True
260271

261272
return False
@@ -280,6 +291,8 @@ def run_tests(self):
280291
if self.test_timeout is not None:
281292
faulthandler.cancel_dump_traceback_later()
282293

294+
# a test failed (and --failfast is set) or all tests completed
295+
self.pending.stop()
283296
self.wait_workers()
284297

285298

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"python3 -m test -jN ..." now continues the execution of next tests when a
2+
worker process crash (CHILD_ERROR state). Previously, the test suite stopped
3+
immediately. Use --failfast to stop at the first error.

0 commit comments

Comments
 (0)