Skip to content

Commit a56c928

Browse files
authored
gh-109162: Refactor libregrtest WorkerJob (#109171)
* Rename --worker-args command line option to --worker-json. * Rename _parse_worker_args() to _parse_worker_json(). * WorkerJob: * Add runtests attribute * Remove test_name and rerun attribute * Rename run_test_in_subprocess() to create_worker_process(). * Rename run_tests_worker() to worker_process(). * create_worker_process() uses json.dump(): write directly JSON to stdout. * Convert MultiprocessResult to a frozen dataclass. * Rename RunTests.match_tests to RunTests.match_tests_dict.
1 parent 489ca0a commit a56c928

File tree

5 files changed

+48
-41
lines changed

5 files changed

+48
-41
lines changed

Lib/test/libregrtest/cmdline.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def __init__(self, **kwargs) -> None:
170170
self.ignore_tests = None
171171
self.pgo = False
172172
self.pgo_extended = False
173+
self.worker_json = None
173174

174175
super().__init__(**kwargs)
175176

@@ -205,7 +206,7 @@ def _create_parser():
205206
group.add_argument('--wait', action='store_true',
206207
help='wait for user input, e.g., allow a debugger '
207208
'to be attached')
208-
group.add_argument('--worker-args', metavar='ARGS')
209+
group.add_argument('--worker-json', metavar='ARGS')
209210
group.add_argument('-S', '--start', metavar='START',
210211
help='the name of the test at which to start.' +
211212
more_details)

Lib/test/libregrtest/main.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def _rerun_failed_tests(self, need_rerun, runtests: RunTests):
336336

337337
# Get tests to re-run
338338
tests = [result.test_name for result in need_rerun]
339-
match_tests = self.get_rerun_match(need_rerun)
339+
match_tests_dict = self.get_rerun_match(need_rerun)
340340

341341
# Clear previously failed tests
342342
self.rerun_bad.extend(self.bad)
@@ -346,7 +346,7 @@ def _rerun_failed_tests(self, need_rerun, runtests: RunTests):
346346
# Re-run failed tests
347347
self.log(f"Re-running {len(tests)} failed tests in verbose mode in subprocesses")
348348
runtests = runtests.copy(tests=tuple(tests),
349-
match_tests=match_tests,
349+
match_tests_dict=match_tests_dict,
350350
rerun=True,
351351
forever=False)
352352
self.set_tests(runtests)
@@ -742,7 +742,7 @@ def set_temp_dir(self):
742742
self.tmp_dir = os.path.abspath(self.tmp_dir)
743743

744744
def is_worker(self):
745-
return (self.ns.worker_args is not None)
745+
return (self.ns.worker_json is not None)
746746

747747
def create_temp_dir(self):
748748
os.makedirs(self.tmp_dir, exist_ok=True)
@@ -870,8 +870,8 @@ def action_run_tests(self):
870870

871871
def _main(self):
872872
if self.is_worker():
873-
from test.libregrtest.runtest_mp import run_tests_worker
874-
run_tests_worker(self.ns.worker_args)
873+
from test.libregrtest.runtest_mp import worker_process
874+
worker_process(self.ns.worker_json)
875875
return
876876

877877
if self.want_wait:

Lib/test/libregrtest/runtest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def get_rerun_match_tests(self) -> FilterTuple | None:
208208
@dataclasses.dataclass(slots=True, frozen=True)
209209
class RunTests:
210210
tests: TestTuple
211-
match_tests: FilterDict | None = None
211+
match_tests_dict: FilterDict | None = None
212212
rerun: bool = False
213213
forever: bool = False
214214

@@ -218,8 +218,8 @@ def copy(self, **override):
218218
return RunTests(**state)
219219

220220
def get_match_tests(self, test_name) -> FilterTuple | None:
221-
if self.match_tests is not None:
222-
return self.match_tests.get(test_name, None)
221+
if self.match_tests_dict is not None:
222+
return self.match_tests_dict.get(test_name, None)
223223
else:
224224
return None
225225

Lib/test/libregrtest/runtest_mp.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,8 @@
4646

4747
@dataclasses.dataclass(slots=True)
4848
class WorkerJob:
49-
test_name: str
49+
runtests: RunTests
5050
namespace: Namespace
51-
rerun: bool = False
5251
match_tests: FilterTuple | None = None
5352

5453

@@ -70,6 +69,7 @@ def default(self, o: Any) -> dict[str, Any]:
7069
def _decode_worker_job(d: dict[str, Any]) -> WorkerJob | dict[str, Any]:
7170
if "__worker_job__" in d:
7271
d.pop('__worker_job__')
72+
d['runtests'] = RunTests(**d['runtests'])
7373
return WorkerJob(**d)
7474
if "__namespace__" in d:
7575
d.pop('__namespace__')
@@ -78,17 +78,16 @@ def _decode_worker_job(d: dict[str, Any]) -> WorkerJob | dict[str, Any]:
7878
return d
7979

8080

81-
def _parse_worker_args(worker_json: str) -> tuple[Namespace, str]:
82-
return json.loads(worker_json,
83-
object_hook=_decode_worker_job)
81+
def _parse_worker_json(worker_json: str) -> tuple[Namespace, str]:
82+
return json.loads(worker_json, object_hook=_decode_worker_job)
8483

8584

86-
def run_test_in_subprocess(worker_job: WorkerJob,
87-
output_file: TextIO,
88-
tmp_dir: str | None = None) -> subprocess.Popen:
85+
def create_worker_process(worker_job: WorkerJob,
86+
output_file: TextIO,
87+
tmp_dir: str | None = None) -> subprocess.Popen:
8988
ns = worker_job.namespace
9089
python = ns.python
91-
worker_args = json.dumps(worker_job, cls=_EncodeWorkerJob)
90+
worker_json = json.dumps(worker_job, cls=_EncodeWorkerJob)
9291

9392
if python is not None:
9493
executable = python
@@ -97,7 +96,7 @@ def run_test_in_subprocess(worker_job: WorkerJob,
9796
cmd = [*executable, *support.args_from_interpreter_flags(),
9897
'-u', # Unbuffered stdout and stderr
9998
'-m', 'test.regrtest',
100-
'--worker-args', worker_args]
99+
'--worker-json', worker_json]
101100

102101
env = dict(os.environ)
103102
if tmp_dir is not None:
@@ -122,31 +121,32 @@ def run_test_in_subprocess(worker_job: WorkerJob,
122121
return subprocess.Popen(cmd, **kw)
123122

124123

125-
def run_tests_worker(worker_json: str) -> NoReturn:
126-
worker_job = _parse_worker_args(worker_json)
124+
def worker_process(worker_json: str) -> NoReturn:
125+
worker_job = _parse_worker_json(worker_json)
126+
runtests = worker_job.runtests
127127
ns = worker_job.namespace
128-
test_name = worker_job.test_name
129-
rerun = worker_job.rerun
130-
match_tests = worker_job.match_tests
128+
test_name = runtests.tests[0]
129+
match_tests: FilterTuple | None = worker_job.match_tests
131130

132131
setup_tests(ns)
133132

134-
if rerun:
133+
if runtests.rerun:
135134
if match_tests:
136135
matching = "matching: " + ", ".join(match_tests)
137136
print(f"Re-running {test_name} in verbose mode ({matching})", flush=True)
138137
else:
139138
print(f"Re-running {test_name} in verbose mode", flush=True)
140139
ns.verbose = True
141140

142-
if match_tests is not None:
143-
ns.match_tests = match_tests
141+
if match_tests is not None:
142+
ns.match_tests = match_tests
144143

145144
result = runtest(ns, test_name)
146145
print() # Force a newline (just in case)
147146

148147
# Serialize TestResult as dict in JSON
149-
print(json.dumps(result, cls=EncodeTestResult), flush=True)
148+
json.dump(result, sys.stdout, cls=EncodeTestResult)
149+
sys.stdout.flush()
150150
sys.exit(0)
151151

152152

@@ -173,7 +173,8 @@ def stop(self):
173173
self.tests_iter = None
174174

175175

176-
class MultiprocessResult(NamedTuple):
176+
@dataclasses.dataclass(slots=True, frozen=True)
177+
class MultiprocessResult:
177178
result: TestResult
178179
# bpo-45410: stderr is written into stdout to keep messages order
179180
worker_stdout: str | None = None
@@ -198,7 +199,6 @@ def __init__(self, worker_id: int, runner: "MultiprocessTestRunner") -> None:
198199
self.ns = runner.ns
199200
self.timeout = runner.worker_timeout
200201
self.regrtest = runner.regrtest
201-
self.rerun = runner.rerun
202202
self.current_test_name = None
203203
self.start_time = None
204204
self._popen = None
@@ -264,9 +264,8 @@ def mp_result_error(
264264

265265
def _run_process(self, worker_job, output_file: TextIO,
266266
tmp_dir: str | None = None) -> int:
267-
self.current_test_name = worker_job.test_name
268267
try:
269-
popen = run_test_in_subprocess(worker_job, output_file, tmp_dir)
268+
popen = create_worker_process(worker_job, output_file, tmp_dir)
270269

271270
self._killed = False
272271
self._popen = popen
@@ -316,6 +315,8 @@ def _run_process(self, worker_job, output_file: TextIO,
316315
self.current_test_name = None
317316

318317
def _runtest(self, test_name: str) -> MultiprocessResult:
318+
self.current_test_name = test_name
319+
319320
if sys.platform == 'win32':
320321
# gh-95027: When stdout is not a TTY, Python uses the ANSI code
321322
# page for the sys.stdout encoding. If the main process runs in a
@@ -324,15 +325,20 @@ def _runtest(self, test_name: str) -> MultiprocessResult:
324325
else:
325326
encoding = sys.stdout.encoding
326327

327-
match_tests = self.runtests.get_match_tests(test_name)
328+
tests = (test_name,)
329+
if self.runtests.rerun:
330+
match_tests = self.runtests.get_match_tests(test_name)
331+
else:
332+
match_tests = None
333+
worker_runtests = self.runtests.copy(tests=tests)
334+
worker_job = WorkerJob(
335+
worker_runtests,
336+
namespace=self.ns,
337+
match_tests=match_tests)
328338

329339
# gh-94026: Write stdout+stderr to a tempfile as workaround for
330340
# non-blocking pipes on Emscripten with NodeJS.
331341
with tempfile.TemporaryFile('w+', encoding=encoding) as stdout_file:
332-
worker_job = WorkerJob(test_name,
333-
namespace=self.ns,
334-
rerun=self.rerun,
335-
match_tests=match_tests)
336342
# gh-93353: Check for leaked temporary files in the parent process,
337343
# since the deletion of temporary files can happen late during
338344
# Python finalization: too late for libregrtest.

Lib/test/test_regrtest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ def test_wait(self):
7575
ns = libregrtest._parse_args(['--wait'])
7676
self.assertTrue(ns.wait)
7777

78-
def test_worker_args(self):
79-
ns = libregrtest._parse_args(['--worker-args', '[[], {}]'])
80-
self.assertEqual(ns.worker_args, '[[], {}]')
81-
self.checkError(['--worker-args'], 'expected one argument')
78+
def test_worker_json(self):
79+
ns = libregrtest._parse_args(['--worker-json', '[[], {}]'])
80+
self.assertEqual(ns.worker_json, '[[], {}]')
81+
self.checkError(['--worker-json'], 'expected one argument')
8282

8383
def test_start(self):
8484
for opt in '-S', '--start':

0 commit comments

Comments
 (0)