diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 9c9ae868a..11bc5d992 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -31,6 +31,10 @@ class Opts(object): '-a', '--append', action='store_true', help="Append coverage data to .coverage, otherwise it starts clean each time.", ) + keep = optparse.make_option( + '', '--keep', action='store_true', + help="Keep combined coverage files, otherwise they are deleted.", + ) branch = optparse.make_option( '', '--branch', action='store_true', help="Measure branch coverage in addition to statement coverage.", @@ -215,6 +219,7 @@ def __init__(self, *args, **kwargs): help=None, ignore_errors=None, include=None, + keep=None, module=None, omit=None, contexts=None, @@ -333,6 +338,7 @@ def get_prog_name(self): "combine", [ Opts.append, + Opts.keep, ] + GLOBAL_ARGS, usage="[options] ... ", description=( @@ -585,7 +591,7 @@ def command_line(self, argv): if options.append: self.coverage.load() data_dirs = args or None - self.coverage.combine(data_dirs, strict=True) + self.coverage.combine(data_dirs, strict=True, keep=bool(options.keep)) self.coverage.save() return OK diff --git a/coverage/control.py b/coverage/control.py index 8d129bcb5..c952afcd5 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -659,7 +659,7 @@ def save(self): data = self.get_data() data.write() - def combine(self, data_paths=None, strict=False): + def combine(self, data_paths=None, strict=False, keep=False): """Combine together a number of similarly-named coverage data files. All coverage data files whose name starts with `data_file` (from the @@ -674,6 +674,8 @@ def combine(self, data_paths=None, strict=False): If `strict` is true, then it is an error to attempt to combine when there are no data files to combine. + If `keep` is true, then combined data files won't be deleted. + .. versionadded:: 4.0 The `data_paths` parameter. @@ -694,7 +696,8 @@ def combine(self, data_paths=None, strict=False): for pattern in paths[1:]: aliases.add(pattern, result) - combine_parallel_data(self._data, aliases=aliases, data_paths=data_paths, strict=strict) + combine_parallel_data(self._data, + aliases=aliases, data_paths=data_paths, strict=strict, keep=keep) def get_data(self): """Get the collected data. diff --git a/coverage/data.py b/coverage/data.py index 82bf1d41c..5dd1dfe3f 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -52,7 +52,7 @@ def add_data_to_hash(data, filename, hasher): hasher.update(data.file_tracer(filename)) -def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): +def combine_parallel_data(data, aliases=None, data_paths=None, strict=False, keep=False): """Combine a number of data files together. Treat `data.filename` as a file prefix, and combine the data from all @@ -68,7 +68,7 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): If `data_paths` is not provided, then the directory portion of `data.filename` is used as the directory to search for data files. - Every data file found and combined is then deleted from disk. If a file + Unless `keep` is True every data file found and combined is then deleted from disk. If a file cannot be read, a warning will be issued, and the file will not be deleted. @@ -116,9 +116,10 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): else: data.update(new_data, aliases=aliases) files_combined += 1 - if data._debug.should('dataio'): - data._debug.write("Deleting combined data file %r" % (f,)) - file_be_gone(f) + if not keep: + if data._debug.should('dataio'): + data._debug.write("Deleting combined data file %r" % (f,)) + file_be_gone(f) if strict and not files_combined: raise CoverageException("No usable data files") diff --git a/doc/help/combine.rst b/doc/help/combine.rst index 35180cdde..c35d5b932 100644 --- a/doc/help/combine.rst +++ b/doc/help/combine.rst @@ -13,6 +13,7 @@ Options: -a, --append Append coverage data to .coverage, otherwise it starts clean each time. + --keep Keep combined coverage files, otherwise they are deleted. --debug=OPTS Debug options, separated by commas. [env: COVERAGE_DEBUG] -h, --help Get help on this command. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt index 0bbd44d0a..10a8927a9 100644 --- a/doc/python-coverage.1.txt +++ b/doc/python-coverage.1.txt @@ -109,6 +109,7 @@ COMMAND REFERENCE Combine data from multiple coverage files collected with ``run -p``. The combined results are written to a single file representing the union of the data. + Unless --keep is provided the combined coverage files are deleted. If `PATH` is specified, they are files or directories containing data to be combined. @@ -119,6 +120,9 @@ COMMAND REFERENCE Append coverage data to .coverage, otherwise it starts clean each time. + \--keep + Keep combined coverage file. + **debug** `TOPIC` ... Display information about the internals of coverage.py, for diagnosing diff --git a/tests/test_api.py b/tests/test_api.py index f8b7b4b2c..62fd9ebca 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -442,7 +442,7 @@ def test_combining_twice(self): cov2 = coverage.Coverage() with self.assertRaisesRegex(CoverageException, r"No data to combine"): - cov2.combine(strict=True) + cov2.combine(strict=True, keep=False) cov3 = coverage.Coverage() cov3.combine() diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 374adb0de..3b11881b0 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -218,20 +218,20 @@ def test_combine(self): # coverage combine with args self.cmd_executes("combine datadir1", """\ cov = Coverage() - cov.combine(["datadir1"], strict=True) + cov.combine(["datadir1"], strict=True, keep=False) cov.save() """) # coverage combine, appending self.cmd_executes("combine --append datadir1", """\ cov = Coverage() cov.load() - cov.combine(["datadir1"], strict=True) + cov.combine(["datadir1"], strict=True, keep=False) cov.save() """) # coverage combine without args self.cmd_executes("combine", """\ cov = Coverage() - cov.combine(None, strict=True) + cov.combine(None, strict=True, keep=False) cov.save() """) @@ -239,12 +239,12 @@ def test_combine_doesnt_confuse_options_with_args(self): # https://github.com/nedbat/coveragepy/issues/385 self.cmd_executes("combine --rcfile cov.ini", """\ cov = Coverage(config_file='cov.ini') - cov.combine(None, strict=True) + cov.combine(None, strict=True, keep=False) cov.save() """) self.cmd_executes("combine --rcfile cov.ini data1 data2/more", """\ cov = Coverage(config_file='cov.ini') - cov.combine(["data1", "data2/more"], strict=True) + cov.combine(["data1", "data2/more"], strict=True, keep=False) cov.save() """) diff --git a/tests/test_process.py b/tests/test_process.py index e48861568..b44147a39 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -244,6 +244,28 @@ def test_combine_parallel_data_no_append(self): data.read() self.assertEqual(line_counts(data)['b_or_c.py'], 7) + def test_combine_parallel_data_keep(self): + self.make_b_or_c_py() + out = self.run_command("coverage run -p b_or_c.py b") + self.assertEqual(out, 'done\n') + self.assert_doesnt_exist(".coverage") + self.assert_file_count(".coverage.*", 1) + + out = self.run_command("coverage run -p b_or_c.py c") + self.assertEqual(out, 'done\n') + self.assert_doesnt_exist(".coverage") + + # After two -p runs, there should be two .coverage.machine.123 files. + self.assert_file_count(".coverage.*", 2) + + # Combine the parallel coverage data files into .coverage with the keep flag. + self.run_command("coverage combine --keep") + + # After combining, the .coverage file & the original combined file should still be there. + self.assert_exists(".coverage") + self.assert_file_count(".coverage.*", 2) + + def test_append_data(self): self.make_b_or_c_py()