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:
parent
85c69d5c4c
commit
b0917df329
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue