Skip to content

Commit 60092e9

Browse files
committed
gh-108834: Cleanup libregrtest
Remove "global variables" from libregrtest: no longer pass 'ns' magic namespace which was used to get and set 'random' attributes. Instead, each class has its own attributes, and some classes are even read-only (frozen dataclass), like RunTests. Reorganize files: * Rename runtest.py to single.py. * Rename runtest_mp.py to mp_runner.py. * Create findtests.py, result.py, results.py and worker.py. Reorganize classes: * Copy command line options ('ns' namespace) to Regrtest and RunTests attributes: add many attributes. * Add Results class with methods: get_state(), display_result(), get_exitcode(), JUnit methods, etc. * Add Logger class: log(), display_progress(), system load tracker. * Remove WorkerJob class: the worker now gets a RunTests instance. Move function and methods: * Convert Regrtest.list_cases() to a function. * Convert display_header(), display_sanitizers(), set_temp_dir() and create_temp_dir() methods of Regrtest to functions in utils.py. Rename set_temp_dir() to select_temp_dir(). Rename create_temp_dir() to make_temp_dir(). * Move abs_module_name() and normalize_test_name() to utils.py. * Move capture_output() to utils.py. * Rename dash_R() to runtest_refleak() * Merge display_sanitizers() code into display_header(). cmdline.py changes: * Rename internal --worker-args command line option to --worker-json. * Rename ns.trace to ns.coverage. * No longer gets the number of CPUs: it's now done by Regrtest class. * Add missing attributes to Namespace: coverage, threshold, wait. Misc: * Add test_parse_memlimit() and test_set_memlimit() to test_support. * Add some type annotations.
1 parent 3f89b25 commit 60092e9

21 files changed

+1820
-1566
lines changed

Lib/test/libregrtest/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1-
from test.libregrtest.cmdline import _parse_args, RESOURCE_NAMES, ALL_RESOURCES
2-
from test.libregrtest.main import main
1+
from test.support import TestStats
2+
from .cmdline import _parse_args, RESOURCE_NAMES, ALL_RESOURCES
3+
from .result import FilterTuple, State, TestResult
4+
from .runtests import TestsTuple, FilterDict, RunTests
5+
from .results import TestsList, Results
6+
from .main import main

Lib/test/libregrtest/cmdline.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import shlex
44
import sys
5+
56
from test.support import os_helper
67

78

@@ -154,7 +155,7 @@ def __init__(self, **kwargs) -> None:
154155
self.fromfile = None
155156
self.fail_env_changed = False
156157
self.use_resources = None
157-
self.trace = False
158+
self.coverage = False
158159
self.coverdir = 'coverage'
159160
self.runleaks = False
160161
self.huntrleaks = False
@@ -170,7 +171,8 @@ def __init__(self, **kwargs) -> None:
170171
self.ignore_tests = None
171172
self.pgo = False
172173
self.pgo_extended = False
173-
174+
self.threshold = None
175+
self.wait = False
174176
super().__init__(**kwargs)
175177

176178

@@ -205,7 +207,7 @@ def _create_parser():
205207
group.add_argument('--wait', action='store_true',
206208
help='wait for user input, e.g., allow a debugger '
207209
'to be attached')
208-
group.add_argument('--worker-args', metavar='ARGS')
210+
group.add_argument('--worker-json', metavar='ARGS')
209211
group.add_argument('-S', '--start', metavar='START',
210212
help='the name of the test at which to start.' +
211213
more_details)
@@ -283,7 +285,6 @@ def _create_parser():
283285
dest='use_mp', type=int,
284286
help='run PROCESSES processes at once')
285287
group.add_argument('-T', '--coverage', action='store_true',
286-
dest='trace',
287288
help='turn on code coverage tracing using the trace '
288289
'module')
289290
group.add_argument('-D', '--coverdir', metavar='DIR',
@@ -378,11 +379,11 @@ def _parse_args(args, **kwargs):
378379

379380
if ns.single and ns.fromfile:
380381
parser.error("-s and -f don't go together!")
381-
if ns.use_mp is not None and ns.trace:
382+
if ns.use_mp is not None and ns.coverage:
382383
parser.error("-T and -j don't go together!")
383384
if ns.python is not None:
384385
if ns.use_mp is None:
385-
parser.error("-p requires -j!")
386+
parser.error("--python option requires the -j option!")
386387
# The "executable" may be two or more parts, e.g. "node python.js"
387388
ns.python = shlex.split(ns.python)
388389
if ns.failfast and not (ns.verbose or ns.verbose3):
@@ -401,10 +402,6 @@ def _parse_args(args, **kwargs):
401402
if ns.timeout is not None:
402403
if ns.timeout <= 0:
403404
ns.timeout = None
404-
if ns.use_mp is not None:
405-
if ns.use_mp <= 0:
406-
# Use all cores + extras for tests that like to sleep
407-
ns.use_mp = 2 + (os.cpu_count() or 1)
408405
if ns.use:
409406
for a in ns.use:
410407
for r in a:
@@ -448,4 +445,13 @@ def _parse_args(args, **kwargs):
448445
# --forever implies --failfast
449446
ns.failfast = True
450447

448+
if ns.huntrleaks:
449+
warmup, repetitions, _ = ns.huntrleaks
450+
if warmup < 1 or repetitions < 1:
451+
msg = ("Invalid values for the --huntrleaks/-R parameters. The "
452+
"number of warmups and repetitions must be at least 1 "
453+
"each (1:1).")
454+
print(msg, file=sys.stderr, flush=True)
455+
sys.exit(2)
456+
451457
return ns

Lib/test/libregrtest/findtests.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import os.path
2+
import sys
3+
import unittest
4+
from test import support
5+
6+
from .result import FilterTuple
7+
from .utils import abs_module_name, count, printlist
8+
9+
10+
#If these test directories are encountered recurse into them and treat each
11+
# test_ .py or dir as a separate test module. This can increase parallelism.
12+
# Beware this can't generally be done for any directory with sub-tests as the
13+
# __init__.py may do things which alter what tests are to be run.
14+
15+
SPLITTESTDIRS = {
16+
"test_asyncio",
17+
"test_concurrent_futures",
18+
"test_multiprocessing_fork",
19+
"test_multiprocessing_forkserver",
20+
"test_multiprocessing_spawn",
21+
}
22+
23+
24+
def findtestdir(path=None):
25+
return path or os.path.dirname(os.path.dirname(__file__)) or os.curdir
26+
27+
28+
def findtests(*, testdir=None, exclude=(),
29+
split_test_dirs=SPLITTESTDIRS, base_mod=""):
30+
"""Return a list of all applicable test modules."""
31+
testdir = findtestdir(testdir)
32+
tests = []
33+
for name in os.listdir(testdir):
34+
mod, ext = os.path.splitext(name)
35+
if (not mod.startswith("test_")) or (mod in exclude):
36+
continue
37+
if mod in split_test_dirs:
38+
subdir = os.path.join(testdir, mod)
39+
mod = f"{base_mod or 'test'}.{mod}"
40+
tests.extend(findtests(testdir=subdir, exclude=exclude,
41+
split_test_dirs=split_test_dirs, base_mod=mod))
42+
elif ext in (".py", ""):
43+
tests.append(f"{base_mod}.{mod}" if base_mod else mod)
44+
return sorted(tests)
45+
46+
47+
def split_test_packages(tests, *, testdir=None, exclude=(),
48+
split_test_dirs=SPLITTESTDIRS):
49+
testdir = findtestdir(testdir)
50+
splitted = []
51+
for name in tests:
52+
if name in split_test_dirs:
53+
subdir = os.path.join(testdir, name)
54+
splitted.extend(findtests(testdir=subdir, exclude=exclude,
55+
split_test_dirs=split_test_dirs,
56+
base_mod=name))
57+
else:
58+
splitted.append(name)
59+
return splitted
60+
61+
62+
def _list_cases(suite):
63+
for test in suite:
64+
if isinstance(test, unittest.loader._FailedTest):
65+
continue
66+
if isinstance(test, unittest.TestSuite):
67+
_list_cases(test)
68+
elif isinstance(test, unittest.TestCase):
69+
if support.match_test(test):
70+
print(test.id())
71+
72+
def list_cases(tests, *, test_dir: str,
73+
match_tests: FilterTuple | None = None,
74+
ignore_tests: FilterTuple | None = None):
75+
support.verbose = False
76+
support.set_match_tests(match_tests, ignore_tests)
77+
78+
skipped = []
79+
for test_name in tests:
80+
module_name = abs_module_name(test_name, test_dir)
81+
try:
82+
suite = unittest.defaultTestLoader.loadTestsFromName(module_name)
83+
_list_cases(suite)
84+
except unittest.SkipTest:
85+
skipped.append(test_name)
86+
87+
if skipped:
88+
sys.stdout.flush()
89+
stderr = sys.stderr
90+
print(file=stderr)
91+
print(count(len(skipped), "test"), "skipped:", file=stderr)
92+
printlist(skipped, file=stderr)

Lib/test/libregrtest/logger.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import os
2+
import sys
3+
import time
4+
5+
from . import RunTests
6+
7+
8+
class Logger:
9+
def __init__(self, pgo: bool):
10+
self.start_time = time.perf_counter()
11+
self.win_load_tracker = None
12+
self.pgo = pgo
13+
14+
# used to display the progress bar "[ 3/100]"
15+
self.test_count_text = ''
16+
self.test_count_width = 1
17+
18+
def set_tests(self, runtests: RunTests):
19+
if runtests.forever:
20+
self.test_count_text = ''
21+
self.test_count_width = 3
22+
else:
23+
self.test_count_text = '/{}'.format(len(runtests.tests))
24+
self.test_count_width = len(self.test_count_text) - 1
25+
26+
def start_load_tracker(self):
27+
if sys.platform != 'win32':
28+
return
29+
30+
# If we're on windows and this is the parent runner (not a worker),
31+
# track the load average.
32+
from .win_utils import WindowsLoadTracker
33+
34+
try:
35+
self.win_load_tracker = WindowsLoadTracker()
36+
except PermissionError as error:
37+
# Standard accounts may not have access to the performance
38+
# counters.
39+
print(f'Failed to create WindowsLoadTracker: {error}')
40+
41+
def stop_load_tracker(self):
42+
if self.win_load_tracker is not None:
43+
self.win_load_tracker.close()
44+
self.win_load_tracker = None
45+
46+
def get_time(self):
47+
return time.perf_counter() - self.start_time
48+
49+
def getloadavg(self):
50+
if self.win_load_tracker is not None:
51+
return self.win_load_tracker.getloadavg()
52+
53+
if hasattr(os, 'getloadavg'):
54+
return os.getloadavg()[0]
55+
56+
return None
57+
58+
def log(self, line=''):
59+
empty = not line
60+
61+
# add the system load prefix: "load avg: 1.80 "
62+
load_avg = self.getloadavg()
63+
if load_avg is not None:
64+
line = f"load avg: {load_avg:.2f} {line}"
65+
66+
# add the timestamp prefix: "0:01:05 "
67+
test_time = self.get_time()
68+
69+
mins, secs = divmod(int(test_time), 60)
70+
hours, mins = divmod(mins, 60)
71+
test_time = "%d:%02d:%02d" % (hours, mins, secs)
72+
73+
line = f"{test_time} {line}"
74+
if empty:
75+
line = line[:-1]
76+
77+
print(line, flush=True)
78+
79+
def display_progress(self, test_index, text, results, runtests):
80+
quiet = runtests.quiet
81+
if quiet:
82+
return
83+
84+
# "[ 51/405/1] test_tcl passed"
85+
line = f"{test_index:{self.test_count_width}}{self.test_count_text}"
86+
fails = len(results.bad) + len(results.environment_changed)
87+
if fails and not self.pgo:
88+
line = f"{line}/{fails}"
89+
self.log(f"[{line}] {text}")

0 commit comments

Comments
 (0)