summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst7
-rw-r--r--coverage/control.py36
-rw-r--r--coverage/sqldata.py7
-rw-r--r--doc/config.rst4
-rw-r--r--tests/coveragetest.py5
-rw-r--r--tests/test_api.py146
6 files changed, 192 insertions, 13 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 2fd7447c..b1d94dbd 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -29,6 +29,11 @@ Unreleased
- Using ``--format=total`` will write a single total number to the
output. This can be useful for making badges or writing status updates.
+- Reporting operations now use the ``[paths]`` setting to remap file paths
+ within a single data file. Combining multiple files still requires the
+ ``coverage combine`` step, but this simplifies some situations. Closes
+ `issue 1212`_ and `issue 713`_.
+
- Combining data files with ``coverage combine`` now quickly hashes the data
files to skip files that provide no new information. This can reduce the
time needed. Many details affect the results, but for coverage.py's own test
@@ -54,6 +59,8 @@ Unreleased
- The deprecated ``[run] note`` setting has been completely removed.
.. _implicit namespace packages: https://peps.python.org/pep-0420/
+.. _issue 713: https://github.com/nedbat/coveragepy/issues/713
+.. _issue 1212: https://github.com/nedbat/coveragepy/issues/1212
.. _issue 1383: https://github.com/nedbat/coveragepy/issues/1383
.. _issue 1418: https://github.com/nedbat/coveragepy/issues/1418
.. _issue 1421: https://github.com/nedbat/coveragepy/issues/1421
diff --git a/coverage/control.py b/coverage/control.py
index 2e58ad85..c0497478 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -733,6 +733,18 @@ class Coverage:
data = self.get_data()
data.write()
+ def _make_aliases(self):
+ """Create a PathAliases from our configuration."""
+ aliases = PathAliases(
+ debugfn=(self._debug.write if self._debug.should("pathmap") else None),
+ relative=self.config.relative_files,
+ )
+ for paths in self.config.paths.values():
+ result = paths[0]
+ for pattern in paths[1:]:
+ aliases.add(pattern, result)
+ return aliases
+
def combine(self, data_paths=None, strict=False, keep=False):
"""Combine together a number of similarly-named coverage data files.
@@ -764,18 +776,9 @@ class Coverage:
self._post_init()
self.get_data()
- aliases = PathAliases(
- debugfn=(self._debug.write if self._debug.should("pathmap") else None),
- relative=self.config.relative_files,
- )
- for paths in self.config.paths.values():
- result = paths[0]
- for pattern in paths[1:]:
- aliases.add(pattern, result)
-
combine_parallel_data(
self._data,
- aliases=aliases,
+ aliases=self._make_aliases(),
data_paths=data_paths,
strict=strict,
keep=keep,
@@ -925,6 +928,13 @@ class Coverage:
file_reporters = [self._get_file_reporter(morf) for morf in morfs]
return file_reporters
+ def _prepare_data_for_reporting(self):
+ """Re-map data before reporting, to get implicit 'combine' behavior."""
+ if self.config.paths:
+ mapped_data = CoverageData(warn=self._warn, debug=self._debug, no_disk=True)
+ mapped_data.update(self._data, aliases=self._make_aliases())
+ self._data = mapped_data
+
def report(
self,
morfs=None,
@@ -990,6 +1000,7 @@ class Coverage:
The `format` parameter.
"""
+ self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
@@ -1034,6 +1045,7 @@ class Coverage:
print("The annotate command will be removed in a future version.")
print("Get in touch if you still use it: ned@nedbatchelder.com")
+ self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
@@ -1083,6 +1095,7 @@ class Coverage:
changing the files in the report folder.
"""
+ self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
@@ -1123,6 +1136,7 @@ class Coverage:
Returns a float, the total percentage covered.
"""
+ self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
@@ -1157,6 +1171,7 @@ class Coverage:
.. versionadded:: 5.0
"""
+ self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
@@ -1187,6 +1202,7 @@ class Coverage:
.. versionadded:: 6.3
"""
+ self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
diff --git a/coverage/sqldata.py b/coverage/sqldata.py
index ea6b1199..68663715 100644
--- a/coverage/sqldata.py
+++ b/coverage/sqldata.py
@@ -648,7 +648,12 @@ class CoverageData(SimpleReprMixin):
"inner join file on file.id = line_bits.file_id " +
"inner join context on context.id = line_bits.context_id"
)
- lines = {(files[path], context): numbits for (path, context, numbits) in cur}
+ lines = {}
+ for path, context, numbits in cur:
+ key = (files[path], context)
+ if key in lines:
+ numbits = numbits_union(lines[key], numbits)
+ lines[key] = numbits
cur.close()
# Get tracer data.
diff --git a/doc/config.rst b/doc/config.rst
index ba3243a7..1c7e9ea2 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -346,6 +346,10 @@ against the source file found at "src/module.py".
If you specify more than one list of paths, they will be considered in order.
The first list that has a match will be used.
+Remapping will also be done during reporting, but only within the single data
+file being reported. Combining multiple files requires the ``combine``
+command.
+
The ``--debug=pathmap`` option can be used to log details of the re-mapping of
paths. See :ref:`the --debug option <cmd_run_debug>`.
diff --git a/tests/coveragetest.py b/tests/coveragetest.py
index 54ae4eb4..56e78853 100644
--- a/tests/coveragetest.py
+++ b/tests/coveragetest.py
@@ -219,11 +219,14 @@ class CoverageTest(
return cov
- def make_data_file(self, basename=None, suffix=None, lines=None, file_tracers=None):
+ def make_data_file(self, basename=None, suffix=None, lines=None, arcs=None, file_tracers=None):
"""Write some data into a coverage data file."""
data = coverage.CoverageData(basename=basename, suffix=suffix)
+ assert lines is None or arcs is None
if lines:
data.add_lines(lines)
+ if arcs:
+ data.add_arcs(arcs)
if file_tracers:
data.add_file_tracers(file_tracers)
data.write()
diff --git a/tests/test_api.py b/tests/test_api.py
index 19545232..c2dbefa8 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -23,7 +23,8 @@ from coverage.files import abs_file, relative_filename
from coverage.misc import import_local_file
from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin
-from tests.helpers import assert_count_equal, assert_coverage_warnings
+from tests.goldtest import contains, doesnt_contain
+from tests.helpers import arcz_to_arcs, assert_count_equal, assert_coverage_warnings
from tests.helpers import change_dir, nice_file, os_sep
BAD_SQLITE_REGEX = r"file( is encrypted or)? is not a database"
@@ -1456,3 +1457,146 @@ class CombiningTest(CoverageTest):
# After combining, the .coverage file & the original combined file should still be there.
self.assert_exists(".coverage")
self.assert_file_count(".coverage.*", 2)
+
+
+class ReportMapsPathsTest(CoverageTest):
+ """Check that reporting implicitly maps paths."""
+
+ def make_files(self, data, settings=False):
+ """Create the test files we need for line coverage."""
+ src = """\
+ if VER == 1:
+ print("line 2")
+ if VER == 2:
+ print("line 4")
+ if VER == 3:
+ print("line 6")
+ """
+ self.make_file("src/program.py", src)
+ self.make_file("ver1/program.py", src)
+ self.make_file("ver2/program.py", src)
+
+ if data == "line":
+ self.make_data_file(
+ lines={
+ abs_file("ver1/program.py"): [1, 2, 3, 5],
+ abs_file("ver2/program.py"): [1, 3, 4, 5],
+ }
+ )
+ else:
+ self.make_data_file(
+ arcs={
+ abs_file("ver1/program.py"): arcz_to_arcs(".1 12 23 35 5."),
+ abs_file("ver2/program.py"): arcz_to_arcs(".1 13 34 45 5."),
+ }
+ )
+
+ if settings:
+ self.make_file(".coveragerc", """\
+ [paths]
+ source =
+ src
+ ver1
+ ver2
+ """)
+
+ def test_map_paths_during_line_report_without_setting(self):
+ self.make_files(data="line")
+ cov = coverage.Coverage()
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent(os_sep("""\
+ Name Stmts Miss Cover Missing
+ -----------------------------------------------
+ ver1/program.py 6 2 67% 4, 6
+ ver2/program.py 6 2 67% 2, 6
+ -----------------------------------------------
+ TOTAL 12 4 67%
+ """))
+ assert expected == self.stdout()
+
+ def test_map_paths_during_line_report(self):
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent(os_sep("""\
+ Name Stmts Miss Cover Missing
+ ----------------------------------------------
+ src/program.py 6 1 83% 6
+ ----------------------------------------------
+ TOTAL 6 1 83%
+ """))
+ assert expected == self.stdout()
+
+ def test_map_paths_during_branch_report_without_setting(self):
+ self.make_files(data="arcs")
+ cov = coverage.Coverage(branch=True)
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent(os_sep("""\
+ Name Stmts Miss Branch BrPart Cover Missing
+ -------------------------------------------------------------
+ ver1/program.py 6 2 6 3 58% 1->3, 4, 6
+ ver2/program.py 6 2 6 3 58% 2, 3->5, 6
+ -------------------------------------------------------------
+ TOTAL 12 4 12 6 58%
+ """))
+ assert expected == self.stdout()
+
+ def test_map_paths_during_branch_report(self):
+ self.make_files(data="arcs", settings=True)
+ cov = coverage.Coverage(branch=True)
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent(os_sep("""\
+ Name Stmts Miss Branch BrPart Cover Missing
+ ------------------------------------------------------------
+ src/program.py 6 1 6 1 83% 6
+ ------------------------------------------------------------
+ TOTAL 6 1 6 1 83%
+ """))
+ assert expected == self.stdout()
+
+ def test_map_paths_during_annotate(self):
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.annotate()
+ self.assert_exists(os_sep("src/program.py,cover"))
+ self.assert_doesnt_exist(os_sep("ver1/program.py,cover"))
+ self.assert_doesnt_exist(os_sep("ver2/program.py,cover"))
+
+ def test_map_paths_during_html_report(self):
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.html_report()
+ contains("htmlcov/index.html", os_sep("src/program.py"))
+ doesnt_contain("htmlcov/index.html", os_sep("ver1/program.py"), os_sep("ver2/program.py"))
+
+ def test_map_paths_during_xml_report(self):
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.xml_report()
+ contains("coverage.xml", "src/program.py")
+ doesnt_contain("coverage.xml", "ver1/program.py", "ver2/program.py")
+
+ def test_map_paths_during_json_report(self):
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.json_report()
+ def os_sepj(s):
+ return os_sep(s).replace("\\", r"\\")
+ contains("coverage.json", os_sepj("src/program.py"))
+ doesnt_contain("coverage.json", os_sepj("ver1/program.py"), os_sepj("ver2/program.py"))
+
+ def test_map_paths_during_lcov_report(self):
+ self.make_files(data="line", settings=True)
+ cov = coverage.Coverage()
+ cov.load()
+ cov.lcov_report()
+ contains("coverage.lcov", os_sep("src/program.py"))
+ doesnt_contain("coverage.lcov", os_sep("ver1/program.py"), os_sep("ver2/program.py"))