Skip to content

Commit 0c139b5

Browse files
authored
gh-109162: libregrtest: rename runtest_mp.py to run_workers.py (#109248)
* Rename runtest_mp.py to run_workers.py * Move exit_timeout() and temp_cwd() context managers from Regrtest.main() to Regrtest.run_tests(). Actions like --list-tests or --list-cases don't need these protections. * Regrtest: remove selected and tests attributes. Pass 'selected' to list_tests(), list_cases() and run_tests(). display_result() now expects a TestTuple, instead of TestList. * Rename setup_tests() to setup_process() and rename setup_support() to setup_tests(). * Move _adjust_resource_limits() to utils and rename it to adjust_rlimit_nofile(). * Move replace_stdout() to utils. * Fix RunTests.verbose type: it's an int.
1 parent 0b6b053 commit 0c139b5

File tree

9 files changed

+171
-164
lines changed

9 files changed

+171
-164
lines changed

Lib/test/libregrtest/main.py

+68-65
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
from test.libregrtest.logger import Logger
1616
from test.libregrtest.result import State
1717
from test.libregrtest.runtests import RunTests, HuntRefleak
18-
from test.libregrtest.setup import setup_tests, setup_test_dir
18+
from test.libregrtest.setup import setup_process, setup_test_dir
1919
from test.libregrtest.single import run_single_test, PROGRESS_MIN_TIME
2020
from test.libregrtest.pgo import setup_pgo_tests
2121
from test.libregrtest.results import TestResults
2222
from test.libregrtest.utils import (
23-
StrPath, StrJSON, TestName, TestList, FilterTuple,
23+
StrPath, StrJSON, TestName, TestList, TestTuple, FilterTuple,
2424
strip_py_suffix, count, format_duration,
2525
printlist, get_build_info, get_temp_dir, get_work_dir, exit_timeout,
2626
abs_module_name)
@@ -51,7 +51,7 @@ class Regrtest:
5151
"""
5252
def __init__(self, ns: Namespace):
5353
# Log verbosity
54-
self.verbose: bool = ns.verbose
54+
self.verbose: int = int(ns.verbose)
5555
self.quiet: bool = ns.quiet
5656
self.pgo: bool = ns.pgo
5757
self.pgo_extended: bool = ns.pgo_extended
@@ -122,8 +122,6 @@ def __init__(self, ns: Namespace):
122122
self.tmp_dir: StrPath | None = ns.tempdir
123123

124124
# tests
125-
self.tests = []
126-
self.selected: TestList = []
127125
self.first_runtests: RunTests | None = None
128126

129127
# used by --slowest
@@ -140,18 +138,18 @@ def __init__(self, ns: Namespace):
140138
def log(self, line=''):
141139
self.logger.log(line)
142140

143-
def find_tests(self):
141+
def find_tests(self, tests: TestList | None = None) -> tuple[TestTuple, TestList | None]:
144142
if self.single_test_run:
145143
self.next_single_filename = os.path.join(self.tmp_dir, 'pynexttest')
146144
try:
147145
with open(self.next_single_filename, 'r') as fp:
148146
next_test = fp.read().strip()
149-
self.tests = [next_test]
147+
tests = [next_test]
150148
except OSError:
151149
pass
152150

153151
if self.fromfile:
154-
self.tests = []
152+
tests = []
155153
# regex to match 'test_builtin' in line:
156154
# '0:00:00 [ 4/400] test_builtin -- test_dict took 1 sec'
157155
regex = re.compile(r'\btest_[a-zA-Z0-9_]+\b')
@@ -161,9 +159,9 @@ def find_tests(self):
161159
line = line.strip()
162160
match = regex.search(line)
163161
if match is not None:
164-
self.tests.append(match.group())
162+
tests.append(match.group())
165163

166-
strip_py_suffix(self.tests)
164+
strip_py_suffix(tests)
167165

168166
if self.pgo:
169167
# add default PGO tests if no tests are specified
@@ -179,26 +177,26 @@ def find_tests(self):
179177
exclude=exclude_tests)
180178

181179
if not self.fromfile:
182-
self.selected = self.tests or self.cmdline_args
183-
if self.selected:
184-
self.selected = split_test_packages(self.selected)
180+
selected = tests or self.cmdline_args
181+
if selected:
182+
selected = split_test_packages(selected)
185183
else:
186-
self.selected = alltests
184+
selected = alltests
187185
else:
188-
self.selected = self.tests
186+
selected = tests
189187

190188
if self.single_test_run:
191-
self.selected = self.selected[:1]
189+
selected = selected[:1]
192190
try:
193-
pos = alltests.index(self.selected[0])
191+
pos = alltests.index(selected[0])
194192
self.next_single_test = alltests[pos + 1]
195193
except IndexError:
196194
pass
197195

198196
# Remove all the selected tests that precede start if it's set.
199197
if self.starting_test:
200198
try:
201-
del self.selected[:self.selected.index(self.starting_test)]
199+
del selected[:selected.index(self.starting_test)]
202200
except ValueError:
203201
print(f"Cannot find starting test: {self.starting_test}")
204202
sys.exit(1)
@@ -207,10 +205,12 @@ def find_tests(self):
207205
if self.random_seed is None:
208206
self.random_seed = random.randrange(100_000_000)
209207
random.seed(self.random_seed)
210-
random.shuffle(self.selected)
208+
random.shuffle(selected)
209+
210+
return (tuple(selected), tests)
211211

212212
@staticmethod
213-
def list_tests(tests: TestList):
213+
def list_tests(tests: TestTuple):
214214
for name in tests:
215215
print(name)
216216

@@ -224,12 +224,12 @@ def _list_cases(self, suite):
224224
if support.match_test(test):
225225
print(test.id())
226226

227-
def list_cases(self):
227+
def list_cases(self, tests: TestTuple):
228228
support.verbose = False
229229
support.set_match_tests(self.match_tests, self.ignore_tests)
230230

231231
skipped = []
232-
for test_name in self.selected:
232+
for test_name in tests:
233233
module_name = abs_module_name(test_name, self.test_dir)
234234
try:
235235
suite = unittest.defaultTestLoader.loadTestsFromName(module_name)
@@ -247,6 +247,10 @@ def list_cases(self):
247247
def _rerun_failed_tests(self, runtests: RunTests):
248248
# Configure the runner to re-run tests
249249
if self.num_workers == 0:
250+
# Always run tests in fresh processes to have more deterministic
251+
# initial state. Don't re-run tests in parallel but limit to a
252+
# single worker process to have side effects (on the system load
253+
# and timings) between tests.
250254
self.num_workers = 1
251255

252256
tests, match_tests_dict = self.results.prepare_rerun()
@@ -294,7 +298,8 @@ def display_result(self, runtests):
294298
print()
295299
print(f"== Tests result: {state} ==")
296300

297-
self.results.display_result(self.selected, self.quiet, self.print_slowest)
301+
self.results.display_result(runtests.tests,
302+
self.quiet, self.print_slowest)
298303

299304
def run_test(self, test_name: TestName, runtests: RunTests, tracer):
300305
if tracer is not None:
@@ -404,7 +409,7 @@ def get_state(self):
404409
return state
405410

406411
def _run_tests_mp(self, runtests: RunTests, num_workers: int) -> None:
407-
from test.libregrtest.runtest_mp import RunWorkers
412+
from test.libregrtest.run_workers import RunWorkers
408413
RunWorkers(num_workers, runtests, self.logger, self.results).run()
409414

410415
def finalize_tests(self, tracer):
@@ -454,39 +459,9 @@ def cleanup_temp_dir(tmp_dir: StrPath):
454459
print("Remove file: %s" % name)
455460
os_helper.unlink(name)
456461

457-
def main(self, tests: TestList | None = None):
458-
if self.junit_filename and not os.path.isabs(self.junit_filename):
459-
self.junit_filename = os.path.abspath(self.junit_filename)
460-
461-
self.tests = tests
462-
463-
strip_py_suffix(self.cmdline_args)
464-
465-
self.tmp_dir = get_temp_dir(self.tmp_dir)
466-
467-
if self.want_cleanup:
468-
self.cleanup_temp_dir(self.tmp_dir)
469-
sys.exit(0)
470-
471-
os.makedirs(self.tmp_dir, exist_ok=True)
472-
work_dir = get_work_dir(parent_dir=self.tmp_dir)
473-
474-
with exit_timeout():
475-
# Run the tests in a context manager that temporarily changes the
476-
# CWD to a temporary and writable directory. If it's not possible
477-
# to create or change the CWD, the original CWD will be used.
478-
# The original CWD is available from os_helper.SAVEDCWD.
479-
with os_helper.temp_cwd(work_dir, quiet=True):
480-
# When using multiprocessing, worker processes will use
481-
# work_dir as their parent temporary directory. So when the
482-
# main process exit, it removes also subdirectories of worker
483-
# processes.
484-
485-
self._main()
486-
487-
def create_run_tests(self):
462+
def create_run_tests(self, tests: TestTuple):
488463
return RunTests(
489-
tuple(self.selected),
464+
tests,
490465
fail_fast=self.fail_fast,
491466
match_tests=self.match_tests,
492467
ignore_tests=self.ignore_tests,
@@ -506,7 +481,7 @@ def create_run_tests(self):
506481
python_cmd=self.python_cmd,
507482
)
508483

509-
def run_tests(self) -> int:
484+
def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
510485
if self.hunt_refleak and self.hunt_refleak.warmups < 3:
511486
msg = ("WARNING: Running tests with --huntrleaks/-R and "
512487
"less than 3 warmup repetitions can give false positives!")
@@ -520,17 +495,17 @@ def run_tests(self) -> int:
520495
# For a partial run, we do not need to clutter the output.
521496
if (self.want_header
522497
or not(self.pgo or self.quiet or self.single_test_run
523-
or self.tests or self.cmdline_args)):
498+
or tests or self.cmdline_args)):
524499
self.display_header()
525500

526501
if self.randomize:
527502
print("Using random seed", self.random_seed)
528503

529-
runtests = self.create_run_tests()
504+
runtests = self.create_run_tests(selected)
530505
self.first_runtests = runtests
531506
self.logger.set_tests(runtests)
532507

533-
setup_tests(runtests)
508+
setup_process()
534509

535510
self.logger.start_load_tracker()
536511
try:
@@ -553,20 +528,48 @@ def run_tests(self) -> int:
553528
return self.results.get_exitcode(self.fail_env_changed,
554529
self.fail_rerun)
555530

556-
def _main(self):
531+
def run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
532+
os.makedirs(self.tmp_dir, exist_ok=True)
533+
work_dir = get_work_dir(parent_dir=self.tmp_dir)
534+
535+
# Put a timeout on Python exit
536+
with exit_timeout():
537+
# Run the tests in a context manager that temporarily changes the
538+
# CWD to a temporary and writable directory. If it's not possible
539+
# to create or change the CWD, the original CWD will be used.
540+
# The original CWD is available from os_helper.SAVEDCWD.
541+
with os_helper.temp_cwd(work_dir, quiet=True):
542+
# When using multiprocessing, worker processes will use
543+
# work_dir as their parent temporary directory. So when the
544+
# main process exit, it removes also subdirectories of worker
545+
# processes.
546+
return self._run_tests(selected, tests)
547+
548+
def main(self, tests: TestList | None = None):
549+
if self.junit_filename and not os.path.isabs(self.junit_filename):
550+
self.junit_filename = os.path.abspath(self.junit_filename)
551+
552+
strip_py_suffix(self.cmdline_args)
553+
554+
self.tmp_dir = get_temp_dir(self.tmp_dir)
555+
556+
if self.want_cleanup:
557+
self.cleanup_temp_dir(self.tmp_dir)
558+
sys.exit(0)
559+
557560
if self.want_wait:
558561
input("Press any key to continue...")
559562

560563
setup_test_dir(self.test_dir)
561-
self.find_tests()
564+
selected, tests = self.find_tests(tests)
562565

563566
exitcode = 0
564567
if self.want_list_tests:
565-
self.list_tests(self.selected)
568+
self.list_tests(selected)
566569
elif self.want_list_cases:
567-
self.list_cases()
570+
self.list_cases(selected)
568571
else:
569-
exitcode = self.run_tests()
572+
exitcode = self.run_tests(selected, tests)
570573

571574
sys.exit(exitcode)
572575

Lib/test/libregrtest/result.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from test.support import TestStats
66

77
from test.libregrtest.utils import (
8-
TestName, FilterTuple,
8+
StrJSON, TestName, FilterTuple,
99
format_duration, normalize_test_name, print_warning)
1010

1111

@@ -160,7 +160,7 @@ def write_json(self, file) -> None:
160160
json.dump(self, file, cls=_EncodeTestResult)
161161

162162
@staticmethod
163-
def from_json(worker_json) -> 'TestResult':
163+
def from_json(worker_json: StrJSON) -> 'TestResult':
164164
return json.loads(worker_json, object_hook=_decode_test_result)
165165

166166

Lib/test/libregrtest/results.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def accumulate_result(self, result: TestResult, runtests: RunTests):
106106

107107
xml_data = result.xml_data
108108
if xml_data:
109-
self.add_junit(result.xml_data)
109+
self.add_junit(xml_data)
110110

111111
def need_rerun(self):
112112
return bool(self.bad_results)
@@ -163,7 +163,7 @@ def write_junit(self, filename: StrPath):
163163
for s in ET.tostringlist(root):
164164
f.write(s)
165165

166-
def display_result(self, tests: TestList, quiet: bool, print_slowest: bool):
166+
def display_result(self, tests: TestTuple, quiet: bool, print_slowest: bool):
167167
if self.interrupted:
168168
print("Test suite interrupted by signal SIGINT.")
169169

Lib/test/libregrtest/runtest_mp.py renamed to Lib/test/libregrtest/run_workers.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from test.support import os_helper
1616

1717
from test.libregrtest.logger import Logger
18-
from test.libregrtest.main import Regrtest
1918
from test.libregrtest.result import TestResult, State
2019
from test.libregrtest.results import TestResults
2120
from test.libregrtest.runtests import RunTests
@@ -154,10 +153,10 @@ def mp_result_error(
154153
) -> MultiprocessResult:
155154
return MultiprocessResult(test_result, stdout, err_msg)
156155

157-
def _run_process(self, worker_job, output_file: TextIO,
156+
def _run_process(self, runtests: RunTests, output_file: TextIO,
158157
tmp_dir: StrPath | None = None) -> int:
159158
try:
160-
popen = create_worker_process(worker_job, output_file, tmp_dir)
159+
popen = create_worker_process(runtests, output_file, tmp_dir)
161160

162161
self._killed = False
163162
self._popen = popen

Lib/test/libregrtest/runtests.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ class RunTests:
2727
pgo_extended: bool = False
2828
output_on_failure: bool = False
2929
timeout: float | None = None
30-
verbose: bool = False
30+
verbose: int = 0
3131
quiet: bool = False
3232
hunt_refleak: HuntRefleak | None = None
3333
test_dir: StrPath | None = None
3434
use_junit: bool = False
3535
memory_limit: str | None = None
3636
gc_threshold: int | None = None
37-
use_resources: list[str] = None
37+
use_resources: list[str] = dataclasses.field(default_factory=list)
3838
python_cmd: list[str] | None = None
3939

4040
def copy(self, **override):

0 commit comments

Comments
 (0)