Skip to content

Commit a22ec49

Browse files
gh-152548: Add a test.support.isolation.runInSubprocess() decorator (GH-152551)
Run a test in a fresh interpreter subprocess, so that it does not share global or interpreter state with the rest of the test run. It can decorate a test method (only that method runs in a subprocess) or a TestCase subclass (the whole class runs in one subprocess, with its setUpClass()/setUp()/tearDown()/ tearDownClass() running there rather than in the parent). Failures, errors and skips, including those of individual subtests, are reported for the test; a failure or an error shows the original subprocess traceback. The subprocess inherits the parent's resource, memory, verbosity and failfast configuration, so that requires_resource(), bigmemtest() and similar behave the same in both processes. A decorated test is skipped where subprocesses are unavailable, since it must spawn one. The test.support.isolation.runningInSubprocess flag is true in the subprocess, so that fixtures can choose what to run there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit bde526d)
1 parent 722bcec commit a22ec49

6 files changed

Lines changed: 657 additions & 0 deletions

File tree

Doc/library/test.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,59 @@ The :mod:`!test.support` module defines the following functions:
963963
:mod:`tracemalloc` is enabled.
964964

965965

966+
.. currentmodule:: test.support.isolation
967+
968+
.. decorator:: runInSubprocess()
969+
970+
Decorator that runs the decorated test in a fresh interpreter subprocess, in
971+
isolation, so that it does not share global or interpreter state with the
972+
rest of the test run. It can decorate a test method or a whole
973+
:class:`~unittest.TestCase` subclass. Decorated methods must take no extra
974+
arguments. A failure, error or skip in the subprocess is reported for the
975+
corresponding test, and individual :meth:`subtests
976+
<unittest.TestCase.subTest>` that fail or are skipped are reported
977+
individually. A reported failure or error shows the original subprocess
978+
traceback as the cause of the exception.
979+
980+
When a **method** is decorated, only that method runs in a subprocess; all
981+
fixtures (:meth:`~unittest.TestCase.setUp` / :meth:`~unittest.TestCase.tearDown`,
982+
:meth:`~unittest.TestCase.setUpClass` / :meth:`~unittest.TestCase.tearDownClass`
983+
and ``setUpModule()`` / ``tearDownModule()``) run both in the parent process
984+
(as usual) and in the subprocess around the method.
985+
986+
When a **class** is decorated, the whole class runs in a single subprocess,
987+
and :meth:`~unittest.TestCase.setUpClass`,
988+
:meth:`~unittest.TestCase.tearDownClass`, :meth:`~unittest.TestCase.setUp`
989+
and :meth:`~unittest.TestCase.tearDown` run once each in the subprocess and
990+
are skipped in the parent process. A failure or skip of
991+
:meth:`~unittest.TestCase.setUpClass` in the subprocess is reported for the
992+
whole class. ``setUpModule()`` cannot be controlled by a class decorator,
993+
so it still runs in the parent process too; test it with
994+
:data:`runningInSubprocess` if needed.
995+
996+
The subprocess inherits the enabled resources (``-u``), memory limit
997+
(``-M``) and verbosity (``-v``) of the parent test run, so that
998+
:func:`~test.support.requires_resource`, :func:`~test.support.requires`,
999+
:func:`~test.support.bigmemtest` and the like behave consistently in both
1000+
processes.
1001+
1002+
The test is skipped on platforms without subprocess support.
1003+
1004+
1005+
.. data:: runningInSubprocess
1006+
1007+
``True`` while the code runs in the isolated subprocess spawned by
1008+
:func:`runInSubprocess`, and ``False`` otherwise (including in the parent
1009+
process and in a normal, non-isolated test run). Fixtures such as
1010+
:meth:`~unittest.TestCase.setUp`, :meth:`~unittest.TestCase.tearDown`,
1011+
:meth:`~unittest.TestCase.setUpClass`, :meth:`~unittest.TestCase.tearDownClass`,
1012+
``setUpModule()`` and ``tearDownModule()`` can test it to choose which code
1013+
to run in the subprocess.
1014+
1015+
1016+
.. currentmodule:: test.support
1017+
1018+
9661019
.. function:: check_free_after_iterating(test, iter, cls, args=())
9671020

9681021
Assert instances of *cls* are deallocated after iterating.

Lib/test/_isolated_sample.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Sample tests driven by test.test_support.TestIsolated.
2+
3+
This module is imported, never run as a test file, so that
4+
:func:`test.support.isolation.runInSubprocess` has a real, importable target to run in
5+
a subprocess. Several of these tests fail, error or are skipped on purpose.
6+
"""
7+
8+
import time
9+
import unittest
10+
from test.support import isolation
11+
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.
14+
DURATION_SLEEP = 0.2
15+
16+
17+
class MethodSample(unittest.TestCase):
18+
19+
@isolation.runInSubprocess()
20+
def test_pass(self):
21+
self.assertTrue(isolation.runningInSubprocess)
22+
23+
@isolation.runInSubprocess()
24+
def test_fail(self):
25+
self.assertEqual(1, 2)
26+
27+
@isolation.runInSubprocess()
28+
def test_error(self):
29+
raise RuntimeError('boom')
30+
31+
@isolation.runInSubprocess()
32+
def test_skip(self):
33+
self.skipTest('nope')
34+
35+
@isolation.runInSubprocess()
36+
@unittest.expectedFailure
37+
def test_expected_failure(self):
38+
self.assertEqual(1, 2)
39+
40+
@isolation.runInSubprocess()
41+
@unittest.expectedFailure
42+
def test_unexpected_success(self):
43+
pass
44+
45+
46+
@isolation.runInSubprocess()
47+
class ClassSample(unittest.TestCase):
48+
49+
def test_pass(self):
50+
self.assertTrue(isolation.runningInSubprocess)
51+
52+
def test_fail(self):
53+
self.assertEqual(1, 2)
54+
55+
@unittest.expectedFailure
56+
def test_expected_failure(self):
57+
self.assertEqual(1, 2)
58+
59+
60+
class SubtestSample(unittest.TestCase):
61+
62+
@isolation.runInSubprocess()
63+
def test_subtests(self):
64+
for i in range(3):
65+
with self.subTest(i=i):
66+
self.assertNotEqual(i, 1)
67+
68+
69+
@isolation.runInSubprocess()
70+
class DurationSample(unittest.TestCase):
71+
72+
def test_slow(self):
73+
time.sleep(DURATION_SLEEP)
74+
75+
76+
@isolation.runInSubprocess()
77+
class SubclassingSample(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 setUp(self):
86+
self.set_up = True
87+
88+
def test_runtime_class(self):
89+
self.assertEqual(self.setup_class_name, type(self).__name__)
90+
91+
92+
class SubclassSample(SubclassingSample):
93+
# What a subclass adds or overrides must run in the subprocess too.
94+
95+
def setUp(self):
96+
super().setUp()
97+
self.set_up_in_subclass = True
98+
99+
def test_added_in_subclass(self):
100+
self.assertTrue(isolation.runningInSubprocess)
101+
self.assertTrue(self.set_up)
102+
self.assertTrue(self.set_up_in_subclass)
103+
104+
105+
class BrokenSubclassSample(SubclassingSample):
106+
# An overriding setUpClass() that does not call super() bypasses the
107+
# subprocess entirely.
108+
109+
@classmethod
110+
def setUpClass(cls):
111+
pass

0 commit comments

Comments
 (0)