Skip to content

Commit e84643a

Browse files
Fix files only covered by one LCOV report showing 100% coverage (#433)
* Fix files only covered by one LCOV report showing 100% coverage * Add LCOV reporter tests
1 parent 33f78b2 commit e84643a

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

diff_cover/violationsreporters/violations_reporter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ def _cache_file(self, src_path):
392392
src_search_path = src_abs_path
393393
if src_search_path not in lcov_document:
394394
src_search_path = src_rel_path
395+
if src_search_path not in lcov_document:
396+
continue
395397

396398
# First case, need to define violations initially
397399
if violations is None:

tests/test_violations_reporter.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import os
77
import subprocess
8+
import tempfile
89
import xml.etree.ElementTree as etree
910
from io import BytesIO, StringIO
1011
from subprocess import Popen
@@ -18,6 +19,7 @@
1819
from diff_cover.violationsreporters.violations_reporter import (
1920
CppcheckDriver,
2021
EslintDriver,
22+
LcovCoverageReporter,
2123
PylintDriver,
2224
Violation,
2325
XmlCoverageReporter,
@@ -794,6 +796,201 @@ def _coverage_xml(self, file_paths, violations, measured):
794796
return root
795797

796798

799+
class TestLcovCoverageReporterTest:
800+
MANY_VIOLATIONS = {
801+
Violation(3, None),
802+
Violation(7, None),
803+
Violation(11, None),
804+
Violation(13, None),
805+
}
806+
FEW_MEASURED = {2, 3, 5, 7, 11, 13}
807+
808+
FEW_VIOLATIONS = {Violation(3, None), Violation(11, None)}
809+
MANY_MEASURED = {2, 3, 5, 7, 11, 13, 17}
810+
811+
ONE_VIOLATION = {Violation(11, None)}
812+
VERY_MANY_MEASURED = {2, 3, 5, 7, 11, 13, 17, 23, 24, 25, 26, 26, 27}
813+
814+
@pytest.fixture(autouse=True)
815+
def patch_git_patch(self, mocker):
816+
# Paths generated by git_path are always the given argument
817+
_git_path_mock = mocker.patch(
818+
"diff_cover.violationsreporters.violations_reporter.GitPathTool"
819+
)
820+
_git_path_mock.relative_path = lambda path: path
821+
_git_path_mock.absolute_path = lambda path: path
822+
823+
def test_violations(self):
824+
# Construct the LCOV report
825+
file_paths = ["file1.java", "subdir/file2.java"]
826+
violations = self.MANY_VIOLATIONS
827+
measured = self.FEW_MEASURED
828+
lcov = self._coverage_lcov(file_paths, violations, measured)
829+
830+
# Parse the report
831+
coverage = LcovCoverageReporter([lcov])
832+
833+
# Expect that the name is set
834+
assert coverage.name() == "LCOV"
835+
836+
# By construction, each file has the same set
837+
# of covered/uncovered lines
838+
assert violations == coverage.violations("file1.java")
839+
assert measured == coverage.measured_lines("file1.java")
840+
841+
# Try getting a smaller range
842+
result = coverage.violations("subdir/file2.java")
843+
assert result == violations
844+
845+
# Once more on the first file (for caching)
846+
result = coverage.violations("file1.java")
847+
assert result == violations
848+
849+
def test_two_inputs_first_violate(self):
850+
# Construct the LCOV report
851+
file_paths = ["file1.java"]
852+
853+
violations1 = self.MANY_VIOLATIONS
854+
violations2 = self.FEW_VIOLATIONS
855+
856+
measured1 = self.FEW_MEASURED
857+
measured2 = self.MANY_MEASURED
858+
859+
lcov = self._coverage_lcov(file_paths, violations1, measured1)
860+
lcov2 = self._coverage_lcov(file_paths, violations2, measured2)
861+
862+
# Parse the report
863+
coverage = LcovCoverageReporter([lcov, lcov2])
864+
865+
# By construction, each file has the same set
866+
# of covered/uncovered lines
867+
assert violations1 & violations2 == coverage.violations("file1.java")
868+
869+
assert measured1 | measured2 == coverage.measured_lines("file1.java")
870+
871+
def test_two_inputs_second_violate(self):
872+
# Construct the LCOV report
873+
file_paths = ["file1.java"]
874+
875+
violations1 = self.MANY_VIOLATIONS
876+
violations2 = self.FEW_VIOLATIONS
877+
878+
measured1 = self.FEW_MEASURED
879+
measured2 = self.MANY_MEASURED
880+
881+
lcov = self._coverage_lcov(file_paths, violations1, measured1)
882+
lcov2 = self._coverage_lcov(file_paths, violations2, measured2)
883+
884+
# Parse the report
885+
coverage = LcovCoverageReporter([lcov2, lcov])
886+
887+
# By construction, each file has the same set
888+
# of covered/uncovered lines
889+
assert violations1 & violations2 == coverage.violations("file1.java")
890+
891+
assert measured1 | measured2 == coverage.measured_lines("file1.java")
892+
893+
def test_three_inputs(self):
894+
# Construct the LCOV report
895+
file_paths = ["file1.java"]
896+
897+
violations1 = self.MANY_VIOLATIONS
898+
violations2 = self.FEW_VIOLATIONS
899+
violations3 = self.ONE_VIOLATION
900+
901+
measured1 = self.FEW_MEASURED
902+
measured2 = self.MANY_MEASURED
903+
measured3 = self.VERY_MANY_MEASURED
904+
905+
lcov = self._coverage_lcov(file_paths, violations1, measured1)
906+
lcov2 = self._coverage_lcov(file_paths, violations2, measured2)
907+
lcov3 = self._coverage_lcov(file_paths, violations3, measured3)
908+
909+
# Parse the report
910+
coverage = LcovCoverageReporter([lcov2, lcov, lcov3])
911+
912+
# By construction, each file has the same set
913+
# of covered/uncovered lines
914+
assert violations1 & violations2 & violations3 == coverage.violations(
915+
"file1.java"
916+
)
917+
918+
assert measured1 | measured2 | measured3 == coverage.measured_lines(
919+
"file1.java"
920+
)
921+
922+
def test_different_files_in_inputs(self):
923+
# Construct the LCOV report
924+
lcov_repots = [
925+
self._coverage_lcov(["file.java"], self.MANY_VIOLATIONS, self.FEW_MEASURED),
926+
self._coverage_lcov(
927+
["other_file.java"], self.FEW_VIOLATIONS, self.MANY_MEASURED
928+
),
929+
]
930+
931+
# Parse the report
932+
coverage = LcovCoverageReporter(lcov_repots)
933+
934+
assert self.MANY_VIOLATIONS == coverage.violations("file.java")
935+
assert self.FEW_VIOLATIONS == coverage.violations("other_file.java")
936+
937+
def test_empty_violations(self):
938+
"""
939+
Test that an empty violations report is handled properly
940+
"""
941+
# Construct the LCOV report
942+
file_paths = ["file1.java"]
943+
944+
violations1 = self.MANY_VIOLATIONS
945+
violations2 = set()
946+
947+
measured1 = self.FEW_MEASURED
948+
measured2 = self.MANY_MEASURED
949+
950+
lcov = self._coverage_lcov(file_paths, violations1, measured1)
951+
lcov2 = self._coverage_lcov(file_paths, violations2, measured2)
952+
953+
# Parse the report
954+
coverage = LcovCoverageReporter([lcov2, lcov])
955+
956+
# By construction, each file has the same set
957+
# of covered/uncovered lines
958+
assert violations1 & violations2 == coverage.violations("file1.java")
959+
960+
assert measured1 | measured2 == coverage.measured_lines("file1.java")
961+
962+
def test_no_such_file(self):
963+
# Construct the LCOV report with no source files
964+
lcov = self._coverage_lcov([], [], [])
965+
966+
# Parse the report
967+
coverage = LcovCoverageReporter(lcov)
968+
969+
# Expect that we get no results
970+
result = coverage.violations("file.java")
971+
assert result == set()
972+
973+
def _coverage_lcov(self, file_paths, violations, measured):
974+
"""
975+
Build an LCOV document based on the provided arguments.
976+
"""
977+
978+
violation_lines = {violation.line for violation in violations}
979+
980+
with tempfile.NamedTemporaryFile("w", delete=False) as f:
981+
for file_path in file_paths:
982+
f.write(f"SF:{file_path}\n")
983+
for line_num in measured:
984+
f.write(
985+
f"DA:{line_num},{0 if line_num in violation_lines else 1}\n"
986+
)
987+
f.write("end_of_record\n")
988+
try:
989+
return LcovCoverageReporter.parse(f.name)
990+
finally:
991+
os.unlink(f.name)
992+
993+
797994
class TestPycodestyleQualityReporterTest:
798995
def test_quality(self, mocker, process_patcher):
799996
# Patch the output of `pycodestyle`

0 commit comments

Comments
 (0)