diff options
author | Victor Stinner <vstinner@python.org> | 2023-09-09 23:41:21 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-09 23:41:21 (GMT) |
commit | 0c0f254230bbc9532a7e78077a639a3ac940953c (patch) | |
tree | d35badf6fac682af73666dc883292a67df5778da | |
parent | 24fa8f2046965b46c70a750a5a004708a63ac770 (diff) | |
download | cpython-0c0f254230bbc9532a7e78077a639a3ac940953c.zip cpython-0c0f254230bbc9532a7e78077a639a3ac940953c.tar.gz cpython-0c0f254230bbc9532a7e78077a639a3ac940953c.tar.bz2 |
gh-109162: libregrtest: remove WorkerJob class (#109204)
* Add attributes to Regrtest and RunTests:
* gc_threshold
* memory_limit
* python_cmd
* use_resources
* Remove WorkerJob class. Add as_json() and from_json() methods to
RunTests. A worker process now only uses RunTests for all
parameters.
* Add tests on support.set_memlimit() in test_support. Create
_parse_memlimit() and also adds tests on it.
* Remove 'ns' parameter from runtest.py.
-rw-r--r-- | Lib/test/libregrtest/cmdline.py | 2 | ||||
-rw-r--r-- | Lib/test/libregrtest/main.py | 21 | ||||
-rw-r--r-- | Lib/test/libregrtest/runtest.py | 56 | ||||
-rw-r--r-- | Lib/test/libregrtest/runtest_mp.py | 58 | ||||
-rw-r--r-- | Lib/test/libregrtest/setup.py | 12 | ||||
-rw-r--r-- | Lib/test/support/__init__.py | 24 | ||||
-rw-r--r-- | Lib/test/test_support.py | 41 |
7 files changed, 126 insertions, 88 deletions
diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index 71e7396..9afb132 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -177,6 +177,8 @@ class Namespace(argparse.Namespace): self.worker_json = None self.start = None self.timeout = None + self.memlimit = None + self.threshold = None super().__init__(**kwargs) diff --git a/Lib/test/libregrtest/main.py b/Lib/test/libregrtest/main.py index bb02101..4066d06 100644 --- a/Lib/test/libregrtest/main.py +++ b/Lib/test/libregrtest/main.py @@ -100,6 +100,10 @@ class Regrtest: self.hunt_refleak = None self.test_dir: str | None = ns.testdir self.junit_filename: str | None = ns.xmlpath + self.memory_limit: str | None = ns.memlimit + self.gc_threshold: int | None = ns.threshold + self.use_resources: list[str] = ns.use_resources + self.python_cmd: list[str] | None = ns.python # tests self.tests = [] @@ -363,7 +367,7 @@ class Regrtest: return runtests def rerun_failed_tests(self, need_rerun, runtests: RunTests): - if self.ns.python: + if self.python_cmd: # Temp patch for https://github.com/python/cpython/issues/94052 self.log( "Re-running failed tests is not supported with --python " @@ -453,12 +457,12 @@ class Regrtest: if tracer is not None: # If we're tracing code coverage, then we don't exit with status # if on a false return value from main. - cmd = ('result = run_single_test(test_name, runtests, self.ns)') + cmd = ('result = run_single_test(test_name, runtests)') ns = dict(locals()) tracer.runctx(cmd, globals=globals(), locals=ns) result = ns['result'] else: - result = run_single_test(test_name, runtests, self.ns) + result = run_single_test(test_name, runtests) self.accumulate_result(result) @@ -876,9 +880,14 @@ class Regrtest: quiet=self.quiet, hunt_refleak=self.hunt_refleak, test_dir=self.test_dir, - junit_filename=self.junit_filename) - - setup_tests(runtests, self.ns) + junit_filename=self.junit_filename, + memory_limit=self.memory_limit, + gc_threshold=self.gc_threshold, + use_resources=self.use_resources, + python_cmd=self.python_cmd, + ) + + setup_tests(runtests) tracer = self.run_tests(runtests) self.display_result(runtests) diff --git a/Lib/test/libregrtest/runtest.py b/Lib/test/libregrtest/runtest.py index ab59d81..dc574ed 100644 --- a/Lib/test/libregrtest/runtest.py +++ b/Lib/test/libregrtest/runtest.py @@ -4,17 +4,18 @@ import faulthandler import gc import importlib import io +import json import os import sys import time import traceback import unittest +from typing import Any from test import support from test.support import TestStats from test.support import os_helper from test.support import threading_helper -from test.libregrtest.cmdline import Namespace from test.libregrtest.save_env import saved_test_environment from test.libregrtest.utils import clear_caches, format_duration, print_warning @@ -230,6 +231,10 @@ class RunTests: hunt_refleak: HuntRefleak | None = None test_dir: str | None = None junit_filename: str | None = None + memory_limit: str | None = None + gc_threshold: int | None = None + use_resources: list[str] = None + python_cmd: list[str] | None = None def copy(self, **override): state = dataclasses.asdict(self) @@ -249,11 +254,32 @@ class RunTests: else: yield from self.tests + def as_json(self): + return json.dumps(self, cls=_EncodeRunTests) + @staticmethod - def from_json_dict(json_dict): - if json_dict['hunt_refleak']: - json_dict['hunt_refleak'] = HuntRefleak(**json_dict['hunt_refleak']) - return RunTests(**json_dict) + def from_json(worker_json): + return json.loads(worker_json, object_hook=_decode_runtests) + + +class _EncodeRunTests(json.JSONEncoder): + def default(self, o: Any) -> dict[str, Any]: + if isinstance(o, RunTests): + result = dataclasses.asdict(o) + result["__runtests__"] = True + return result + else: + return super().default(o) + + +def _decode_runtests(data: dict[str, Any]) -> RunTests | dict[str, Any]: + if "__runtests__" in data: + data.pop('__runtests__') + if data['hunt_refleak']: + data['hunt_refleak'] = HuntRefleak(**data['hunt_refleak']) + return RunTests(**data) + else: + return data # Minimum duration of a test to display its duration or to mention that @@ -320,7 +346,7 @@ def abs_module_name(test_name: str, test_dir: str | None) -> str: return 'test.' + test_name -def setup_support(runtests: RunTests, ns: Namespace): +def setup_support(runtests: RunTests): support.PGO = runtests.pgo support.PGO_EXTENDED = runtests.pgo_extended support.set_match_tests(runtests.match_tests, runtests.ignore_tests) @@ -332,7 +358,7 @@ def setup_support(runtests: RunTests, ns: Namespace): support.junit_xml_list = None -def _runtest(result: TestResult, runtests: RunTests, ns: Namespace) -> None: +def _runtest(result: TestResult, runtests: RunTests) -> None: # Capture stdout and stderr, set faulthandler timeout, # and create JUnit XML report. verbose = runtests.verbose @@ -346,7 +372,7 @@ def _runtest(result: TestResult, runtests: RunTests, ns: Namespace) -> None: faulthandler.dump_traceback_later(timeout, exit=True) try: - setup_support(runtests, ns) + setup_support(runtests) if output_on_failure: support.verbose = True @@ -366,7 +392,7 @@ def _runtest(result: TestResult, runtests: RunTests, ns: Namespace) -> None: # warnings will be written to sys.stderr below. print_warning.orig_stderr = stream - _runtest_env_changed_exc(result, runtests, ns, display_failure=False) + _runtest_env_changed_exc(result, runtests, display_failure=False) # Ignore output if the test passed successfully if result.state != State.PASSED: output = stream.getvalue() @@ -381,7 +407,7 @@ def _runtest(result: TestResult, runtests: RunTests, ns: Namespace) -> None: else: # Tell tests to be moderately quiet support.verbose = verbose - _runtest_env_changed_exc(result, runtests, ns, + _runtest_env_changed_exc(result, runtests, display_failure=not verbose) xml_list = support.junit_xml_list @@ -395,10 +421,9 @@ def _runtest(result: TestResult, runtests: RunTests, ns: Namespace) -> None: support.junit_xml_list = None -def run_single_test(test_name: str, runtests: RunTests, ns: Namespace) -> TestResult: +def run_single_test(test_name: str, runtests: RunTests) -> TestResult: """Run a single test. - ns -- regrtest namespace of options test_name -- the name of the test Returns a TestResult. @@ -410,7 +435,7 @@ def run_single_test(test_name: str, runtests: RunTests, ns: Namespace) -> TestRe result = TestResult(test_name) pgo = runtests.pgo try: - _runtest(result, runtests, ns) + _runtest(result, runtests) except: if not pgo: msg = traceback.format_exc() @@ -472,7 +497,7 @@ def regrtest_runner(result: TestResult, test_func, runtests: RunTests) -> None: FOUND_GARBAGE = [] -def _load_run_test(result: TestResult, runtests: RunTests, ns: Namespace) -> None: +def _load_run_test(result: TestResult, runtests: RunTests) -> None: # Load the test function, run the test function. module_name = abs_module_name(result.test_name, runtests.test_dir) @@ -513,7 +538,6 @@ def _load_run_test(result: TestResult, runtests: RunTests, ns: Namespace) -> Non def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, - ns: Namespace, display_failure: bool = True) -> None: # Detect environment changes, handle exceptions. @@ -532,7 +556,7 @@ def _runtest_env_changed_exc(result: TestResult, runtests: RunTests, support.gc_collect() with save_env(test_name, runtests): - _load_run_test(result, runtests, ns) + _load_run_test(result, runtests) except support.ResourceDenied as msg: if not quiet and not pgo: print(f"{test_name} skipped -- {msg}", flush=True) diff --git a/Lib/test/libregrtest/runtest_mp.py b/Lib/test/libregrtest/runtest_mp.py index 4e3148f..e4a9301 100644 --- a/Lib/test/libregrtest/runtest_mp.py +++ b/Lib/test/libregrtest/runtest_mp.py @@ -47,49 +47,16 @@ USE_PROCESS_GROUP = (hasattr(os, "setsid") and hasattr(os, "killpg")) @dataclasses.dataclass(slots=True) class WorkerJob: runtests: RunTests - namespace: Namespace -class _EncodeWorkerJob(json.JSONEncoder): - def default(self, o: Any) -> dict[str, Any]: - match o: - case WorkerJob(): - result = dataclasses.asdict(o) - result["__worker_job__"] = True - return result - case Namespace(): - result = vars(o) - result["__namespace__"] = True - return result - case _: - return super().default(o) - - -def _decode_worker_job(d: dict[str, Any]) -> WorkerJob | dict[str, Any]: - if "__worker_job__" in d: - d.pop('__worker_job__') - d['runtests'] = RunTests.from_json_dict(d['runtests']) - return WorkerJob(**d) - if "__namespace__" in d: - d.pop('__namespace__') - return Namespace(**d) - else: - return d - - -def _parse_worker_json(worker_json: str) -> tuple[Namespace, str]: - return json.loads(worker_json, object_hook=_decode_worker_job) - - -def create_worker_process(worker_job: WorkerJob, +def create_worker_process(runtests: RunTests, output_file: TextIO, tmp_dir: str | None = None) -> subprocess.Popen: - ns = worker_job.namespace - python = ns.python - worker_json = json.dumps(worker_job, cls=_EncodeWorkerJob) + python_cmd = runtests.python_cmd + worker_json = runtests.as_json() - if python is not None: - executable = python + if python_cmd is not None: + executable = python_cmd else: executable = [sys.executable] cmd = [*executable, *support.args_from_interpreter_flags(), @@ -121,14 +88,12 @@ def create_worker_process(worker_job: WorkerJob, def worker_process(worker_json: str) -> NoReturn: - worker_job = _parse_worker_json(worker_json) - runtests = worker_job.runtests - ns = worker_job.namespace + runtests = RunTests.from_json(worker_json) test_name = runtests.tests[0] match_tests: FilterTuple | None = runtests.match_tests setup_test_dir(runtests.test_dir) - setup_tests(runtests, ns) + setup_tests(runtests) if runtests.rerun: if match_tests: @@ -137,7 +102,7 @@ def worker_process(worker_json: str) -> NoReturn: else: print(f"Re-running {test_name} in verbose mode", flush=True) - result = run_single_test(test_name, runtests, ns) + result = run_single_test(test_name, runtests) print() # Force a newline (just in case) # Serialize TestResult as dict in JSON @@ -330,9 +295,6 @@ class TestWorkerProcess(threading.Thread): if match_tests: kwargs['match_tests'] = match_tests worker_runtests = self.runtests.copy(tests=tests, **kwargs) - worker_job = WorkerJob( - worker_runtests, - namespace=self.ns) # gh-94026: Write stdout+stderr to a tempfile as workaround for # non-blocking pipes on Emscripten with NodeJS. @@ -347,12 +309,12 @@ class TestWorkerProcess(threading.Thread): tmp_dir = tempfile.mkdtemp(prefix="test_python_") tmp_dir = os.path.abspath(tmp_dir) try: - retcode = self._run_process(worker_job, stdout_file, tmp_dir) + retcode = self._run_process(worker_runtests, stdout_file, tmp_dir) finally: tmp_files = os.listdir(tmp_dir) os_helper.rmtree(tmp_dir) else: - retcode = self._run_process(worker_job, stdout_file) + retcode = self._run_process(worker_runtests, stdout_file) tmp_files = () stdout_file.seek(0) diff --git a/Lib/test/libregrtest/setup.py b/Lib/test/libregrtest/setup.py index 59bbf2c..5944983 100644 --- a/Lib/test/libregrtest/setup.py +++ b/Lib/test/libregrtest/setup.py @@ -25,7 +25,7 @@ def setup_test_dir(testdir: str | None) -> None: sys.path.insert(0, os.path.abspath(testdir)) -def setup_tests(runtests, ns): +def setup_tests(runtests): try: stderr_fd = sys.__stderr__.fileno() except (ValueError, AttributeError): @@ -71,15 +71,15 @@ def setup_tests(runtests, ns): if runtests.hunt_refleak: unittest.BaseTestSuite._cleanup = False - if ns.memlimit is not None: - support.set_memlimit(ns.memlimit) + if runtests.memory_limit is not None: + support.set_memlimit(runtests.memory_limit) - if ns.threshold is not None: - gc.set_threshold(ns.threshold) + if runtests.gc_threshold is not None: + gc.set_threshold(runtests.gc_threshold) support.suppress_msvcrt_asserts(runtests.verbose and runtests.verbose >= 2) - support.use_resources = ns.use_resources + support.use_resources = runtests.use_resources if hasattr(sys, 'addaudithook'): # Add an auditing hook for all tests to ensure PySys_Audit is tested diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index f40b73b..6fc85ff 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -878,27 +878,31 @@ _4G = 4 * _1G MAX_Py_ssize_t = sys.maxsize -def set_memlimit(limit): - global max_memuse - global real_max_memuse +def _parse_memlimit(limit: str) -> int: sizes = { 'k': 1024, 'm': _1M, 'g': _1G, 't': 1024*_1G, } - m = re.match(r'(\d+(\.\d+)?) (K|M|G|T)b?$', limit, + m = re.match(r'(\d+(?:\.\d+)?) (K|M|G|T)b?$', limit, re.IGNORECASE | re.VERBOSE) if m is None: - raise ValueError('Invalid memory limit %r' % (limit,)) - memlimit = int(float(m.group(1)) * sizes[m.group(3).lower()]) - real_max_memuse = memlimit - if memlimit > MAX_Py_ssize_t: - memlimit = MAX_Py_ssize_t + raise ValueError(f'Invalid memory limit: {limit!r}') + return int(float(m.group(1)) * sizes[m.group(2).lower()]) + +def set_memlimit(limit: str) -> None: + global max_memuse + global real_max_memuse + memlimit = _parse_memlimit(limit) if memlimit < _2G - 1: - raise ValueError('Memory limit %r too low to be useful' % (limit,)) + raise ValueError('Memory limit {limit!r} too low to be useful') + + real_max_memuse = memlimit + memlimit = min(memlimit, MAX_Py_ssize_t) max_memuse = memlimit + class _MemoryWatchdog: """An object which periodically watches the process' memory consumption and prints it out. diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 6428073..5b57c5f 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -760,7 +760,45 @@ class TestSupport(unittest.TestCase): else: self.fail("RecursionError was not raised") - #self.assertEqual(available, 2) + def test_parse_memlimit(self): + parse = support._parse_memlimit + KiB = 1024 + MiB = KiB * 1024 + GiB = MiB * 1024 + TiB = GiB * 1024 + self.assertEqual(parse('0k'), 0) + self.assertEqual(parse('3k'), 3 * KiB) + self.assertEqual(parse('2.4m'), int(2.4 * MiB)) + self.assertEqual(parse('4g'), int(4 * GiB)) + self.assertEqual(parse('1t'), TiB) + + for limit in ('', '3', '3.5.10k', '10x'): + with self.subTest(limit=limit): + with self.assertRaises(ValueError): + parse(limit) + + def test_set_memlimit(self): + _4GiB = 4 * 1024 ** 3 + TiB = 1024 ** 4 + old_max_memuse = support.max_memuse + old_real_max_memuse = support.real_max_memuse + try: + if sys.maxsize > 2**32: + support.set_memlimit('4g') + self.assertEqual(support.max_memuse, _4GiB) + self.assertEqual(support.real_max_memuse, _4GiB) + + big = 2**100 // TiB + support.set_memlimit(f'{big}t') + self.assertEqual(support.max_memuse, sys.maxsize) + self.assertEqual(support.real_max_memuse, big * TiB) + else: + support.set_memlimit('4g') + self.assertEqual(support.max_memuse, sys.maxsize) + self.assertEqual(support.real_max_memuse, _4GiB) + finally: + support.max_memuse = old_max_memuse + support.real_max_memuse = old_real_max_memuse # XXX -follows a list of untested API # make_legacy_pyc @@ -773,7 +811,6 @@ class TestSupport(unittest.TestCase): # EnvironmentVarGuard # transient_internet # run_with_locale - # set_memlimit # bigmemtest # precisionbigmemtest # bigaddrspacetest |