Skip to content

Commit 67b8a1f

Browse files
vstinnerpablogsal
andauthored
[3.8] Update libregrtest from master (GH-19516)
* bpo-37531: regrtest now catchs ProcessLookupError (GH-16827) Fix a warning on a race condition on TestWorkerProcess.kill(): ignore silently ProcessLookupError rather than logging an useless warning. (cherry picked from commit a661392) * bpo-38502: regrtest uses process groups if available (GH-16829) test.regrtest now uses process groups in the multiprocessing mode (-jN command line option) if process groups are available: if os.setsid() and os.killpg() functions are available. (cherry picked from commit ecb035c) * bpo-37957: Allow regrtest to receive a file with test (and subtests) to ignore (GH-16989) When building Python in some uncommon platforms there are some known tests that will fail. Right now, the test suite has the ability to ignore entire tests using the -x option and to receive a filter file using the --matchfile filter. The problem with the --matchfile option is that it receives a file with patterns to accept and when you want to ignore a couple of tests and subtests, is too cumbersome to lists ALL tests that are not the ones that you want to accept and he problem with -x is that is not easy to ignore just a subtests that fail and the whole test needs to be ignored. For these reasons, add a new option to allow to ignore a list of test and subtests for these situations. (cherry picked from commit e0cd8aa) * regrtest: log timeout at startup (GH-19514) Reduce also worker timeout. (cherry picked from commit 4cf65a6) Co-authored-by: Pablo Galindo <Pablogsal@gmail.com>
1 parent c496e29 commit 67b8a1f

File tree

9 files changed

+211
-32
lines changed

9 files changed

+211
-32
lines changed

Lib/test/libregrtest/cmdline.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,17 @@ def _create_parser():
207207
group.add_argument('-m', '--match', metavar='PAT',
208208
dest='match_tests', action='append',
209209
help='match test cases and methods with glob pattern PAT')
210+
group.add_argument('-i', '--ignore', metavar='PAT',
211+
dest='ignore_tests', action='append',
212+
help='ignore test cases and methods with glob pattern PAT')
210213
group.add_argument('--matchfile', metavar='FILENAME',
211214
dest='match_filename',
212215
help='similar to --match but get patterns from a '
213216
'text file, one pattern per line')
217+
group.add_argument('--ignorefile', metavar='FILENAME',
218+
dest='ignore_filename',
219+
help='similar to --matchfile but it receives patterns '
220+
'from text file to ignore')
214221
group.add_argument('-G', '--failfast', action='store_true',
215222
help='fail as soon as a test fails (only with -v or -W)')
216223
group.add_argument('-u', '--use', metavar='RES1,RES2,...',
@@ -317,7 +324,8 @@ def _parse_args(args, **kwargs):
317324
findleaks=1, use_resources=None, trace=False, coverdir='coverage',
318325
runleaks=False, huntrleaks=False, verbose2=False, print_slow=False,
319326
random_seed=None, use_mp=None, verbose3=False, forever=False,
320-
header=False, failfast=False, match_tests=None, pgo=False)
327+
header=False, failfast=False, match_tests=None, ignore_tests=None,
328+
pgo=False)
321329
for k, v in kwargs.items():
322330
if not hasattr(ns, k):
323331
raise TypeError('%r is an invalid keyword argument '
@@ -395,6 +403,12 @@ def _parse_args(args, **kwargs):
395403
with open(ns.match_filename) as fp:
396404
for line in fp:
397405
ns.match_tests.append(line.strip())
406+
if ns.ignore_filename:
407+
if ns.ignore_tests is None:
408+
ns.ignore_tests = []
409+
with open(ns.ignore_filename) as fp:
410+
for line in fp:
411+
ns.ignore_tests.append(line.strip())
398412
if ns.forever:
399413
# --forever implies --failfast
400414
ns.failfast = True

Lib/test/libregrtest/main.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ def _list_cases(self, suite):
287287

288288
def list_cases(self):
289289
support.verbose = False
290-
support.set_match_tests(self.ns.match_tests)
290+
support.set_match_tests(self.ns.match_tests, self.ns.ignore_tests)
291291

292292
for test_name in self.selected:
293293
abstest = get_abs_module(self.ns, test_name)
@@ -394,7 +394,10 @@ def run_tests_sequential(self):
394394

395395
save_modules = sys.modules.keys()
396396

397-
self.log("Run tests sequentially")
397+
msg = "Run tests sequentially"
398+
if self.ns.timeout:
399+
msg += " (timeout: %s)" % format_duration(self.ns.timeout)
400+
self.log(msg)
398401

399402
previous_test = None
400403
for test_index, test_name in enumerate(self.tests, 1):

Lib/test/libregrtest/runtest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def _runtest(ns, test_name):
123123

124124
start_time = time.perf_counter()
125125
try:
126-
support.set_match_tests(ns.match_tests)
126+
support.set_match_tests(ns.match_tests, ns.ignore_tests)
127127
support.junit_xml_list = xml_list = [] if ns.xmlpath else None
128128
if ns.failfast:
129129
support.failfast = True

Lib/test/libregrtest/runtest_mp.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import os
55
import queue
6+
import signal
67
import subprocess
78
import sys
89
import threading
@@ -31,6 +32,8 @@
3132
# Time to wait until a worker completes: should be immediate
3233
JOIN_TIMEOUT = 30.0 # seconds
3334

35+
USE_PROCESS_GROUP = (hasattr(os, "setsid") and hasattr(os, "killpg"))
36+
3437

3538
def must_stop(result, ns):
3639
if result.result == INTERRUPTED:
@@ -59,12 +62,16 @@ def run_test_in_subprocess(testname, ns):
5962
# Running the child from the same working directory as regrtest's original
6063
# invocation ensures that TEMPDIR for the child is the same when
6164
# sysconfig.is_python_build() is true. See issue 15300.
65+
kw = {}
66+
if USE_PROCESS_GROUP:
67+
kw['start_new_session'] = True
6268
return subprocess.Popen(cmd,
6369
stdout=subprocess.PIPE,
6470
stderr=subprocess.PIPE,
6571
universal_newlines=True,
6672
close_fds=(os.name != 'nt'),
67-
cwd=support.SAVEDCWD)
73+
cwd=support.SAVEDCWD,
74+
**kw)
6875

6976

7077
def run_tests_worker(ns, test_name):
@@ -149,11 +156,24 @@ def _kill(self):
149156
return
150157
self._killed = True
151158

152-
print(f"Kill {self}", file=sys.stderr, flush=True)
159+
if USE_PROCESS_GROUP:
160+
what = f"{self} process group"
161+
else:
162+
what = f"{self}"
163+
164+
print(f"Kill {what}", file=sys.stderr, flush=True)
153165
try:
154-
popen.kill()
166+
if USE_PROCESS_GROUP:
167+
os.killpg(popen.pid, signal.SIGKILL)
168+
else:
169+
popen.kill()
170+
except ProcessLookupError:
171+
# popen.kill(): the process completed, the TestWorkerProcess thread
172+
# read its exit status, but Popen.send_signal() read the returncode
173+
# just before Popen.wait() set returncode.
174+
pass
155175
except OSError as exc:
156-
print_warning(f"Failed to kill {self}: {exc!r}")
176+
print_warning(f"Failed to kill {what}: {exc!r}")
157177

158178
def stop(self):
159179
# Method called from a different thread to stop this thread
@@ -332,16 +352,24 @@ def __init__(self, regrtest):
332352
self.output = queue.Queue()
333353
self.pending = MultiprocessIterator(self.regrtest.tests)
334354
if self.ns.timeout is not None:
335-
self.worker_timeout = self.ns.timeout * 1.5
355+
# Rely on faulthandler to kill a worker process. This timouet is
356+
# when faulthandler fails to kill a worker process. Give a maximum
357+
# of 5 minutes to faulthandler to kill the worker.
358+
self.worker_timeout = min(self.ns.timeout * 1.5,
359+
self.ns.timeout + 5 * 60)
336360
else:
337361
self.worker_timeout = None
338362
self.workers = None
339363

340364
def start_workers(self):
341365
self.workers = [TestWorkerProcess(index, self)
342366
for index in range(1, self.ns.use_mp + 1)]
343-
self.log("Run tests in parallel using %s child processes"
344-
% len(self.workers))
367+
msg = f"Run tests in parallel using {len(self.workers)} child processes"
368+
if self.ns.timeout:
369+
msg += (" (timeout: %s, worker timeout: %s)"
370+
% (format_duration(self.ns.timeout),
371+
format_duration(self.worker_timeout)))
372+
self.log(msg)
345373
for worker in self.workers:
346374
worker.start()
347375

Lib/test/support/__init__.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,7 +2047,9 @@ def _run_suite(suite):
20472047

20482048
# By default, don't filter tests
20492049
_match_test_func = None
2050-
_match_test_patterns = None
2050+
2051+
_accept_test_patterns = None
2052+
_ignore_test_patterns = None
20512053

20522054

20532055
def match_test(test):
@@ -2063,18 +2065,45 @@ def _is_full_match_test(pattern):
20632065
# as a full test identifier.
20642066
# Example: 'test.test_os.FileTests.test_access'.
20652067
#
2066-
# Reject patterns which contain fnmatch patterns: '*', '?', '[...]'
2067-
# or '[!...]'. For example, reject 'test_access*'.
2068+
# ignore patterns which contain fnmatch patterns: '*', '?', '[...]'
2069+
# or '[!...]'. For example, ignore 'test_access*'.
20682070
return ('.' in pattern) and (not re.search(r'[?*\[\]]', pattern))
20692071

20702072

2071-
def set_match_tests(patterns):
2072-
global _match_test_func, _match_test_patterns
2073+
def set_match_tests(accept_patterns=None, ignore_patterns=None):
2074+
global _match_test_func, _accept_test_patterns, _ignore_test_patterns
20732075

2074-
if patterns == _match_test_patterns:
2075-
# No change: no need to recompile patterns.
2076-
return
20772076

2077+
if accept_patterns is None:
2078+
accept_patterns = ()
2079+
if ignore_patterns is None:
2080+
ignore_patterns = ()
2081+
2082+
accept_func = ignore_func = None
2083+
2084+
if accept_patterns != _accept_test_patterns:
2085+
accept_patterns, accept_func = _compile_match_function(accept_patterns)
2086+
if ignore_patterns != _ignore_test_patterns:
2087+
ignore_patterns, ignore_func = _compile_match_function(ignore_patterns)
2088+
2089+
# Create a copy since patterns can be mutable and so modified later
2090+
_accept_test_patterns = tuple(accept_patterns)
2091+
_ignore_test_patterns = tuple(ignore_patterns)
2092+
2093+
if accept_func is not None or ignore_func is not None:
2094+
def match_function(test_id):
2095+
accept = True
2096+
ignore = False
2097+
if accept_func:
2098+
accept = accept_func(test_id)
2099+
if ignore_func:
2100+
ignore = ignore_func(test_id)
2101+
return accept and not ignore
2102+
2103+
_match_test_func = match_function
2104+
2105+
2106+
def _compile_match_function(patterns):
20782107
if not patterns:
20792108
func = None
20802109
# set_match_tests(None) behaves as set_match_tests(())
@@ -2102,10 +2131,7 @@ def match_test_regex(test_id):
21022131

21032132
func = match_test_regex
21042133

2105-
# Create a copy since patterns can be mutable and so modified later
2106-
_match_test_patterns = tuple(patterns)
2107-
_match_test_func = func
2108-
2134+
return patterns, func
21092135

21102136

21112137
def run_unittest(*classes):

Lib/test/test_regrtest.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,24 @@ def test_single(self):
157157
self.assertTrue(ns.single)
158158
self.checkError([opt, '-f', 'foo'], "don't go together")
159159

160+
def test_ignore(self):
161+
for opt in '-i', '--ignore':
162+
with self.subTest(opt=opt):
163+
ns = libregrtest._parse_args([opt, 'pattern'])
164+
self.assertEqual(ns.ignore_tests, ['pattern'])
165+
self.checkError([opt], 'expected one argument')
166+
167+
self.addCleanup(support.unlink, support.TESTFN)
168+
with open(support.TESTFN, "w") as fp:
169+
print('matchfile1', file=fp)
170+
print('matchfile2', file=fp)
171+
172+
filename = os.path.abspath(support.TESTFN)
173+
ns = libregrtest._parse_args(['-m', 'match',
174+
'--ignorefile', filename])
175+
self.assertEqual(ns.ignore_tests,
176+
['matchfile1', 'matchfile2'])
177+
160178
def test_match(self):
161179
for opt in '-m', '--match':
162180
with self.subTest(opt=opt):
@@ -960,6 +978,42 @@ def parse_methods(self, output):
960978
regex = re.compile("^(test[^ ]+).*ok$", flags=re.MULTILINE)
961979
return [match.group(1) for match in regex.finditer(output)]
962980

981+
def test_ignorefile(self):
982+
code = textwrap.dedent("""
983+
import unittest
984+
985+
class Tests(unittest.TestCase):
986+
def test_method1(self):
987+
pass
988+
def test_method2(self):
989+
pass
990+
def test_method3(self):
991+
pass
992+
def test_method4(self):
993+
pass
994+
""")
995+
all_methods = ['test_method1', 'test_method2',
996+
'test_method3', 'test_method4']
997+
testname = self.create_test(code=code)
998+
999+
# only run a subset
1000+
filename = support.TESTFN
1001+
self.addCleanup(support.unlink, filename)
1002+
1003+
subset = [
1004+
# only ignore the method name
1005+
'test_method1',
1006+
# ignore the full identifier
1007+
'%s.Tests.test_method3' % testname]
1008+
with open(filename, "w") as fp:
1009+
for name in subset:
1010+
print(name, file=fp)
1011+
1012+
output = self.run_tests("-v", "--ignorefile", filename, testname)
1013+
methods = self.parse_methods(output)
1014+
subset = ['test_method2', 'test_method4']
1015+
self.assertEqual(methods, subset)
1016+
9631017
def test_matchfile(self):
9641018
code = textwrap.dedent("""
9651019
import unittest

0 commit comments

Comments
 (0)