Skip to content

Commit 81c5e43

Browse files
committed
docs: rewrite the subprocess page
Now multiprocessing is first, with an example of how to use Pool properly.
1 parent 878410c commit 81c5e43

12 files changed

+85
-67
lines changed

CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Unreleased
3737
understand the problem or the solution, but ``git bisect`` helped find it,
3838
and now it's fixed.
3939

40+
- Docs: re-wrote the :ref:`subprocess` page to put multiprocessing first and to
41+
highlight the correct use of :class:`multiprocessing.Pool
42+
<python:multiprocessing.pool.Pool>`.
43+
4044
.. _issue 1874: https://github.com/nedbat/coveragepy/issues/1874
4145
.. _issue 1875: https://github.com/nedbat/coveragepy/issues/1875
4246
.. _issue 1902: https://github.com/nedbat/coveragepy/issues/1902

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ cogdoc: $(DOCBIN) ## Run docs through cog.
255255

256256
dochtml: cogdoc $(DOCBIN) ## Build the docs HTML output.
257257
$(SPHINXBUILD) -b html doc doc/_build/html
258+
@echo "Start at: doc/_build/html/index.html"
258259

259260
docdev: dochtml ## Build docs, and auto-watch for changes.
260261
PATH=$(DOCBIN):$(PATH) $(SPHINXAUTOBUILD) -b html doc doc/_build/html

coverage/control.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def __init__( # pylint: disable=too-many-arguments
301301
context=context,
302302
)
303303

304-
# If we have sub-process measurement happening automatically, then we
304+
# If we have subprocess measurement happening automatically, then we
305305
# want any explicit creation of a Coverage object to mean, this process
306306
# is already coverage-aware, so don't auto-measure it. By now, the
307307
# auto-creation of a Coverage object has already happened. But we can

coverage/multiproc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def patch_multiprocessing(rcfile: str) -> None:
9494

9595
# When spawning processes rather than forking them, we have no state in the
9696
# new process. We sneak in there with a Stowaway: we stuff one of our own
97-
# objects into the data that gets pickled and sent to the sub-process. When
97+
# objects into the data that gets pickled and sent to the subprocess. When
9898
# the Stowaway is unpickled, its __setstate__ method is called, which
9999
# re-applies the monkey-patch.
100100
# Windows only spawns, so this is needed to keep Windows working.

doc/changes.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -1060,12 +1060,12 @@ Work from the PyCon 2016 Sprints!
10601060
- The ``concurrency`` option can now take multiple values, to support programs
10611061
using multiprocessing and another library such as eventlet. This is only
10621062
possible in the configuration file, not from the command line. The
1063-
configuration file is the only way for sub-processes to all run with the same
1063+
configuration file is the only way for subprocesses to all run with the same
10641064
options. Fixes `issue 484`_. Thanks to Josh Williams for prototyping.
10651065

10661066
- Using a ``concurrency`` setting of ``multiprocessing`` now implies
10671067
``--parallel`` so that the main program is measured similarly to the
1068-
sub-processes.
1068+
subprocesses.
10691069

10701070
- When using `automatic subprocess measurement`_, running coverage commands
10711071
would create spurious data files. This is now fixed, thanks to diagnosis and

doc/cmd.rst

+4-4
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,10 @@ You can combine multiple values for ``--concurrency``, separated with commas.
176176
You can specify ``thread`` and also one of ``eventlet``, ``gevent``, or
177177
``greenlet``.
178178

179-
If you are using ``--concurrency=multiprocessing``, you must set other options
180-
in the configuration file. Options on the command line will not be passed to
181-
the processes that multiprocessing creates. Best practice is to use the
182-
configuration file for all options.
179+
If you are using ``--concurrency=multiprocessing``, you must set your other
180+
options in the configuration file. Options on the command line will not be
181+
passed to the processes that multiprocessing creates. Best practice is to use
182+
the configuration file for all options.
183183

184184
.. _multiprocessing: https://docs.python.org/3/library/multiprocessing.html
185185
.. _greenlet: https://greenlet.readthedocs.io/

doc/config.rst

+4-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ specification of options that are otherwise only available in the
2525
:ref:`API <api>`.
2626

2727
Configuration files also make it easier to get coverage testing of spawned
28-
sub-processes. See :ref:`subprocess` for more details.
28+
subprocesses. See :ref:`subprocess` for more details.
2929

3030
The default name for the configuration file is ``.coveragerc``, in the same
3131
directory coverage.py is being run in. Most of the settings in the
@@ -443,11 +443,12 @@ need to know the source origin.
443443

444444
(boolean, default False) if true, register a SIGTERM signal handler to capture
445445
data when the process ends due to a SIGTERM signal. This includes
446-
:meth:`Process.terminate <python:multiprocessing.Process.terminate>`, and other
446+
:meth:`Process.terminate <python:multiprocessing.Process.terminate>` and other
447447
ways to terminate a process. This can help when collecting data in usual
448448
situations, but can also introduce problems (see `issue 1310`_).
449449

450-
Only on Linux and Mac.
450+
The signal handler is only registered on Linux and Mac. On Windows, this
451+
setting has no effect.
451452

452453
.. _issue 1310: https://github.com/nedbat/coveragepy/issues/1310
453454

doc/subprocess.rst

+58-46
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,75 @@
33
44
.. _subprocess:
55

6-
=======================
7-
Measuring sub-processes
8-
=======================
6+
======================
7+
Measuring subprocesses
8+
======================
99

10-
Complex test suites may spawn sub-processes to run tests, either to run them in
11-
parallel, or because sub-process behavior is an important part of the system
12-
under test. Measuring coverage in those sub-processes can be tricky because you
13-
have to modify the code spawning the process to invoke coverage.py.
10+
If your system under test spawns subprocesses, you'll have to take extra steps
11+
to measure coverage in those processes. There are a few ways to ensure they
12+
get measured. The approach you use depends on how you create the processes.
1413

15-
There's an easier way to do it: coverage.py includes a function,
16-
:func:`coverage.process_startup` designed to be invoked when Python starts. It
17-
examines the ``COVERAGE_PROCESS_START`` environment variable, and if it is set,
18-
begins coverage measurement. The environment variable's value will be used as
19-
the name of the :ref:`configuration file <config>` to use.
14+
No matter how your subprocesses are created, you will need the :ref:`parallel
15+
option <config_run_parallel>` to collect separate data for each process, and
16+
the :ref:`coverage combine <cmd_combine>` command to combine them together
17+
before reporting.
2018

21-
.. note::
19+
To successfully write a coverage data file, the Python subprocess under
20+
measurement must shut down cleanly and have a chance for coverage.py to run its
21+
termination code. It will do that when the process ends naturally, or when a
22+
SIGTERM signal is received.
2223

23-
The subprocess only sees options in the configuration file. Options set on
24-
the command line will not be used in the subprocesses.
24+
If your processes are ending with SIGTERM, you must enable the
25+
:ref:`config_run_sigterm` setting to configure coverage to catch SIGTERM
26+
signals and write its data.
27+
28+
Other ways of ending a process, like SIGKILL or :func:`os._exit
29+
<python:os._exit>`, will prevent coverage.py from writing its data file,
30+
leaving you with incomplete or non-existent coverage data.
2531

2632
.. note::
2733

28-
If you have subprocesses created with :mod:`multiprocessing
29-
<python:multiprocessing>`, the ``--concurrency=multiprocessing``
30-
command-line option should take care of everything for you. See
31-
:ref:`cmd_run` for details.
34+
Subprocesses will only see coverage options in the configuration file.
35+
Options set on the command line will not be visible to subprocesses.
36+
37+
38+
Using multiprocessing
39+
---------------------
3240

33-
When using this technique, be sure to set the parallel option to true so that
34-
multiple coverage.py runs will each write their data to a distinct file.
41+
The :mod:`multiprocessing <python:multiprocessing>` module in the Python
42+
standard library provides high-level tools for managing subprocesses. If you
43+
use it, the :ref:`concurrency=multiprocessing <config_run_concurrency>` and
44+
:ref:`sigterm <config_run_sigterm>` settings will configure coverage to measure
45+
the subprocesses.
3546

47+
Even with multiprocessing, you have to be careful that all subprocesses
48+
terminate cleanly or they won't record their coverage measurements. For
49+
example, the correct way to use a Pool requires closing and joining the pool
50+
before terminating::
3651

37-
Configuring Python for sub-process measurement
38-
----------------------------------------------
52+
with multiprocessing.Pool() as pool:
53+
# ... use any of the pool methods ...
54+
pool.close()
55+
pool.join()
3956

40-
Measuring coverage in sub-processes is a little tricky. When you spawn a
41-
sub-process, you are invoking Python to run your program. Usually, to get
42-
coverage measurement, you have to use coverage.py to run your program. Your
43-
sub-process won't be using coverage.py, so we have to convince Python to use
44-
coverage.py even when not explicitly invoked.
4557

46-
To do that, we'll configure Python to run a little coverage.py code when it
47-
starts. That code will look for an environment variable that tells it to start
48-
coverage measurement at the start of the process.
58+
Implicit coverage
59+
-----------------
60+
61+
If you are starting subprocesses another way, you can configure Python to start
62+
coverage when it runs. Coverage.py includes a function designed to be invoked
63+
when Python starts: :func:`coverage.process_startup`. It examines the
64+
``COVERAGE_PROCESS_START`` environment variable, and if it is set, begins
65+
coverage measurement. The environment variable's value will be used as the name
66+
of the :ref:`configuration file <config>` to use.
4967

5068
To arrange all this, you have to do two things: set a value for the
5169
``COVERAGE_PROCESS_START`` environment variable, and then configure Python to
5270
invoke :func:`coverage.process_startup` when Python processes start.
5371

5472
How you set ``COVERAGE_PROCESS_START`` depends on the details of how you create
55-
sub-processes. As long as the environment variable is visible in your
56-
sub-process, it will work.
73+
subprocesses. As long as the environment variable is visible in your
74+
subprocess, it will work.
5775

5876
You can configure your Python installation to invoke the ``process_startup``
5977
function in two ways:
@@ -84,17 +102,11 @@ start-up. Be sure to remove the change when you uninstall coverage.py, or use
84102
a more defensive approach to importing it.
85103

86104

87-
Process termination
88-
-------------------
89-
90-
To successfully write a coverage data file, the Python sub-process under
91-
analysis must shut down cleanly and have a chance for coverage.py to run its
92-
termination code. It will do that when the process ends naturally, or when a
93-
SIGTERM signal is received.
94-
95-
Coverage.py uses :mod:`atexit <python:atexit>` to handle usual process ends,
96-
and a :mod:`signal <python:signal>` handler to catch SIGTERM signals.
105+
Explicit coverage
106+
-----------------
97107

98-
Other ways of ending a process, like SIGKILL or :func:`os._exit
99-
<python:os._exit>`, will prevent coverage.py from writing its data file,
100-
leaving you with incomplete or non-existent coverage data.
108+
Another option for running coverage on your subprocesses it to run coverage
109+
explicitly as the command for your subprocess instead of using "python" as the
110+
command. This isn't recommended, since it requires running different code
111+
when running coverage than when not, which can complicate your test
112+
environment.

igor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def run_tests_with_coverage(core, *runner_args):
180180
context = os.environ[context[1:]]
181181
os.environ["COVERAGE_CONTEXT"] = context + "." + core
182182

183-
# Create the .pth file that will let us measure coverage in sub-processes.
183+
# Create the .pth file that will let us measure coverage in subprocesses.
184184
# The .pth file seems to have to be alphabetically after easy-install.pth
185185
# or the sys.path entries aren't created right?
186186
# There's an entry in "make clean" to get rid of this file.

tests/coveragetest.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,11 @@ def command_line(self, args: str, ret: int = OK) -> None:
377377
coverage_command = "coverage"
378378

379379
def run_command(self, cmd: str) -> str:
380-
"""Run the command-line `cmd` in a sub-process.
380+
"""Run the command-line `cmd` in a subprocess.
381381
382-
`cmd` is the command line to invoke in a sub-process. Returns the
382+
`cmd` is the command line to invoke in a subprocess. Returns the
383383
combined content of `stdout` and `stderr` output streams from the
384-
sub-process.
384+
subprocess.
385385
386386
See `run_command_status` for complete semantics.
387387
@@ -394,7 +394,7 @@ def run_command(self, cmd: str) -> str:
394394
return output
395395

396396
def run_command_status(self, cmd: str) -> tuple[int, str]:
397-
"""Run the command-line `cmd` in a sub-process, and print its output.
397+
"""Run the command-line `cmd` in a subprocess, and print its output.
398398
399399
Use this when you need to test the process behavior of coverage.
400400
@@ -420,7 +420,7 @@ def run_command_status(self, cmd: str) -> tuple[int, str]:
420420
command_args = split_commandline[1:]
421421

422422
if command_name == "python":
423-
# Running a Python interpreter in a sub-processes can be tricky.
423+
# Running a Python interpreter in a subprocesses can be tricky.
424424
# Use the real name of our own executable. So "python foo.py" might
425425
# get executed as "python3.3 foo.py". This is important because
426426
# Python 3.x doesn't install as "python", so you might get a Python

tests/helpers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434

3535
def run_command(cmd: str) -> tuple[int, str]:
36-
"""Run a command in a sub-process.
36+
"""Run a command in a subprocess.
3737
3838
Returns the exit status code and the combined stdout and stderr.
3939

tests/test_process.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ def test_deprecation_warnings(self) -> None:
606606
""")
607607

608608
# Some of our testing infrastructure can issue warnings.
609-
# Turn it all off for the sub-process.
609+
# Turn it all off for the subprocess.
610610
self.del_environ("COVERAGE_TESTING")
611611

612612
out = self.run_command("python allok.py")
@@ -1197,9 +1197,9 @@ def test_removing_directory_with_error(self) -> None:
11971197
assert all(line in out for line in lines)
11981198

11991199

1200-
@pytest.mark.skipif(env.METACOV, reason="Can't test sub-process pth file during metacoverage")
1200+
@pytest.mark.skipif(env.METACOV, reason="Can't test subprocess pth file during metacoverage")
12011201
class ProcessStartupTest(CoverageTest):
1202-
"""Test that we can measure coverage in sub-processes."""
1202+
"""Test that we can measure coverage in subprocesses."""
12031203

12041204
def setUp(self) -> None:
12051205
super().setUp()

0 commit comments

Comments
 (0)