Skip to content

Commit

Permalink
feat: implicit path mapping during reporting. #1212
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Nov 27, 2022
1 parent ff9839f commit 0818611
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 13 deletions.
7 changes: 7 additions & 0 deletions CHANGES.rst
Expand Up @@ -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
Expand All @@ -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
Expand Down
36 changes: 26 additions & 10 deletions coverage/control.py
Expand Up @@ -733,6 +733,18 @@ def save(self):
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.
Expand Down Expand Up @@ -764,18 +776,9 @@ def combine(self, data_paths=None, strict=False, keep=False):
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,
Expand Down Expand Up @@ -925,6 +928,13 @@ def _get_file_reporters(self, morfs=None):
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,
Expand Down Expand Up @@ -990,6 +1000,7 @@ def report(
The `format` parameter.
"""
self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
Expand Down Expand Up @@ -1034,6 +1045,7 @@ def annotate(
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,
Expand Down Expand Up @@ -1083,6 +1095,7 @@ def html_report(
changing the files in the report folder.
"""
self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
Expand Down Expand Up @@ -1123,6 +1136,7 @@ def xml_report(
Returns a float, the total percentage covered.
"""
self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
Expand Down Expand Up @@ -1157,6 +1171,7 @@ def json_report(
.. versionadded:: 5.0
"""
self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
Expand Down Expand Up @@ -1187,6 +1202,7 @@ def lcov_report(
.. versionadded:: 6.3
"""
self._prepare_data_for_reporting()
with override_config(
self,
ignore_errors=ignore_errors,
Expand Down
7 changes: 6 additions & 1 deletion coverage/sqldata.py
Expand Up @@ -648,7 +648,12 @@ def update(self, other_data, aliases=None):
"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.
Expand Down
4 changes: 4 additions & 0 deletions doc/config.rst
Expand Up @@ -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>`.

Expand Down
5 changes: 4 additions & 1 deletion tests/coveragetest.py
Expand Up @@ -219,11 +219,14 @@ def check_coverage(

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()
Expand Down
146 changes: 145 additions & 1 deletion tests/test_api.py
Expand Up @@ -23,7 +23,8 @@
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"
Expand Down Expand Up @@ -1456,3 +1457,146 @@ def test_combine_parallel_data_keep(self):
# 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"))

0 comments on commit 0818611

Please sign in to comment.