-
Notifications
You must be signed in to change notification settings - Fork 232
Remove multiprocess finalizer and improve subprocess docs #265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
3160976
1210cdc
0533855
892f4e1
b494c37
5d49467
1cb8ce6
8014767
38c89a8
5374581
710e4cc
8a2bfa8
4046fc2
65d50cf
49f55d6
b449d92
c32c158
ab9d7bc
cd0c4a4
3d3b488
2291d76
8f1b9e6
478152e
7aa50d9
3709127
35f38f4
bcdce59
c11fe04
103d1ef
66d8ade
65959fc
fdc43ec
7557f67
42f0307
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ Contents: | |
| reporting | ||
| debuggers | ||
| xdist | ||
| mp | ||
| subprocess-support | ||
| plugins | ||
| markers-fixtures | ||
| changelog | ||
|
|
||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| ================== | ||
| Subprocess support | ||
| ================== | ||
|
|
||
| pytest-cov supports subprocesses and multiprocessing. However, there are a few pitfalls that need to be | ||
| explained. | ||
|
|
||
| Normally coverage writes the data via a pretty standard atexit handler. However, if the subprocess doesn't exit on its | ||
| own then the atexit handler might not run. Why that happens is best left to the adventurous to discover by waddling | ||
| though the Python bug tracker. | ||
|
|
||
| For now pytest-cov provides opt-in workarounds for these problems. | ||
|
|
||
| If you use ``multiprocessing.Pool`` | ||
| =================================== | ||
|
|
||
| You need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from multiprocessing import Pool | ||
|
|
||
| def f(x): | ||
| return x*x | ||
|
|
||
| if __name__ == '__main__': | ||
| p = Pool(5) | ||
| try: | ||
| print(p.map(f, [1, 2, 3])) | ||
| finally: # <= THIS IS ESSENTIAL | ||
| p.close() # <= THIS IS ESSENTIAL | ||
| p.join() # <= THIS IS ESSENTIAL | ||
|
|
||
| Previously this guide recommended using ``multiprocessing.Pool``'s context manager API, however, that was wrong as | ||
| ``multiprocessing.Pool.__exit__`` is an alias to ``multiprocessing.Pool.terminate``, and that doesn't always run the | ||
| finalizers (sometimes the problem in `cleanup_on_sigterm`_ will appear). | ||
|
|
||
| If you use ``multiprocessing.Process`` | ||
| ====================================== | ||
|
|
||
| There's an identical issue when using the ``Process`` objects. Don't forget to use ``.join()``: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| from multiprocessing import Process | ||
|
|
||
| def f(name): | ||
| print('hello', name) | ||
|
|
||
| if __name__ == '__main__': | ||
| p = Process(target=f, args=('bob',)) | ||
| try: | ||
| p.start() | ||
| finally: # <= THIS IS ESSENTIAL | ||
| p.join() # <= THIS IS ESSENTIAL | ||
|
|
||
| .. _cleanup_on_sigterm: | ||
|
|
||
| If you abuse ``multiprocessing.Process.terminate`` | ||
| ================================================== | ||
|
|
||
| It appears that many people are using the ``terminate`` method and then get unreliable coverage results. That usually | ||
| means a SIGTERM gets sent to the process. Unfortunately Python don't have a default handler for SIGTERM so you need to | ||
| install your own. Because ``pytest-cov`` doesn't want to second-guess (not yet, add your thoughts on the issue tracker | ||
| if you disagree) it doesn't install a handler by default, but you can activate it by doing this: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| try: | ||
| from pytest_cov.embed import cleanup_on_sigterm | ||
| except ImportError: | ||
| pass | ||
| else: | ||
| cleanup_on_sigterm() | ||
|
|
||
| If anything else | ||
| ================ | ||
|
|
||
| If you have custom signal handling, eg: you do reload on SIGHUP you should have something like this: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import os | ||
| import signal | ||
|
|
||
| def restart_service(frame, signum): | ||
| os.exec( ... ) # or whatever your custom signal would do | ||
| signal.signal(signal.SIGHUP, restart_service) | ||
|
|
||
| try: | ||
| from pytest_cov.embed import cleanup_on_signal | ||
| except ImportError: | ||
| pass | ||
| else: | ||
| cleanup_on_signal(signal.SIGHUP) | ||
|
|
||
| Note that both ``cleanup_on_signal`` and ``cleanup_on_sigterm`` will run the previous signal handler. | ||
|
|
||
| Alternatively you can do this: | ||
|
|
||
| import os | ||
| import signal | ||
|
|
||
| try: | ||
| from pytest_cov.embed import cleanup | ||
| except ImportError: | ||
| cleanup = None | ||
|
|
||
| def restart_service(frame, signum): | ||
| if cleanup is not None: | ||
| cleanup() | ||
|
|
||
| os.exec( ... ) # or whatever your custom signal would do | ||
| signal.signal(signal.SIGHUP, restart_service) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -889,6 +889,38 @@ def test_funcarg_not_active(testdir): | |
| assert result.ret == 0 | ||
|
|
||
|
|
||
| def test_multiprocessing_pool(testdir): | ||
| py.test.importorskip('multiprocessing.util') | ||
|
|
||
| script = testdir.makepyfile(''' | ||
| import multiprocessing | ||
|
|
||
| def target_fn(a): | ||
| return a + 1 | ||
|
|
||
| def test_run_target(): | ||
| for i in range(100): | ||
| with multiprocessing.Pool(10) as p: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why this test doesn't use the recommended way https://github.com/pytest-dev/pytest-cov/pull/265/files#diff-c1d4de3cfc01566955163b584f0efcc0R29 ?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I added support for the old way while fixing the regressions. Need to update the docs again. The new way is still the best way for the pytest-cov<=2.6.1 releases. |
||
| p.map(target_fn, range(10)) | ||
| p.join() | ||
| ''') | ||
|
|
||
| result = testdir.runpytest('-v', | ||
| '--cov=%s' % script.dirpath(), | ||
| '--cov-report=term-missing', | ||
| script) | ||
|
|
||
| result.stdout.fnmatch_lines([ | ||
| '*- coverage: platform *, python * -*', | ||
| 'test_multiprocessing_pool* 8 * 100%*', | ||
| '*1 passed*' | ||
| ]) | ||
| assert result.ret == 0 | ||
| # assert "Doesn't seem to be a coverage.py data file" not in result.stdout.str() | ||
| # assert "Doesn't seem to be a coverage.py data file" not in result.stderr.str() | ||
| assert not testdir.tmpdir.listdir(".coverage.*") | ||
|
|
||
|
|
||
| def test_multiprocessing_subprocess(testdir): | ||
| py.test.importorskip('multiprocessing.util') | ||
|
|
||
|
|
@@ -1112,6 +1144,7 @@ def test_run(): | |
| ]) | ||
| assert result.ret == 0 | ||
|
|
||
|
|
||
| @pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows") | ||
| def test_cleanup_on_sigterm_sig_ign(testdir): | ||
| script = testdir.makepyfile(''' | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.