Skip to content

Commit

Permalink
Fix files only covered by one LCOV report showing 100% coverage (#433)
Browse files Browse the repository at this point in the history
* Fix files only covered by one LCOV report showing 100% coverage

* Add LCOV reporter tests
  • Loading branch information
matsjoyce-refeyn authored Jan 28, 2025
1 parent 33f78b2 commit e84643a
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 0 deletions.
2 changes: 2 additions & 0 deletions diff_cover/violationsreporters/violations_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ def _cache_file(self, src_path):
src_search_path = src_abs_path
if src_search_path not in lcov_document:
src_search_path = src_rel_path
if src_search_path not in lcov_document:
continue

# First case, need to define violations initially
if violations is None:
Expand Down
197 changes: 197 additions & 0 deletions tests/test_violations_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import os
import subprocess
import tempfile
import xml.etree.ElementTree as etree
from io import BytesIO, StringIO
from subprocess import Popen
Expand All @@ -18,6 +19,7 @@
from diff_cover.violationsreporters.violations_reporter import (
CppcheckDriver,
EslintDriver,
LcovCoverageReporter,
PylintDriver,
Violation,
XmlCoverageReporter,
Expand Down Expand Up @@ -794,6 +796,201 @@ def _coverage_xml(self, file_paths, violations, measured):
return root


class TestLcovCoverageReporterTest:
MANY_VIOLATIONS = {
Violation(3, None),
Violation(7, None),
Violation(11, None),
Violation(13, None),
}
FEW_MEASURED = {2, 3, 5, 7, 11, 13}

FEW_VIOLATIONS = {Violation(3, None), Violation(11, None)}
MANY_MEASURED = {2, 3, 5, 7, 11, 13, 17}

ONE_VIOLATION = {Violation(11, None)}
VERY_MANY_MEASURED = {2, 3, 5, 7, 11, 13, 17, 23, 24, 25, 26, 26, 27}

@pytest.fixture(autouse=True)
def patch_git_patch(self, mocker):
# Paths generated by git_path are always the given argument
_git_path_mock = mocker.patch(
"diff_cover.violationsreporters.violations_reporter.GitPathTool"
)
_git_path_mock.relative_path = lambda path: path
_git_path_mock.absolute_path = lambda path: path

def test_violations(self):
# Construct the LCOV report
file_paths = ["file1.java", "subdir/file2.java"]
violations = self.MANY_VIOLATIONS
measured = self.FEW_MEASURED
lcov = self._coverage_lcov(file_paths, violations, measured)

# Parse the report
coverage = LcovCoverageReporter([lcov])

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

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

# Try getting a smaller range
result = coverage.violations("subdir/file2.java")
assert result == violations

# Once more on the first file (for caching)
result = coverage.violations("file1.java")
assert result == violations

def test_two_inputs_first_violate(self):
# Construct the LCOV report
file_paths = ["file1.java"]

violations1 = self.MANY_VIOLATIONS
violations2 = self.FEW_VIOLATIONS

measured1 = self.FEW_MEASURED
measured2 = self.MANY_MEASURED

lcov = self._coverage_lcov(file_paths, violations1, measured1)
lcov2 = self._coverage_lcov(file_paths, violations2, measured2)

# Parse the report
coverage = LcovCoverageReporter([lcov, lcov2])

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

assert measured1 | measured2 == coverage.measured_lines("file1.java")

def test_two_inputs_second_violate(self):
# Construct the LCOV report
file_paths = ["file1.java"]

violations1 = self.MANY_VIOLATIONS
violations2 = self.FEW_VIOLATIONS

measured1 = self.FEW_MEASURED
measured2 = self.MANY_MEASURED

lcov = self._coverage_lcov(file_paths, violations1, measured1)
lcov2 = self._coverage_lcov(file_paths, violations2, measured2)

# Parse the report
coverage = LcovCoverageReporter([lcov2, lcov])

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

assert measured1 | measured2 == coverage.measured_lines("file1.java")

def test_three_inputs(self):
# Construct the LCOV report
file_paths = ["file1.java"]

violations1 = self.MANY_VIOLATIONS
violations2 = self.FEW_VIOLATIONS
violations3 = self.ONE_VIOLATION

measured1 = self.FEW_MEASURED
measured2 = self.MANY_MEASURED
measured3 = self.VERY_MANY_MEASURED

lcov = self._coverage_lcov(file_paths, violations1, measured1)
lcov2 = self._coverage_lcov(file_paths, violations2, measured2)
lcov3 = self._coverage_lcov(file_paths, violations3, measured3)

# Parse the report
coverage = LcovCoverageReporter([lcov2, lcov, lcov3])

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

assert measured1 | measured2 | measured3 == coverage.measured_lines(
"file1.java"
)

def test_different_files_in_inputs(self):
# Construct the LCOV report
lcov_repots = [
self._coverage_lcov(["file.java"], self.MANY_VIOLATIONS, self.FEW_MEASURED),
self._coverage_lcov(
["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED
),
]

# Parse the report
coverage = LcovCoverageReporter(lcov_repots)

assert self.MANY_VIOLATIONS == coverage.violations("file.java")
assert self.FEW_VIOLATIONS == coverage.violations("other_file.java")

def test_empty_violations(self):
"""
Test that an empty violations report is handled properly
"""
# Construct the LCOV report
file_paths = ["file1.java"]

violations1 = self.MANY_VIOLATIONS
violations2 = set()

measured1 = self.FEW_MEASURED
measured2 = self.MANY_MEASURED

lcov = self._coverage_lcov(file_paths, violations1, measured1)
lcov2 = self._coverage_lcov(file_paths, violations2, measured2)

# Parse the report
coverage = LcovCoverageReporter([lcov2, lcov])

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

assert measured1 | measured2 == coverage.measured_lines("file1.java")

def test_no_such_file(self):
# Construct the LCOV report with no source files
lcov = self._coverage_lcov([], [], [])

# Parse the report
coverage = LcovCoverageReporter(lcov)

# Expect that we get no results
result = coverage.violations("file.java")
assert result == set()

def _coverage_lcov(self, file_paths, violations, measured):
"""
Build an LCOV document based on the provided arguments.
"""

violation_lines = {violation.line for violation in violations}

with tempfile.NamedTemporaryFile("w", delete=False) as f:
for file_path in file_paths:
f.write(f"SF:{file_path}\n")
for line_num in measured:
f.write(
f"DA:{line_num},{0 if line_num in violation_lines else 1}\n"
)
f.write("end_of_record\n")
try:
return LcovCoverageReporter.parse(f.name)
finally:
os.unlink(f.name)


class TestPycodestyleQualityReporterTest:
def test_quality(self, mocker, process_patcher):
# Patch the output of `pycodestyle`
Expand Down

0 comments on commit e84643a

Please sign in to comment.