Skip to content

Commit 5ce1772

Browse files
committed
Rework the subprocess documentation. Closes #650. Closes #651.
1 parent d4ef48f commit 5ce1772

File tree

1 file changed

+72
-56
lines changed

1 file changed

+72
-56
lines changed

docs/subprocess-support.rst

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,72 @@ Normally coverage writes the data via a pretty standard atexit handler. However,
66
own then the atexit handler might not run. Why that happens is best left to the adventurous to discover by waddling
77
through the Python bug tracker.
88

9-
pytest-cov supports subprocesses and multiprocessing, and works around these atexit limitations. However, there are a
10-
few pitfalls that need to be explained.
9+
pytest-cov supports subprocesses, and works around these atexit limitations. However, there are a few pitfalls that need to be explained.
1110

12-
If you use ``multiprocessing.Pool``
13-
===================================
11+
But first, how does pytest-cov's subprocess support works?
1412

15-
**pytest-cov** automatically registers a multiprocessing finalizer. The finalizer will only run reliably if the pool is
16-
closed. Closing the pool basically signals the workers that there will be no more work, and they will eventually exit.
17-
Thus one also needs to call `join` on the pool.
18-
19-
If you use ``multiprocessing.Pool.terminate`` or the context manager API (``__exit__``
20-
will just call ``terminate``) then the workers can get SIGTERM and then the finalizers won't run or complete in time.
21-
Thus you need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit:
13+
pytest-cov packaging injects a pytest-cov.pth into the installation. This file effectively runs this at *every* python startup:
2214

2315
.. code-block:: python
2416
25-
from multiprocessing import Pool
17+
if 'COV_CORE_SOURCE' in os.environ:
18+
try:
19+
from pytest_cov.embed import init
20+
init()
21+
except Exception as exc:
22+
sys.stderr.write(
23+
"pytest-cov: Failed to setup subprocess coverage. "
24+
"Environ: {0!r} "
25+
"Exception: {1!r}\n".format(
26+
dict((k, v) for k, v in os.environ.items() if k.startswith('COV_CORE')),
27+
exc
28+
)
29+
)
2630
27-
def f(x):
28-
return x*x
31+
The pytest plugin will set this ``COV_CORE_SOURCE`` environment variable thus any subprocess that inherits the environment variables
32+
(the default behavior) will run ``pytest_cov.embed.init`` which in turn sets up coverage according to these variables:
2933

30-
if __name__ == '__main__':
31-
p = Pool(5)
32-
try:
33-
print(p.map(f, [1, 2, 3]))
34-
finally:
35-
p.close() # Marks the pool as closed.
36-
p.join() # Waits for workers to exit.
34+
* ``COV_CORE_SOURCE``
35+
* ``COV_CORE_CONFIG``
36+
* ``COV_CORE_DATAFILE``
37+
* ``COV_CORE_BRANCH``
38+
* ``COV_CORE_CONTEXT``
39+
40+
Why does it have the ``COV_CORE`` you wonder? Well, it's mostly historical reasons: long time ago pytest-cov depended on a cov-core package
41+
that implemented common functionality for pytest-cov, nose-cov and nose2-cov. The dependency is gone but the convention is kept. It could
42+
be changed but it would break all projects that manually set these intended-to-be-internal-but-sadly-not-in-reality environment variables.
43+
44+
Coverage's subprocess support
45+
=============================
46+
47+
Now that you understand how pytest-cov works you can easily figure out that using
48+
`coverage's recommended <https://coverage.readthedocs.io/en/latest/subprocess.html>`_ way of dealing with subprocesses,
49+
by either having this in a ``.pth`` file or ``sitecustomize.py`` will break everything:
50+
51+
.. code-block::
52+
53+
import coverage; coverage.process_startup() # this will break pytest-cov
3754
55+
Do not do that as that will restart coverage with the wrong options.
3856

39-
If you must use the context manager API (e.g.: the pool is managed in third party code you can't change) then you can
40-
register a cleaning SIGTERM handler like so:
57+
If you use ``multiprocessing``
58+
==============================
4159

42-
.. warning::
60+
Builtin support for multiprocessing was dropped in pytest-cov 4.0.
61+
This support was mostly working but very broken in certain scenarios (see `issue 82408 <https://github.com/python/cpython/issues/82408>`_)
62+
and made the test suite very flaky and slow.
4363

44-
**This technique cannot be used on Python 3.8** (registering signal handlers will cause deadlocks in the pool,
45-
see: https://bugs.python.org/issue38227).
64+
However, there is `builtin multiprocessing support in coverage <https://coverage.readthedocs.io/en/latest/config.html#run-concurrency>`_
65+
and you can migrate to that. All you need is this in your preferred configuration file (example: ``.coveragerc``):
66+
67+
.. code-block:: ini
68+
69+
[run]
70+
concurrency = multiprocessing
71+
parallel = true
72+
sigterm = true
73+
74+
Now as a side-note, it's a good idea in general to properly close your Pool by using ``Pool.join()``:
4675

4776
.. code-block:: python
4877
@@ -52,46 +81,33 @@ register a cleaning SIGTERM handler like so:
5281
return x*x
5382
5483
if __name__ == '__main__':
84+
p = Pool(5)
5585
try:
56-
from pytest_cov.embed import cleanup_on_sigterm
57-
except ImportError:
58-
pass
59-
else:
60-
cleanup_on_sigterm()
61-
62-
with Pool(5) as p:
6386
print(p.map(f, [1, 2, 3]))
87+
finally:
88+
p.close() # Marks the pool as closed.
89+
p.join() # Waits for workers to exit.
6490
65-
If you use ``multiprocessing.Process``
66-
======================================
67-
68-
There's similar issue when using the ``Process`` objects. Don't forget to use ``.join()``:
6991
70-
.. code-block:: python
92+
.. _cleanup_on_sigterm:
7193

72-
from multiprocessing import Process
94+
Signal handlers
95+
===============
7396

74-
def f(name):
75-
print('hello', name)
97+
pytest-cov provides a signal handling routines, mostly for special situations where you'd have custom signal handling that doesn't
98+
allow atexit to properly run and the now-gone multiprocessing support:
7699

77-
if __name__ == '__main__':
78-
try:
79-
from pytest_cov.embed import cleanup_on_sigterm
80-
except ImportError:
81-
pass
82-
else:
83-
cleanup_on_sigterm()
100+
* ``pytest_cov.embed.cleanup_on_sigterm()``
101+
* ``pytest_cov.embed.cleanup_on_signal(signum)`` (e.g.: ``cleanup_on_signal(signal.SIGHUP)``)
84102

85-
p = Process(target=f, args=('bob',))
86-
try:
87-
p.start()
88-
finally:
89-
p.join() # necessary so that the Process exists before the test suite exits (thus coverage is collected)
103+
If you use multiprocessing
104+
--------------------------
90105

91-
.. _cleanup_on_sigterm:
106+
It is not recommanded to use these signal handlers with multiprocessing as registering signal handlers will cause deadlocks in the pool,
107+
see: https://bugs.python.org/issue38227).
92108

93109
If you got custom signal handling
94-
=================================
110+
---------------------------------
95111

96112
**pytest-cov 2.6** has a rudimentary ``pytest_cov.embed.cleanup_on_sigterm`` you can use to register a SIGTERM handler
97113
that flushes the coverage data.
@@ -140,7 +156,7 @@ Alternatively you can do this:
140156
signal.signal(signal.SIGHUP, restart_service)
141157
142158
If you use Windows
143-
==================
159+
------------------
144160

145161
On Windows you can register a handler for SIGTERM but it doesn't actually work. It will work if you
146162
`os.kill(os.getpid(), signal.SIGTERM)` (send SIGTERM to the current process) but for most intents and purposes that's

0 commit comments

Comments
 (0)