From 3160976f95a5a3a8619d08843c030b08d66580e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 16 Feb 2019 22:14:58 +0200 Subject: [PATCH 01/34] Stop using multiprocess finalizers. Better document workarounds for unclean subprocess exits. --- docs/mp.rst | 128 +++++++++++++++++++++++++++------------- src/pytest_cov/embed.py | 13 ++-- 2 files changed, 94 insertions(+), 47 deletions(-) diff --git a/docs/mp.rst b/docs/mp.rst index 6298cb80..41ba1312 100644 --- a/docs/mp.rst +++ b/docs/mp.rst @@ -1,41 +1,20 @@ -======================= -Multiprocessing support -======================= +================== +Subprocess support +================== -Although pytest-cov supports multiprocessing there are few pitfalls that need to be explained. +Although pytest-cov supports subprocesses and multiprocessing. However, there are few pitfalls that need to be +explained. -Abusing ``Process.terminate`` -============================= +Normally coverage writes the data via a pretty standard atexit handler. However, if the subprocess doesn't exit on it's +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. -It appears that many people are using the ``terminate`` method and then get unreliable coverage results. - -On Linux usually that 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 anything like: - -.. code-block:: python - - from pytest_cov.embed import cleanup_on_sigterm - cleanup_on_sigterm() - - # alternatively you can do this - - from pytest_cov.embed import cleanup - - def my_handler(signum, frame): - cleanup() - # custom cleanup - signal.signal(signal.SIGTERM, my_handler) - -On Windows there's no nice way to do cleanup (no signal handlers) so you're left to your own devices. - -Ungraceful Pool shutdown -======================== +For now pytest-cov provides opt-in workarounds for these problems. -Another problem is when using the ``Pool`` object. If you run some work on a pool in a test you're not guaranteed to get all -the coverage data unless you use the ``join`` method. +If you use ``multiprocessing.Pool`` +=================================== -Eg: +You need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit: .. code-block:: python @@ -45,14 +24,19 @@ Eg: return x*x if __name__ == '__main__': - with Pool(5) as p: + 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 - 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). -Ungraceful Process shutdown -=========================== +If you use ``multiprocessing.Process`` +====================================== There's an identical issue when using the ``Process`` objects. Don't forget to use ``.join()``: @@ -65,6 +49,70 @@ There's an identical issue when using the ``Process`` objects. Don't forget to u if __name__ == '__main__': p = Process(target=f, args=('bob',)) - p.start() + 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. + +On Linux usually that 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() + + +On Windows there's no nice way to do cleanup (no signal handlers) so you're left to your own devices. + +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() - p.join() # <= THIS IS ESSENTIAL + os.exec( ... ) # or whatever your custom signal would do + signal.signal(signal.SIGHUP, restart_service) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index df28ae67..fc7cd11c 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -20,9 +20,11 @@ def multiprocessing_start(_): + global active_cov cov = init() if cov: - multiprocessing.util.Finalize(None, cleanup, args=(cov,), exitpriority=1000) + active_cov = cov + cleanup_on_sigterm() try: @@ -77,12 +79,9 @@ def _cleanup(cov): cov.save() -def cleanup(cov=None): +def cleanup(): global active_cov - - _cleanup(cov) - if active_cov is not cov: - _cleanup(active_cov) + _cleanup(active_cov) active_cov = None @@ -96,7 +95,7 @@ def _signal_cleanup_handler(signum, frame): _previous_handler = _previous_handlers.get(signum) if _previous_handler == signal.SIG_IGN: return - elif _previous_handler: + elif _previous_handler is not _signal_cleanup_handler: _previous_handler(signum, frame) elif signum == signal.SIGTERM: os._exit(128 + signum) From 1210cdcb3c21761021717c22572de664b970042a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 16 Feb 2019 22:16:50 +0200 Subject: [PATCH 02/34] Rename file. --- docs/index.rst | 2 +- docs/{mp.rst => subprocess-support.rst} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/{mp.rst => subprocess-support.rst} (100%) diff --git a/docs/index.rst b/docs/index.rst index 155784d8..eb27e7a0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,7 +11,7 @@ Contents: reporting debuggers xdist - mp + subprocess-support plugins markers-fixtures changelog diff --git a/docs/mp.rst b/docs/subprocess-support.rst similarity index 100% rename from docs/mp.rst rename to docs/subprocess-support.rst From 0533855468907e5976ef065dfb2e0fca90dc674f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 17 Feb 2019 01:41:01 +0200 Subject: [PATCH 03/34] Update docs/subprocess-support.rst Co-Authored-By: ionelmc --- docs/subprocess-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 41ba1312..1bd432e6 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -2,7 +2,7 @@ Subprocess support ================== -Although pytest-cov supports subprocesses and multiprocessing. However, there are few pitfalls that need to be +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 it's From 892f4e1c65907176ca312709330651034382e6bf Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 17 Feb 2019 01:41:54 +0200 Subject: [PATCH 04/34] Update docs/subprocess-support.rst Co-Authored-By: ionelmc --- docs/subprocess-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 1bd432e6..7af23189 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -5,7 +5,7 @@ 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 it's +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. From b494c37dca43ed0d8ca2a1e668218e3a972821ea Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 17 Feb 2019 01:42:04 +0200 Subject: [PATCH 05/34] Update docs/subprocess-support.rst Co-Authored-By: ionelmc --- docs/subprocess-support.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 7af23189..878c33fe 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -7,7 +7,7 @@ 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. +though the Python bug tracker. For now pytest-cov provides opt-in workarounds for these problems. From 5d49467156d17e5366b8cd574f51f2f55f0447e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 17 Feb 2019 01:55:53 +0200 Subject: [PATCH 06/34] Remove the note about windows completely. --- docs/subprocess-support.rst | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 878c33fe..9688059d 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -59,11 +59,10 @@ There's an identical issue when using the ``Process`` objects. Don't forget to u If you abuse ``multiprocessing.Process.terminate`` ================================================== -It appears that many people are using the ``terminate`` method and then get unreliable coverage results. - -On Linux usually that 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: +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 @@ -74,9 +73,6 @@ on the issue tracker if you disagree) it doesn't install a handler by default, b else: cleanup_on_sigterm() - -On Windows there's no nice way to do cleanup (no signal handlers) so you're left to your own devices. - If anything else ================ From 1cb8ce69223c900ab95e67c3a7d0816722c1556c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 17 Feb 2019 03:15:04 +0200 Subject: [PATCH 07/34] Well ... bring back the finalizer, but make the cleanup reentrant. --- src/pytest_cov/embed.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index fc7cd11c..4c9651bb 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -16,14 +16,15 @@ import os import signal -active_cov = None +_active_cov = None def multiprocessing_start(_): - global active_cov + global _active_cov cov = init() if cov: - active_cov = cov + _active_cov = cov + multiprocessing.util.Finalize(None, cleanup, exitpriority=1000) cleanup_on_sigterm() @@ -38,7 +39,7 @@ def multiprocessing_start(_): def init(): # Only continue if ancestor process has set everything needed in # the env. - global active_cov + global _active_cov cov_source = os.environ.get('COV_CORE_SOURCE') cov_config = os.environ.get('COV_CORE_CONFIG') @@ -58,7 +59,7 @@ def init(): cov_config = True # Activate coverage for this process. - cov = active_cov = coverage.Coverage( + cov = _active_cov = coverage.Coverage( source=cov_source, branch=cov_branch, data_suffix=True, @@ -80,17 +81,29 @@ def _cleanup(cov): def cleanup(): - global active_cov - _cleanup(active_cov) - active_cov = None + global _active_cov + global _cleanup_in_progress + global _pending_signal + + _cleanup_in_progress = True + _cleanup(_active_cov) + _active_cov = None + if _pending_signal: + _signal_cleanup_handler(*_pending_signal) + _pending_signal = None multiprocessing_finish = cleanup # in case someone dared to use this internal _previous_handlers = {} +_pending_signal = None +_cleanup_in_progress = False def _signal_cleanup_handler(signum, frame): + if _cleanup_in_progress: + _pending_signal = signum, frame + return cleanup() _previous_handler = _previous_handlers.get(signum) if _previous_handler == signal.SIG_IGN: From 8014767fd59cbd7c73e162a56723817bbe519805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 17 Feb 2019 03:15:58 +0200 Subject: [PATCH 08/34] Remove this attribute and rely on pytest_cov.embed's internal storage. Fixes regression (cleanup don't take no argument no more). --- src/pytest_cov/plugin.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 619b9a38..8e69fd10 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -118,7 +118,6 @@ def __init__(self, options, pluginmanager, start=True): # Our implementation is unknown at this time. self.pid = None - self.cov = None self.cov_controller = None self.cov_report = compat.StringIO() self.cov_total = None @@ -286,12 +285,10 @@ def pytest_runtest_setup(self, item): if os.getpid() != self.pid: # test is run in another process than session, run # coverage manually - self.cov = embed.init() + embed.init() def pytest_runtest_teardown(self, item): - if self.cov is not None: - embed.cleanup(self.cov) - self.cov = None + embed.cleanup() @compat.hookwrapper def pytest_runtest_call(self, item): From 38c89a8c56bbfae6429e7c29e9a9d144049dbdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 17 Feb 2019 03:17:36 +0200 Subject: [PATCH 09/34] Ignore SIG_DFL (lil regression). --- src/pytest_cov/embed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 4c9651bb..97616b87 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -108,7 +108,7 @@ def _signal_cleanup_handler(signum, frame): _previous_handler = _previous_handlers.get(signum) if _previous_handler == signal.SIG_IGN: return - elif _previous_handler is not _signal_cleanup_handler: + elif _previous_handler and _previous_handler is not _signal_cleanup_handler: _previous_handler(signum, frame) elif signum == signal.SIGTERM: os._exit(128 + signum) From 537458115989384e9401fd600395b6933f0f05ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 17 Feb 2019 03:18:16 +0200 Subject: [PATCH 10/34] Avoid doubly registering the signal handler (so the previous handler is not lost). --- src/pytest_cov/embed.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 97616b87..063b6770 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -117,8 +117,10 @@ def _signal_cleanup_handler(signum, frame): def cleanup_on_signal(signum): - _previous_handlers[signum] = signal.getsignal(signum) - signal.signal(signum, _signal_cleanup_handler) + previous = signal.getsignal(signum) + if previous is not _signal_cleanup_handler: + _previous_handlers[signum] = previous + signal.signal(signum, _signal_cleanup_handler) def cleanup_on_sigterm(): From 710e4cccd72f2fc77dbac954af49393d7f129349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 17 Feb 2019 05:09:06 +0200 Subject: [PATCH 11/34] Add missing global. --- src/pytest_cov/embed.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 063b6770..0af3b779 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -101,6 +101,7 @@ def cleanup(): def _signal_cleanup_handler(signum, frame): + global _pending_signal if _cleanup_in_progress: _pending_signal = signum, frame return From 8a2bfa884aaa00b36ba67782a706a8d1b1314ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sun, 17 Feb 2019 06:19:41 +0200 Subject: [PATCH 12/34] Add a test for #250. --- tests/test_pytest_cov.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 8809eaed..a67d4267 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -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: + 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(''' From 4046fc20d0169bc7dc723117502f5620b9609469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 18 Feb 2019 01:05:37 +0200 Subject: [PATCH 13/34] Add a windows specific test. --- tests/test_pytest_cov.py | 47 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index a67d4267..ae38cfe5 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1065,7 +1065,52 @@ def test_run(): assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows") +@pytest.mark.skipif('sys.platform != "win32"') +@pytest.mark.parametrize('setup', [ + ('signal.signal(signal.SIGBREAK, signal.SIG_DFL); cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), + ('cleanup_on_signal(signal.SIGBREAK)', '87% 21-22'), + ('cleanup()', '73% 19-22'), +]) +def test_cleanup_on_sigterm_sig_break(testdir, setup): + # worth a read: https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/ + script = testdir.makepyfile(''' +import os, signal, subprocess, sys, time + +def test_run(): + proc = subprocess.Popen( + [sys.executable, __file__], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, shell=True + ) + time.sleep(1) + proc.send_signal(signal.CTRL_BREAK_EVENT) + stdout, stderr = proc.communicate() + assert not stderr + assert stdout in [b"^C", b""] + +if __name__ == "__main__": + from pytest_cov.embed import cleanup_on_signal, cleanup + ''' + setup[0] + ''' + + try: + time.sleep(10) + except BaseException as exc: + print("captured %r" % exc) +''') + + result = testdir.runpytest('-vv', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing', + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'test_cleanup_on_sigterm* %s' % setup[1], + '*1 passed*' + ]) + assert result.ret == 0 + + @pytest.mark.parametrize('setup', [ ('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'), ('cleanup_on_sigterm()', '88% 18-19'), From 65d50cf5355d78bd6655b1a98962b0161cea5037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 18 Feb 2019 01:21:45 +0200 Subject: [PATCH 14/34] Some renaming to better reflect what is actually tested. --- tests/test_pytest_cov.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index ae38cfe5..c4e09d8f 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -921,7 +921,7 @@ def test_run_target(): assert not testdir.tmpdir.listdir(".coverage.*") -def test_multiprocessing_subprocess(testdir): +def test_multiprocessing_process(testdir): py.test.importorskip('multiprocessing.util') script = testdir.makepyfile(''' @@ -950,7 +950,7 @@ def test_run_target(): assert result.ret == 0 -def test_multiprocessing_subprocess_no_source(testdir): +def test_multiprocessing_process_no_source(testdir): py.test.importorskip('multiprocessing.util') script = testdir.makepyfile(''' @@ -981,7 +981,7 @@ def test_run_target(): @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing don't support clean process temination on Windows") -def test_multiprocessing_subprocess_with_terminate(testdir): +def test_multiprocessing_process_with_terminate(testdir): py.test.importorskip('multiprocessing.util') script = testdir.makepyfile(''' From 49f55d604c9eb8cdc63e9f7aea42b1eb650e6ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 18 Feb 2019 01:37:40 +0200 Subject: [PATCH 15/34] Add few more multiprocessing tests and change some details for skips. --- tests/test_pytest_cov.py | 104 ++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index c4e09d8f..475cc492 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -889,6 +889,8 @@ def test_funcarg_not_active(testdir): assert result.ret == 0 +@pytest.mark.skipif("sys.version_info[0] < 3", reason="no context manager api on Python 2") +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_pool(testdir): py.test.importorskip('multiprocessing.util') @@ -916,11 +918,84 @@ def test_run_target(): '*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 "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.*") +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +def test_multiprocessing_pool_terminate(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): + p = multiprocessing.Pool(10) + try: + p.map(target_fn, range(10)) + finally: + p.terminate() + 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.*") + + +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +def test_multiprocessing_pool_close(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): + p = multiprocessing.Pool(10) + try: + p.map(target_fn, range(10)) + finally: + p.close() + 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.*") + + +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_process(testdir): py.test.importorskip('multiprocessing.util') @@ -950,6 +1025,7 @@ def test_run_target(): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_process_no_source(testdir): py.test.importorskip('multiprocessing.util') @@ -979,8 +1055,7 @@ def test_run_target(): assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', - reason="multiprocessing don't support clean process temination on Windows") +@pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_process_with_terminate(testdir): py.test.importorskip('multiprocessing.util') @@ -1019,8 +1094,7 @@ def test_run_target(): assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', - reason="fork not available on Windows") +@pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") def test_cleanup_on_sigterm(testdir): script = testdir.makepyfile(''' import os, signal, subprocess, sys, time @@ -1111,6 +1185,7 @@ def test_run(): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32"', reason="SIGTERM isn't really supported on Windows") @pytest.mark.parametrize('setup', [ ('signal.signal(signal.SIGTERM, signal.SIG_DFL); cleanup_on_sigterm()', '88% 18-19'), ('cleanup_on_sigterm()', '88% 18-19'), @@ -1121,9 +1196,16 @@ def test_cleanup_on_sigterm_sig_dfl(testdir, setup): import os, signal, subprocess, sys, time def test_run(): - proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if sys.platform == 'win32': + options = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP, 'shell': True} + signum = signal.CTRL_BREAK_EVENT + else: + options = {} + signum = signal.SIGTERM + + proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **options) time.sleep(1) - proc.terminate() + proc.send_signal(signum) stdout, stderr = proc.communicate() assert not stderr assert stdout == b"" @@ -1131,13 +1213,13 @@ def test_run(): if __name__ == "__main__": from pytest_cov.embed import cleanup_on_sigterm, cleanup - {0} + ''' + setup[0] + ''' try: time.sleep(10) except BaseException as exc: print("captured %r" % exc) -'''.format(setup[0])) +''') result = testdir.runpytest('-vv', '--cov=%s' % script.dirpath(), @@ -1152,7 +1234,7 @@ def test_run(): assert result.ret == 0 -@pytest.mark.skipif('sys.platform == "win32"', reason="fork not available on Windows") +@pytest.mark.skipif('sys.platform == "win32"', reason="SIGINT is subtly broken on Windows") def test_cleanup_on_sigterm_sig_dfl_sigint(testdir): script = testdir.makepyfile(''' import os, signal, subprocess, sys, time From b449d92ddb74206d8744a4fe88f0bdd0e52dd2f5 Mon Sep 17 00:00:00 2001 From: Ionel Cristian M?rie? Date: Mon, 18 Feb 2019 05:29:21 +0200 Subject: [PATCH 16/34] Skip a bunch of stuff on windows+pypy - it's broken, see https://github.com/pytest-dev/pytest-xdist/issues/142 --- tests/test_pytest_cov.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 475cc492..fea60198 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1,5 +1,6 @@ import glob import os +import platform import subprocess import sys from distutils.version import StrictVersion @@ -23,7 +24,7 @@ import pytest_cov.plugin -coverage, StrictVersion # required for skipif mark on test_cov_min_from_coveragerc +coverage, platform, StrictVersion # required for skipif mark on test_cov_min_from_coveragerc SCRIPT = ''' import sys, helper @@ -149,7 +150,10 @@ def test_foo(cov): DEST_DIR = 'cov_dest' REPORT_NAME = 'cov.xml' -xdist_params = pytest.mark.parametrize('opts', ['', '-n 1'], ids=['nodist', 'xdist']) +xdist_params = pytest.mark.parametrize('opts', [ + '', + pytest.param('-n 1', marks=pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"')) +], ids=['nodist', 'xdist']) @pytest.fixture(params=[ @@ -545,6 +549,7 @@ def test_fail(p): ]) +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_combine_racecondition(testdir): script = testdir.makepyfile(""" import pytest @@ -573,6 +578,7 @@ def test_foo(foo): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_collocated(testdir, prop): script = testdir.makepyfile(prop.code) testdir.tmpdir.join('.coveragerc').write(prop.fullconf) @@ -592,6 +598,7 @@ def test_dist_collocated(testdir, prop): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_not_collocated(testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') @@ -624,6 +631,7 @@ def test_dist_not_collocated(testdir, prop): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_not_collocated_coveragerc_source(testdir, prop): script = testdir.makepyfile(prop.code) dir1 = testdir.mkdir('dir1') @@ -747,6 +755,7 @@ def test_foo(): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_collocated(testdir): scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) @@ -768,6 +777,7 @@ def test_dist_subprocess_collocated(testdir): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_subprocess_not_collocated(testdir, tmpdir): scripts = testdir.makepyfile(parent_script=SCRIPT_PARENT, child_script=SCRIPT_CHILD) @@ -833,7 +843,10 @@ def test_dist_missing_data(testdir): venv_path = os.path.join(str(testdir.tmpdir), 'venv') virtualenv.create_environment(venv_path) if sys.platform == 'win32': - exe = os.path.join(venv_path, 'Scripts', 'python.exe') + if platform.python_implementation() == "PyPy": + exe = os.path.join(venv_path, 'bin', 'python.exe') + else: + exe = os.path.join(venv_path, 'Scripts', 'python.exe') else: exe = os.path.join(venv_path, 'bin', 'python') subprocess.check_call([ @@ -1350,6 +1363,7 @@ def test_cover_conftest(testdir): result.stdout.fnmatch_lines([CONF_RESULT]) +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cover_looponfail(testdir, monkeypatch): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) @@ -1369,6 +1383,7 @@ def test_cover_looponfail(testdir, monkeypatch): ) +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cover_conftest_dist(testdir): testdir.makepyfile(mod=MODULE) testdir.makeconftest(CONFTEST) @@ -1458,6 +1473,7 @@ def test_coveragerc(testdir): result.stdout.fnmatch_lines(['test_coveragerc* %s' % EXCLUDED_RESULT]) +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_coveragerc_dist(testdir): testdir.makefile('', coveragerc=COVERAGERC) script = testdir.makepyfile(EXCLUDED_TEST) @@ -1646,6 +1662,7 @@ def test_external_data_file(testdir): assert glob.glob(str(testdir.tmpdir.join('some/special/place/coverage-data*'))) +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_external_data_file_xdist(testdir): script = testdir.makepyfile(SCRIPT) testdir.tmpdir.join('.coveragerc').write(""" @@ -1769,6 +1786,7 @@ def test_double_cov2(testdir): assert result.ret == 0 +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_cov_and_no_cov(testdir): script = testdir.makepyfile(SCRIPT_SIMPLE) result = testdir.runpytest('-v', From c32c1581055289ed6cf288efd65e867fd240faf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 18 Feb 2019 13:20:50 +0200 Subject: [PATCH 17/34] Correct some assertions. Revert bogus change. --- tests/test_pytest_cov.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index fea60198..25d3d9bd 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -963,7 +963,7 @@ def test_run_target(): result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'test_multiprocessing_pool* 8 * 100%*', + 'test_multiprocessing_pool* 10 * 100%*', '*1 passed*' ]) assert result.ret == 0 @@ -999,7 +999,7 @@ def test_run_target(): result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'test_multiprocessing_pool* 8 * 100%*', + 'test_multiprocessing_pool* 10 * 100%*', '*1 passed*' ]) assert result.ret == 0 @@ -1032,7 +1032,7 @@ def test_run_target(): result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'test_multiprocessing_subprocess* 8 * 100%*', + 'test_multiprocessing_process* 8 * 100%*', '*1 passed*' ]) assert result.ret == 0 @@ -1062,7 +1062,7 @@ def test_run_target(): result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'test_multiprocessing_subprocess* 8 * 100%*', + 'test_multiprocessing_process* 8 * 100%*', '*1 passed*' ]) assert result.ret == 0 @@ -1101,7 +1101,7 @@ def test_run_target(): result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'test_multiprocessing_subprocess* 16 * 100%*', + 'test_multiprocessing_process* 16 * 100%*', '*1 passed*' ]) assert result.ret == 0 @@ -1209,16 +1209,9 @@ def test_cleanup_on_sigterm_sig_dfl(testdir, setup): import os, signal, subprocess, sys, time def test_run(): - if sys.platform == 'win32': - options = {'creationflags': subprocess.CREATE_NEW_PROCESS_GROUP, 'shell': True} - signum = signal.CTRL_BREAK_EVENT - else: - options = {} - signum = signal.SIGTERM - - proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **options) + proc = subprocess.Popen([sys.executable, __file__], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) time.sleep(1) - proc.send_signal(signum) + proc.terminate() stdout, stderr = proc.communicate() assert not stderr assert stdout == b"" From ab9d7bcce92648614e12db92ef89dec023e81b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 18 Feb 2019 13:57:48 +0200 Subject: [PATCH 18/34] Run this fewer times. Maybe travis too slow for such heavy test. --- tests/test_pytest_cov.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 25d3d9bd..a18d560a 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -914,7 +914,7 @@ def target_fn(a): return a + 1 def test_run_target(): - for i in range(100): + for i in range(20): with multiprocessing.Pool(10) as p: p.map(target_fn, range(10)) p.join() @@ -947,7 +947,7 @@ def target_fn(a): return a + 1 def test_run_target(): - for i in range(100): + for i in range(20): p = multiprocessing.Pool(10) try: p.map(target_fn, range(10)) @@ -983,7 +983,7 @@ def target_fn(a): return a + 1 def test_run_target(): - for i in range(100): + for i in range(20): p = multiprocessing.Pool(10) try: p.map(target_fn, range(10)) From cd0c4a45fad3873c708d247a946463be5c00f29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 21 Feb 2019 19:15:22 +0200 Subject: [PATCH 19/34] Rework a bit the mp pool integration tests to generate a line of code for each iteration (more precise). Also spawns a bit less workers. --- tests/test_pytest_cov.py | 70 ++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index a18d560a..8b8e5019 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -905,107 +905,115 @@ def test_funcarg_not_active(testdir): @pytest.mark.skipif("sys.version_info[0] < 3", reason="no context manager api on Python 2") @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_pool(testdir): - py.test.importorskip('multiprocessing.util') + pytest.importorskip('multiprocessing.util') script = testdir.makepyfile(''' import multiprocessing def target_fn(a): - return a + 1 + %sse: # pragma: nocover + return None def test_run_target(): - for i in range(20): - with multiprocessing.Pool(10) as p: - p.map(target_fn, range(10)) + for i in range(33): + with multiprocessing.Pool(3) as p: + p.map(target_fn, [i * 3 + j for j in range(3)]) p.join() -''') +''' % ''.join('''if a == %r: + return a + el''' % i for i in range(99))) result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=term-missing', script) + 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.*") result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'test_multiprocessing_pool* 8 * 100%*', + 'test_multiprocessing_pool* 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.*") @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_pool_terminate(testdir): - py.test.importorskip('multiprocessing.util') + pytest.importorskip('multiprocessing.util') script = testdir.makepyfile(''' import multiprocessing def target_fn(a): - return a + 1 + %sse: # pragma: nocover + return None def test_run_target(): - for i in range(20): - p = multiprocessing.Pool(10) + for i in range(33): + p = multiprocessing.Pool(3) try: - p.map(target_fn, range(10)) + p.map(target_fn, [i * 3 + j for j in range(3)]) finally: p.terminate() p.join() -''') +''' % ''.join('''if a == %r: + return a + el''' % i for i in range(99))) result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=term-missing', script) + 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.*") result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'test_multiprocessing_pool* 10 * 100%*', + 'test_multiprocessing_pool* 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.*") @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_pool_close(testdir): - py.test.importorskip('multiprocessing.util') + pytest.importorskip('multiprocessing.util') script = testdir.makepyfile(''' import multiprocessing def target_fn(a): - return a + 1 + %sse: # pragma: nocover + return None def test_run_target(): - for i in range(20): - p = multiprocessing.Pool(10) + for i in range(33): + p = multiprocessing.Pool(3) try: - p.map(target_fn, range(10)) + p.map(target_fn, [i * 3 + j for j in range(3)]) finally: p.close() p.join() -''') +''' % ''.join('''if a == %r: + return a + el''' % i for i in range(99))) result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), '--cov-report=term-missing', script) - + 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.*") result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'test_multiprocessing_pool* 10 * 100%*', + 'test_multiprocessing_pool* 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.*") @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") From 3d3b48800e45b8ade91acb4d483a3d17a7eace0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Thu, 21 Feb 2019 19:16:39 +0200 Subject: [PATCH 20/34] Some cleanup. --- tests/test_pytest_cov.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 8b8e5019..46adb89e 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1018,7 +1018,7 @@ def test_run_target(): @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_process(testdir): - py.test.importorskip('multiprocessing.util') + pytest.importorskip('multiprocessing.util') script = testdir.makepyfile(''' import multiprocessing @@ -1048,7 +1048,7 @@ def test_run_target(): @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_process_no_source(testdir): - py.test.importorskip('multiprocessing.util') + pytest.importorskip('multiprocessing.util') script = testdir.makepyfile(''' import multiprocessing @@ -1078,7 +1078,7 @@ def test_run_target(): @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") def test_multiprocessing_process_with_terminate(testdir): - py.test.importorskip('multiprocessing.util') + pytest.importorskip('multiprocessing.util') script = testdir.makepyfile(''' import multiprocessing @@ -1411,7 +1411,7 @@ def test_no_cover_marker(testdir): @pytest.mark.no_cover def test_basic(): mod.func() - subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) + subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) ''') result = testdir.runpytest('-v', '-ra', '--strict', '--cov=%s' % script.dirpath(), @@ -1430,7 +1430,7 @@ def test_no_cover_fixture(testdir): def test_basic(no_cover): mod.func() - subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) + subprocess.check_call([sys.executable, '-c', 'from mod import func; func()']) ''') result = testdir.runpytest('-v', '-ra', '--strict', '--cov=%s' % script.dirpath(), From 2291d7686362007457e3294b6ee2c2c8cfd214cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Fri, 22 Feb 2019 07:25:10 +0200 Subject: [PATCH 21/34] Skip this on windows/pypy (xdist broken). --- tests/test_pytest_cov.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 46adb89e..30ea8b18 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -839,6 +839,7 @@ def test_invalid_coverage_source(testdir): @pytest.mark.skipif("'dev' in pytest.__version__") +@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') def test_dist_missing_data(testdir): venv_path = os.path.join(str(testdir.tmpdir), 'venv') virtualenv.create_environment(venv_path) From 8f1b9e6c4b6e66a7d329323ab6106970ab3420e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Fri, 22 Feb 2019 07:33:45 +0200 Subject: [PATCH 22/34] Extend assertion a bit. --- tests/test_pytest_cov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 30ea8b18..8a626b71 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1182,7 +1182,7 @@ def test_run(): proc.send_signal(signal.CTRL_BREAK_EVENT) stdout, stderr = proc.communicate() assert not stderr - assert stdout in [b"^C", b""] + assert stdout in [b"^C", b"", "captured IOError(4, 'Interrupted function call')\n"] if __name__ == "__main__": from pytest_cov.embed import cleanup_on_signal, cleanup From 478152e1cdc82f4432b4c8d3c279842679b78835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Fri, 22 Feb 2019 08:38:16 +0200 Subject: [PATCH 23/34] Change the docs again to reflect the current implementation. --- docs/subprocess-support.rst | 96 ++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 32 deletions(-) diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 9688059d..6dbd9a75 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -2,19 +2,20 @@ 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. +pytest-cov supports subprocesses and multiprocessing, and works around these atexit limitations. However, there are a +few pitfalls that need to be explained. If you use ``multiprocessing.Pool`` =================================== -You need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit: +In **pytest-cov 2.6** and older a multiprocessing finalizer is automatically registered. The finalizer will only run +reliably if the pool is closed. If you use ``multiprocessing.Pool.terminate`` or the context manager API (``__exit__`` +will just call ``terminate``) then the workers can get SIGTERM and then the finalizers won't run or complete in time. +Thus you need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit: .. code-block:: python @@ -27,18 +28,29 @@ You need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit: 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 + finally: + p.close() # Marks the pool as closed. + p.join() # Waits for workers to exit. + + +In **pytest-cov 2.7** a SIGTERM handler is also automatically registered if multiprocessing is used. Thus you can use +the convenient context manger API: + +.. code-block:: python -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). + from multiprocessing import Pool + + def f(x): + return x*x + + if __name__ == '__main__': + with Pool(5) as p: + print(p.map(f, [1, 2, 3])) If you use ``multiprocessing.Process`` ====================================== -There's an identical issue when using the ``Process`` objects. Don't forget to use ``.join()``: +There's similar issue when using the ``Process`` objects. Don't forget to use ``.join()``: .. code-block:: python @@ -51,32 +63,22 @@ There's an identical issue when using the ``Process`` objects. Don't forget to u p = Process(target=f, args=('bob',)) try: p.start() - finally: # <= THIS IS ESSENTIAL - p.join() # <= THIS IS ESSENTIAL + finally: + p.join() # necessary so that the Process exists before the test suite exits (thus coverage is collected) .. _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 +If you got custom signal handling +================================= - try: - from pytest_cov.embed import cleanup_on_sigterm - except ImportError: - pass - else: - cleanup_on_sigterm() +**pytest-cov 2.6** has a rudimentary ``pytest_cov.embed.cleanup_on_sigterm`` you can use to register a SIGTERM handler +that flushes the coverage data. -If anything else -================ +**pytest-cov 2.7** adds a ``pytest_cov.embed.cleanup_on_signal`` function and changes the implementation to be more +robust: the handler will call the previous handler (if you had previously registered any), and is re-entrant (will +defer extra signals if delivered while the handler runs). -If you have custom signal handling, eg: you do reload on SIGHUP you should have something like this: +For example, if you reload on SIGHUP you should have something like this: .. code-block:: python @@ -98,6 +100,8 @@ Note that both ``cleanup_on_signal`` and ``cleanup_on_sigterm`` will run the pre Alternatively you can do this: +.. code-block:: python + import os import signal @@ -112,3 +116,31 @@ Alternatively you can do this: os.exec( ... ) # or whatever your custom signal would do signal.signal(signal.SIGHUP, restart_service) + +If you use Windows +================== + +On Windows you can register a handler for SIGTERM but it doesn't actually work. However you can have a working handler +for SIGBREAK: + +.. code-block:: python + + import os + import signal + + def shutdown(frame, signum): + # your app's shutdown or whatever + signal.signal(signal.SIGBREAK, shutdown) + + try: + from pytest_cov.embed import cleanup_on_signal + except ImportError: + pass + else: + cleanup_on_signal(signal.SIGBREAK) + +Note that `SIGBREAK is tricky +`_: + +* you need to deliver ``signal.CTRL_BREAK_EVENT`` +* it gets delivered to the whole process group, and that can have unforeseen consequences From 7aa50d9c038d59fde5bf2d27823047e9175a77f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 23 Feb 2019 03:47:33 +0200 Subject: [PATCH 24/34] Fix escaping. --- tests/test_pytest_cov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 8a626b71..5dc12f14 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -1182,7 +1182,7 @@ def test_run(): proc.send_signal(signal.CTRL_BREAK_EVENT) stdout, stderr = proc.communicate() assert not stderr - assert stdout in [b"^C", b"", "captured IOError(4, 'Interrupted function call')\n"] + assert stdout in [b"^C", b"", b"captured IOError(4, 'Interrupted function call')\\n"] if __name__ == "__main__": from pytest_cov.embed import cleanup_on_signal, cleanup From 3709127e970adf68036cc27de8da7dac383bbf2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 23 Feb 2019 03:49:18 +0200 Subject: [PATCH 25/34] Use travis_wait (mainly for pypy which often times out). --- .travis.yml | 2 +- ci/templates/.travis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c59bac4c..a21bd91c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -111,7 +111,7 @@ install: fi set +x script: - - tox -v + - travis_wait 30 tox -v after_failure: - more .tox/log/* | cat - more .tox/*/log/* | cat diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index 60674e0d..43f841f6 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -51,7 +51,7 @@ install: fi set +x script: - - tox -v + - travis_wait 30 tox -v after_failure: - more .tox/log/* | cat - more .tox/*/log/* | cat From 35f38f4779709e7e6a23b977a61633af33c72bc6 Mon Sep 17 00:00:00 2001 From: Ionel Cristian M?rie? Date: Sat, 23 Feb 2019 05:52:35 +0200 Subject: [PATCH 26/34] Use travispls instead - travis_wait is so broken ... --- .travis.yml | 4 ++-- ci/templates/.travis.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a21bd91c..f42d4c1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -84,7 +84,7 @@ before_install: - uname -a - lsb_release -a install: - - pip install tox + - pip install tox travispls - virtualenv --version - easy_install --version - pip --version @@ -111,7 +111,7 @@ install: fi set +x script: - - travis_wait 30 tox -v + - travis-pls tox -v after_failure: - more .tox/log/* | cat - more .tox/*/log/* | cat diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index 43f841f6..b5866c5b 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -24,7 +24,7 @@ before_install: - uname -a - lsb_release -a install: - - pip install tox + - pip install tox travispls - virtualenv --version - easy_install --version - pip --version @@ -51,7 +51,7 @@ install: fi set +x script: - - travis_wait 30 tox -v + - travis-pls tox -v after_failure: - more .tox/log/* | cat - more .tox/*/log/* | cat From bcdce59da7d2adba21c2351f919ae2068e16d591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 23 Feb 2019 06:53:58 +0200 Subject: [PATCH 27/34] Don't run pypy3 on Windows. --- appveyor.yml | 2 +- ci/bootstrap.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 9018b78c..63af1ac4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,7 +8,7 @@ environment: - TOXENV: 'py27-t310-c45,py27-t40-c45,py27-t41-c45' - TOXENV: 'py34-t310-c45,py34-t40-c45,py34-t41-c45' - TOXENV: 'py35-t310-c45,py35-t40-c45,py35-t41-c45' - - TOXENV: 'pypy-t310-c45,pypy-t40-c45,pypy-t41-c45,pypy3-t310-c45,pypy3-t40-c45,pypy3-t41-c45' + - TOXENV: 'pypy-t310-c45,pypy-t40-c45,pypy-t41-c45' init: - ps: echo $env:TOXENV diff --git a/ci/bootstrap.py b/ci/bootstrap.py index 885f20bc..d380392f 100755 --- a/ci/bootstrap.py +++ b/ci/bootstrap.py @@ -52,7 +52,7 @@ template_vars = {'tox_environments': tox_environments} for py_ver in '27 34 35 py'.split(): - template_vars['py%s_environments' % py_ver] = [x for x in tox_environments if x.startswith('py' + py_ver)] + template_vars['py%s_environments' % py_ver] = [x for x in tox_environments if x.startswith('py' + py_ver + '-')] for name in os.listdir(join("ci", "templates")): with open(join(base_path, name), "w") as fh: From c11fe04b1fdb92dc8d8a82fb222eabe06ee110ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 23 Feb 2019 16:54:48 +0200 Subject: [PATCH 28/34] Skip the terminate tests on PyPy and remove travispls (doesn't work on Python2). --- .travis.yml | 4 ++-- ci/templates/.travis.yml | 4 ++-- tests/test_pytest_cov.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f42d4c1c..c59bac4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -84,7 +84,7 @@ before_install: - uname -a - lsb_release -a install: - - pip install tox travispls + - pip install tox - virtualenv --version - easy_install --version - pip --version @@ -111,7 +111,7 @@ install: fi set +x script: - - travis-pls tox -v + - tox -v after_failure: - more .tox/log/* | cat - more .tox/*/log/* | cat diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml index b5866c5b..60674e0d 100644 --- a/ci/templates/.travis.yml +++ b/ci/templates/.travis.yml @@ -24,7 +24,7 @@ before_install: - uname -a - lsb_release -a install: - - pip install tox travispls + - pip install tox - virtualenv --version - easy_install --version - pip --version @@ -51,7 +51,7 @@ install: fi set +x script: - - travis-pls tox -v + - tox -v after_failure: - more .tox/log/* | cat - more .tox/*/log/* | cat diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 5dc12f14..0f4d72bd 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -905,6 +905,7 @@ def test_funcarg_not_active(testdir): @pytest.mark.skipif("sys.version_info[0] < 3", reason="no context manager api on Python 2") @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +@pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason="often deadlocks on PyPy") def test_multiprocessing_pool(testdir): pytest.importorskip('multiprocessing.util') @@ -941,6 +942,7 @@ def test_run_target(): @pytest.mark.skipif('sys.platform == "win32"', reason="multiprocessing support is broken on Windows") +@pytest.mark.skipif('platform.python_implementation() == "PyPy"', reason="often deadlocks on PyPy") def test_multiprocessing_pool_terminate(testdir): pytest.importorskip('multiprocessing.util') From 103d1efc3d79f3208c2260c4083d39934a65d6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 25 Feb 2019 15:16:26 +0200 Subject: [PATCH 29/34] Remove the automatic SIGTERM handler install from the afterfork handler and update docs. The reason for this change being that multiprocessing is going to be broken on windows or pypy anyway, so this is best left to the users to deal in their own code. The documentation includes a working pattern to use (close/join). If the users really needs to use terminate then they should have a platform check and compromise between lower coverage measurement on pypy or chance to deadlock (as seen in the travis failures in https://github.com/pytest-dev/pytest-cov/pull/265). --- docs/subprocess-support.rst | 39 +++++++++++++++++++++++++++++-------- src/pytest_cov/embed.py | 1 - tests/test_pytest_cov.py | 6 ++++++ 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/docs/subprocess-support.rst b/docs/subprocess-support.rst index 6dbd9a75..1d45389d 100644 --- a/docs/subprocess-support.rst +++ b/docs/subprocess-support.rst @@ -12,8 +12,11 @@ few pitfalls that need to be explained. If you use ``multiprocessing.Pool`` =================================== -In **pytest-cov 2.6** and older a multiprocessing finalizer is automatically registered. The finalizer will only run -reliably if the pool is closed. If you use ``multiprocessing.Pool.terminate`` or the context manager API (``__exit__`` +**pytest-cov** automatically registers a multiprocessing finalizer. The finalizer will only run reliably if the pool is +closed. Closing the pool basically signals the workers that there will be no more work, and they will eventually exit. +Thus one also needs to call `join` on the pool. + +If you use ``multiprocessing.Pool.terminate`` or the context manager API (``__exit__`` will just call ``terminate``) then the workers can get SIGTERM and then the finalizers won't run or complete in time. Thus you need to make sure your ``multiprocessing.Pool`` gets a nice and clean exit: @@ -33,8 +36,8 @@ Thus you need to make sure your ``multiprocessing.Pool`` gets a nice and clean e p.join() # Waits for workers to exit. -In **pytest-cov 2.7** a SIGTERM handler is also automatically registered if multiprocessing is used. Thus you can use -the convenient context manger API: +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 +register a cleaning SIGTERM handler like so: .. code-block:: python @@ -44,6 +47,13 @@ the convenient context manger API: return x*x if __name__ == '__main__': + try: + from pytest_cov.embed import cleanup_on_sigterm + except ImportError: + pass + else: + cleanup_on_sigterm() + with Pool(5) as p: print(p.map(f, [1, 2, 3])) @@ -60,6 +70,13 @@ There's similar issue when using the ``Process`` objects. Don't forget to use `` print('hello', name) if __name__ == '__main__': + try: + from pytest_cov.embed import cleanup_on_sigterm + except ImportError: + pass + else: + cleanup_on_sigterm() + p = Process(target=f, args=('bob',)) try: p.start() @@ -120,8 +137,14 @@ Alternatively you can do this: If you use Windows ================== -On Windows you can register a handler for SIGTERM but it doesn't actually work. However you can have a working handler -for SIGBREAK: +On Windows you can register a handler for SIGTERM but it doesn't actually work. It will work if you +`os.kill(os.getpid(), signal.SIGTERM)` (send SIGTERM to the current process) but for most intents and purposes that's +completely useless. + +Consequently this means that if you use multiprocessing you got no choice but to use the close/join pattern as described +above. Using the context manager API or `terminate` won't work as it relies on SIGTERM. + +However you can have a working handler for SIGBREAK (with some caveats): .. code-block:: python @@ -139,8 +162,8 @@ for SIGBREAK: else: cleanup_on_signal(signal.SIGBREAK) -Note that `SIGBREAK is tricky -`_: +The `caveats `_ being +roughly: * you need to deliver ``signal.CTRL_BREAK_EVENT`` * it gets delivered to the whole process group, and that can have unforeseen consequences diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 0af3b779..0fad9425 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -25,7 +25,6 @@ def multiprocessing_start(_): if cov: _active_cov = cov multiprocessing.util.Finalize(None, cleanup, exitpriority=1000) - cleanup_on_sigterm() try: diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 0f4d72bd..0e196bd1 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -917,6 +917,9 @@ def target_fn(a): return None def test_run_target(): + from pytest_cov.embed import cleanup_on_sigterm + cleanup_on_sigterm() + for i in range(33): with multiprocessing.Pool(3) as p: p.map(target_fn, [i * 3 + j for j in range(3)]) @@ -954,6 +957,9 @@ def target_fn(a): return None def test_run_target(): + from pytest_cov.embed import cleanup_on_sigterm + cleanup_on_sigterm() + for i in range(33): p = multiprocessing.Pool(3) try: From 66d8ade51280249483239639907a071645282af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 25 Feb 2019 15:26:23 +0200 Subject: [PATCH 30/34] Avoid having stray tracers around. This fixes an "AssertionError: Expected current collector to be , but it's " error (caused by the embed.cleanup running way too late). --- src/pytest_cov/embed.py | 2 ++ src/pytest_cov/engine.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 0fad9425..4e0f4bfd 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -46,6 +46,8 @@ def init(): cov_branch = True if os.environ.get('COV_CORE_BRANCH') == 'enabled' else None if cov_datafile: + if _active_cov: + cleanup() # Import what we need to activate coverage. import coverage diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index abfb7452..afb98c53 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -8,6 +8,7 @@ import coverage from coverage.data import CoverageData +from .embed import cleanup from .compat import StringIO @@ -145,7 +146,8 @@ class Central(CovController): """Implementation for centralised operation.""" def start(self): - """Erase any previous coverage data and start coverage.""" + cleanup() + self.cov = coverage.Coverage(source=self.cov_source, branch=self.cov_branch, config_file=self.cov_config) @@ -153,6 +155,8 @@ def start(self): branch=self.cov_branch, data_file=os.path.abspath(self.cov.config.data_file), config_file=self.cov_config) + + # Erase or load any previous coverage data and start coverage. if self.cov_append: self.cov.load() else: @@ -180,8 +184,9 @@ class DistMaster(CovController): """Implementation for distributed master.""" def start(self): - """Ensure coverage rc file rsynced if appropriate.""" + cleanup() + # Ensure coverage rc file rsynced if appropriate. if self.cov_config and os.path.exists(self.cov_config): self.config.option.rsyncdir.append(self.cov_config) @@ -258,7 +263,7 @@ class DistSlave(CovController): """Implementation for distributed slaves.""" def start(self): - """Determine what data file and suffix to contribute to and start coverage.""" + cleanup() # Determine whether we are collocated with master. self.is_collocated = (socket.gethostname() == self.config.slaveinput['cov_master_host'] and From 65959fc0b1484f958e0038cf6b0c82b5456a9da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 25 Feb 2019 15:27:05 +0200 Subject: [PATCH 31/34] Avoid writing bogus data files from dead coverage tracers. --- src/pytest_cov/embed.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 4e0f4bfd..60ebdf03 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -13,6 +13,7 @@ that code coverage is being collected we activate coverage based on info passed via env vars. """ +import atexit import os import signal @@ -79,6 +80,11 @@ def _cleanup(cov): if cov is not None: cov.stop() cov.save() + cov._auto_save = False # prevent autosaving from cov._atexit in case the interpreter lacks atexit.unregister + try: + atexit.unregister(cov._atexit) + except Exception: + pass def cleanup(): From fdc43ec67ede1d915cdba56e7b5a1438e421463b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Mon, 25 Feb 2019 15:29:08 +0200 Subject: [PATCH 32/34] Allow COV_CORE_SOURCE to be empty (it'd be converted to None). Also update docs regarding using pytest-cov with other pytest plugins. --- docs/plugins.rst | 4 ++-- src/pytest_cov/embed.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index 12269ced..c152dec4 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -14,9 +14,9 @@ Alternatively you can have this in ``tox.ini`` (if you're using `Tox Date: Mon, 25 Feb 2019 17:06:23 +0200 Subject: [PATCH 33/34] Fix cleanup leaving unusable state. --- src/pytest_cov/embed.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pytest_cov/embed.py b/src/pytest_cov/embed.py index 74ac2093..025622f7 100644 --- a/src/pytest_cov/embed.py +++ b/src/pytest_cov/embed.py @@ -95,6 +95,7 @@ def cleanup(): _cleanup_in_progress = True _cleanup(_active_cov) _active_cov = None + _cleanup_in_progress = False if _pending_signal: _signal_cleanup_handler(*_pending_signal) _pending_signal = None From 42f03074c24887875ba13b6cb58d9ededee50c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Sat, 9 Mar 2019 16:23:27 +0200 Subject: [PATCH 34/34] Always skip this on PyPy as it sometimes fail with `error: release unlocked lock` and the goal of this test was rather to assert that combining is done in the right place not that xdist works well on pypy. The traceback: /home/travis/pypy2-v6.0.0-linux64/lib-python/2.7/logging/__init__.py:846: in __init__ Handler.__init__(self) /home/travis/pypy2-v6.0.0-linux64/lib-python/2.7/logging/__init__.py:688: in __init__ _addHandlerRef(self) /home/travis/pypy2-v6.0.0-linux64/lib-python/2.7/logging/__init__.py:667: in _addHandlerRef _releaseLock() /home/travis/pypy2-v6.0.0-linux64/lib-python/2.7/logging/__init__.py:232: in _releaseLock _lock.release() --- tests/test_pytest_cov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 0e196bd1..6e21c300 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -549,7 +549,7 @@ def test_fail(p): ]) -@pytest.mark.skipif('sys.platform == "win32" and platform.python_implementation() == "PyPy"') +@pytest.mark.skipif('sys.platform == "win32" or platform.python_implementation() == "PyPy"') def test_dist_combine_racecondition(testdir): script = testdir.makepyfile(""" import pytest