gh-109566: regrtest reexecutes the process (#109909)

When --fast-ci or --slow-ci option is used, regrtest now replaces the
current process with a new process to add "-u -W default -bb -E"
options to Python.

Changes:

* PCbuild/rt.bat and Tools/scripts/run_tests.py no longer need to add
  "-u -W default -bb -E" options to Python: it's now done by
  regrtest.
* Fix Tools/scripts/run_tests.py: flush stdout before replacing the
  process. Previously, buffered messages were lost.
This commit is contained in:
Victor Stinner 2023-09-26 20:46:52 +02:00 committed by GitHub
parent ecd813f054
commit fbfec5642e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 107 additions and 14 deletions

View file

@ -1,2 +1,2 @@
from test.libregrtest.main import main from test.libregrtest.main import main
main() main(reexec=True)

View file

@ -184,6 +184,7 @@ class Namespace(argparse.Namespace):
self.threshold = None self.threshold = None
self.fail_rerun = False self.fail_rerun = False
self.tempdir = None self.tempdir = None
self.no_reexec = False
super().__init__(**kwargs) super().__init__(**kwargs)
@ -343,6 +344,8 @@ def _create_parser():
help='override the working directory for the test run') help='override the working directory for the test run')
group.add_argument('--cleanup', action='store_true', group.add_argument('--cleanup', action='store_true',
help='remove old test_python_* directories') help='remove old test_python_* directories')
group.add_argument('--no-reexec', action='store_true',
help="internal option, don't use it")
return parser return parser
@ -421,6 +424,8 @@ def _parse_args(args, **kwargs):
ns.verbose3 = True ns.verbose3 = True
if MS_WINDOWS: if MS_WINDOWS:
ns.nowindows = True # Silence alerts under Windows ns.nowindows = True # Silence alerts under Windows
else:
ns.no_reexec = True
# When both --slow-ci and --fast-ci options are present, # When both --slow-ci and --fast-ci options are present,
# --slow-ci has the priority # --slow-ci has the priority

View file

@ -1,6 +1,7 @@
import os import os
import random import random
import re import re
import shlex
import sys import sys
import time import time
@ -20,7 +21,7 @@ from .utils import (
StrPath, StrJSON, TestName, TestList, TestTuple, FilterTuple, StrPath, StrJSON, TestName, TestList, TestTuple, FilterTuple,
strip_py_suffix, count, format_duration, strip_py_suffix, count, format_duration,
printlist, get_temp_dir, get_work_dir, exit_timeout, printlist, get_temp_dir, get_work_dir, exit_timeout,
display_header, cleanup_temp_dir, display_header, cleanup_temp_dir, print_warning,
MS_WINDOWS) MS_WINDOWS)
@ -47,7 +48,7 @@ class Regrtest:
directly to set the values that would normally be set by flags directly to set the values that would normally be set by flags
on the command line. on the command line.
""" """
def __init__(self, ns: Namespace): def __init__(self, ns: Namespace, reexec: bool = False):
# Log verbosity # Log verbosity
self.verbose: int = int(ns.verbose) self.verbose: int = int(ns.verbose)
self.quiet: bool = ns.quiet self.quiet: bool = ns.quiet
@ -69,6 +70,7 @@ class Regrtest:
self.want_cleanup: bool = ns.cleanup self.want_cleanup: bool = ns.cleanup
self.want_rerun: bool = ns.rerun self.want_rerun: bool = ns.rerun
self.want_run_leaks: bool = ns.runleaks self.want_run_leaks: bool = ns.runleaks
self.want_reexec: bool = (reexec and not ns.no_reexec)
# Select tests # Select tests
if ns.match_tests: if ns.match_tests:
@ -95,6 +97,7 @@ class Regrtest:
self.worker_json: StrJSON | None = ns.worker_json self.worker_json: StrJSON | None = ns.worker_json
# Options to run tests # Options to run tests
self.ci_mode: bool = (ns.fast_ci or ns.slow_ci)
self.fail_fast: bool = ns.failfast self.fail_fast: bool = ns.failfast
self.fail_env_changed: bool = ns.fail_env_changed self.fail_env_changed: bool = ns.fail_env_changed
self.fail_rerun: bool = ns.fail_rerun self.fail_rerun: bool = ns.fail_rerun
@ -483,7 +486,37 @@ class Regrtest:
# processes. # processes.
return self._run_tests(selected, tests) return self._run_tests(selected, tests)
def _reexecute_python(self):
if self.python_cmd:
# Do nothing if --python=cmd option is used
return
python_opts = [
'-u', # Unbuffered stdout and stderr
'-W', 'default', # Add warnings filter 'default'
'-bb', # Error on bytes/str comparison
'-E', # Ignore PYTHON* environment variables
]
cmd = [*sys.orig_argv, "--no-reexec"]
cmd[1:1] = python_opts
# Make sure that messages before execv() are logged
sys.stdout.flush()
sys.stderr.flush()
try:
os.execv(cmd[0], cmd)
# execv() do no return and so we don't get to this line on success
except OSError as exc:
cmd_text = shlex.join(cmd)
print_warning(f"Failed to reexecute Python: {exc!r}\n"
f"Command: {cmd_text}")
def main(self, tests: TestList | None = None): def main(self, tests: TestList | None = None):
if self.want_reexec and self.ci_mode:
self._reexecute_python()
if self.junit_filename and not os.path.isabs(self.junit_filename): if self.junit_filename and not os.path.isabs(self.junit_filename):
self.junit_filename = os.path.abspath(self.junit_filename) self.junit_filename = os.path.abspath(self.junit_filename)
@ -515,7 +548,7 @@ class Regrtest:
sys.exit(exitcode) sys.exit(exitcode)
def main(tests=None, **kwargs): def main(tests=None, reexec=False, **kwargs):
"""Run the Python suite.""" """Run the Python suite."""
ns = _parse_args(sys.argv[1:], **kwargs) ns = _parse_args(sys.argv[1:], **kwargs)
Regrtest(ns).main(tests=tests) Regrtest(ns, reexec=reexec).main(tests=tests)

View file

@ -382,7 +382,8 @@ class ParseArgsTestCase(unittest.TestCase):
# Check Regrtest attributes which are more reliable than Namespace # Check Regrtest attributes which are more reliable than Namespace
# which has an unclear API # which has an unclear API
regrtest = main.Regrtest(ns) regrtest = main.Regrtest(ns)
self.assertNotEqual(regrtest.num_workers, 0) self.assertTrue(regrtest.ci_mode)
self.assertEqual(regrtest.num_workers, -1)
self.assertTrue(regrtest.want_rerun) self.assertTrue(regrtest.want_rerun)
self.assertTrue(regrtest.randomize) self.assertTrue(regrtest.randomize)
self.assertIsNone(regrtest.random_seed) self.assertIsNone(regrtest.random_seed)
@ -1960,6 +1961,61 @@ class ArgsTestCase(BaseTestCase):
self.check_executed_tests(output, tests, self.check_executed_tests(output, tests,
stats=len(tests), parallel=True) stats=len(tests), parallel=True)
def check_reexec(self, option):
# --fast-ci and --slow-ci add "-u -W default -bb -E" options to Python
code = textwrap.dedent(r"""
import sys
import unittest
try:
from _testinternalcapi import get_config
except ImportError:
get_config = None
class WorkerTests(unittest.TestCase):
@unittest.skipUnless(get_config is None, 'need get_config()')
def test_config(self):
config = get_config()['config']
# -u option
self.assertEqual(config['buffered_stdio'], 0)
# -W default option
self.assertTrue(config['warnoptions'], ['default'])
# -bb option
self.assertTrue(config['bytes_warning'], 2)
# -E option
self.assertTrue(config['use_environment'], 0)
# test if get_config() is not available
def test_unbuffered(self):
# -u option
self.assertFalse(sys.stdout.line_buffering)
self.assertFalse(sys.stderr.line_buffering)
def test_python_opts(self):
# -W default option
self.assertTrue(sys.warnoptions, ['default'])
# -bb option
self.assertEqual(sys.flags.bytes_warning, 2)
# -E option
self.assertTrue(sys.flags.ignore_environment)
""")
testname = self.create_test(code=code)
cmd = [sys.executable,
"-m", "test", option,
f'--testdir={self.tmptestdir}',
testname]
proc = subprocess.run(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True)
self.assertEqual(proc.returncode, 0, proc)
def test_reexec_fast_ci(self):
self.check_reexec("--fast-ci")
def test_reexec_slow_ci(self):
self.check_reexec("--slow-ci")
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
def test_format_duration(self): def test_format_duration(self):

View file

@ -0,0 +1,3 @@
regrtest: When ``--fast-ci`` or ``--slow-ci`` option is used, regrtest now
replaces the current process with a new process to add ``-u -W default -bb -E``
options to Python. Patch by Victor Stinner.

View file

@ -48,7 +48,7 @@ if NOT "%1"=="" (set regrtestargs=%regrtestargs% %1) & shift & goto CheckOpts
if not defined prefix set prefix=%pcbuild%amd64 if not defined prefix set prefix=%pcbuild%amd64
set exe=%prefix%\python%suffix%.exe set exe=%prefix%\python%suffix%.exe
set cmd="%exe%" %dashO% -u -Wd -E -bb -m test %regrtestargs% set cmd="%exe%" %dashO% -m test %regrtestargs%
if defined qmode goto Qmode if defined qmode goto Qmode
echo Deleting .pyc files ... echo Deleting .pyc files ...

View file

@ -23,11 +23,7 @@ def is_python_flag(arg):
def main(regrtest_args): def main(regrtest_args):
args = [sys.executable, args = [sys.executable]
'-u', # Unbuffered stdout and stderr
'-W', 'default', # Warnings set to 'default'
'-bb', # Warnings about bytes/bytearray
]
cross_compile = '_PYTHON_HOST_PLATFORM' in os.environ cross_compile = '_PYTHON_HOST_PLATFORM' in os.environ
if (hostrunner := os.environ.get("_PYTHON_HOSTRUNNER")) is None: if (hostrunner := os.environ.get("_PYTHON_HOSTRUNNER")) is None:
@ -47,7 +43,6 @@ def main(regrtest_args):
} }
else: else:
environ = os.environ.copy() environ = os.environ.copy()
args.append("-E")
# Allow user-specified interpreter options to override our defaults. # Allow user-specified interpreter options to override our defaults.
args.extend(test.support.args_from_interpreter_flags()) args.extend(test.support.args_from_interpreter_flags())
@ -70,7 +65,8 @@ def main(regrtest_args):
args.extend(regrtest_args) args.extend(regrtest_args)
print(shlex.join(args)) print(shlex.join(args), flush=True)
if sys.platform == 'win32': if sys.platform == 'win32':
from subprocess import call from subprocess import call
sys.exit(call(args)) sys.exit(call(args))