From e84643a193ac3f2c7bd4963e6aa6b73fbe74cb2c Mon Sep 17 00:00:00 2001 From: matsjoyce-refeyn <103422031+matsjoyce-refeyn@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:09:26 +0000 Subject: [PATCH] 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 --- .../violations_reporter.py | 2 + tests/test_violations_reporter.py | 197 ++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/diff_cover/violationsreporters/violations_reporter.py b/diff_cover/violationsreporters/violations_reporter.py index 15163c3..a06efe0 100644 --- a/diff_cover/violationsreporters/violations_reporter.py +++ b/diff_cover/violationsreporters/violations_reporter.py @@ -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: diff --git a/tests/test_violations_reporter.py b/tests/test_violations_reporter.py index ae08860..2c4be0a 100644 --- a/tests/test_violations_reporter.py +++ b/tests/test_violations_reporter.py @@ -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 @@ -18,6 +19,7 @@ from diff_cover.violationsreporters.violations_reporter import ( CppcheckDriver, EslintDriver, + LcovCoverageReporter, PylintDriver, Violation, XmlCoverageReporter, @@ -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`