Skip to content

Commit e06ddc0

Browse files
Add expand_coverage_report option to for #270 (#416)
* 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
1 parent f8dba1b commit e06ddc0

File tree

8 files changed

+160
-2
lines changed

8 files changed

+160
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.idea
55
venv
66
27venv
7+
.venv
78

89
# C extensions
910
*.so

README.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,18 @@ It can be enabled by using the ``-q``/``--quiet`` flag:
274274
275275
If enabled, the tool will only print errors and failures but no information or warning messages.
276276

277+
Compatibility with multi-line statements
278+
----------------------------------------
279+
``diff-cover`` relies on the comparison of diff reports and coverage reports, and does not report
280+
lines that appear in one and not in the other. While diff reports list all lines that changed,
281+
coverage reports usually list code statements. As a result, a change in a multi-line statement may not be analyzed by ``diff-cover``.
282+
283+
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.
284+
285+
Notes:
286+
- This argument is only available for XML coverage reports.
287+
- 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.
288+
277289
Configuration files
278290
-------------------
279291
Both tools allow users to specify the options in a configuration file with `--config-file`/`-c`:

diff_cover/diff_cover_tool.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
)
4242
QUIET_HELP = "Only print errors and failures"
4343
SHOW_UNCOVERED = "Show uncovered lines on the console"
44+
EXPAND_COVERAGE_REPORT = (
45+
"Append missing lines in coverage reports based on the hits of the previous line."
46+
)
4447
INCLUDE_UNTRACKED_HELP = "Include untracked files"
4548
CONFIG_FILE_HELP = "The configuration file to use"
4649
DIFF_FILE_HELP = "The diff file to use"
@@ -93,6 +96,13 @@ def parse_coverage_args(argv):
9396
"--show-uncovered", action="store_true", default=None, help=SHOW_UNCOVERED
9497
)
9598

99+
parser.add_argument(
100+
"--expand-coverage-report",
101+
action="store_true",
102+
default=None,
103+
help=EXPAND_COVERAGE_REPORT,
104+
)
105+
96106
parser.add_argument(
97107
"--external-css-file",
98108
metavar="FILENAME",
@@ -183,6 +193,7 @@ def parse_coverage_args(argv):
183193
"ignore_whitespace": False,
184194
"diff_range_notation": "...",
185195
"quiet": False,
196+
"expand_coverage_report": False,
186197
}
187198

188199
return get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_COVER)
@@ -204,6 +215,7 @@ def generate_coverage_report(
204215
src_roots=None,
205216
quiet=False,
206217
show_uncovered=False,
218+
expand_coverage_report=False,
207219
):
208220
"""
209221
Generate the diff coverage report, using kwargs from `parse_args()`.
@@ -231,7 +243,7 @@ def generate_coverage_report(
231243
if len(xml_roots) > 0 and len(lcov_roots) > 0:
232244
raise ValueError(f"Mixing LCov and XML reports is not supported yet")
233245
elif len(xml_roots) > 0:
234-
coverage = XmlCoverageReporter(xml_roots, src_roots)
246+
coverage = XmlCoverageReporter(xml_roots, src_roots, expand_coverage_report)
235247
else:
236248
coverage = LcovCoverageReporter(lcov_roots, src_roots)
237249

@@ -308,6 +320,7 @@ def main(argv=None, directory=None):
308320
src_roots=arg_dict["src_roots"],
309321
quiet=quiet,
310322
show_uncovered=arg_dict["show_uncovered"],
323+
expand_coverage_report=arg_dict["expand_coverage_report"],
311324
)
312325

313326
if percent_covered >= fail_under:

diff_cover/violationsreporters/violations_reporter.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class XmlCoverageReporter(BaseViolationReporter):
2424
Query information from a Cobertura|Clover|JaCoCo XML coverage report.
2525
"""
2626

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

4343
self._src_roots = src_roots or [""]
44+
self._expand_coverage_report = expand_coverage_report
4445

4546
def _get_xml_classes(self, xml_document):
4647
"""
@@ -216,6 +217,28 @@ def _cache_file(self, src_path):
216217
if line_nodes is None:
217218
continue
218219

220+
# Expand coverage report with not reported lines
221+
if self._expand_coverage_report:
222+
reported_line_hits = {}
223+
for line in line_nodes:
224+
reported_line_hits[int(line.get(_number))] = int(
225+
line.get(_hits, 0)
226+
)
227+
if reported_line_hits:
228+
last_hit_number = 0
229+
for line_number in range(
230+
min(reported_line_hits.keys()),
231+
max(reported_line_hits.keys()),
232+
):
233+
if line_number in reported_line_hits:
234+
last_hit_number = reported_line_hits[line_number]
235+
else:
236+
# This is an unreported line.
237+
# We add it with the previous line hit score
238+
line_nodes.append(
239+
{_hits: last_hit_number, _number: line_number}
240+
)
241+
219242
# First case, need to define violations initially
220243
if violations is None:
221244
violations = {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" ?>
2+
<!DOCTYPE coverage
3+
SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-03.dtd'>
4+
<coverage branch-rate="0" line-rate="0.8792" timestamp="1371471706873" version="3.6">
5+
<packages>
6+
<package branch-rate="0" complexity="0" line-rate="0.9915" name="">
7+
<classes>
8+
<class branch-rate="0" complexity="0" filename="test_src.txt" line-rate="0.9643" name="test_src.txt">
9+
<methods/>
10+
<lines>
11+
<line hits="1" number="1"/>
12+
<line hits="0" number="2"/>
13+
<line hits="0" number="4"/>
14+
<line hits="1" number="5"/>
15+
<line hits="1" number="7"/>
16+
<line hits="0" number="8"/>
17+
<line hits="1" number="9"/>
18+
<line hits="0" number="10"/>
19+
</lines>
20+
</class>
21+
</classes>
22+
</package>
23+
</packages>
24+
</coverage>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-------------
2+
Diff Coverage
3+
Diff: origin/main...HEAD, staged and unstaged changes
4+
-------------
5+
test_src.txt (50.0%): Missing lines 2-4,8,10
6+
-------------
7+
Total: 10 lines
8+
Missing: 5 lines
9+
Coverage: 50%
10+
-------------

tests/test_integration.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,20 @@ def test_show_uncovered_lines_console(self):
412412
["diff-cover", "--show-uncovered", "coverage.xml"],
413413
)
414414

415+
def test_expand_coverage_report_complete_report(self):
416+
self._check_console_report(
417+
"git_diff_add.txt",
418+
"add_console_report.txt",
419+
["diff-cover", "coverage.xml", "--expand-coverage-report"],
420+
)
421+
422+
def test_expand_coverage_report_uncomplete_report(self):
423+
self._check_console_report(
424+
"git_diff_add.txt",
425+
"expand_console_report.txt",
426+
["diff-cover", "coverage_missing_lines.xml", "--expand-coverage-report"],
427+
)
428+
415429

416430
class TestDiffQualityIntegration(ToolsIntegrationBase):
417431
"""

tests/test_violations_reporter.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ class TestXmlCoverageReporterTest:
6666
ONE_VIOLATION = {Violation(11, None)}
6767
VERY_MANY_MEASURED = {2, 3, 5, 7, 11, 13, 17, 23, 24, 25, 26, 26, 27}
6868

69+
MANY_VIOLATIONS_EXPANDED_MANY_MEASURED = {
70+
Violation(3, None),
71+
Violation(4, None),
72+
Violation(7, None),
73+
Violation(8, None),
74+
Violation(9, None),
75+
Violation(10, None),
76+
Violation(11, None),
77+
Violation(12, None),
78+
Violation(13, None),
79+
Violation(14, None),
80+
Violation(15, None),
81+
Violation(16, None),
82+
}
83+
6984
@pytest.fixture(autouse=True)
7085
def patch_git_patch(self, mocker):
7186
# Paths generated by git_path are always the given argument
@@ -266,6 +281,52 @@ def test_no_such_file(self):
266281
result = coverage.violations("file.py")
267282
assert result == set()
268283

284+
def test_expand_unreported_lines_when_configured(self):
285+
# Construct the XML report
286+
file_paths = ["file1.java"]
287+
# fixture
288+
violations = self.MANY_VIOLATIONS
289+
measured = self.MANY_MEASURED
290+
xml = self._coverage_xml(file_paths, violations, measured)
291+
292+
# Parse the reports
293+
coverage = XmlCoverageReporter([xml], expand_coverage_report=True)
294+
295+
# Expect that the name is set
296+
assert coverage.name() == "XML"
297+
298+
# By construction, each file has the same set
299+
# of covered/uncovered lines
300+
assert self.MANY_VIOLATIONS_EXPANDED_MANY_MEASURED == coverage.violations(
301+
"file1.java"
302+
)
303+
304+
def test_expand_unreported_lines_without_violations(self):
305+
# Construct the XML report
306+
file_paths = ["file1.java"]
307+
# fixture
308+
violations = {}
309+
measured = self.MANY_MEASURED
310+
xml = self._coverage_xml(file_paths, violations, measured)
311+
312+
# Parse the reports
313+
coverage = XmlCoverageReporter([xml], expand_coverage_report=True)
314+
315+
assert set() == coverage.violations("file1.java")
316+
317+
def test_expand_unreported_lines_without_measured(self):
318+
# Construct the XML report
319+
file_paths = ["file1.java"]
320+
# fixture
321+
violations = {}
322+
measured = {}
323+
xml = self._coverage_xml(file_paths, violations, measured)
324+
325+
# Parse the reports
326+
coverage = XmlCoverageReporter([xml], expand_coverage_report=True)
327+
328+
assert set() == coverage.violations("file1.java")
329+
269330
def _coverage_xml(self, file_paths, violations, measured, source_paths=None):
270331
"""
271332
Build an XML tree with source files specified by `file_paths`.

0 commit comments

Comments
 (0)