summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst8
-rw-r--r--coverage/cmdline.py2
-rw-r--r--coverage/control.py12
-rw-r--r--coverage/data.py8
-rw-r--r--tests/test_api.py31
-rw-r--r--tests/test_cmdline.py10
-rw-r--r--tests/test_process.py12
7 files changed, 69 insertions, 14 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 2457973d..cc732cb6 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -13,13 +13,21 @@ Unreleased
carried forward from run to run. Now those files are not read, so each
subprocess only writes its own data. Fixes `issue 510`_.
+- The ``coverage combine`` command will now fail if there are no data files to
+ combine. The combine changes in 4.2 meant that multiple combines could lose
+ data, leaving you with an empty .coverage data file. Fixes issues
+ `issue 412`_, `issue 516`_, and probably `issue 511`_.
+
- Corrected the name of the jquery.ba-throttle-debounce.js library. Thanks,
Ben Finney. Closes `issue 505`_.
- Support PyPy3 5.2 alpha 1.
+.. _issue 412: https://bitbucket.org/ned/coveragepy/issues/412/coverage-combine-should-error-if-no
.. _issue 505: https://bitbucket.org/ned/coveragepy/issues/505/use-canonical-filename-for-debounce
.. _issue 510: https://bitbucket.org/ned/coveragepy/issues/510/erase-still-needed-in-42
+.. _issue 511: https://bitbucket.org/ned/coveragepy/issues/511/version-42-coverage-combine-empties
+.. _issue 516: https://bitbucket.org/ned/coveragepy/issues/516/running-coverage-combine-twice-deletes-all
Version 4.2 --- 2016-07-26
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 09e82323..a83d619e 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -483,7 +483,7 @@ class CoverageScript(object):
if options.append:
self.coverage.load()
data_dirs = args or None
- self.coverage.combine(data_dirs)
+ self.coverage.combine(data_dirs, strict=True)
self.coverage.save()
return OK
diff --git a/coverage/control.py b/coverage/control.py
index cc661967..32fb30c0 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -791,7 +791,7 @@ class Coverage(object):
self.get_data()
self.data_files.write(self.data, suffix=self.data_suffix)
- def combine(self, data_paths=None):
+ def combine(self, data_paths=None, strict=False):
"""Combine together a number of similarly-named coverage data files.
All coverage data files whose name starts with `data_file` (from the
@@ -803,9 +803,15 @@ class Coverage(object):
directory indicated by the current data file (probably the current
directory) will be combined.
+ If `strict` is true, then it is an error to attempt to combine when
+ there are no data files to combine.
+
.. versionadded:: 4.0
The `data_paths` parameter.
+ .. versionadded:: 4.3
+ The `strict` parameter.
+
"""
self._init()
self.get_data()
@@ -818,7 +824,9 @@ class Coverage(object):
for pattern in paths[1:]:
aliases.add(pattern, result)
- self.data_files.combine_parallel_data(self.data, aliases=aliases, data_paths=data_paths)
+ self.data_files.combine_parallel_data(
+ self.data, aliases=aliases, data_paths=data_paths, strict=strict,
+ )
def get_data(self):
"""Get the collected data and reset the collector.
diff --git a/coverage/data.py b/coverage/data.py
index 60e104d9..94d83302 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -666,7 +666,7 @@ class CoverageDataFiles(object):
filename += "." + suffix
data.write_file(filename)
- def combine_parallel_data(self, data, aliases=None, data_paths=None):
+ def combine_parallel_data(self, data, aliases=None, data_paths=None, strict=False):
"""Combine a number of data files together.
Treat `self.filename` as a file prefix, and combine the data from all
@@ -686,6 +686,9 @@ class CoverageDataFiles(object):
cannot be read, a warning will be issued, and the file will not be
deleted.
+ If `strict` is true, and no files are found to combine, an error is
+ raised.
+
"""
# Because of the os.path.abspath in the constructor, data_dir will
# never be an empty string.
@@ -703,6 +706,9 @@ class CoverageDataFiles(object):
else:
raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,))
+ if strict and not files_to_combine:
+ raise CoverageException("No data to combine")
+
for f in files_to_combine:
new_data = CoverageData()
try:
diff --git a/tests/test_api.py b/tests/test_api.py
index 27092098..9feea5bf 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -293,8 +293,8 @@ class ApiTest(CoverageTest):
self.check_code1_code2(cov)
- def make_corrupt_data_files(self):
- """Make some good and some bad data files."""
+ def make_good_data_files(self):
+ """Make some good data files."""
self.make_code1_code2()
cov = coverage.Coverage(data_suffix=True)
self.start_import_stop(cov, "code1")
@@ -304,12 +304,15 @@ class ApiTest(CoverageTest):
self.start_import_stop(cov, "code2")
cov.save()
+ def make_bad_data_file(self):
+ """Make one bad data file."""
self.make_file(".coverage.foo", """La la la, this isn't coverage data!""")
def test_combining_corrupt_data(self):
# If you combine a corrupt data file, then you will get a warning,
# and the file will remain.
- self.make_corrupt_data_files()
+ self.make_good_data_files()
+ self.make_bad_data_file()
cov = coverage.Coverage()
warning_regex = (
r"Couldn't read data from '.*\.coverage\.foo': "
@@ -324,6 +327,27 @@ class ApiTest(CoverageTest):
# The bad file still exists.
self.assert_exists(".coverage.foo")
+ def test_combining_twice(self):
+ self.make_good_data_files()
+ cov1 = coverage.Coverage()
+ cov1.combine()
+ cov1.save()
+ self.check_code1_code2(cov1)
+
+ cov2 = coverage.Coverage()
+ with self.assertRaisesRegex(CoverageException, r"No data to combine"):
+ cov2.combine(strict=True)
+
+ cov3 = coverage.Coverage()
+ cov3.combine()
+ # Now the data is empty!
+ _, statements, missing, _ = cov3.analysis("code1.py")
+ self.assertEqual(statements, [1])
+ self.assertEqual(missing, [1])
+ _, statements, missing, _ = cov3.analysis("code2.py")
+ self.assertEqual(statements, [1, 2])
+ self.assertEqual(missing, [1, 2])
+
class NamespaceModuleTest(CoverageTest):
"""Test PEP-420 namespace modules."""
@@ -344,7 +368,6 @@ class NamespaceModuleTest(CoverageTest):
cov.analysis(sys.modules['namespace'])
-
class UsingModulesMixin(object):
"""A mixin for importing modules from test/modules and test/moremodules."""
diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py
index 1e72c4f4..3b982ebe 100644
--- a/tests/test_cmdline.py
+++ b/tests/test_cmdline.py
@@ -190,20 +190,20 @@ class CmdLineTest(BaseCmdLineTest):
# coverage combine with args
self.cmd_executes("combine datadir1", """\
.coverage()
- .combine(["datadir1"])
+ .combine(["datadir1"], strict=True)
.save()
""")
# coverage combine, appending
self.cmd_executes("combine --append datadir1", """\
.coverage()
.load()
- .combine(["datadir1"])
+ .combine(["datadir1"], strict=True)
.save()
""")
# coverage combine without args
self.cmd_executes("combine", """\
.coverage()
- .combine(None)
+ .combine(None, strict=True)
.save()
""")
@@ -211,12 +211,12 @@ class CmdLineTest(BaseCmdLineTest):
# https://bitbucket.org/ned/coveragepy/issues/385/coverage-combine-doesnt-work-with-rcfile
self.cmd_executes("combine --rcfile cov.ini", """\
.coverage(config_file='cov.ini')
- .combine(None)
+ .combine(None, strict=True)
.save()
""")
self.cmd_executes("combine --rcfile cov.ini data1 data2/more", """\
.coverage(config_file='cov.ini')
- .combine(["data1", "data2/more"])
+ .combine(["data1", "data2/more"], strict=True)
.save()
""")
diff --git a/tests/test_process.py b/tests/test_process.py
index 53d2362a..f7f46ab0 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -95,6 +95,17 @@ class ProcessTest(CoverageTest):
data.read_file(".coverage")
self.assertEqual(data.line_counts()['b_or_c.py'], 7)
+ # Running combine again should fail, because there are no parallel data
+ # files to combine.
+ status, out = self.run_command_status("coverage combine")
+ self.assertEqual(status, 1)
+ self.assertEqual(out, "No data to combine\n")
+
+ # And the originally combined data is still there.
+ data = coverage.CoverageData()
+ data.read_file(".coverage")
+ self.assertEqual(data.line_counts()['b_or_c.py'], 7)
+
def test_combine_parallel_data_with_a_corrupt_file(self):
self.make_b_or_c_py()
out = self.run_command("coverage run -p b_or_c.py b")
@@ -732,7 +743,6 @@ class ProcessTest(CoverageTest):
inst.start()
import foo
inst.stop()
- inst.combine()
inst.save()
""")
out = self.run_command("python run_twice.py")