Skip to content

Commit a939b65

Browse files
authored
pythongh-109162: libregrtest: add worker.py (python#109229)
Add new worker.py file: * Move create_worker_process() and worker_process() to this file. * Add main() function to worker.py. create_worker_process() now runs the command: "python -m test.libregrtest.worker JSON". * create_worker_process() now starts the worker process in the current working directory. Regrtest now gets the absolute path of the reflog.txt filename: -R command line option filename. * Remove --worker-json command line option. Remove test_regrtest.test_worker_json(). Related changes: * Add write_json() and from_json() methods to TestResult. * Rename select_temp_dir() to get_temp_dir() and move it to utils. * Rename make_temp_dir() to get_work_dir() and move it to utils. It no longer calls os.makedirs(): Regrtest.main() now calls it. * Move fix_umask() to utils. The function is now called by setup_tests(). * Move StrPath to utils. * Add exit_timeout() context manager to utils. * RunTests: Replace junit_filename (StrPath) with use_junit (bool).
1 parent e55aab9 commit a939b65

10 files changed

+238
-213
lines changed

Lib/test/libregrtest/cmdline.py

-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,6 @@ def _create_parser():
216216
group.add_argument('--wait', action='store_true',
217217
help='wait for user input, e.g., allow a debugger '
218218
'to be attached')
219-
group.add_argument('--worker-json', metavar='ARGS')
220219
group.add_argument('-S', '--start', metavar='START',
221220
help='the name of the test at which to start.' +
222221
more_details)

Lib/test/libregrtest/main.py

+16-92
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,27 @@
1-
import faulthandler
21
import locale
32
import os
43
import platform
54
import random
65
import re
76
import sys
8-
import sysconfig
9-
import tempfile
107
import time
118
import unittest
9+
10+
from test import support
11+
from test.support import os_helper
12+
1213
from test.libregrtest.cmdline import _parse_args, Namespace
1314
from test.libregrtest.logger import Logger
1415
from test.libregrtest.runtest import (
1516
findtests, split_test_packages, run_single_test, abs_module_name,
1617
PROGRESS_MIN_TIME, State, RunTests, HuntRefleak,
17-
FilterTuple, TestList, StrPath, StrJSON, TestName)
18+
FilterTuple, TestList, StrJSON, TestName)
1819
from test.libregrtest.setup import setup_tests, setup_test_dir
1920
from test.libregrtest.pgo import setup_pgo_tests
2021
from test.libregrtest.results import TestResults
21-
from test.libregrtest.utils import (strip_py_suffix, count, format_duration,
22-
printlist, get_build_info)
23-
from test import support
24-
from test.support import os_helper
25-
from test.support import threading_helper
26-
27-
28-
# bpo-38203: Maximum delay in seconds to exit Python (call Py_Finalize()).
29-
# Used to protect against threading._shutdown() hang.
30-
# Must be smaller than buildbot "1200 seconds without output" limit.
31-
EXIT_TIMEOUT = 120.0
22+
from test.libregrtest.utils import (
23+
strip_py_suffix, count, format_duration, StrPath,
24+
printlist, get_build_info, get_temp_dir, get_work_dir, exit_timeout)
3225

3326

3427
class Regrtest:
@@ -104,7 +97,9 @@ def __init__(self, ns: Namespace):
10497
self.verbose: bool = ns.verbose
10598
self.quiet: bool = ns.quiet
10699
if ns.huntrleaks:
107-
self.hunt_refleak: HuntRefleak = HuntRefleak(*ns.huntrleaks)
100+
warmups, runs, filename = ns.huntrleaks
101+
filename = os.path.abspath(filename)
102+
self.hunt_refleak: HuntRefleak = HuntRefleak(warmups, runs, filename)
108103
else:
109104
self.hunt_refleak = None
110105
self.test_dir: StrPath | None = ns.testdir
@@ -454,64 +449,6 @@ def display_summary(self):
454449
state = self.get_state()
455450
print(f"Result: {state}")
456451

457-
@staticmethod
458-
def fix_umask():
459-
if support.is_emscripten:
460-
# Emscripten has default umask 0o777, which breaks some tests.
461-
# see https://github.com/emscripten-core/emscripten/issues/17269
462-
old_mask = os.umask(0)
463-
if old_mask == 0o777:
464-
os.umask(0o027)
465-
else:
466-
os.umask(old_mask)
467-
468-
@staticmethod
469-
def select_temp_dir(tmp_dir):
470-
if tmp_dir:
471-
tmp_dir = os.path.expanduser(tmp_dir)
472-
else:
473-
# When tests are run from the Python build directory, it is best practice
474-
# to keep the test files in a subfolder. This eases the cleanup of leftover
475-
# files using the "make distclean" command.
476-
if sysconfig.is_python_build():
477-
tmp_dir = sysconfig.get_config_var('abs_builddir')
478-
if tmp_dir is None:
479-
# bpo-30284: On Windows, only srcdir is available. Using
480-
# abs_builddir mostly matters on UNIX when building Python
481-
# out of the source tree, especially when the source tree
482-
# is read only.
483-
tmp_dir = sysconfig.get_config_var('srcdir')
484-
tmp_dir = os.path.join(tmp_dir, 'build')
485-
else:
486-
tmp_dir = tempfile.gettempdir()
487-
488-
return os.path.abspath(tmp_dir)
489-
490-
def is_worker(self):
491-
return (self.worker_json is not None)
492-
493-
@staticmethod
494-
def make_temp_dir(tmp_dir: StrPath, is_worker: bool):
495-
os.makedirs(tmp_dir, exist_ok=True)
496-
497-
# Define a writable temp dir that will be used as cwd while running
498-
# the tests. The name of the dir includes the pid to allow parallel
499-
# testing (see the -j option).
500-
# Emscripten and WASI have stubbed getpid(), Emscripten has only
501-
# milisecond clock resolution. Use randint() instead.
502-
if sys.platform in {"emscripten", "wasi"}:
503-
nounce = random.randint(0, 1_000_000)
504-
else:
505-
nounce = os.getpid()
506-
507-
if is_worker:
508-
work_dir = 'test_python_worker_{}'.format(nounce)
509-
else:
510-
work_dir = 'test_python_{}'.format(nounce)
511-
work_dir += os_helper.FS_NONASCII
512-
work_dir = os.path.join(tmp_dir, work_dir)
513-
return work_dir
514-
515452
@staticmethod
516453
def cleanup_temp_dir(tmp_dir: StrPath):
517454
import glob
@@ -534,17 +471,16 @@ def main(self, tests: TestList | None = None):
534471

535472
strip_py_suffix(self.cmdline_args)
536473

537-
self.tmp_dir = self.select_temp_dir(self.tmp_dir)
538-
539-
self.fix_umask()
474+
self.tmp_dir = get_temp_dir(self.tmp_dir)
540475

541476
if self.want_cleanup:
542477
self.cleanup_temp_dir(self.tmp_dir)
543478
sys.exit(0)
544479

545-
work_dir = self.make_temp_dir(self.tmp_dir, self.is_worker())
480+
os.makedirs(self.tmp_dir, exist_ok=True)
481+
work_dir = get_work_dir(parent_dir=self.tmp_dir)
546482

547-
try:
483+
with exit_timeout():
548484
# Run the tests in a context manager that temporarily changes the
549485
# CWD to a temporary and writable directory. If it's not possible
550486
# to create or change the CWD, the original CWD will be used.
@@ -556,13 +492,6 @@ def main(self, tests: TestList | None = None):
556492
# processes.
557493

558494
self._main()
559-
except SystemExit as exc:
560-
# bpo-38203: Python can hang at exit in Py_Finalize(), especially
561-
# on threading._shutdown() call: put a timeout
562-
if threading_helper.can_start_thread:
563-
faulthandler.dump_traceback_later(EXIT_TIMEOUT, exit=True)
564-
565-
sys.exit(exc.code)
566495

567496
def create_run_tests(self):
568497
return RunTests(
@@ -579,7 +508,7 @@ def create_run_tests(self):
579508
quiet=self.quiet,
580509
hunt_refleak=self.hunt_refleak,
581510
test_dir=self.test_dir,
582-
junit_filename=self.junit_filename,
511+
use_junit=(self.junit_filename is not None),
583512
memory_limit=self.memory_limit,
584513
gc_threshold=self.gc_threshold,
585514
use_resources=self.use_resources,
@@ -634,11 +563,6 @@ def run_tests(self) -> int:
634563
self.fail_rerun)
635564

636565
def _main(self):
637-
if self.is_worker():
638-
from test.libregrtest.runtest_mp import worker_process
639-
worker_process(self.worker_json)
640-
return
641-
642566
if self.want_wait:
643567
input("Press any key to continue...")
644568

Lib/test/libregrtest/refleak.py

-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ def get_pooled_int(value):
6868
warmups = hunt_refleak.warmups
6969
runs = hunt_refleak.runs
7070
filename = hunt_refleak.filename
71-
filename = os.path.join(os_helper.SAVEDCWD, filename)
7271
repcount = warmups + runs
7372

7473
# Pre-allocate to ensure that the loop doesn't allocate anything new

Lib/test/libregrtest/results.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
from test.support import TestStats
33

44
from test.libregrtest.runtest import (
5-
TestName, TestTuple, TestList, FilterDict, StrPath, State,
5+
TestName, TestTuple, TestList, FilterDict, State,
66
TestResult, RunTests)
7-
from test.libregrtest.utils import printlist, count, format_duration
7+
from test.libregrtest.utils import (
8+
printlist, count, format_duration, StrPath)
89

910

1011
EXITCODE_BAD_TEST = 2

Lib/test/libregrtest/runtest.py

+33-6
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
from test.support import os_helper
1818
from test.support import threading_helper
1919
from test.libregrtest.save_env import saved_test_environment
20-
from test.libregrtest.utils import clear_caches, format_duration, print_warning
20+
from test.libregrtest.utils import (
21+
clear_caches, format_duration, print_warning, StrPath)
2122

2223

2324
StrJSON = str
24-
StrPath = str
2525
TestName = str
2626
TestTuple = tuple[TestName, ...]
2727
TestList = list[TestName]
@@ -215,6 +215,33 @@ def get_rerun_match_tests(self) -> FilterTuple | None:
215215
return None
216216
return tuple(match_tests)
217217

218+
def write_json(self, file) -> None:
219+
json.dump(self, file, cls=_EncodeTestResult)
220+
221+
@staticmethod
222+
def from_json(worker_json) -> 'TestResult':
223+
return json.loads(worker_json, object_hook=_decode_test_result)
224+
225+
226+
class _EncodeTestResult(json.JSONEncoder):
227+
def default(self, o: Any) -> dict[str, Any]:
228+
if isinstance(o, TestResult):
229+
result = dataclasses.asdict(o)
230+
result["__test_result__"] = o.__class__.__name__
231+
return result
232+
else:
233+
return super().default(o)
234+
235+
236+
def _decode_test_result(data: dict[str, Any]) -> TestResult | dict[str, Any]:
237+
if "__test_result__" in data:
238+
data.pop('__test_result__')
239+
if data['stats'] is not None:
240+
data['stats'] = TestStats(**data['stats'])
241+
return TestResult(**data)
242+
else:
243+
return data
244+
218245

219246
@dataclasses.dataclass(slots=True, frozen=True)
220247
class RunTests:
@@ -234,7 +261,7 @@ class RunTests:
234261
quiet: bool = False
235262
hunt_refleak: HuntRefleak | None = None
236263
test_dir: StrPath | None = None
237-
junit_filename: StrPath | None = None
264+
use_junit: bool = False
238265
memory_limit: str | None = None
239266
gc_threshold: int | None = None
240267
use_resources: list[str] = None
@@ -358,7 +385,7 @@ def setup_support(runtests: RunTests):
358385
support.set_match_tests(runtests.match_tests, runtests.ignore_tests)
359386
support.failfast = runtests.fail_fast
360387
support.verbose = runtests.verbose
361-
if runtests.junit_filename:
388+
if runtests.use_junit:
362389
support.junit_xml_list = []
363390
else:
364391
support.junit_xml_list = None
@@ -434,8 +461,8 @@ def run_single_test(test_name: TestName, runtests: RunTests) -> TestResult:
434461
435462
Returns a TestResult.
436463
437-
If runtests.junit_filename is not None, xml_data is a list containing each
438-
generated testsuite element.
464+
If runtests.use_junit, xml_data is a list containing each generated
465+
testsuite element.
439466
"""
440467
start_time = time.perf_counter()
441468
result = TestResult(test_name)

0 commit comments

Comments
 (0)