summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2010-01-03 10:23:06 -0500
committerNed Batchelder <ned@nedbatchelder.com>2010-01-03 10:23:06 -0500
commita5bc551f78df166a0d0e272fae7a7b5205b416f7 (patch)
tree652c49d9cf3c401b0e8263764d24d7456f634f89
parent47c3f02dab1a745f595008d006f1474f148a9157 (diff)
downloadpython-coveragepy-a5bc551f78df166a0d0e272fae7a7b5205b416f7.tar.gz
Parallel mode can be set from the .coveragerc file.
-rw-r--r--CHANGES.txt11
-rw-r--r--covcov.ini1
-rw-r--r--coverage/cmdline.py2
-rw-r--r--coverage/config.py3
-rw-r--r--coverage/control.py27
-rw-r--r--coverage/data.py10
-rw-r--r--coverage/misc.py8
-rw-r--r--test/coverage_coverage.py3
-rw-r--r--test/test_api.py2
-rw-r--r--test/test_cmdline.py32
-rw-r--r--test/test_coverage.py58
11 files changed, 122 insertions, 35 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 6bfa891..34c75c8 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -6,7 +6,9 @@ Change history for Coverage.py
Version 3.3
-----------
-- Settings are now read from a .coveragerc file.
+- Settings are now read from a .coveragerc file. The name of the file can be
+ set with the `config_file` argument to the coverage() constructor, or reading
+ a config file can be disabled with `config_file=False`.
- Fixed a problem with nested loops having their branch possibilities
mischaracterized: `issue 39`_.
@@ -14,9 +16,16 @@ Version 3.3
- Added coverage.process_start to enable coverage measurement when Python
starts.
+- Parallel data file names now have a random number appended to them in
+ addition to the machine name and process id.
+
- Parallel data files combined with "coverage combine" are deleted after
they're combined, to clean up unneeded files. Fixes `issue 40`_.
+- The `data_suffix` argument to the coverage constructor is now appended with
+ an added dot rather than simply appended, so that .coveragerc files will not
+ be confused for data files.
+
- Added an AUTHORS.txt file.
.. _issue 39: http://bitbucket.org/ned/coveragepy/issue/39
diff --git a/covcov.ini b/covcov.ini
index 9af570c..9e1733f 100644
--- a/covcov.ini
+++ b/covcov.ini
@@ -2,6 +2,7 @@
[run]
branch = true
data_file = c:\ned\coverage\trunk\.coverage
+parallel = true
[report]
exclude_lines =
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 5f1919d..e82cf27 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -427,7 +427,7 @@ class CoverageScript(object):
# Do something.
self.coverage = self.covpkg.coverage(
- data_suffix = bool(options.parallel_mode),
+ data_suffix = options.parallel_mode,
cover_pylib = options.pylib,
timid = options.timid,
branch = options.branch,
diff --git a/coverage/config.py b/coverage/config.py
index 3248177..b8f114a 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -18,6 +18,7 @@ class CoverageConfig(object):
self.branch = False
self.cover_pylib = False
self.data_file = ".coverage"
+ self.parallel = False
self.timid = False
# Defaults for [report]
@@ -56,6 +57,8 @@ class CoverageConfig(object):
self.cover_pylib = cp.getboolean('run', 'cover_pylib')
if cp.has_option('run', 'data_file'):
self.data_file = cp.get('run', 'data_file')
+ if cp.has_option('run', 'parallel'):
+ self.parallel = cp.getboolean('run', 'parallel')
if cp.has_option('run', 'timid'):
self.timid = cp.getboolean('run', 'timid')
diff --git a/coverage/control.py b/coverage/control.py
index 18fc2aa..0e60fc5 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -10,6 +10,7 @@ from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.files import FileLocator
from coverage.html import HtmlReporter
+from coverage.misc import bool_or_none
from coverage.results import Analysis
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
@@ -29,13 +30,13 @@ class coverage(object):
"""
- def __init__(self, data_file=None, data_suffix=False, cover_pylib=None,
+ def __init__(self, data_file=None, data_suffix=None, cover_pylib=None,
auto_data=False, timid=None, branch=None, config_file=True):
"""
`data_file` is the base name of the data file to use, defaulting to
- ".coverage". `data_suffix` is appended to `data_file` to create the
- final file name. If `data_suffix` is simply True, then a suffix is
- created with the machine and process identity included.
+ ".coverage". `data_suffix` is appended (with a dot) to `data_file` to
+ create the final file name. If `data_suffix` is simply True, then a
+ suffix is created with the machine and process identity included.
`cover_pylib` is a boolean determining whether Python code installed
with the Python interpreter is measured. This includes the Python
@@ -79,7 +80,7 @@ class coverage(object):
# 4: from constructor arguments:
self.config.from_args(
data_file=data_file, cover_pylib=cover_pylib, timid=timid,
- branch=branch
+ branch=branch, parallel=bool_or_none(data_suffix)
)
self.auto_data = auto_data
@@ -96,11 +97,11 @@ class coverage(object):
)
# Create the data file.
- if data_suffix:
+ if data_suffix or self.config.parallel:
if not isinstance(data_suffix, string_class):
# if data_suffix=True, use .machinename.pid.random
- data_suffix = ".%s.%s.%06d" % (
- socket.gethostname(), os.getpid(), random.randint(0,999999)
+ data_suffix = "%s.%s.%06d" % (
+ socket.gethostname(), os.getpid(), random.randint(0, 999999)
)
else:
data_suffix = None
@@ -250,6 +251,14 @@ class coverage(object):
current measurements.
"""
+ # If the .coveragerc file specifies parallel=True, then self.data
+ # already points to a suffixed data file. This won't be right for
+ # combining, so make a new self.data with no suffix.
+ from coverage import __version__
+ self.data = CoverageData(
+ basename=self.config.data_file,
+ collector="coverage v%s" % __version__
+ )
self.data.combine_parallel_data()
def _harvest_data(self):
@@ -412,7 +421,7 @@ def process_startup():
"""
cps = os.environ.get("COVERAGE_PROCESS_START")
if cps:
- cov = coverage(config_file=cps, auto_data=True, data_suffix=True)
+ cov = coverage(config_file=cps, auto_data=True)
if os.environ.get("COVERAGE_COVERAGE"):
# Measuring coverage within coverage.py takes yet more trickery.
cov.cover_prefix = "Please measure coverage.py!"
diff --git a/coverage/data.py b/coverage/data.py
index f9d0edb..9359af1 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -34,7 +34,8 @@ class CoverageData(object):
`suffix` is a suffix to append to the base file name. This can be used
for multiple or parallel execution, so that many coverage data files
- can exist simultaneously.
+ can exist simultaneously. A dot will be used to join the base name and
+ the suffix.
`collector` is a string describing the coverage measurement software.
@@ -47,7 +48,7 @@ class CoverageData(object):
# ever do any file storage.
self.filename = basename or ".coverage"
if suffix:
- self.filename += suffix
+ self.filename += "." + suffix
self.filename = os.path.abspath(self.filename)
# A map from canonical Python source file name to a dictionary in
@@ -168,12 +169,13 @@ class CoverageData(object):
"""Combine a number of data files together.
Treat `self.filename` as a file prefix, and combine the data from all
- of the data files starting with that prefix.
+ of the data files starting with that prefix plus a dot.
"""
data_dir, local = os.path.split(self.filename)
+ localdot = local + '.'
for f in os.listdir(data_dir or '.'):
- if f.startswith(local):
+ if f.startswith(localdot):
full_path = os.path.join(data_dir, f)
new_lines, new_arcs = self._read_file(full_path)
for filename, file_data in new_lines.items():
diff --git a/coverage/misc.py b/coverage/misc.py
index 0e6bcf9..4959efe 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -60,6 +60,14 @@ def expensive(fn):
return _wrapped
+def bool_or_none(b):
+ """Return bool(b), but preserve None."""
+ if b is None:
+ return None
+ else:
+ return bool(b)
+
+
class CoverageException(Exception):
"""An exception specific to Coverage."""
pass
diff --git a/test/coverage_coverage.py b/test/coverage_coverage.py
index 5cfef04..1e1cba0 100644
--- a/test/coverage_coverage.py
+++ b/test/coverage_coverage.py
@@ -23,7 +23,7 @@ def run_tests_with_coverage():
tracer = os.environ.get('COVERAGE_TEST_TRACER', 'c')
version = "%s%s" % sys.version_info[:2]
- suffix = ".%s_%s" % (version, tracer)
+ suffix = "%s_%s" % (version, tracer)
cov = coverage.coverage(config_file="covcov.ini", data_suffix=suffix)
# Cheap trick: the coverage code itself is excluded from measurement, but
@@ -66,7 +66,6 @@ def report_on_combined_files():
cov = coverage.coverage(config_file="covcov.ini")
cov.combine()
cov.save()
-
cov.html_report(directory=HTML_DIR)
diff --git a/test/test_api.py b/test/test_api.py
index 69eb649..1f0ad83 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -238,7 +238,7 @@ class ApiTest(CoverageTest):
""")
self.assertSameElements(os.listdir("."), ["datatest3.py"])
- cov = coverage.coverage(data_file="cov.data", data_suffix=".14")
+ cov = coverage.coverage(data_file="cov.data", data_suffix="14")
cov.start()
self.import_module("datatest3") # pragma: recursive coverage
cov.stop() # pragma: recursive coverage
diff --git a/test/test_cmdline.py b/test/test_cmdline.py
index fda282b..9196873 100644
--- a/test/test_cmdline.py
+++ b/test/test_cmdline.py
@@ -1,6 +1,6 @@
"""Test cmdline.py for coverage."""
-import os, re, shlex, sys, textwrap, unittest
+import os, pprint, re, shlex, sys, textwrap, unittest
import mock
import coverage
@@ -14,7 +14,7 @@ class CmdLineTest(CoverageTest):
run_in_temp_dir = False
INIT_LOAD = """\
- .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True)
+ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True)
.load()\n"""
def model_object(self):
@@ -47,14 +47,24 @@ class CmdLineTest(CoverageTest):
m2 = self.model_object()
code_obj = compile(code, "<code>", "exec")
eval(code_obj, globals(), { 'm2': m2 })
- self.assertEqual(m1.method_calls, m2.method_calls)
+ self.assert_same_method_calls(m1, m2)
def cmd_executes_same(self, args1, args2):
"""Assert that the `args1` executes the same as `args2`."""
m1, r1 = self.mock_command_line(args1)
m2, r2 = self.mock_command_line(args2)
self.assertEqual(r1, r2)
- self.assertEqual(m1.method_calls, m2.method_calls)
+ self.assert_same_method_calls(m1, m2)
+
+ def assert_same_method_calls(self, m1, m2):
+ """Assert that `m1.method_calls` and `m2.method_calls` are the same."""
+ # Use a real equality comparison, but if it fails, use a nicer assert
+ # so we can tell what's going on. We have to use the real == first due
+ # to CmdOptionParser.__eq__
+ if m1.method_calls != m2.method_calls:
+ pp1 = pprint.pformat(m1.method_calls)
+ pp2 = pprint.pformat(m2.method_calls)
+ self.assertMultiLineEqual(pp1+'\n', pp2+'\n')
def cmd_help(self, args, help_msg=None, topic=None, ret=ERR):
"""Run a command line, and check that it prints the right help.
@@ -83,7 +93,7 @@ class ClassicCmdLineTest(CmdLineTest):
def testErase(self):
# coverage -e
self.cmd_executes("-e", """\
- .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True)
+ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True)
.erase()
""")
self.cmd_executes_same("-e", "--erase")
@@ -93,7 +103,7 @@ class ClassicCmdLineTest(CmdLineTest):
# -x calls coverage.load first.
self.cmd_executes("-x foo.py", """\
- .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True)
+ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True)
.load()
.start()
.run_python_file('foo.py', ['foo.py'])
@@ -102,7 +112,7 @@ class ClassicCmdLineTest(CmdLineTest):
""")
# -e -x calls coverage.erase first.
self.cmd_executes("-e -x foo.py", """\
- .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True)
+ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True)
.erase()
.start()
.run_python_file('foo.py', ['foo.py'])
@@ -111,7 +121,7 @@ class ClassicCmdLineTest(CmdLineTest):
""")
# --timid sets a flag, and program arguments get passed through.
self.cmd_executes("-x --timid foo.py abc 123", """\
- .coverage(cover_pylib=None, data_suffix=False, timid=True, branch=None, config_file=True)
+ .coverage(cover_pylib=None, data_suffix=None, timid=True, branch=None, config_file=True)
.load()
.start()
.run_python_file('foo.py', ['foo.py', 'abc', '123'])
@@ -137,7 +147,7 @@ class ClassicCmdLineTest(CmdLineTest):
def testCombine(self):
# coverage -c
self.cmd_executes("-c", """\
- .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True)
+ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True)
.load()
.combine()
.save()
@@ -440,7 +450,7 @@ class NewCmdLineTest(CmdLineTest):
self.cmd_executes_same("run --timid f.py", "-e -x --timid f.py")
self.cmd_executes_same("run", "-x")
self.cmd_executes("run --branch foo.py", """\
- .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=True, config_file=True)
+ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=True, config_file=True)
.erase()
.start()
.run_python_file('foo.py', ['foo.py'])
@@ -448,7 +458,7 @@ class NewCmdLineTest(CmdLineTest):
.save()
""")
self.cmd_executes("run --rcfile=myrc.rc foo.py", """\
- .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file="myrc.rc")
+ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file="myrc.rc")
.erase()
.start()
.run_python_file('foo.py', ['foo.py'])
diff --git a/test/test_coverage.py b/test/test_coverage.py
index 772075e..f899140 100644
--- a/test/test_coverage.py
+++ b/test/test_coverage.py
@@ -1680,6 +1680,14 @@ class ModuleTest(CoverageTest):
class ProcessTest(CoverageTest):
"""Tests of the per-process behavior of coverage.py."""
+ def number_of_data_files(self):
+ """Return the number of coverage data files in this directory."""
+ num = 0
+ for f in os.listdir('.'):
+ if f.startswith('.coverage.') or f == '.coverage':
+ num += 1
+ return num
+
def testSaveOnExit(self):
self.make_file("mycode.py", """\
h = "Hello"
@@ -1725,18 +1733,56 @@ class ProcessTest(CoverageTest):
self.assertFalse(os.path.exists(".coverage"))
# After two -p runs, there should be two .coverage.machine.123 files.
- self.assertEqual(
- len([f for f in os.listdir('.') if f.startswith('.coverage')]),
- 2)
+ self.assertEqual(self.number_of_data_files(), 2)
# Combine the parallel coverage data files into .coverage .
self.run_command("coverage -c")
self.assertTrue(os.path.exists(".coverage"))
# After combining, there should be only the .coverage file.
- self.assertEqual(
- len([f for f in os.listdir('.') if f.startswith('.coverage')]),
- 1)
+ self.assertEqual(self.number_of_data_files(), 1)
+
+ # Read the coverage file and see that b_or_c.py has all 7 lines
+ # executed.
+ data = coverage.CoverageData()
+ data.read_file(".coverage")
+ self.assertEqual(data.summary()['b_or_c.py'], 7)
+
+ def test_combine_with_rc(self):
+ self.make_file("b_or_c.py", """\
+ import sys
+ a = 1
+ if sys.argv[1] == 'b':
+ b = 1
+ else:
+ c = 1
+ d = 1
+ print ('done')
+ """)
+
+ self.make_file(".coveragerc", """\
+ [run]
+ parallel = true
+ """)
+
+ out = self.run_command("coverage run b_or_c.py b")
+ self.assertEqual(out, 'done\n')
+ self.assertFalse(os.path.exists(".coverage"))
+
+ out = self.run_command("coverage run b_or_c.py c")
+ self.assertEqual(out, 'done\n')
+ self.assertFalse(os.path.exists(".coverage"))
+
+ # After two runs, there should be two .coverage.machine.123 files.
+ self.assertEqual(self.number_of_data_files(), 2)
+
+ # Combine the parallel coverage data files into .coverage .
+ self.run_command("coverage combine")
+ self.assertTrue(os.path.exists(".coverage"))
+ self.assertTrue(os.path.exists(".coveragerc"))
+
+ # After combining, there should be only the .coverage file.
+ self.assertEqual(self.number_of_data_files(), 1)
# Read the coverage file and see that b_or_c.py has all 7 lines
# executed.