Skip to content

Commit e7a45f4

Browse files
committed
Merge branch '3.14' of https://github.com/python/cpython into 3.14
2 parents 25b6512 + 065a910 commit e7a45f4

11 files changed

Lines changed: 409 additions & 27 deletions

File tree

Doc/library/test.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,7 @@ The :mod:`!test.support` module defines the following functions:
965965

966966
.. currentmodule:: test.support.isolation
967967

968-
.. decorator:: runInSubprocess()
968+
.. decorator:: runInSubprocess(*, options=(), env=None, timeout=None)
969969

970970
Decorator that runs the decorated test in a fresh interpreter subprocess, in
971971
isolation, so that it does not share global or interpreter state with the
@@ -999,6 +999,19 @@ The :mod:`!test.support` module defines the following functions:
999999
:func:`~test.support.bigmemtest` and the like behave consistently in both
10001000
processes.
10011001

1002+
*options* is a sequence of interpreter command line options
1003+
to run the subprocess with,
1004+
and *env* is a mapping of environment variables to set in it,
1005+
on top of the inherited environment.
1006+
A value of ``None`` in *env* unsets the variable.
1007+
Note that :option:`-E` and :option:`-I` make the subprocess ignore
1008+
the ``PYTHON*`` environment variables, including :envvar:`PYTHONPATH`.
1009+
1010+
*timeout* is the number of seconds to wait for the subprocess;
1011+
the test is reported as an error if it does not complete in time.
1012+
By default there is no timeout,
1013+
and a hung test is left to the timeout of the test runner.
1014+
10021015
The test is skipped on platforms without subprocess support.
10031016

10041017

Lib/test/_isolated_sample.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import atexit
99
import os
10+
import sys
1011
import time
1112
import unittest
1213
from test.support import isolation
@@ -141,3 +142,39 @@ def test_pass(self):
141142

142143
def test_dies(self):
143144
_die_at_exit()
145+
146+
147+
@isolation.runInSubprocess(options=['-X', 'dev', '-W', 'error::BytesWarning'])
148+
class OptionsSample(unittest.TestCase):
149+
150+
def test_options_applied(self):
151+
self.assertTrue(sys.flags.dev_mode)
152+
self.assertIn('error::BytesWarning', sys.warnoptions)
153+
154+
155+
class EnvSample(unittest.TestCase):
156+
157+
@isolation.runInSubprocess(env={'_PYTHON_ISOLATION_PROBE': 'set-by-test'})
158+
def test_env_set(self):
159+
self.assertEqual(os.environ.get('_PYTHON_ISOLATION_PROBE'), 'set-by-test')
160+
161+
@isolation.runInSubprocess(env={'_PYTHON_ISOLATION_PROBE': None})
162+
def test_env_unset(self):
163+
self.assertNotIn('_PYTHON_ISOLATION_PROBE', os.environ)
164+
165+
@isolation.runInSubprocess()
166+
def test_env_inherited(self):
167+
# Without env= the subprocess inherits the parent environment as it is.
168+
self.assertEqual(os.environ.get('_PYTHON_ISOLATION_PROBE'), 'set-by-parent')
169+
170+
171+
# TimeoutSample hangs this long, so that the timeout always fires first.
172+
TIMEOUT_HANG = 60.0
173+
TIMEOUT = 0.5
174+
175+
176+
class TimeoutSample(unittest.TestCase):
177+
178+
@isolation.runInSubprocess(timeout=TIMEOUT)
179+
def test_hang(self):
180+
time.sleep(TIMEOUT_HANG)

Lib/test/clinic.test.c

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5641,6 +5641,67 @@ Test___init___impl(TestObj *self, PyObject *a, int group_right_1,
56415641
/*[clinic end generated code: output=2bbb8ea60e8f57a6 input=10f5d0f1e8e466ef]*/
56425642

56435643

5644+
/*[clinic input]
5645+
group_and_optional_parameter
5646+
[
5647+
a: object
5648+
b: object
5649+
]
5650+
c: object = None
5651+
/
5652+
The optional parameter can be omitted with or without the group.
5653+
[clinic start generated code]*/
5654+
5655+
PyDoc_STRVAR(group_and_optional_parameter__doc__,
5656+
"group_and_optional_parameter([a, b,] c=None)\n"
5657+
"The optional parameter can be omitted with or without the group.");
5658+
5659+
#define GROUP_AND_OPTIONAL_PARAMETER_METHODDEF \
5660+
{"group_and_optional_parameter", (PyCFunction)group_and_optional_parameter, METH_VARARGS, group_and_optional_parameter__doc__},
5661+
5662+
static PyObject *
5663+
group_and_optional_parameter_impl(PyObject *module, int group_left_1,
5664+
PyObject *a, PyObject *b, PyObject *c);
5665+
5666+
static PyObject *
5667+
group_and_optional_parameter(PyObject *module, PyObject *args)
5668+
{
5669+
PyObject *return_value = NULL;
5670+
int group_left_1 = 0;
5671+
PyObject *a = NULL;
5672+
PyObject *b = NULL;
5673+
PyObject *c = Py_None;
5674+
5675+
switch (PyTuple_GET_SIZE(args)) {
5676+
case 0:
5677+
case 1:
5678+
if (!PyArg_ParseTuple(args, "|O:group_and_optional_parameter", &c)) {
5679+
goto exit;
5680+
}
5681+
break;
5682+
case 2:
5683+
case 3:
5684+
if (!PyArg_ParseTuple(args, "OO|O:group_and_optional_parameter", &a, &b, &c)) {
5685+
goto exit;
5686+
}
5687+
group_left_1 = 1;
5688+
break;
5689+
default:
5690+
PyErr_SetString(PyExc_TypeError, "group_and_optional_parameter requires 0 to 3 arguments");
5691+
goto exit;
5692+
}
5693+
return_value = group_and_optional_parameter_impl(module, group_left_1, a, b, c);
5694+
5695+
exit:
5696+
return return_value;
5697+
}
5698+
5699+
static PyObject *
5700+
group_and_optional_parameter_impl(PyObject *module, int group_left_1,
5701+
PyObject *a, PyObject *b, PyObject *c)
5702+
/*[clinic end generated code: output=3faea69eafd5bbbe input=7f0fbb6124f5a972]*/
5703+
5704+
56445705
/*[clinic input]
56455706
Test._pyarg_parsestackandkeywords
56465707
cls: defining_class

Lib/test/support/isolation.py

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ def _decode(data):
7878

7979
def _remote(detail):
8080
# Wrap the subprocess traceback the way concurrent.futures does, so it is
81-
# clearly delimited when shown as the cause.
81+
# clearly delimited when shown as the cause. Return None if the subprocess
82+
# said nothing (a hung one usually does not), so that "raise ... from None"
83+
# suppresses an empty cause.
84+
if not detail:
85+
return None
8286
return _RemoteTraceback(f'\n"""\n{detail}"""')
8387

8488

@@ -90,7 +94,21 @@ def _check_subprocess_support():
9094
raise unittest.SkipTest('requires subprocess support')
9195

9296

93-
def _run_in_subprocess(module, qualname):
97+
def _child_environ(env):
98+
# Start from the inherited environment, so that *env* only has to name what
99+
# the test changes.
100+
if not env:
101+
return None
102+
environ = dict(os.environ)
103+
for name, value in env.items():
104+
if value is None:
105+
environ.pop(name, None)
106+
else:
107+
environ[name] = value
108+
return environ
109+
110+
111+
def _run_in_subprocess(module, qualname, options, env, timeout):
94112
"""Run module.qualname (a test method or class) in a fresh subprocess.
95113
96114
Return ``(payload, output, returncode)``, where *payload* is the decoded
@@ -104,13 +122,22 @@ def _run_in_subprocess(module, qualname):
104122
os.close(fd)
105123
try:
106124
# Pass the config on the command line, not in the environment, so that
107-
# the test cannot pass it on to the processes it spawns itself. Use
108-
# marshal, not json: it is built in, so the child imports nothing that
109-
# the test would not see in a normal test run.
110-
cmd = [sys.executable, '-m', 'test.support.subprocess_runner',
125+
# the test cannot pass it on to the processes it spawns itself, and so
126+
# that it survives the -E and -I options. Use marshal, not json: it is
127+
# built in, so the child imports nothing that the test would not see in
128+
# a normal test run.
129+
cmd = [sys.executable, *options, '-m', 'test.support.subprocess_runner',
111130
module, qualname, result_path,
112131
marshal.dumps(_child_config()).hex()]
113-
proc = subprocess.run(cmd, capture_output=True)
132+
try:
133+
proc = subprocess.run(cmd, capture_output=True,
134+
env=_child_environ(env), timeout=timeout)
135+
except subprocess.TimeoutExpired as exc:
136+
# Report the hang rather than leaving the test runner stuck.
137+
output = _decode(exc.stdout) + _decode(exc.stderr)
138+
raise _SubprocessTestError(
139+
f'test did not complete in a subprocess '
140+
f'within {timeout} seconds') from _remote(output)
114141
try:
115142
with open(result_path, 'rb') as f:
116143
payload = marshal.load(f)
@@ -173,7 +200,7 @@ def _check_returncode(returncode, output, what):
173200
raise exc from _remote(output)
174201

175202

176-
def _isolate_method(func):
203+
def _isolate_method(func, options, env, timeout):
177204
@functools.wraps(func)
178205
def wrapper(self, /, *args, **kwargs):
179206
if runningInSubprocess:
@@ -183,7 +210,8 @@ def wrapper(self, /, *args, **kwargs):
183210
cls = type(self)
184211
qualname = f'{cls.__qualname__}.{func.__name__}'
185212
payload, output, returncode = _run_in_subprocess(cls.__module__,
186-
qualname)
213+
qualname, options,
214+
env, timeout)
187215
if payload is None:
188216
exc = _SubprocessTestError(
189217
f'test did not complete in a subprocess (exit code {returncode})')
@@ -196,7 +224,7 @@ def wrapper(self, /, *args, **kwargs):
196224
return wrapper
197225

198226

199-
def _isolate_class(cls):
227+
def _isolate_class(cls, options, env, timeout):
200228
# Unwrap to the plain functions so the replacements can call them with the
201229
# runtime cls; a bound classmethod would freeze the decoration-time class
202230
# and a subclass would run the fixtures bound to the base class.
@@ -217,7 +245,8 @@ def setUpClass(cls):
217245
# Run the whole class in a single subprocess and stash the outcomes
218246
# for the test methods to replay.
219247
payload, output, returncode = _run_in_subprocess(cls.__module__,
220-
cls.__qualname__)
248+
cls.__qualname__,
249+
options, env, timeout)
221250
if payload is None:
222251
exc = _SubprocessTestError(
223252
f'class did not complete in a subprocess (exit code {returncode})')
@@ -283,7 +312,7 @@ def _addDuration(self, result, elapsed):
283312
return cls
284313

285314

286-
def runInSubprocess():
315+
def runInSubprocess(*, options=(), env=None, timeout=None):
287316
"""Decorator to run a test method or class in a fresh subprocess.
288317
289318
The decorated test runs in a separate, fresh Python process, so it does not
@@ -293,6 +322,16 @@ def runInSubprocess():
293322
once there; when a method is decorated, only that method runs in a
294323
subprocess. Decorated methods must take no extra arguments.
295324
325+
*options* is a sequence of interpreter command line options for the
326+
subprocess, and *env* is a mapping of environment variables to set in it,
327+
on top of the inherited environment; a value of ``None`` unsets a variable.
328+
Note that ``-E`` and ``-I`` make the subprocess ignore the ``PYTHON*``
329+
variables, including ``PYTHONPATH``.
330+
331+
*timeout* is the number of seconds to wait for the subprocess; the test is
332+
reported as an error if it does not complete in time. By default there is
333+
no timeout, and a hung test is left to the timeout of the test runner.
334+
296335
A failure, error or skip of the whole test is reported for the test, and
297336
individual subtests (:meth:`~unittest.TestCase.subTest`) that fail or are
298337
skipped are reported individually. The original subprocess traceback is
@@ -304,6 +343,6 @@ def runInSubprocess():
304343
"""
305344
def decorator(obj):
306345
if isinstance(obj, type) and issubclass(obj, unittest.TestCase):
307-
return _isolate_class(obj)
308-
return _isolate_method(obj)
346+
return _isolate_class(obj, options, env, timeout)
347+
return _isolate_method(obj, options, env, timeout)
309348
return decorator

Lib/test/test_clinic.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,24 @@ def __init__(self):
330330
"""
331331
self.expect_failure(block, err, lineno=8)
332332

333+
def test_ambiguous_group_and_optional_parameters(self):
334+
err = ("Function 'my_test_func' has an ambiguous group configuration: "
335+
"a call with 2 argument(s) can be parsed in more than one way.")
336+
block = """
337+
/*[clinic input]
338+
my_test_func
339+
340+
[
341+
a: object
342+
b: object
343+
]
344+
c: object = None
345+
d: object = None
346+
/
347+
[clinic start generated code]*/
348+
"""
349+
self.expect_failure(block, err)
350+
333351
def test_star_after_vararg(self):
334352
err = "'my_test_func' uses '*' more than once."
335353
block = """
@@ -3828,6 +3846,27 @@ def test_varpos_kwonly_req_opt(self):
38283846
self.assertEqual(fn(1, a=2, b=3), ((1,), 2, 3, False))
38293847
self.assertEqual(fn(1, a=2, b=3, c=4), ((1,), 2, 3, 4))
38303848

3849+
def test_group_and_opt(self):
3850+
# fn([a, b,] c=None)
3851+
fn = ac_tester.group_and_opt
3852+
self.assertEqual(fn(), (False, None, None, None))
3853+
self.assertEqual(fn(1), (False, None, None, 1))
3854+
self.assertEqual(fn(1, 2), (True, 1, 2, None))
3855+
self.assertEqual(fn(1, 2, 3), (True, 1, 2, 3))
3856+
self.assertRaises(TypeError, fn, 1, 2, 3, 4)
3857+
self.assertRaises(TypeError, fn, c=1)
3858+
3859+
def test_group_and_two_opt(self):
3860+
# fn([a, b, c,] d=None, e=None)
3861+
fn = ac_tester.group_and_two_opt
3862+
self.assertEqual(fn(), (False, None, None, None, None, None))
3863+
self.assertEqual(fn(1), (False, None, None, None, 1, None))
3864+
self.assertEqual(fn(1, 2), (False, None, None, None, 1, 2))
3865+
self.assertEqual(fn(1, 2, 3), (True, 1, 2, 3, None, None))
3866+
self.assertEqual(fn(1, 2, 3, 4), (True, 1, 2, 3, 4, None))
3867+
self.assertEqual(fn(1, 2, 3, 4, 5), (True, 1, 2, 3, 4, 5))
3868+
self.assertRaises(TypeError, fn, 1, 2, 3, 4, 5, 6)
3869+
38313870
def test_gh_32092_oob(self):
38323871
ac_tester.gh_32092_oob(1, 2, 3, 4, kw1=5, kw2=6)
38333872

Lib/test/test_support.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,32 @@ def test_class_subprocess_dying_after_the_tests_is_reported(self):
962962
self.assertIn('tearDownClass', str(result.errors[0][0]))
963963
self.assertIn(f'exited with code {EXIT_CODE}', result.errors[0][1])
964964

965+
@support.requires_subprocess()
966+
def test_options_passed_to_subprocess(self):
967+
result = self._run('OptionsSample')
968+
self.assertEqual(result.testsRun, 1)
969+
self.assertEqual(result.failures, [])
970+
self.assertEqual(result.errors, [])
971+
972+
@support.requires_subprocess()
973+
def test_env_passed_to_subprocess(self):
974+
# The samples check the variable, so set it here to let them tell
975+
# env= from the inherited environment.
976+
with os_helper.EnvironmentVarGuard() as env:
977+
env['_PYTHON_ISOLATION_PROBE'] = 'set-by-parent'
978+
result = self._run('EnvSample')
979+
self.assertEqual(result.testsRun, 3)
980+
self.assertEqual(result.failures, [])
981+
self.assertEqual(result.errors, [])
982+
983+
@support.requires_subprocess()
984+
def test_timeout_reported_as_error(self):
985+
from test._isolated_sample import TIMEOUT
986+
result = self._run('TimeoutSample')
987+
self.assertEqual(result.testsRun, 1)
988+
self.assertEqual(len(result.errors), 1)
989+
self.assertIn(f'within {TIMEOUT} seconds', result.errors[0][1])
990+
965991
def test_skipped_without_subprocess_support(self):
966992
# On a platform without subprocess support the test is skipped in the
967993
# parent, before any subprocess is spawned.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix Argument Clinic support of parameters with a default value used together
2+
with optional groups.
3+
Such parameters were always required in the generated parsing code.

0 commit comments

Comments
 (0)