diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2016-07-04 08:20:11 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2016-07-04 08:20:11 -0400 |
commit | 98539b47a2790255f633260bcd60243dc7b6647c (patch) | |
tree | bb1b2bf11f0a547fda96c8cfb386da953ae7238c | |
parent | 1fc4c9681034a2123ce27fa336d16df5dc1cee9c (diff) | |
download | python-coveragepy-git-98539b47a2790255f633260bcd60243dc7b6647c.tar.gz |
Let the concurrency option be multi-valued. #484
-rw-r--r-- | CHANGES.rst | 12 | ||||
-rw-r--r-- | coverage/cmdline.py | 1 | ||||
-rw-r--r-- | coverage/collector.py | 29 | ||||
-rw-r--r-- | coverage/config.py | 4 | ||||
-rw-r--r-- | coverage/control.py | 14 | ||||
-rw-r--r-- | doc/cmd.rst | 4 | ||||
-rw-r--r-- | doc/config.rst | 6 | ||||
-rw-r--r-- | tests/test_cmdline.py | 8 | ||||
-rw-r--r-- | tests/test_concurrency.py | 92 | ||||
-rw-r--r-- | tests/test_config.py | 5 |
10 files changed, 125 insertions, 50 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index fcb61a9f..470b17b0 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 395e2c44..7b76f590 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 5668877b..3e28b3b1 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 f7b7eb63..23ec2328 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 9bd0def1..41239be0 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 90e21057..27c83aac 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 7c1aeb2c..b149f4f6 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 795a01fb..d72fd83c 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 4bbc3b90..031d7fd1 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 2f32c525..cf8a6a7f 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"]) |