Skip to content

Commit b4e03bf

Browse files
committed
pythongh-109566: regrtest reexecutes the process
regrtest now replaces the process with a new process to add options to the Python command line. Add options: "-u -W default -bb". When --fast-ci and --slow-ci options are used, the -E option is also added. The following methods to run the Python test suite don't replace the process: * "import test.autotest" * "from test.regrtest import main; main()" 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.
1 parent 859618c commit b4e03bf

File tree

10 files changed

+71
-56
lines changed

10 files changed

+71
-56
lines changed

Lib/test/autotest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
# It can be especially handy if you're in an interactive shell, e.g.,
33
# from test import autotest.
44
from test.libregrtest.main import main
5-
main()
5+
main(reexec=False)

Lib/test/libregrtest/cmdline.py

+3
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def __init__(self, **kwargs) -> None:
184184
self.threshold = None
185185
self.fail_rerun = False
186186
self.tempdir = None
187+
self.no_reexec = False
187188

188189
super().__init__(**kwargs)
189190

@@ -343,6 +344,8 @@ def _create_parser():
343344
help='override the working directory for the test run')
344345
group.add_argument('--cleanup', action='store_true',
345346
help='remove old test_python_* directories')
347+
group.add_argument('--no-reexec', action='store_true',
348+
help="internal option, don't use it")
346349
return parser
347350

348351

Lib/test/libregrtest/main.py

+45-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import random
33
import re
4+
import shlex
45
import sys
56
import time
67

@@ -20,7 +21,7 @@
2021
StrPath, StrJSON, TestName, TestList, TestTuple, FilterTuple,
2122
strip_py_suffix, count, format_duration,
2223
printlist, get_temp_dir, get_work_dir, exit_timeout,
23-
display_header, cleanup_temp_dir,
24+
display_header, cleanup_temp_dir, print_warning,
2425
MS_WINDOWS)
2526

2627

@@ -47,7 +48,7 @@ class Regrtest:
4748
directly to set the values that would normally be set by flags
4849
on the command line.
4950
"""
50-
def __init__(self, ns: Namespace):
51+
def __init__(self, ns: Namespace, reexec: bool = True):
5152
# Log verbosity
5253
self.verbose: int = int(ns.verbose)
5354
self.quiet: bool = ns.quiet
@@ -69,6 +70,7 @@ def __init__(self, ns: Namespace):
6970
self.want_cleanup: bool = ns.cleanup
7071
self.want_rerun: bool = ns.rerun
7172
self.want_run_leaks: bool = ns.runleaks
73+
self.want_reexec: bool = (reexec and not ns.no_reexec)
7274

7375
# Select tests
7476
if ns.match_tests:
@@ -95,6 +97,7 @@ def __init__(self, ns: Namespace):
9597
self.worker_json: StrJSON | None = ns.worker_json
9698

9799
# Options to run tests
100+
self.ci_mode: bool = (ns.fast_ci or ns.slow_ci)
98101
self.fail_fast: bool = ns.failfast
99102
self.fail_env_changed: bool = ns.fail_env_changed
100103
self.fail_rerun: bool = ns.fail_rerun
@@ -411,6 +414,11 @@ def create_run_tests(self, tests: TestTuple):
411414
)
412415

413416
def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
417+
# Set sys.stdout encoder error handler to backslashreplace,
418+
# similar to sys.stderr error handler, to avoid UnicodeEncodeError
419+
# when printing a traceback or any other non-encodable character.
420+
sys.stdout.reconfigure(errors="backslashreplace")
421+
414422
if self.hunt_refleak and self.hunt_refleak.warmups < 3:
415423
msg = ("WARNING: Running tests with --huntrleaks/-R and "
416424
"less than 3 warmup repetitions can give false positives!")
@@ -483,7 +491,40 @@ def run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
483491
# processes.
484492
return self._run_tests(selected, tests)
485493

494+
def _reexecute_python(self):
495+
if self.python_cmd:
496+
# Do nothing if --python=cmd option is used
497+
return
498+
499+
python_opts = [
500+
'-u', # Unbuffered stdout and stderr
501+
'-W', 'default', # Add warnings filter 'default'
502+
'-bb', # Error on bytes/str comparison
503+
]
504+
if self.ci_mode:
505+
python_opts.append('-E') # Ignore PYTHON* environment variables
506+
507+
cmd = [*sys.orig_argv, "--no-reexec"]
508+
cmd[1:1] = python_opts
509+
510+
# Make sure that messages before execv() are logged
511+
sys.stdout.flush()
512+
sys.stderr.flush()
513+
514+
try:
515+
os.execv(cmd[0], cmd)
516+
# execv() do no return and so we don't get to this line on success
517+
except OSError as exc:
518+
cmd_text = shlex.join(cmd)
519+
print_warning(f"Failed to reexecute Python: {exc!r}\n"
520+
f"Command: {cmd_text}")
521+
486522
def main(self, tests: TestList | None = None):
523+
if self.want_reexec:
524+
self._reexecute_python()
525+
526+
print(f"{sys.flags.ignore_environment=}")
527+
487528
if self.junit_filename and not os.path.isabs(self.junit_filename):
488529
self.junit_filename = os.path.abspath(self.junit_filename)
489530

@@ -515,7 +556,7 @@ def main(self, tests: TestList | None = None):
515556
sys.exit(exitcode)
516557

517558

518-
def main(tests=None, **kwargs):
559+
def main(tests=None, reexec=True, **kwargs):
519560
"""Run the Python suite."""
520561
ns = _parse_args(sys.argv[1:], **kwargs)
521-
Regrtest(ns).main(tests=tests)
562+
Regrtest(ns, reexec=reexec).main(tests=tests)

Lib/test/libregrtest/setup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .runtests import RunTests
1212
from .utils import (
1313
setup_unraisable_hook, setup_threading_excepthook, fix_umask,
14-
replace_stdout, adjust_rlimit_nofile)
14+
adjust_rlimit_nofile)
1515

1616

1717
UNICODE_GUARD_ENV = "PYTHONREGRTEST_UNICODE_GUARD"
@@ -49,7 +49,7 @@ def setup_process():
4949
faulthandler.register(signum, chain=True, file=stderr_fd)
5050

5151
adjust_rlimit_nofile()
52-
replace_stdout()
52+
5353
support.record_original_stdout(sys.stdout)
5454

5555
# Some times __path__ and __file__ are not absolute (e.g. while running from

Lib/test/libregrtest/utils.py

+12-39
Original file line numberDiff line numberDiff line change
@@ -495,32 +495,6 @@ def normalize_test_name(test_full_name, *, is_error=False):
495495
return short_name
496496

497497

498-
def replace_stdout():
499-
"""Set stdout encoder error handler to backslashreplace (as stderr error
500-
handler) to avoid UnicodeEncodeError when printing a traceback"""
501-
stdout = sys.stdout
502-
try:
503-
fd = stdout.fileno()
504-
except ValueError:
505-
# On IDLE, sys.stdout has no file descriptor and is not a TextIOWrapper
506-
# object. Leaving sys.stdout unchanged.
507-
#
508-
# Catch ValueError to catch io.UnsupportedOperation on TextIOBase
509-
# and ValueError on a closed stream.
510-
return
511-
512-
sys.stdout = open(fd, 'w',
513-
encoding=stdout.encoding,
514-
errors="backslashreplace",
515-
closefd=False,
516-
newline='\n')
517-
518-
def restore_stdout():
519-
sys.stdout.close()
520-
sys.stdout = stdout
521-
atexit.register(restore_stdout)
522-
523-
524498
def adjust_rlimit_nofile():
525499
"""
526500
On macOS the default fd limit (RLIMIT_NOFILE) is sometimes too low (256)
@@ -569,7 +543,6 @@ def display_header(use_resources: tuple[str, ...]):
569543
print("== encodings: locale=%s, FS=%s"
570544
% (locale.getencoding(), sys.getfilesystemencoding()))
571545

572-
573546
if use_resources:
574547
print(f"== resources ({len(use_resources)}): "
575548
f"{', '.join(sorted(use_resources))}")
@@ -588,18 +561,18 @@ def display_header(use_resources: tuple[str, ...]):
588561
sanitizers.append("memory")
589562
if ubsan:
590563
sanitizers.append("undefined behavior")
591-
if not sanitizers:
592-
return
593-
594-
print(f"== sanitizers: {', '.join(sanitizers)}")
595-
for sanitizer, env_var in (
596-
(asan, "ASAN_OPTIONS"),
597-
(msan, "MSAN_OPTIONS"),
598-
(ubsan, "UBSAN_OPTIONS"),
599-
):
600-
options= os.environ.get(env_var)
601-
if sanitizer and options is not None:
602-
print(f"== {env_var}={options!r}")
564+
if sanitizers:
565+
print(f"== sanitizers: {', '.join(sanitizers)}")
566+
for sanitizer, env_var in (
567+
(asan, "ASAN_OPTIONS"),
568+
(msan, "MSAN_OPTIONS"),
569+
(ubsan, "UBSAN_OPTIONS"),
570+
):
571+
options= os.environ.get(env_var)
572+
if sanitizer and options is not None:
573+
print(f"== {env_var}={options!r}")
574+
575+
print(flush=True)
603576

604577

605578
def cleanup_temp_dir(tmp_dir: StrPath):

Lib/test/regrtest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def _main():
4040
# sanity check
4141
assert __file__ == os.path.abspath(sys.argv[0])
4242

43-
main()
43+
main(reexec=False)
4444

4545

4646
if __name__ == '__main__':

Lib/test/support/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,7 @@ def print_warning(msg):
13461346
stream = print_warning.orig_stderr
13471347
for line in msg.splitlines():
13481348
print(f"Warning -- {line}", file=stream)
1349+
print(file=stream)
13491350
stream.flush()
13501351

13511352
# bpo-39983: Store the original sys.stderr at Python startup to be able to

Lib/test/test_regrtest.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,8 @@ def check_ci_mode(self, args, use_resources):
382382
# Check Regrtest attributes which are more reliable than Namespace
383383
# which has an unclear API
384384
regrtest = main.Regrtest(ns)
385-
self.assertNotEqual(regrtest.num_workers, 0)
385+
self.assertTrue(regrtest.ci_mode)
386+
self.assertGreaterEqual(regrtest.num_workers, 1)
386387
self.assertTrue(regrtest.want_rerun)
387388
self.assertTrue(regrtest.randomize)
388389
self.assertIsNone(regrtest.random_seed)

PCbuild/rt.bat

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ if NOT "%1"=="" (set regrtestargs=%regrtestargs% %1) & shift & goto CheckOpts
4848

4949
if not defined prefix set prefix=%pcbuild%amd64
5050
set exe=%prefix%\python%suffix%.exe
51-
set cmd="%exe%" %dashO% -u -Wd -E -bb -m test %regrtestargs%
51+
set cmd="%exe%" %dashO% -m test %regrtestargs%
5252
if defined qmode goto Qmode
5353

5454
echo Deleting .pyc files ...

Tools/scripts/run_tests.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@ def is_python_flag(arg):
2323

2424

2525
def main(regrtest_args):
26-
args = [sys.executable,
27-
'-u', # Unbuffered stdout and stderr
28-
'-W', 'default', # Warnings set to 'default'
29-
'-bb', # Warnings about bytes/bytearray
30-
]
26+
args = [sys.executable]
3127

3228
cross_compile = '_PYTHON_HOST_PLATFORM' in os.environ
3329
if (hostrunner := os.environ.get("_PYTHON_HOSTRUNNER")) is None:
@@ -47,7 +43,6 @@ def main(regrtest_args):
4743
}
4844
else:
4945
environ = os.environ.copy()
50-
args.append("-E")
5146

5247
# Allow user-specified interpreter options to override our defaults.
5348
args.extend(test.support.args_from_interpreter_flags())
@@ -70,7 +65,8 @@ def main(regrtest_args):
7065

7166
args.extend(regrtest_args)
7267

73-
print(shlex.join(args))
68+
print(shlex.join(args), flush=True)
69+
7470
if sys.platform == 'win32':
7571
from subprocess import call
7672
sys.exit(call(args))

0 commit comments

Comments
 (0)