Skip to content

Commit 3169629

Browse files
committed
gh-109276: libregrtest: add RunTests.work_dir
* WorkerThread now always creates a temporary directory, even on Emscripten and WASI: it's used as the working directory of the test worker process. * Fix Emscripten and WASI: start the test worker process in the Python source code directory, where 'python.js' and 'python.wasm' can be found. Then worker_process() goes to the temporary directory created to run tests. * --cleanup now also removes "temporary" directories of test worker processes (in /tmp).
1 parent 1ee50e2 commit 3169629

File tree

6 files changed

+67
-49
lines changed

6 files changed

+67
-49
lines changed

Lib/test/libregrtest/cmdline.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ def _create_parser():
331331
help='writes JUnit-style XML results to the specified '
332332
'file')
333333
group.add_argument('--tempdir', metavar='PATH',
334-
help='override the working directory for the test run')
334+
help='Override the working directory for the test run. '
335+
'It not be too long and only use ASCII characters.')
335336
group.add_argument('--cleanup', action='store_true',
336337
help='remove old test_python_* directories')
337338
return parser

Lib/test/libregrtest/main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44
import sys
55
import time
6+
import tempfile
67

78
from test import support
89
from test.support import os_helper
@@ -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, set_temp_dir_environ)
2425

2526

2627
class Regrtest:
@@ -393,6 +394,7 @@ def create_run_tests(self, tests: TestTuple):
393394
gc_threshold=self.gc_threshold,
394395
use_resources=self.use_resources,
395396
python_cmd=self.python_cmd,
397+
work_dir=os.getcwd(),
396398
)
397399

398400
def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
@@ -465,10 +467,18 @@ def main(self, tests: TestList | None = None):
465467

466468
strip_py_suffix(self.cmdline_args)
467469

470+
if self.tmp_dir:
471+
self.tmp_dir = os.path.abspath(os.path.expanduser(self.tmp_dir))
472+
set_temp_dir_environ(os.environ, self.tmp_dir)
468473
self.tmp_dir = get_temp_dir(self.tmp_dir)
474+
# Don't use set_temp_dir_environ() on get_temp_dir() result, since many
475+
# tests fail if the temporary directory is non-ASCII or is too long.
469476

470477
if self.want_cleanup:
471478
cleanup_temp_dir(self.tmp_dir)
479+
tmp_dir2 = os.path.abspath(tempfile.gettempdir())
480+
if tmp_dir2 != self.tmp_dir:
481+
cleanup_temp_dir(tmp_dir2)
472482
sys.exit(0)
473483

474484
if self.want_wait:

Lib/test/libregrtest/run_workers.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import threading
1010
import time
1111
import traceback
12-
from typing import Literal, TextIO
12+
from typing import Literal
1313

1414
from test import support
1515
from test.support import os_helper
@@ -21,7 +21,7 @@
2121
from .single import PROGRESS_MIN_TIME
2222
from .utils import (
2323
StrPath, StrJSON, TestName, MS_WINDOWS,
24-
format_duration, print_warning)
24+
format_duration, print_warning, WORKER_DIR_PREFIX)
2525
from .worker import create_worker_process, USE_PROCESS_GROUP
2626

2727
if MS_WINDOWS:
@@ -156,10 +156,10 @@ def mp_result_error(
156156
return MultiprocessResult(test_result, stdout, err_msg)
157157

158158
def _run_process(self, runtests: RunTests, output_fd: int, json_fd: int,
159-
tmp_dir: StrPath | None = None) -> int:
159+
start_work_dir: StrPath | None = None) -> int:
160160
try:
161161
popen = create_worker_process(runtests, output_fd, json_fd,
162-
tmp_dir)
162+
start_work_dir)
163163

164164
self._killed = False
165165
self._popen = popen
@@ -235,33 +235,39 @@ def _runtest(self, test_name: TestName) -> MultiprocessResult:
235235
if MS_WINDOWS:
236236
json_fd = msvcrt.get_osfhandle(json_fd)
237237

238+
# gh-93353: Check for leaked temporary files in the parent process,
239+
# since the deletion of temporary files can happen late during
240+
# Python finalization: too late for libregrtest.
241+
tmp_dir = tempfile.mkdtemp(prefix=WORKER_DIR_PREFIX)
242+
tmp_dir = os.path.abspath(tmp_dir)
243+
238244
kwargs = {}
239245
if match_tests:
240246
kwargs['match_tests'] = match_tests
241247
worker_runtests = self.runtests.copy(
242248
tests=tests,
243249
json_fd=json_fd,
250+
work_dir=tmp_dir,
244251
**kwargs)
245252

246-
# gh-93353: Check for leaked temporary files in the parent process,
247-
# since the deletion of temporary files can happen late during
248-
# Python finalization: too late for libregrtest.
249-
if not support.is_wasi:
250-
# Don't check for leaked temporary files and directories if Python is
251-
# run on WASI. WASI don't pass environment variables like TMPDIR to
252-
# worker processes.
253-
tmp_dir = tempfile.mkdtemp(prefix="test_python_")
254-
tmp_dir = os.path.abspath(tmp_dir)
255-
try:
256-
retcode = self._run_process(worker_runtests,
257-
stdout_fd, json_fd, tmp_dir)
258-
finally:
259-
tmp_files = os.listdir(tmp_dir)
260-
os_helper.rmtree(tmp_dir)
253+
# Starting working directory of the worker process.
254+
#
255+
# Emscripten and WASI Python must start in the Python source code
256+
# directory to get 'python.js' or 'python.wasm' file.
257+
#
258+
# Then worker_process() calls change_cwd(runtests.work_dir).
259+
if support.is_emscripten or support.is_wasi:
260+
start_work_dir = os_helper.SAVEDCWD
261261
else:
262+
start_work_dir = tmp_dir
263+
264+
try:
262265
retcode = self._run_process(worker_runtests,
263-
stdout_fd, json_fd)
264-
tmp_files = ()
266+
stdout_fd, json_fd,
267+
start_work_dir)
268+
finally:
269+
tmp_files = os.listdir(tmp_dir)
270+
os_helper.rmtree(tmp_dir)
265271
stdout_file.seek(0)
266272

267273
try:
@@ -280,7 +286,7 @@ def _runtest(self, test_name: TestName) -> MultiprocessResult:
280286
if worker_json:
281287
result = TestResult.from_json(worker_json)
282288
else:
283-
err_msg = f"empty JSON"
289+
err_msg = "empty JSON"
284290
except Exception as exc:
285291
# gh-101634: Catch UnicodeDecodeError if stdout cannot be
286292
# decoded from encoding

Lib/test/libregrtest/runtests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class RunTests:
3939
# On Unix, it's a file descriptor.
4040
# On Windows, it's a handle.
4141
json_fd: int | None = None
42+
work_dir: StrPath = None
4243

4344
def copy(self, **override):
4445
state = dataclasses.asdict(self)

Lib/test/libregrtest/utils.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818

1919
MS_WINDOWS = (sys.platform == 'win32')
20+
WORK_DIR_PREFIX = 'test_python_'
21+
WORKER_DIR_PREFIX = f'{WORK_DIR_PREFIX}worker_'
2022

2123
# bpo-38203: Maximum delay in seconds to exit Python (call Py_Finalize()).
2224
# Used to protect against threading._shutdown() hang.
@@ -338,9 +340,7 @@ def get_build_info():
338340

339341

340342
def get_temp_dir(tmp_dir):
341-
if tmp_dir:
342-
tmp_dir = os.path.expanduser(tmp_dir)
343-
else:
343+
if not tmp_dir:
344344
# When tests are run from the Python build directory, it is best practice
345345
# to keep the test files in a subfolder. This eases the cleanup of leftover
346346
# files using the "make distclean" command.
@@ -359,6 +359,12 @@ def get_temp_dir(tmp_dir):
359359
return os.path.abspath(tmp_dir)
360360

361361

362+
def set_temp_dir_environ(environ: dict, tmp_dir: StrPath) -> None:
363+
environ['TMPDIR'] = tmp_dir
364+
environ['TEMP'] = tmp_dir
365+
environ['TMP'] = tmp_dir
366+
367+
362368
def fix_umask():
363369
if support.is_emscripten:
364370
# Emscripten has default umask 0o777, which breaks some tests.
@@ -370,21 +376,18 @@ def fix_umask():
370376
os.umask(old_mask)
371377

372378

373-
def get_work_dir(*, parent_dir: StrPath = '', worker: bool = False):
379+
def get_work_dir(*, parent_dir: StrPath | None = None) -> StrPath:
374380
# Define a writable temp dir that will be used as cwd while running
375381
# the tests. The name of the dir includes the pid to allow parallel
376382
# testing (see the -j option).
377383
# Emscripten and WASI have stubbed getpid(), Emscripten has only
378384
# milisecond clock resolution. Use randint() instead.
379-
if sys.platform in {"emscripten", "wasi"}:
385+
if support.is_emscripten or support.is_wasi:
380386
nounce = random.randint(0, 1_000_000)
381387
else:
382388
nounce = os.getpid()
383389

384-
if worker:
385-
work_dir = 'test_python_worker_{}'.format(nounce)
386-
else:
387-
work_dir = 'test_python_{}'.format(nounce)
390+
work_dir = WORK_DIR_PREFIX + str(nounce)
388391
work_dir += os_helper.FS_NONASCII
389392
if parent_dir:
390393
work_dir = os.path.join(parent_dir, work_dir)
@@ -570,7 +573,7 @@ def display_header():
570573
def cleanup_temp_dir(tmp_dir: StrPath):
571574
import glob
572575

573-
path = os.path.join(glob.escape(tmp_dir), 'test_python_*')
576+
path = os.path.join(glob.escape(tmp_dir), WORK_DIR_PREFIX + '*')
574577
print("Cleanup %s directory" % tmp_dir)
575578
for name in glob.glob(path):
576579
if os.path.isdir(name):

Lib/test/libregrtest/worker.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import subprocess
22
import sys
33
import os
4-
from typing import TextIO, NoReturn
4+
from typing import NoReturn
55

66
from test import support
77
from test.support import os_helper
@@ -11,15 +11,15 @@
1111
from .single import run_single_test
1212
from .utils import (
1313
StrPath, StrJSON, FilterTuple, MS_WINDOWS,
14-
get_work_dir, exit_timeout)
14+
exit_timeout, set_temp_dir_environ)
1515

1616

1717
USE_PROCESS_GROUP = (hasattr(os, "setsid") and hasattr(os, "killpg"))
1818

1919

2020
def create_worker_process(runtests: RunTests,
2121
output_fd: int, json_fd: int,
22-
tmp_dir: StrPath | None = None) -> subprocess.Popen:
22+
start_work_dir: StrPath | None = None) -> subprocess.Popen:
2323
python_cmd = runtests.python_cmd
2424
worker_json = runtests.as_json()
2525

@@ -33,10 +33,7 @@ def create_worker_process(runtests: RunTests,
3333
worker_json]
3434

3535
env = dict(os.environ)
36-
if tmp_dir is not None:
37-
env['TMPDIR'] = tmp_dir
38-
env['TEMP'] = tmp_dir
39-
env['TMP'] = tmp_dir
36+
set_temp_dir_environ(env, runtests.work_dir)
4037

4138
# Running the child from the same working directory as regrtest's original
4239
# invocation ensures that TEMPDIR for the child is the same when
@@ -48,6 +45,7 @@ def create_worker_process(runtests: RunTests,
4845
stderr=output_fd,
4946
text=True,
5047
close_fds=True,
48+
cwd=start_work_dir,
5149
)
5250
if not MS_WINDOWS:
5351
kwargs['pass_fds'] = [json_fd]
@@ -67,8 +65,9 @@ def create_worker_process(runtests: RunTests,
6765
os.set_handle_inheritable(json_fd, False)
6866

6967

70-
def worker_process(worker_json: StrJSON) -> NoReturn:
71-
runtests = RunTests.from_json(worker_json)
68+
def worker_process(runtests: RunTests) -> NoReturn:
69+
set_temp_dir_environ(os.environ, os.getcwd())
70+
7271
test_name = runtests.tests[0]
7372
match_tests: FilterTuple | None = runtests.match_tests
7473
json_fd: int = runtests.json_fd
@@ -77,7 +76,6 @@ def worker_process(worker_json: StrJSON) -> NoReturn:
7776
import msvcrt
7877
json_fd = msvcrt.open_osfhandle(json_fd, os.O_WRONLY)
7978

80-
8179
setup_test_dir(runtests.test_dir)
8280
setup_process()
8381

@@ -100,13 +98,12 @@ def main():
10098
if len(sys.argv) != 2:
10199
print("usage: python -m test.libregrtest.worker JSON")
102100
sys.exit(1)
103-
worker_json = sys.argv[1]
104-
105-
work_dir = get_work_dir(worker=True)
101+
worker_json: StrJSON = sys.argv[1]
106102

107103
with exit_timeout():
108-
with os_helper.temp_cwd(work_dir, quiet=True):
109-
worker_process(worker_json)
104+
runtests = RunTests.from_json(worker_json)
105+
with os_helper.change_cwd(runtests.work_dir):
106+
worker_process(runtests)
110107

111108

112109
if __name__ == "__main__":

0 commit comments

Comments
 (0)