Skip to content

Commit e566ce5

Browse files
authored
gh-93353: regrtest checks for leaked temporary files (#93776)
When running tests with -jN, create a temporary directory per process and mark a test as "environment changed" if a test leaks a temporary file or directory.
1 parent b083450 commit e566ce5

File tree

3 files changed

+55
-7
lines changed

3 files changed

+55
-7
lines changed

Lib/test/libregrtest/runtest.py

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ class EnvChanged(Failed):
8080
def __str__(self) -> str:
8181
return f"{self.name} failed (env changed)"
8282

83+
# Convert Passed to EnvChanged
84+
@staticmethod
85+
def from_passed(other):
86+
return EnvChanged(other.name, other.duration_sec, other.xml_data)
87+
8388

8489
class RefLeak(Failed):
8590
def __str__(self) -> str:

Lib/test/libregrtest/runtest_mp.py

+30-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import faulthandler
22
import json
3-
import os
3+
import os.path
44
import queue
55
import shlex
66
import signal
@@ -17,7 +17,8 @@
1717
from test.libregrtest.cmdline import Namespace
1818
from test.libregrtest.main import Regrtest
1919
from test.libregrtest.runtest import (
20-
runtest, is_failed, TestResult, Interrupted, Timeout, ChildError, PROGRESS_MIN_TIME)
20+
runtest, is_failed, TestResult, Interrupted, Timeout, ChildError,
21+
PROGRESS_MIN_TIME, Passed, EnvChanged)
2122
from test.libregrtest.setup import setup_tests
2223
from test.libregrtest.utils import format_duration, print_warning
2324

@@ -52,7 +53,7 @@ def parse_worker_args(worker_args) -> tuple[Namespace, str]:
5253
return (ns, test_name)
5354

5455

55-
def run_test_in_subprocess(testname: str, ns: Namespace) -> subprocess.Popen:
56+
def run_test_in_subprocess(testname: str, ns: Namespace, tmp_dir: str) -> subprocess.Popen:
5657
ns_dict = vars(ns)
5758
worker_args = (ns_dict, testname)
5859
worker_args = json.dumps(worker_args)
@@ -66,10 +67,14 @@ def run_test_in_subprocess(testname: str, ns: Namespace) -> subprocess.Popen:
6667
'-m', 'test.regrtest',
6768
'--worker-args', worker_args]
6869

70+
env = dict(os.environ)
71+
env['TMPDIR'] = tmp_dir
72+
env['TEMPDIR'] = tmp_dir
73+
6974
# Running the child from the same working directory as regrtest's original
7075
# invocation ensures that TEMPDIR for the child is the same when
7176
# sysconfig.is_python_build() is true. See issue 15300.
72-
kw = {}
77+
kw = {'env': env}
7378
if USE_PROCESS_GROUP:
7479
kw['start_new_session'] = True
7580
return subprocess.Popen(cmd,
@@ -206,12 +211,12 @@ def mp_result_error(
206211
test_result.duration_sec = time.monotonic() - self.start_time
207212
return MultiprocessResult(test_result, stdout, err_msg)
208213

209-
def _run_process(self, test_name: str) -> tuple[int, str, str]:
214+
def _run_process(self, test_name: str, tmp_dir: str) -> tuple[int, str, str]:
210215
self.start_time = time.monotonic()
211216

212217
self.current_test_name = test_name
213218
try:
214-
popen = run_test_in_subprocess(test_name, self.ns)
219+
popen = run_test_in_subprocess(test_name, self.ns, tmp_dir)
215220

216221
self._killed = False
217222
self._popen = popen
@@ -266,7 +271,17 @@ def _run_process(self, test_name: str) -> tuple[int, str, str]:
266271
self.current_test_name = None
267272

268273
def _runtest(self, test_name: str) -> MultiprocessResult:
269-
retcode, stdout = self._run_process(test_name)
274+
# gh-93353: Check for leaked temporary files in the parent process,
275+
# since the deletion of temporary files can happen late during
276+
# Python finalization: too late for libregrtest.
277+
tmp_dir = os.getcwd() + '_tmpdir'
278+
tmp_dir = os.path.abspath(tmp_dir)
279+
try:
280+
os.mkdir(tmp_dir)
281+
retcode, stdout = self._run_process(test_name, tmp_dir)
282+
finally:
283+
tmp_files = os.listdir(tmp_dir)
284+
os_helper.rmtree(tmp_dir)
270285

271286
if retcode is None:
272287
return self.mp_result_error(Timeout(test_name), stdout)
@@ -289,6 +304,14 @@ def _runtest(self, test_name: str) -> MultiprocessResult:
289304
if err_msg is not None:
290305
return self.mp_result_error(ChildError(test_name), stdout, err_msg)
291306

307+
if tmp_files:
308+
msg = (f'\n\n'
309+
f'Warning -- Test leaked temporary files ({len(tmp_files)}): '
310+
f'{", ".join(sorted(tmp_files))}')
311+
stdout += msg
312+
if isinstance(result, Passed):
313+
result = EnvChanged.from_passed(result)
314+
292315
return MultiprocessResult(result, stdout, err_msg)
293316

294317
def run(self) -> None:

Lib/test/test_regrtest.py

+20
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,26 @@ def test_cleanup(self):
13571357
for name in names:
13581358
self.assertFalse(os.path.exists(name), name)
13591359

1360+
def test_leak_tmp_file(self):
1361+
code = textwrap.dedent(r"""
1362+
import os.path
1363+
import tempfile
1364+
import unittest
1365+
1366+
class FileTests(unittest.TestCase):
1367+
def test_leak_tmp_file(self):
1368+
filename = os.path.join(tempfile.gettempdir(), 'mytmpfile')
1369+
with open(filename, "wb") as fp:
1370+
fp.write(b'content')
1371+
""")
1372+
testname = self.create_test(code=code)
1373+
1374+
output = self.run_tests("--fail-env-changed", "-v", "-j1", testname, exitcode=3)
1375+
self.check_executed_tests(output, [testname],
1376+
env_changed=[testname],
1377+
fail_env_changed=True)
1378+
self.assertIn("Warning -- Test leaked temporary files (1): mytmpfile", output)
1379+
13601380

13611381
class TestUtils(unittest.TestCase):
13621382
def test_format_duration(self):

0 commit comments

Comments
 (0)