Skip to content

Commit 235aacd

Browse files
authored
gh-109566: regrtest _add_python_opts() handles KeyboardInterrupt (#110062)
In the subprocess code path, wait until the child process completes with a timeout of EXIT_TIMEOUT seconds. Fix create_worker_process() regression: use start_new_session=True if USE_PROCESS_GROUP is true. WorkerThread.wait_stopped() uses a timeout of 60 seconds, instead of 30 seconds.
1 parent bd4518c commit 235aacd

File tree

4 files changed

+33
-14
lines changed

4 files changed

+33
-14
lines changed

Lib/test/libregrtest/main.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@
1111
from .cmdline import _parse_args, Namespace
1212
from .findtests import findtests, split_test_packages, list_cases
1313
from .logger import Logger
14+
from .pgo import setup_pgo_tests
1415
from .result import State
16+
from .results import TestResults, EXITCODE_INTERRUPTED
1517
from .runtests import RunTests, HuntRefleak
1618
from .setup import setup_process, setup_test_dir
1719
from .single import run_single_test, PROGRESS_MIN_TIME
18-
from .pgo import setup_pgo_tests
19-
from .results import TestResults
2020
from .utils import (
2121
StrPath, StrJSON, TestName, TestList, TestTuple, FilterTuple,
2222
strip_py_suffix, count, format_duration,
2323
printlist, get_temp_dir, get_work_dir, exit_timeout,
2424
display_header, cleanup_temp_dir, print_warning,
25-
MS_WINDOWS)
25+
MS_WINDOWS, EXIT_TIMEOUT)
2626

2727

2828
class Regrtest:
@@ -525,10 +525,23 @@ def _add_python_opts(self):
525525
try:
526526
if hasattr(os, 'execv') and not MS_WINDOWS:
527527
os.execv(cmd[0], cmd)
528-
# execv() do no return and so we don't get to this line on success
528+
# On success, execv() do no return.
529+
# On error, it raises an OSError.
529530
else:
530531
import subprocess
531-
proc = subprocess.run(cmd)
532+
with subprocess.Popen(cmd) as proc:
533+
try:
534+
proc.wait()
535+
except KeyboardInterrupt:
536+
# There is no need to call proc.terminate(): on CTRL+C,
537+
# SIGTERM is also sent to the child process.
538+
try:
539+
proc.wait(timeout=EXIT_TIMEOUT)
540+
except subprocess.TimeoutExpired:
541+
proc.kill()
542+
proc.wait()
543+
sys.exit(EXITCODE_INTERRUPTED)
544+
532545
sys.exit(proc.returncode)
533546
except Exception as exc:
534547
print_warning(f"Failed to change Python options: {exc!r}\n"

Lib/test/libregrtest/run_workers.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@
4242
assert MAIN_PROCESS_TIMEOUT >= PROGRESS_UPDATE
4343

4444
# Time to wait until a worker completes: should be immediate
45-
JOIN_TIMEOUT = 30.0 # seconds
45+
WAIT_COMPLETED_TIMEOUT = 30.0 # seconds
46+
47+
# Time to wait a killed process (in seconds)
48+
WAIT_KILLED_TIMEOUT = 60.0
4649

4750

4851
# We do not use a generator so multiple threads can call next().
@@ -138,7 +141,7 @@ def _kill(self) -> None:
138141
if USE_PROCESS_GROUP:
139142
what = f"{self} process group"
140143
else:
141-
what = f"{self}"
144+
what = f"{self} process"
142145

143146
print(f"Kill {what}", file=sys.stderr, flush=True)
144147
try:
@@ -390,10 +393,10 @@ def _wait_completed(self) -> None:
390393
popen = self._popen
391394

392395
try:
393-
popen.wait(JOIN_TIMEOUT)
396+
popen.wait(WAIT_COMPLETED_TIMEOUT)
394397
except (subprocess.TimeoutExpired, OSError) as exc:
395398
print_warning(f"Failed to wait for {self} completion "
396-
f"(timeout={format_duration(JOIN_TIMEOUT)}): "
399+
f"(timeout={format_duration(WAIT_COMPLETED_TIMEOUT)}): "
397400
f"{exc!r}")
398401

399402
def wait_stopped(self, start_time: float) -> None:
@@ -414,7 +417,7 @@ def wait_stopped(self, start_time: float) -> None:
414417
break
415418
dt = time.monotonic() - start_time
416419
self.log(f"Waiting for {self} thread for {format_duration(dt)}")
417-
if dt > JOIN_TIMEOUT:
420+
if dt > WAIT_KILLED_TIMEOUT:
418421
print_warning(f"Failed to join {self} in {format_duration(dt)}")
419422
break
420423

Lib/test/libregrtest/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ def display_header(use_resources: tuple[str, ...]):
541541
print(f"== resources ({len(use_resources)}): "
542542
f"{', '.join(sorted(use_resources))}")
543543
else:
544-
print(f"== resources: (all disabled, use -u option)")
544+
print("== resources: (all disabled, use -u option)")
545545

546546
# This makes it easier to remember what to set in your local
547547
# environment when trying to reproduce a sanitizer failure.

Lib/test/libregrtest/worker.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,15 @@ def create_worker_process(runtests: RunTests, output_fd: int,
4141
env['TEMP'] = tmp_dir
4242
env['TMP'] = tmp_dir
4343

44+
# Running the child from the same working directory as regrtest's original
45+
# invocation ensures that TEMPDIR for the child is the same when
46+
# sysconfig.is_python_build() is true. See issue 15300.
47+
#
4448
# Emscripten and WASI Python must start in the Python source code directory
4549
# to get 'python.js' or 'python.wasm' file. Then worker_process() changes
4650
# to a temporary directory created to run tests.
4751
work_dir = os_helper.SAVEDCWD
4852

49-
# Running the child from the same working directory as regrtest's original
50-
# invocation ensures that TEMPDIR for the child is the same when
51-
# sysconfig.is_python_build() is true. See issue 15300.
5253
kwargs: dict[str, Any] = dict(
5354
env=env,
5455
stdout=output_fd,
@@ -58,6 +59,8 @@ def create_worker_process(runtests: RunTests, output_fd: int,
5859
close_fds=True,
5960
cwd=work_dir,
6061
)
62+
if USE_PROCESS_GROUP:
63+
kwargs['start_new_session'] = True
6164

6265
# Pass json_file to the worker process
6366
json_file = runtests.json_file

0 commit comments

Comments
 (0)