Skip to content
Merged
53 changes: 53 additions & 0 deletions Doc/library/test.rst
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,59 @@ The :mod:`!test.support` module defines the following functions:
:mod:`tracemalloc` is enabled.


.. currentmodule:: test.support.isolation

.. decorator:: runInSubprocess()

Decorator that runs the decorated test in a fresh interpreter subprocess, in
isolation, so that it does not share global or interpreter state with the
rest of the test run. It can decorate a test method or a whole
:class:`~unittest.TestCase` subclass. Decorated methods must take no extra
arguments. A failure, error or skip in the subprocess is reported for the
corresponding test, and individual :meth:`subtests
<unittest.TestCase.subTest>` that fail or are skipped are reported
individually. A reported failure or error shows the original subprocess
traceback as the cause of the exception.

When a **method** is decorated, only that method runs in a subprocess; all
fixtures (:meth:`~unittest.TestCase.setUp` / :meth:`~unittest.TestCase.tearDown`,
:meth:`~unittest.TestCase.setUpClass` / :meth:`~unittest.TestCase.tearDownClass`
and ``setUpModule()`` / ``tearDownModule()``) run both in the parent process
(as usual) and in the subprocess around the method.

When a **class** is decorated, the whole class runs in a single subprocess,
and :meth:`~unittest.TestCase.setUpClass`,
:meth:`~unittest.TestCase.tearDownClass`, :meth:`~unittest.TestCase.setUp`
and :meth:`~unittest.TestCase.tearDown` run once each in the subprocess and
are skipped in the parent process. A failure or skip of
:meth:`~unittest.TestCase.setUpClass` in the subprocess is reported for the
whole class. ``setUpModule()`` cannot be controlled by a class decorator,
so it still runs in the parent process too; test it with
:data:`runningInSubprocess` if needed.

The subprocess inherits the enabled resources (``-u``), memory limit
(``-M``) and verbosity (``-v``) of the parent test run, so that
:func:`~test.support.requires_resource`, :func:`~test.support.requires`,
:func:`~test.support.bigmemtest` and the like behave consistently in both
processes.

The test is skipped on platforms without subprocess support.


.. data:: runningInSubprocess

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


.. currentmodule:: test.support


.. function:: check_free_after_iterating(test, iter, cls, args=())

Assert instances of *cls* are deallocated after iterating.
Expand Down
111 changes: 111 additions & 0 deletions Lib/test/_isolated_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Sample tests driven by test.test_support.TestIsolated.

This module is imported, never run as a test file, so that
:func:`test.support.isolation.runInSubprocess` has a real, importable target to run in
a subprocess. Several of these tests fail, error or are skipped on purpose.
"""

import time
import unittest
from test.support import isolation

# DurationSample sleeps this long in the subprocess; a parent-reported duration
# close to it proves the subprocess timing was forwarded, not the replay time.
DURATION_SLEEP = 0.2


class MethodSample(unittest.TestCase):

@isolation.runInSubprocess()
def test_pass(self):
self.assertTrue(isolation.runningInSubprocess)

@isolation.runInSubprocess()
def test_fail(self):
self.assertEqual(1, 2)

@isolation.runInSubprocess()
def test_error(self):
raise RuntimeError('boom')

@isolation.runInSubprocess()
def test_skip(self):
self.skipTest('nope')

@isolation.runInSubprocess()
@unittest.expectedFailure
def test_expected_failure(self):
self.assertEqual(1, 2)

@isolation.runInSubprocess()
@unittest.expectedFailure
def test_unexpected_success(self):
pass


@isolation.runInSubprocess()
class ClassSample(unittest.TestCase):

def test_pass(self):
self.assertTrue(isolation.runningInSubprocess)

def test_fail(self):
self.assertEqual(1, 2)

@unittest.expectedFailure
def test_expected_failure(self):
self.assertEqual(1, 2)


class SubtestSample(unittest.TestCase):

@isolation.runInSubprocess()
def test_subtests(self):
for i in range(3):
with self.subTest(i=i):
self.assertNotEqual(i, 1)


@isolation.runInSubprocess()
class DurationSample(unittest.TestCase):

def test_slow(self):
time.sleep(DURATION_SLEEP)


@isolation.runInSubprocess()
class SubclassingSample(unittest.TestCase):
# setUpClass must run bound to the runtime class, so a subclass sees its own
# name here rather than the base class's.

@classmethod
def setUpClass(cls):
cls.setup_class_name = cls.__name__

def setUp(self):
self.set_up = True

def test_runtime_class(self):
self.assertEqual(self.setup_class_name, type(self).__name__)


class SubclassSample(SubclassingSample):
# What a subclass adds or overrides must run in the subprocess too.

def setUp(self):
super().setUp()
self.set_up_in_subclass = True

def test_added_in_subclass(self):
self.assertTrue(isolation.runningInSubprocess)
self.assertTrue(self.set_up)
self.assertTrue(self.set_up_in_subclass)


class BrokenSubclassSample(SubclassingSample):
# An overriding setUpClass() that does not call super() bypasses the
# subprocess entirely.

@classmethod
def setUpClass(cls):
pass
Loading
Loading