Skip to content

Commit

Permalink
wip: implicit path mapping during reporting. #1212
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Nov 26, 2022
1 parent ff9839f commit 5ee7c64
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Expand Up @@ -29,6 +29,8 @@ Unreleased
- Using ``--format=total`` will write a single total number to the
output. This can be useful for making badges or writing status updates.

- TODO: implicit path mapping during reporting.

- 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 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
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
144 changes: 143 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,144 @@ 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="lines", 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", os_sep("src/program.py"))
doesnt_contain("coverage.xml", os_sep("ver1/program.py"), os_sep("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()
contains("coverage.json", os_sep("src/program.py"))
doesnt_contain("coverage.json", os_sep("ver1/program.py"), os_sep("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 5ee7c64

Please sign in to comment.