summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2016-07-04 08:20:11 -0400
committerNed Batchelder <ned@nedbatchelder.com>2016-07-04 08:20:11 -0400
commite7fdd272fe8780230ef3aee9910324a932ab1d43 (patch)
treec7898b5f8e220598b962ed04f5d56ba3f08b4f8b
parentffe481b845ef14289f6920cd2a5b7928e6c78d6a (diff)
downloadpython-coveragepy-e7fdd272fe8780230ef3aee9910324a932ab1d43.tar.gz
Let the concurrency option be multi-valued. #484
-rw-r--r--CHANGES.rst12
-rw-r--r--coverage/cmdline.py1
-rw-r--r--coverage/collector.py29
-rw-r--r--coverage/config.py4
-rw-r--r--coverage/control.py14
-rw-r--r--doc/cmd.rst4
-rw-r--r--doc/config.rst6
-rw-r--r--tests/test_cmdline.py8
-rw-r--r--tests/test_concurrency.py92
-rw-r--r--tests/test_config.py5
10 files changed, 125 insertions, 50 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index fcb61a9..470b17b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -13,8 +13,15 @@ Unreleased
combining. This caused confusing results, and extra tox "clean" steps. If
you want the old behavior, use the new ``coverage combine --append`` option.
-- Using ``--concurrency=multiprocessing`` now implies ``--parallel`` so that
- the main program is measured similarly to the sub-processes.
+- The ``concurrency`` option can now take multiple values, to support programs
+ using multiprocessing and another library such as eventlet. This is only
+ possible in the configuration file, not from the command line. The
+ configuration file is the only way for sub-processes to all run with the same
+ options. Fixes `issue 484`_. Thanks to Josh Williams for prototyping.
+
+- Using a ``concurrency`` setting of ``multiprocessing`` now implies
+ ``--parallel`` so that the main program is measured similarly to the
+ sub-processes.
- When using `automatic subprocess measurement`_, running coverage commands
would create spurious data files. This is now fixed, thanks to diagnosis and
@@ -52,6 +59,7 @@ Unreleased
.. _issue 396: https://bitbucket.org/ned/coveragepy/issues/396/coverage-xml-shouldnt-bail-out-on-parse
.. _issue 454: https://bitbucket.org/ned/coveragepy/issues/454/coverage-debug-config-should-be
.. _issue 478: https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running
+.. _issue 484: https://bitbucket.org/ned/coveragepy/issues/484/multiprocessing-greenlet-concurrency
.. _issue 492: https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of
.. _unittest-mixins: https://pypi.python.org/pypi/unittest-mixins
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 395e2c4..7b76f59 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -526,7 +526,6 @@ class CoverageScript(object):
self.coverage.set_option("report:fail_under", options.fail_under)
if self.coverage.get_option("report:fail_under"):
-
# Total needs to be rounded, but be careful of 0 and 100.
if 0 < total < 1:
total = 1
diff --git a/coverage/collector.py b/coverage/collector.py
index 5668877..3e28b3b 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -65,6 +65,9 @@ class Collector(object):
# the top, and resumed when they become the top again.
_collectors = []
+ # The concurrency settings we support here.
+ SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"])
+
def __init__(self, should_trace, check_include, timid, branch, warn, concurrency):
"""Create a collector.
@@ -86,9 +89,10 @@ class Collector(object):
`warn` is a warning function, taking a single string message argument,
to be used if a warning needs to be issued.
- `concurrency` is a string indicating the concurrency library in use.
- Valid values are "greenlet", "eventlet", "gevent", or "thread" (the
- default).
+ `concurrency` is a list of strings indicating the concurrency libraries
+ in use. Valid values are "greenlet", "eventlet", "gevent", or "thread"
+ (the default). Of these four values, only one can be supplied. Other
+ values are ignored.
"""
self.should_trace = should_trace
@@ -96,21 +100,26 @@ class Collector(object):
self.warn = warn
self.branch = branch
self.threading = None
- self.concurrency = concurrency
self.concur_id_func = None
+ # We can handle a few concurrency options here, but only one at a time.
+ these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency)
+ if len(these_concurrencies) > 1:
+ raise CoverageException("Conflicting concurrency settings: %s" % concurrency)
+ self.concurrency = these_concurrencies.pop() if these_concurrencies else ''
+
try:
- if concurrency == "greenlet":
+ if self.concurrency == "greenlet":
import greenlet
self.concur_id_func = greenlet.getcurrent
- elif concurrency == "eventlet":
+ elif self.concurrency == "eventlet":
import eventlet.greenthread # pylint: disable=import-error,useless-suppression
self.concur_id_func = eventlet.greenthread.getcurrent
- elif concurrency == "gevent":
+ elif self.concurrency == "gevent":
import gevent # pylint: disable=import-error,useless-suppression
self.concur_id_func = gevent.getcurrent
- elif concurrency == "thread" or not concurrency:
+ elif self.concurrency == "thread" or not self.concurrency:
# It's important to import threading only if we need it. If
# it's imported early, and the program being measured uses
# gevent, then gevent's monkey-patching won't work properly.
@@ -120,7 +129,9 @@ class Collector(object):
raise CoverageException("Don't understand concurrency=%s" % concurrency)
except ImportError:
raise CoverageException(
- "Couldn't trace with concurrency=%s, the module isn't installed." % concurrency
+ "Couldn't trace with concurrency=%s, the module isn't installed." % (
+ self.concurrency,
+ )
)
# Who-Tests-What is just a hack at the moment, so turn it on with an
diff --git a/coverage/config.py b/coverage/config.py
index f7b7eb6..23ec232 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -191,7 +191,7 @@ class CoverageConfig(object):
# Options for plugins
self.plugin_options = {}
- MUST_BE_LIST = ["omit", "include", "debug", "plugins"]
+ MUST_BE_LIST = ["omit", "include", "debug", "plugins", "concurrency"]
def from_args(self, **kwargs):
"""Read config values from `kwargs`."""
@@ -267,7 +267,7 @@ class CoverageConfig(object):
# [run]
('branch', 'run:branch', 'boolean'),
- ('concurrency', 'run:concurrency'),
+ ('concurrency', 'run:concurrency', 'list'),
('cover_pylib', 'run:cover_pylib', 'boolean'),
('data_file', 'run:data_file'),
('debug', 'run:debug', 'list'),
diff --git a/coverage/control.py b/coverage/control.py
index 9bd0def..41239be 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -110,12 +110,16 @@ class Coverage(object):
`concurrency` is a string indicating the concurrency library being used
in the measured code. Without this, coverage.py will get incorrect
- results. Valid strings are "greenlet", "eventlet", "gevent",
- "multiprocessing", or "thread" (the default).
+ results if these libraries are in use. Valid strings are "greenlet",
+ "eventlet", "gevent", "multiprocessing", or "thread" (the default).
+ This can also be a list of these strings.
.. versionadded:: 4.0
The `concurrency` parameter.
+ .. versionadded:: 4.2
+ The `concurrency` parameter can now be a list of strings.
+
"""
# Build our configuration from a number of sources:
# 1: defaults:
@@ -244,10 +248,10 @@ class Coverage(object):
self.omit = prep_patterns(self.config.omit)
self.include = prep_patterns(self.config.include)
- concurrency = self.config.concurrency
- if concurrency == "multiprocessing":
+ concurrency = self.config.concurrency or []
+ if "multiprocessing" in concurrency:
patch_multiprocessing()
- concurrency = None
+ #concurrency = None
# Multi-processing uses parallel for the subprocesses, so also use
# it for the main process.
self.config.parallel = True
diff --git a/doc/cmd.rst b/doc/cmd.rst
index 90e2105..27c83aa 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -108,6 +108,10 @@ Give it a value of ``multiprocessing``, ``thread``, ``greenlet``, ``eventlet``,
or ``gevent``. Values other than ``thread`` require the :ref:`C extension
<install_extension>`.
+If you are using ``--concurrency=multiprocessing``, you must set other options
+in the configuration file. Other options on the command line will not be
+passed to the other processes.
+
.. _multiprocessing: https://docs.python.org/2/library/multiprocessing.html
.. _greenlet: http://greenlet.readthedocs.org/en/latest/
.. _gevent: http://www.gevent.org/
diff --git a/doc/config.rst b/doc/config.rst
index 7c1aeb2..b149f4f 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -107,8 +107,8 @@ to more than one command.
``cover_pylib`` (boolean, default False): whether to measure the Python
standard library.
-``concurrency`` (string, default "thread"): the name of the concurrency library
-in use by the product code. If your program uses `multiprocessing`_,
+``concurrency`` (multi-string, default "thread"): the name concurrency
+libraries in use by the product code. If your program uses `multiprocessing`_,
`gevent`_, `greenlet`_, or `eventlet`_, you must name that library in this
option, or coverage.py will produce very wrong results.
@@ -117,6 +117,8 @@ option, or coverage.py will produce very wrong results.
.. _gevent: http://www.gevent.org/
.. _eventlet: http://eventlet.net/
+Before version 4.2, this option only accepted a single string.
+
.. versionadded:: 4.0
``data_file`` (string, default ".coverage"): the name of the data file to use
diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py
index 795a01f..d72fd83 100644
--- a/tests/test_cmdline.py
+++ b/tests/test_cmdline.py
@@ -456,6 +456,14 @@ class CmdLineTest(BaseCmdLineTest):
out = self.stdout()
self.assertIn("option --concurrency: invalid choice: 'nothing'", out)
+ def test_no_multiple_concurrency(self):
+ # You can't use multiple concurrency values on the command line.
+ # I would like to have a better message about not allowing multiple
+ # values for this option, but optparse is not that flexible.
+ self.command_line("run --concurrency=multiprocessing,gevent foo.py", ret=ERR)
+ out = self.stdout()
+ self.assertIn("option --concurrency: invalid choice: 'multiprocessing,gevent'", out)
+
def test_run_debug(self):
self.cmd_executes("run --debug=opt1 foo.py", """\
.coverage(debug=["opt1"])
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
index 4bbc3b9..031d7fd 100644
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -159,6 +159,31 @@ SIMPLE = """
"""
+def cant_trace_msg(concurrency, the_module):
+ """What might coverage.py say about a concurrency setting and imported module?"""
+ # In the concurrency choices, "multiprocessing" doesn't count, so remove it.
+ if "multiprocessing" in concurrency:
+ parts = concurrency.split(",")
+ parts.remove("multiprocessing")
+ concurrency = ",".join(parts)
+
+ if the_module is None:
+ # We don't even have the underlying module installed, we expect
+ # coverage to alert us to this fact.
+ expected_out = (
+ "Couldn't trace with concurrency=%s, "
+ "the module isn't installed.\n" % concurrency
+ )
+ elif env.C_TRACER or concurrency == "thread" or concurrency == "":
+ expected_out = None
+ else:
+ expected_out = (
+ "Can't support concurrency=%s with PyTracer, "
+ "only threads are supported\n" % concurrency
+ )
+ return expected_out
+
+
class ConcurrencyTest(CoverageTest):
"""Tests of the concurrency support in coverage.py."""
@@ -179,15 +204,11 @@ class ConcurrencyTest(CoverageTest):
cmd = "coverage run --concurrency=%s try_it.py" % concurrency
out = self.run_command(cmd)
- if not the_module:
- # We don't even have the underlying module installed, we expect
- # coverage to alert us to this fact.
- expected_out = (
- "Couldn't trace with concurrency=%s, "
- "the module isn't installed.\n" % concurrency
- )
- self.assertEqual(out, expected_out)
- elif env.C_TRACER or concurrency == "thread":
+ expected_cant_trace = cant_trace_msg(concurrency, the_module)
+
+ if expected_cant_trace is not None:
+ self.assertEqual(out, expected_cant_trace)
+ else:
# We can fully measure the code if we are using the C tracer, which
# can support all the concurrency, or if we are using threads.
if expected_out is None:
@@ -208,12 +229,6 @@ class ConcurrencyTest(CoverageTest):
lines = line_count(code)
self.assertEqual(data.line_counts()['try_it.py'], lines)
- else:
- expected_out = (
- "Can't support concurrency=%s with PyTracer, "
- "only threads are supported\n" % concurrency
- )
- self.assertEqual(out, expected_out)
def test_threads(self):
code = (THREAD + SUM_THEM_Q + PRINT_SUM_THEM).format(QLIMIT=self.QLIMIT)
@@ -290,6 +305,11 @@ SQUARE_OR_CUBE_WORK = """
return y
"""
+SUM_THEM_WORK = """
+ def work(x):
+ return sum_them((x+1)*100)
+ """
+
MULTI_CODE = """
# Above this will be a defintion of work().
import multiprocessing
@@ -325,9 +345,15 @@ MULTI_CODE = """
class MultiprocessingTest(CoverageTest):
"""Test support of the multiprocessing module."""
- def try_multiprocessing_code(self, code, expected_out):
+ def try_multiprocessing_code(
+ self, code, expected_out, the_module, concurrency="multiprocessing"
+ ):
"""Run code using multiprocessing, it should produce `expected_out`."""
self.make_file("multi.py", code)
+ self.make_file(".coveragerc", """\
+ [run]
+ concurrency = %s
+ """ % concurrency)
if env.PYVERSION >= (3, 4):
start_methods = ['fork', 'spawn']
@@ -338,23 +364,35 @@ class MultiprocessingTest(CoverageTest):
if start_method and start_method not in multiprocessing.get_all_start_methods():
continue
- out = self.run_command(
- "coverage run --concurrency=multiprocessing multi.py %s" % start_method
- )
- self.assertEqual(out.rstrip(), expected_out)
+ out = self.run_command("coverage run multi.py %s" % (start_method,))
+ expected_cant_trace = cant_trace_msg(concurrency, the_module)
- self.run_command("coverage combine")
- out = self.run_command("coverage report -m")
+ if expected_cant_trace is not None:
+ self.assertEqual(out, expected_cant_trace)
+ else:
+ self.assertEqual(out.rstrip(), expected_out)
- last_line = self.squeezed_lines(out)[-1]
- expected_report = "multi.py {lines} 0 100%".format(lines=line_count(code))
- self.assertEqual(last_line, expected_report)
+ self.run_command("coverage combine")
+ out = self.run_command("coverage report -m")
+
+ last_line = self.squeezed_lines(out)[-1]
+ expected_report = "multi.py {lines} 0 100%".format(lines=line_count(code))
+ self.assertEqual(last_line, expected_report)
def test_multiprocessing(self):
nprocs = 3
upto = 30
code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
total = sum(x*x if x%2 else x*x*x for x in range(upto))
- expected = "{nprocs} pids, total = {total}".format(nprocs=nprocs, total=total)
+ expected_out = "{nprocs} pids, total = {total}".format(nprocs=nprocs, total=total)
+ self.try_multiprocessing_code(code, expected_out, threading)
- self.try_multiprocessing_code(code, expected)
+ def test_multiprocessing_and_gevent(self):
+ nprocs = 3
+ upto = 30
+ code = (SUM_THEM_WORK + EVENTLET + SUM_THEM_Q + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto)
+ total = sum(sum(range((x + 1) * 100)) for x in range(upto))
+ expected_out = "{nprocs} pids, total = {total}".format(nprocs=nprocs, total=total)
+ self.try_multiprocessing_code(
+ code, expected_out, eventlet, concurrency="multiprocessing,eventlet"
+ )
diff --git a/tests/test_config.py b/tests/test_config.py
index 2f32c52..cf8a6a7 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -25,10 +25,11 @@ class ConfigTest(CoverageTest):
def test_arguments(self):
# Arguments to the constructor are applied to the configuration.
- cov = coverage.Coverage(timid=True, data_file="fooey.dat")
+ cov = coverage.Coverage(timid=True, data_file="fooey.dat", concurrency="multiprocessing")
self.assertTrue(cov.config.timid)
self.assertFalse(cov.config.branch)
self.assertEqual(cov.config.data_file, "fooey.dat")
+ self.assertEqual(cov.config.concurrency, ["multiprocessing"])
def test_config_file(self):
# A .coveragerc file will be read into the configuration.
@@ -300,7 +301,7 @@ class ConfigFileTest(CoverageTest):
self.assertTrue(cov.config.branch)
self.assertTrue(cov.config.cover_pylib)
self.assertTrue(cov.config.parallel)
- self.assertEqual(cov.config.concurrency, "thread")
+ self.assertEqual(cov.config.concurrency, ["thread"])
self.assertEqual(cov.config.source, ["myapp"])
self.assertEqual(cov.get_exclude_list(), ["if 0:", r"pragma:?\s+no cover", "another_tab"])