Skip to content

Commit 35ab90e

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 31b4886 commit 35ab90e

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
@@ -925,6 +925,59 @@ The :mod:`test.support` module defines the following functions:
925925
:mod:`tracemalloc` is enabled.
926926

927927

928+
.. currentmodule:: test.support.isolation
929+
930+
.. decorator:: runInSubprocess()
931+
932+
Decorator that runs the decorated test in a fresh interpreter subprocess, in
933+
isolation, so that it does not share global or interpreter state with the
934+
rest of the test run. It can decorate a test method or a whole
935+
:class:`~unittest.TestCase` subclass. Decorated methods must take no extra
936+
arguments. A failure, error or skip in the subprocess is reported for the
937+
corresponding test, and individual :meth:`subtests
938+
<unittest.TestCase.subTest>` that fail or are skipped are reported
939+
individually. A reported failure or error shows the original subprocess
940+
traceback as the cause of the exception.
941+
942+
When a **method** is decorated, only that method runs in a subprocess; all
943+
fixtures (:meth:`~unittest.TestCase.setUp` / :meth:`~unittest.TestCase.tearDown`,
944+
:meth:`~unittest.TestCase.setUpClass` / :meth:`~unittest.TestCase.tearDownClass`
945+
and ``setUpModule()`` / ``tearDownModule()``) run both in the parent process
946+
(as usual) and in the subprocess around the method.
947+
948+
When a **class** is decorated, the whole class runs in a single subprocess,
949+
and :meth:`~unittest.TestCase.setUpClass`,
950+
:meth:`~unittest.TestCase.tearDownClass`, :meth:`~unittest.TestCase.setUp`
951+
and :meth:`~unittest.TestCase.tearDown` run once each in the subprocess and
952+
are skipped in the parent process. A failure or skip of
953+
:meth:`~unittest.TestCase.setUpClass` in the subprocess is reported for the
954+
whole class. ``setUpModule()`` cannot be controlled by a class decorator,
955+
so it still runs in the parent process too; test it with
956+
:data:`runningInSubprocess` if needed.
957+
958+
The subprocess inherits the enabled resources (``-u``), memory limit
959+
(``-M``) and verbosity (``-v``) of the parent test run, so that
960+
:func:`~test.support.requires_resource`, :func:`~test.support.requires`,
961+
:func:`~test.support.bigmemtest` and the like behave consistently in both
962+
processes.
963+
964+
The test is skipped on platforms without subprocess support.
965+
966+
967+
.. data:: runningInSubprocess
968+
969+
``True`` while the code runs in the isolated subprocess spawned by
970+
:func:`runInSubprocess`, and ``False`` otherwise (including in the parent
971+
process and in a normal, non-isolated test run). Fixtures such as
972+
:meth:`~unittest.TestCase.setUp`, :meth:`~unittest.TestCase.tearDown`,
973+
:meth:`~unittest.TestCase.setUpClass`, :meth:`~unittest.TestCase.tearDownClass`,
974+
``setUpModule()`` and ``tearDownModule()`` can test it to choose which code
975+
to run in the subprocess.
976+
977+
978+
.. currentmodule:: test.support
979+
980+
928981
.. function:: check_free_after_iterating(test, iter, cls, args=())
929982

930983
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)