Skip to content

Commit 84bd154

Browse files
gh-152548: Rename isolated() to runInSubprocess() and address review
Rename the decorator to runInSubprocess() and the flag to runningInSubprocess so the subprocess mechanism (and the skip when it is unavailable) is explicit. Suppress Windows CRT assertion dialogs in the isolated child so a debug build does not hang on a modal dialog, and add a subclass fixture-binding test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 63ccc24 commit 84bd154

6 files changed

Lines changed: 86 additions & 62 deletions

File tree

Doc/library/test.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -963,10 +963,10 @@ The :mod:`!test.support` module defines the following functions:
963963

964964
.. currentmodule:: test.support.isolation
965965

966-
.. decorator:: isolated()
966+
.. decorator:: runInSubprocess()
967967

968-
Decorator that runs the decorated test in isolation, in a fresh interpreter
969-
subprocess, so that it does not share global or interpreter state with the
968+
Decorator that runs the decorated test in a fresh interpreter subprocess, in
969+
isolation, so that it does not share global or interpreter state with the
970970
rest of the test run. It can decorate a test method or a whole
971971
:class:`~unittest.TestCase` subclass. Decorated methods must take no extra
972972
arguments. A failure, error or skip in the subprocess is reported for the
@@ -989,7 +989,7 @@ The :mod:`!test.support` module defines the following functions:
989989
:meth:`~unittest.TestCase.setUpClass` in the subprocess is reported for the
990990
whole class. ``setUpModule()`` cannot be controlled by a class decorator,
991991
so it still runs in the parent process too; test it with
992-
:data:`running_isolated` if needed.
992+
:data:`runningInSubprocess` if needed.
993993

994994
The subprocess inherits the enabled resources (``-u``), memory limit
995995
(``-M``) and verbosity (``-v``) of the parent test run, so that
@@ -1000,11 +1000,11 @@ The :mod:`!test.support` module defines the following functions:
10001000
The test is skipped on platforms without subprocess support.
10011001

10021002

1003-
.. data:: running_isolated
1003+
.. data:: runningInSubprocess
10041004

10051005
``True`` while the code runs in the isolated subprocess spawned by
1006-
:func:`isolated`, and ``False`` otherwise (including in the parent process
1007-
and in a normal, non-isolated test run). Fixtures such as
1006+
:func:`runInSubprocess`, and ``False`` otherwise (including in the parent
1007+
process and in a normal, non-isolated test run). Fixtures such as
10081008
:meth:`~unittest.TestCase.setUp`, :meth:`~unittest.TestCase.tearDown`,
10091009
:meth:`~unittest.TestCase.setUpClass`, :meth:`~unittest.TestCase.tearDownClass`,
10101010
``setUpModule()`` and ``tearDownModule()`` can test it to choose which code

Lib/test/_isolated_sample.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,53 @@
11
"""Sample tests driven by test.test_support.TestIsolated.
22
33
This module is imported, never run as a test file, so that
4-
:func:`test.support.isolation.isolated` has a real, importable target to run in
4+
:func:`test.support.isolation.runInSubprocess` has a real, importable target to run in
55
a subprocess. Several of these tests fail, error or are skipped on purpose.
66
"""
77

88
import time
99
import unittest
1010
from test.support import isolation
1111

12-
# A test in DurationSample sleeps this long in the subprocess; the parent
13-
# replays it instantly, so a parent-reported duration close to this proves the
14-
# subprocess timing was forwarded rather than the replay time measured.
12+
# DurationSample sleeps this long in the subprocess; a parent-reported duration
13+
# close to it proves the subprocess timing was forwarded, not the replay time.
1514
DURATION_SLEEP = 0.2
1615

1716

1817
class MethodSample(unittest.TestCase):
1918

20-
@isolation.isolated()
19+
@isolation.runInSubprocess()
2120
def test_pass(self):
22-
self.assertTrue(isolation.running_isolated)
21+
self.assertTrue(isolation.runningInSubprocess)
2322

24-
@isolation.isolated()
23+
@isolation.runInSubprocess()
2524
def test_fail(self):
2625
self.assertEqual(1, 2)
2726

28-
@isolation.isolated()
27+
@isolation.runInSubprocess()
2928
def test_error(self):
3029
raise RuntimeError('boom')
3130

32-
@isolation.isolated()
31+
@isolation.runInSubprocess()
3332
def test_skip(self):
3433
self.skipTest('nope')
3534

36-
@isolation.isolated()
35+
@isolation.runInSubprocess()
3736
@unittest.expectedFailure
3837
def test_expected_failure(self):
3938
self.assertEqual(1, 2)
4039

41-
@isolation.isolated()
40+
@isolation.runInSubprocess()
4241
@unittest.expectedFailure
4342
def test_unexpected_success(self):
4443
pass
4544

4645

47-
@isolation.isolated()
46+
@isolation.runInSubprocess()
4847
class ClassSample(unittest.TestCase):
4948

5049
def test_pass(self):
51-
self.assertTrue(isolation.running_isolated)
50+
self.assertTrue(isolation.runningInSubprocess)
5251

5352
def test_fail(self):
5453
self.assertEqual(1, 2)
@@ -60,15 +59,32 @@ def test_expected_failure(self):
6059

6160
class SubtestSample(unittest.TestCase):
6261

63-
@isolation.isolated()
62+
@isolation.runInSubprocess()
6463
def test_subtests(self):
6564
for i in range(3):
6665
with self.subTest(i=i):
6766
self.assertNotEqual(i, 1)
6867

6968

70-
@isolation.isolated()
69+
@isolation.runInSubprocess()
7170
class DurationSample(unittest.TestCase):
7271

7372
def test_slow(self):
7473
time.sleep(DURATION_SLEEP)
74+
75+
76+
@isolation.runInSubprocess()
77+
class FixtureBindingSample(unittest.TestCase):
78+
# setUpClass must run bound to the runtime class, so a subclass sees its own
79+
# name here rather than the base class's.
80+
81+
@classmethod
82+
def setUpClass(cls):
83+
cls.setup_class_name = cls.__name__
84+
85+
def test_runtime_class(self):
86+
self.assertEqual(self.setup_class_name, type(self).__name__)
87+
88+
89+
class FixtureBindingSubclassSample(FixtureBindingSample):
90+
pass

Lib/test/support/isolation.py

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Run tests in isolated subprocesses (the test.support.isolation.isolated decorator).
1+
"""Run tests in isolated subprocesses (the test.support.isolation.runInSubprocess decorator).
22
33
A failure, error or skip that happens in the subprocess is replayed in the
44
parent process so that the test runner records it. The original (subprocess)
@@ -11,10 +11,8 @@
1111
import sys
1212
import unittest
1313

14-
# Mark this module's frames as belonging to the test machinery, so that
15-
# unittest strips them from reported tracebacks (see TestResult._clean_tracebacks
16-
# in Lib/unittest/result.py). Only the original subprocess traceback, attached
17-
# as the cause, is then shown -- not the parent-side replay frames.
14+
# Let unittest strip this module's frames from tracebacks, so only the original
15+
# subprocess traceback (attached as the cause) is shown, not the replay frames.
1816
__unittest = True
1917

2018
# Environment variable set in the child process so that the decorated test
@@ -39,23 +37,23 @@ def _child_config():
3937
return {name: getattr(support, name) for name in _PROPAGATED_CONFIG}
4038

4139
def _apply_child_config():
42-
"""Mirror the parent's test.support configuration in the subprocess.
40+
"""Set up the child to run the test like a regrtest worker would.
4341
44-
Called by subprocess_runner before loading the test, so that import-time
45-
decorators (e.g. requires_resource) and runtime checks see the same -u/-M/-v
46-
configuration as the parent process.
42+
Mirror the parent's -u/-M/-v config, then suppress the Windows CRT assertion
43+
dialogs that would otherwise block a debug build on a modal dialog and hang
44+
the parent.
4745
"""
4846
import json
4947
import test.support as support
5048
data = os.environ.get(_CONFIG_ENV)
5149
if data:
5250
for name, value in json.loads(data).items():
5351
setattr(support, name, value)
52+
support.suppress_msvcrt_asserts(support.verbose >= 2)
5453

55-
# True while running inside the isolated subprocess spawned by @isolated().
56-
# setUp()/tearDown() and the class- and module-level fixtures can test it to
57-
# decide which code to run in the subprocess as opposed to the parent process.
58-
running_isolated = bool(os.environ.get(_RUN_IN_SUBPROCESS_ENV))
54+
# True inside the subprocess spawned by @runInSubprocess(). Fixtures can test
55+
# it to decide what to run in the subprocess as opposed to the parent process.
56+
runningInSubprocess = bool(os.environ.get(_RUN_IN_SUBPROCESS_ENV))
5957

6058

6159
class _RemoteTraceback(Exception):
@@ -82,8 +80,8 @@ def _remote(detail):
8280

8381

8482
def _check_subprocess_support():
85-
# isolated() always runs the test in a subprocess, so skip (in the parent)
86-
# on platforms that do not support spawning one.
83+
# runInSubprocess() always runs the test in a subprocess, so skip (in the
84+
# parent) on platforms that do not support spawning one.
8785
import test.support as support
8886
if not support.has_subprocess_support:
8987
raise unittest.SkipTest('requires subprocess support')
@@ -127,9 +125,9 @@ def _replay_outcome(test, outcome):
127125
if kind == 'skipped':
128126
test.skipTest(detail) # the detail is the skip reason, not a traceback
129127
elif kind in ('failure', 'expected_failure'):
130-
# An expected failure is replayed like a failure: the wrapper carries
131-
# the @expectedFailure marker (copied by functools.wraps), so the parent
132-
# records the raised exception as an expectedFailure, not a failure.
128+
# Replay an expected failure like a failure: the wrapper keeps the
129+
# @expectedFailure marker (via functools.wraps), so the parent records
130+
# the raised exception as an expectedFailure.
133131
exc = test.failureException('test failed in the subprocess')
134132
raise exc from _remote(detail)
135133
else: # 'error'
@@ -163,7 +161,7 @@ def _raise_fixture_outcome(outcome):
163161
def _isolate_method(func):
164162
@functools.wraps(func)
165163
def wrapper(self, /, *args, **kwargs):
166-
if running_isolated:
164+
if runningInSubprocess:
167165
# Already running in the subprocess: run the real test.
168166
return func(self, *args, **kwargs)
169167
_check_subprocess_support()
@@ -182,17 +180,17 @@ def wrapper(self, /, *args, **kwargs):
182180

183181

184182
def _isolate_class(cls):
185-
# Unwrap to the plain functions: the replacements below call them with the
186-
# runtime cls, so a subclass of an isolated class runs the fixtures bound to
187-
# itself (a bound classmethod would freeze the decoration-time class).
183+
# Unwrap to the plain functions so the replacements can call them with the
184+
# runtime cls; a bound classmethod would freeze the decoration-time class
185+
# and a subclass would run the fixtures bound to the base class.
188186
orig_setUpClass = cls.setUpClass.__func__
189187
orig_tearDownClass = cls.tearDownClass.__func__
190188
orig_setUp = cls.setUp
191189
orig_tearDown = cls.tearDown
192190
orig_addDuration = getattr(cls, '_addDuration', None)
193191

194192
def setUpClass(cls):
195-
if running_isolated:
193+
if runningInSubprocess:
196194
orig_setUpClass(cls)
197195
return
198196
_check_subprocess_support()
@@ -215,26 +213,25 @@ def setUpClass(cls):
215213
cls._isolated_durations = dict(payload.get('durations', ()))
216214

217215
def tearDownClass(cls):
218-
if running_isolated:
216+
if runningInSubprocess:
219217
orig_tearDownClass(cls)
220218
else:
221219
cls._isolated_outcomes = None
222220
cls._isolated_durations = None
223221

224222
def setUp(self):
225223
# In the parent the real test does not run, so neither should setUp().
226-
if running_isolated:
224+
if runningInSubprocess:
227225
orig_setUp(self)
228226

229227
def tearDown(self):
230-
if running_isolated:
228+
if runningInSubprocess:
231229
orig_tearDown(self)
232230

233231
def _addDuration(self, result, elapsed):
234-
# In the parent, report the per-test duration measured in the subprocess
235-
# rather than the replay time (subprocess startup is paid once, in
236-
# setUpClass).
237-
if not running_isolated:
232+
# In the parent, report the subprocess timing rather than the (instant)
233+
# replay time; subprocess startup is paid once, in setUpClass.
234+
if not runningInSubprocess:
238235
durations = getattr(type(self), '_isolated_durations', None) or {}
239236
elapsed = durations.get(self.id(), elapsed)
240237
orig_addDuration(self, result, elapsed)
@@ -253,15 +250,15 @@ def replay(self):
253250
method = getattr(cls, name)
254251
@functools.wraps(method)
255252
def wrapper(self, /, *args, __func=method, **kwargs):
256-
if running_isolated:
253+
if runningInSubprocess:
257254
return __func(self, *args, **kwargs)
258255
replay(self)
259256
setattr(cls, name, wrapper)
260257
return cls
261258

262259

263-
def isolated():
264-
"""Decorator to run a test method or class in isolation from the rest.
260+
def runInSubprocess():
261+
"""Decorator to run a test method or class in a fresh subprocess.
265262
266263
The decorated test runs in a separate, fresh Python process, so it does not
267264
share global or interpreter state with the rest of the test run. When a
@@ -274,7 +271,7 @@ def isolated():
274271
individual subtests (:meth:`~unittest.TestCase.subTest`) that fail or are
275272
skipped are reported individually. The original subprocess traceback is
276273
shown as the cause of a reported failure or error. Use
277-
:data:`running_isolated` in fixtures to choose what to run in the subprocess.
274+
:data:`runningInSubprocess` in fixtures to choose what to run in the subprocess.
278275
279276
The test is skipped on platforms without subprocess support, since it must
280277
spawn one.

Lib/test/support/subprocess_runner.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Run a single test method in this (sub)process and report the result.
22
33
Invoked as ``python -m test.support.subprocess_runner MODULE QUALNAME OUTFILE``
4-
by :func:`test.support.isolation.isolated`. The outcome of the test (including
4+
by :func:`test.support.isolation.runInSubprocess`. The outcome of the test (including
55
that of each individual subtest) is written as JSON to OUTFILE. This module is
66
not meant to be imported.
77
"""
@@ -23,8 +23,8 @@
2323
qualname = sys.argv[2]
2424
outfile = sys.argv[3]
2525

26-
# Mirror the parent's regrtest configuration (-u, -M, -v, ...) before importing
27-
# the test, so resource gating and bigmem sizing match the parent process.
26+
# Set up the child (mirror the parent's -u/-M/-v config, suppress Windows CRT
27+
# assertion dialogs, ...) before importing the test.
2828
from test.support.isolation import _apply_child_config
2929
_apply_child_config()
3030

Lib/test/test_support.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,8 +1078,8 @@ def test_disable_hash_md5_in_fips_mode_allow_all(self):
10781078

10791079
class TestIsolated(unittest.TestCase):
10801080
# Drive the sample tests in test._isolated_sample (which really spawn
1081-
# subprocesses through @isolation.isolated()) under a private TestResult,
1082-
# and check that each subprocess outcome is replayed in the parent.
1081+
# subprocesses through @isolation.runInSubprocess()) under a private
1082+
# TestResult, and check that each subprocess outcome is replayed in the parent.
10831083

10841084
@staticmethod
10851085
def _run(name):
@@ -1161,6 +1161,17 @@ def test_durations_forwarded_for_class(self):
11611161
self.assertEqual(name.split()[0], 'test_slow')
11621162
self.assertGreaterEqual(elapsed, DURATION_SLEEP / 2)
11631163

1164+
@support.requires_subprocess()
1165+
def test_subclass_fixtures_bound_to_runtime_class(self):
1166+
# A decorated class and a subclass of it each run setUpClass bound to
1167+
# their own class; both samples pass only if that holds.
1168+
for name in ('FixtureBindingSample', 'FixtureBindingSubclassSample'):
1169+
with self.subTest(sample=name):
1170+
result = self._run(name)
1171+
self.assertEqual(result.testsRun, 1)
1172+
self.assertEqual(result.failures, [])
1173+
self.assertEqual(result.errors, [])
1174+
11641175
def test_skipped_without_subprocess_support(self):
11651176
# On a platform without subprocess support the test is skipped in the
11661177
# parent, before any subprocess is spawned.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
Add the :func:`test.support.isolation.isolated` decorator to run a test method or
1+
Add the :func:`test.support.isolation.runInSubprocess` decorator to run a test method or
22
``TestCase`` subclass in a fresh interpreter subprocess, isolated from the rest
33
of the test run.

0 commit comments

Comments
 (0)