diff options
-rw-r--r-- | CHANGES.rst | 8 | ||||
-rw-r--r-- | coverage/cmdline.py | 2 | ||||
-rw-r--r-- | coverage/control.py | 12 | ||||
-rw-r--r-- | coverage/data.py | 8 | ||||
-rw-r--r-- | tests/test_api.py | 31 | ||||
-rw-r--r-- | tests/test_cmdline.py | 10 | ||||
-rw-r--r-- | tests/test_process.py | 12 |
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") |