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.
This commit is contained in:
Victor Stinner 2019-05-13 19:17:54 +02:00 committed by GitHub
parent 85c69d5c4c
commit b0917df329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 49 additions and 18 deletions

View File

@ -256,7 +256,7 @@ def _create_parser():
help='suppress error message boxes on Windows') help='suppress error message boxes on Windows')
group.add_argument('-F', '--forever', action='store_true', group.add_argument('-F', '--forever', action='store_true',
help='run the specified tests in a loop, until an ' help='run the specified tests in a loop, until an '
'error happens') 'error happens; imply --failfast')
group.add_argument('--list-tests', action='store_true', group.add_argument('--list-tests', action='store_true',
help="only write the name of tests that will be run, " help="only write the name of tests that will be run, "
"don't execute them") "don't execute them")
@ -389,5 +389,8 @@ def _parse_args(args, **kwargs):
with open(ns.match_filename) as fp: with open(ns.match_filename) as fp:
for line in fp: for line in fp:
ns.match_tests.append(line.strip()) ns.match_tests.append(line.strip())
if ns.forever:
# --forever implies --failfast
ns.failfast = True
return ns return ns

View File

@ -16,7 +16,7 @@ from test.libregrtest.runtest import (
findtests, runtest, get_abs_module, findtests, runtest, get_abs_module,
STDTESTS, NOTTESTS, PASSED, FAILED, ENV_CHANGED, SKIPPED, RESOURCE_DENIED, STDTESTS, NOTTESTS, PASSED, FAILED, ENV_CHANGED, SKIPPED, RESOURCE_DENIED,
INTERRUPTED, CHILD_ERROR, TEST_DID_NOT_RUN, INTERRUPTED, CHILD_ERROR, TEST_DID_NOT_RUN,
PROGRESS_MIN_TIME, format_test_result) PROGRESS_MIN_TIME, format_test_result, is_failed)
from test.libregrtest.setup import setup_tests from test.libregrtest.setup import setup_tests
from test.libregrtest.utils import removepy, count, format_duration, printlist from test.libregrtest.utils import removepy, count, format_duration, printlist
from test import support from test import support
@ -404,7 +404,7 @@ class Regrtest:
test_time = time.monotonic() - start_time test_time = time.monotonic() - start_time
if test_time >= PROGRESS_MIN_TIME: if test_time >= PROGRESS_MIN_TIME:
previous_test = "%s in %s" % (previous_test, format_duration(test_time)) previous_test = "%s in %s" % (previous_test, format_duration(test_time))
elif result[0] == PASSED: elif result.result == PASSED:
# be quiet: say nothing if the test passed shortly # be quiet: say nothing if the test passed shortly
previous_test = None previous_test = None
@ -413,6 +413,9 @@ class Regrtest:
if module not in save_modules and module.startswith("test."): if module not in save_modules and module.startswith("test."):
support.unload(module) support.unload(module)
if self.ns.failfast and is_failed(result, self.ns):
break
if previous_test: if previous_test:
print(previous_test) print(previous_test)

View File

@ -24,7 +24,7 @@ SKIPPED = -2
RESOURCE_DENIED = -3 RESOURCE_DENIED = -3
INTERRUPTED = -4 INTERRUPTED = -4
CHILD_ERROR = -5 # error in a child process CHILD_ERROR = -5 # error in a child process
TEST_DID_NOT_RUN = -6 # error in a child process TEST_DID_NOT_RUN = -6
_FORMAT_TEST_RESULT = { _FORMAT_TEST_RESULT = {
PASSED: '%s passed', PASSED: '%s passed',
@ -64,6 +64,15 @@ NOTTESTS = set()
FOUND_GARBAGE = [] FOUND_GARBAGE = []
def is_failed(result, ns):
ok = result.result
if ok in (PASSED, RESOURCE_DENIED, SKIPPED, TEST_DID_NOT_RUN):
return False
if ok == ENV_CHANGED:
return ns.fail_env_changed
return True
def format_test_result(result): def format_test_result(result):
fmt = _FORMAT_TEST_RESULT.get(result.result, "%s") fmt = _FORMAT_TEST_RESULT.get(result.result, "%s")
return fmt % result.test_name return fmt % result.test_name

View File

@ -13,7 +13,7 @@ from test import support
from test.libregrtest.runtest import ( from test.libregrtest.runtest import (
runtest, INTERRUPTED, CHILD_ERROR, PROGRESS_MIN_TIME, runtest, INTERRUPTED, CHILD_ERROR, PROGRESS_MIN_TIME,
format_test_result, TestResult) format_test_result, TestResult, is_failed)
from test.libregrtest.setup import setup_tests from test.libregrtest.setup import setup_tests
from test.libregrtest.utils import format_duration from test.libregrtest.utils import format_duration
@ -22,8 +22,12 @@ from test.libregrtest.utils import format_duration
PROGRESS_UPDATE = 30.0 # seconds PROGRESS_UPDATE = 30.0 # seconds
def must_stop(result): def must_stop(result, ns):
return result.result in (INTERRUPTED, CHILD_ERROR) if result.result == INTERRUPTED:
return True
if ns.failfast and is_failed(result, ns):
return True
return False
def run_test_in_subprocess(testname, ns): def run_test_in_subprocess(testname, ns):
@ -66,16 +70,22 @@ class MultiprocessIterator:
"""A thread-safe iterator over tests for multiprocess mode.""" """A thread-safe iterator over tests for multiprocess mode."""
def __init__(self, tests): def __init__(self, tests_iter):
self.lock = threading.Lock() self.lock = threading.Lock()
self.tests = tests self.tests_iter = tests_iter
def __iter__(self): def __iter__(self):
return self return self
def __next__(self): def __next__(self):
with self.lock: with self.lock:
return next(self.tests) if self.tests_iter is None:
raise StopIteration
return next(self.tests_iter)
def stop(self):
with self.lock:
self.tests_iter = None
MultiprocessResult = collections.namedtuple('MultiprocessResult', MultiprocessResult = collections.namedtuple('MultiprocessResult',
@ -92,23 +102,24 @@ class MultiprocessThread(threading.Thread):
self._popen = None self._popen = None
def kill(self): def kill(self):
if not self.is_alive(): popen = self._popen
if popen is None:
return return
if self._popen is not None: print("Kill regrtest worker process %s" % popen.pid)
self._popen.kill() popen.kill()
def _runtest(self, test_name): def _runtest(self, test_name):
try: try:
self.start_time = time.monotonic() self.start_time = time.monotonic()
self.current_test_name = test_name self.current_test_name = test_name
popen = run_test_in_subprocess(test_name, self.ns) self._popen = run_test_in_subprocess(test_name, self.ns)
self._popen = popen popen = self._popen
with popen: with popen:
try: try:
stdout, stderr = popen.communicate() stdout, stderr = popen.communicate()
except: except:
popen.kill() self.kill()
popen.wait() popen.wait()
raise raise
@ -153,7 +164,7 @@ class MultiprocessThread(threading.Thread):
mp_result = self._runtest(test_name) mp_result = self._runtest(test_name)
self.output.put((False, mp_result)) self.output.put((False, mp_result))
if must_stop(mp_result.result): if must_stop(mp_result.result, self.ns):
break break
except BaseException: except BaseException:
self.output.put((True, traceback.format_exc())) self.output.put((True, traceback.format_exc()))
@ -255,7 +266,7 @@ class MultiprocessRunner:
if mp_result.stderr and not self.ns.pgo: if mp_result.stderr and not self.ns.pgo:
print(mp_result.stderr, file=sys.stderr, flush=True) print(mp_result.stderr, file=sys.stderr, flush=True)
if must_stop(mp_result.result): if must_stop(mp_result.result, self.ns):
return True return True
return False return False
@ -280,6 +291,8 @@ class MultiprocessRunner:
if self.test_timeout is not None: if self.test_timeout is not None:
faulthandler.cancel_dump_traceback_later() faulthandler.cancel_dump_traceback_later()
# a test failed (and --failfast is set) or all tests completed
self.pending.stop()
self.wait_workers() self.wait_workers()

View File

@ -0,0 +1,3 @@
"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.