Skip to content

Commit

Permalink
Add expand_coverage_report option to for #270 (#416)
Browse files Browse the repository at this point in the history
* Add expand_coverage_report option to  add missing lines in a coverage report based on the previous line hit score

* run black

* adds .venv to .gitignore

* changes expand-coverage-report argument to use - instead of _

* Revert "changes expand-coverage-report argument to use - instead of _"

This reverts commit 381d2b4.

* changes expand-coverage-report argument to use - instead of _

* documents --expand-coverage-report in README.rst

* adds tests to expand-coverage-report

* fix readme styling issues

* Fix trailing white space on readme 287
  • Loading branch information
MathieuLamiot authored Sep 6, 2024
1 parent f8dba1b commit e06ddc0
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.idea
venv
27venv
.venv

# C extensions
*.so
Expand Down
12 changes: 12 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,18 @@ It can be enabled by using the ``-q``/``--quiet`` flag:
If enabled, the tool will only print errors and failures but no information or warning messages.

Compatibility with multi-line statements
----------------------------------------
``diff-cover`` relies on the comparison of diff reports and coverage reports, and does not report
lines that appear in one and not in the other. While diff reports list all lines that changed,
coverage reports usually list code statements. As a result, a change in a multi-line statement may not be analyzed by ``diff-cover``.

As a workaround, you can use the argument ``--expand-coverage-report``: lines not appearing in the coverage reports will be added to them with the same number of hits as the previously reported line. ``diff-cover`` will then perform diff coverage analysis on all changed lines.

Notes:
- This argument is only available for XML coverage reports.
- This workaround is designed under the assumption that the coverage tool reports untested statements with hits set to 0, and it reports statements based on the opening line.

Configuration files
-------------------
Both tools allow users to specify the options in a configuration file with `--config-file`/`-c`:
Expand Down
15 changes: 14 additions & 1 deletion diff_cover/diff_cover_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
)
QUIET_HELP = "Only print errors and failures"
SHOW_UNCOVERED = "Show uncovered lines on the console"
EXPAND_COVERAGE_REPORT = (
"Append missing lines in coverage reports based on the hits of the previous line."
)
INCLUDE_UNTRACKED_HELP = "Include untracked files"
CONFIG_FILE_HELP = "The configuration file to use"
DIFF_FILE_HELP = "The diff file to use"
Expand Down Expand Up @@ -93,6 +96,13 @@ def parse_coverage_args(argv):
"--show-uncovered", action="store_true", default=None, help=SHOW_UNCOVERED
)

parser.add_argument(
"--expand-coverage-report",
action="store_true",
default=None,
help=EXPAND_COVERAGE_REPORT,
)

parser.add_argument(
"--external-css-file",
metavar="FILENAME",
Expand Down Expand Up @@ -183,6 +193,7 @@ def parse_coverage_args(argv):
"ignore_whitespace": False,
"diff_range_notation": "...",
"quiet": False,
"expand_coverage_report": False,
}

return get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_COVER)
Expand All @@ -204,6 +215,7 @@ def generate_coverage_report(
src_roots=None,
quiet=False,
show_uncovered=False,
expand_coverage_report=False,
):
"""
Generate the diff coverage report, using kwargs from `parse_args()`.
Expand Down Expand Up @@ -231,7 +243,7 @@ def generate_coverage_report(
if len(xml_roots) > 0 and len(lcov_roots) > 0:
raise ValueError(f"Mixing LCov and XML reports is not supported yet")
elif len(xml_roots) > 0:
coverage = XmlCoverageReporter(xml_roots, src_roots)
coverage = XmlCoverageReporter(xml_roots, src_roots, expand_coverage_report)
else:
coverage = LcovCoverageReporter(lcov_roots, src_roots)

Expand Down Expand Up @@ -308,6 +320,7 @@ def main(argv=None, directory=None):
src_roots=arg_dict["src_roots"],
quiet=quiet,
show_uncovered=arg_dict["show_uncovered"],
expand_coverage_report=arg_dict["expand_coverage_report"],
)

if percent_covered >= fail_under:
Expand Down
25 changes: 24 additions & 1 deletion diff_cover/violationsreporters/violations_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class XmlCoverageReporter(BaseViolationReporter):
Query information from a Cobertura|Clover|JaCoCo XML coverage report.
"""

def __init__(self, xml_roots, src_roots=None):
def __init__(self, xml_roots, src_roots=None, expand_coverage_report=False):
"""
Load the XML coverage report represented
by the cElementTree with root element `xml_root`.
Expand All @@ -41,6 +41,7 @@ def __init__(self, xml_roots, src_roots=None):
self._xml_cache = [{} for i in range(len(xml_roots))]

self._src_roots = src_roots or [""]
self._expand_coverage_report = expand_coverage_report

def _get_xml_classes(self, xml_document):
"""
Expand Down Expand Up @@ -216,6 +217,28 @@ def _cache_file(self, src_path):
if line_nodes is None:
continue

# Expand coverage report with not reported lines
if self._expand_coverage_report:
reported_line_hits = {}
for line in line_nodes:
reported_line_hits[int(line.get(_number))] = int(
line.get(_hits, 0)
)
if reported_line_hits:
last_hit_number = 0
for line_number in range(
min(reported_line_hits.keys()),
max(reported_line_hits.keys()),
):
if line_number in reported_line_hits:
last_hit_number = reported_line_hits[line_number]
else:
# This is an unreported line.
# We add it with the previous line hit score
line_nodes.append(
{_hits: last_hit_number, _number: line_number}
)

# First case, need to define violations initially
if violations is None:
violations = {
Expand Down
24 changes: 24 additions & 0 deletions tests/fixtures/coverage_missing_lines.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<!DOCTYPE coverage
SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
<coverage branch-rate="0" line-rate="0.8792" timestamp="1371471706873" version="3.6">
<packages>
<package branch-rate="0" complexity="0" line-rate="0.9915" name="">
<classes>
<class branch-rate="0" complexity="0" filename="test_src.txt" line-rate="0.9643" name="test_src.txt">
<methods/>
<lines>
<line hits="1" number="1"/>
<line hits="0" number="2"/>
<line hits="0" number="4"/>
<line hits="1" number="5"/>
<line hits="1" number="7"/>
<line hits="0" number="8"/>
<line hits="1" number="9"/>
<line hits="0" number="10"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
10 changes: 10 additions & 0 deletions tests/fixtures/expand_console_report.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-------------
Diff Coverage
Diff: origin/main...HEAD, staged and unstaged changes
-------------
test_src.txt (50.0%): Missing lines 2-4,8,10
-------------
Total: 10 lines
Missing: 5 lines
Coverage: 50%
-------------
14 changes: 14 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,20 @@ def test_show_uncovered_lines_console(self):
["diff-cover", "--show-uncovered", "coverage.xml"],
)

def test_expand_coverage_report_complete_report(self):
self._check_console_report(
"git_diff_add.txt",
"add_console_report.txt",
["diff-cover", "coverage.xml", "--expand-coverage-report"],
)

def test_expand_coverage_report_uncomplete_report(self):
self._check_console_report(
"git_diff_add.txt",
"expand_console_report.txt",
["diff-cover", "coverage_missing_lines.xml", "--expand-coverage-report"],
)


class TestDiffQualityIntegration(ToolsIntegrationBase):
"""
Expand Down
61 changes: 61 additions & 0 deletions tests/test_violations_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ class TestXmlCoverageReporterTest:
ONE_VIOLATION = {Violation(11, None)}
VERY_MANY_MEASURED = {2, 3, 5, 7, 11, 13, 17, 23, 24, 25, 26, 26, 27}

MANY_VIOLATIONS_EXPANDED_MANY_MEASURED = {
Violation(3, None),
Violation(4, None),
Violation(7, None),
Violation(8, None),
Violation(9, None),
Violation(10, None),
Violation(11, None),
Violation(12, None),
Violation(13, None),
Violation(14, None),
Violation(15, None),
Violation(16, None),
}

@pytest.fixture(autouse=True)
def patch_git_patch(self, mocker):
# Paths generated by git_path are always the given argument
Expand Down Expand Up @@ -266,6 +281,52 @@ def test_no_such_file(self):
result = coverage.violations("file.py")
assert result == set()

def test_expand_unreported_lines_when_configured(self):
# Construct the XML report
file_paths = ["file1.java"]
# fixture
violations = self.MANY_VIOLATIONS
measured = self.MANY_MEASURED
xml = self._coverage_xml(file_paths, violations, measured)

# Parse the reports
coverage = XmlCoverageReporter([xml], expand_coverage_report=True)

# Expect that the name is set
assert coverage.name() == "XML"

# By construction, each file has the same set
# of covered/uncovered lines
assert self.MANY_VIOLATIONS_EXPANDED_MANY_MEASURED == coverage.violations(
"file1.java"
)

def test_expand_unreported_lines_without_violations(self):
# Construct the XML report
file_paths = ["file1.java"]
# fixture
violations = {}
measured = self.MANY_MEASURED
xml = self._coverage_xml(file_paths, violations, measured)

# Parse the reports
coverage = XmlCoverageReporter([xml], expand_coverage_report=True)

assert set() == coverage.violations("file1.java")

def test_expand_unreported_lines_without_measured(self):
# Construct the XML report
file_paths = ["file1.java"]
# fixture
violations = {}
measured = {}
xml = self._coverage_xml(file_paths, violations, measured)

# Parse the reports
coverage = XmlCoverageReporter([xml], expand_coverage_report=True)

assert set() == coverage.violations("file1.java")

def _coverage_xml(self, file_paths, violations, measured, source_paths=None):
"""
Build an XML tree with source files specified by `file_paths`.
Expand Down

0 comments on commit e06ddc0

Please sign in to comment.