Skip to content

Commit

Permalink
Added support for Jacoco XML parsing (#132)
Browse files Browse the repository at this point in the history
* Added support for Jacoco XML parsing

* fixing UTs

* Use os library to extract out a path name and adding UTs

---------

Co-authored-by: Leandro Gianotti <[email protected]>
Co-authored-by: Embedded DevOps <[email protected]>
  • Loading branch information
3 people authored Oct 8, 2024
1 parent 32b2058 commit 5df1038
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 8 deletions.
42 changes: 38 additions & 4 deletions cover_agent/CoverageProcessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,16 +208,46 @@ def parse_coverage_report_jacoco(self) -> Tuple[list, list, float]:
lines_covered, lines_missed = [], []

package_name, class_name = self.extract_package_and_class_java()
missed, covered = self.parse_missed_covered_lines_jacoco(
package_name, class_name
)
file_extension = self.get_file_extension(self.file_path)

missed, covered = 0, 0
if file_extension == 'xml':
missed, covered = self.parse_missed_covered_lines_jacoco_xml(
class_name
)
elif file_extension == 'csv':
missed, covered = self.parse_missed_covered_lines_jacoco_csv(
package_name, class_name
)
else:
raise ValueError(f"Unsupported JaCoCo code coverage report format: {file_extension}")

total_lines = missed + covered
coverage_percentage = (float(covered) / total_lines) if total_lines > 0 else 0

return lines_covered, lines_missed, coverage_percentage

def parse_missed_covered_lines_jacoco(
def parse_missed_covered_lines_jacoco_xml(
self, class_name: str
) -> tuple[int, int]:
"""Parses a JaCoCo XML code coverage report to extract covered and missed line numbers for a specific file."""
tree = ET.parse(self.file_path)
root = tree.getroot()
sourcefile = root.find(f".//sourcefile[@name='{class_name}.java']")

if sourcefile is None:
return 0, 0

missed, covered = 0, 0
for counter in sourcefile.findall('counter'):
if counter.attrib.get('type') == 'LINE':
missed += int(counter.attrib.get('missed', 0))
covered += int(counter.attrib.get('covered', 0))
break

return missed, covered

def parse_missed_covered_lines_jacoco_csv(
self, package_name: str, class_name: str
) -> tuple[int, int]:
with open(self.file_path, "r") as file:
Expand Down Expand Up @@ -261,3 +291,7 @@ def extract_package_and_class_java(self):
raise

return package_name, class_name

def get_file_extension(self, filename: str) -> str | None:
"""Get the file extension from a given filename."""
return os.path.splitext(filename)[1].lstrip(".")
118 changes: 114 additions & 4 deletions tests/test_CoverageProcessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def test_correct_parsing_for_matching_package_and_class(self, mocker):
)

# Action
missed, covered = processor.parse_missed_covered_lines_jacoco(
missed, covered = processor.parse_missed_covered_lines_jacoco_csv(
"com.example", "MyClass"
)

Expand All @@ -86,7 +86,7 @@ def test_returns_empty_lists_and_float(self, mocker):
return_value=("com.example", "Example"),
)
mocker.patch(
"cover_agent.CoverageProcessor.CoverageProcessor.parse_missed_covered_lines_jacoco",
"cover_agent.CoverageProcessor.CoverageProcessor.parse_missed_covered_lines_jacoco_xml",
return_value=(0, 0),
)

Expand Down Expand Up @@ -173,7 +173,7 @@ def test_process_coverage_report(self, mocker):
mock_parse.assert_called_once()
assert result == ([], [], 0.0), "Expected result to be ([], [], 0.0)"

def test_parse_missed_covered_lines_jacoco_key_error(self, mocker):
def test_parse_missed_covered_lines_jacoco_csv_key_error(self, mocker):
mock_open = mocker.patch(
"builtins.open",
mocker.mock_open(
Expand All @@ -192,7 +192,7 @@ def test_parse_missed_covered_lines_jacoco_key_error(self, mocker):
)

with pytest.raises(KeyError):
processor.parse_missed_covered_lines_jacoco("com.example", "MyClass")
processor.parse_missed_covered_lines_jacoco_csv("com.example", "MyClass")

def test_parse_coverage_report_lcov_no_coverage_data(self, mocker):
"""
Expand Down Expand Up @@ -248,6 +248,116 @@ def test_parse_coverage_report_lcov_with_multiple_files(self, mocker):
assert missed_lines == [2], "Expected line 2 to be missed for app.py"
assert coverage_pct == 2/3, "Expected 66.67% coverage for app.py"

def test_parse_coverage_report_unsupported_type(self, mocker):
mocker.patch(
"cover_agent.CoverageProcessor.CoverageProcessor.extract_package_and_class_java",
return_value=("com.example", "Example"),
)

processor = CoverageProcessor(
"path/to/coverage_report.html", "path/to/MyClass.java", "jacoco"
)
with pytest.raises(
ValueError, match="Unsupported JaCoCo code coverage report format: html"
):
processor.parse_coverage_report_jacoco()

def test_parse_missed_covered_lines_jacoco_xml_no_source_file(self, mocker):
#, mock_xml_tree
mocker.patch(
"cover_agent.CoverageProcessor.CoverageProcessor.extract_package_and_class_java",
return_value=("com.example", "Example"),
)

xml_str = """<report>
<package name="path/to">
<sourcefile name="MyClass.java">
<counter type="INSTRUCTION" missed="53" covered="387"/>
<counter type="BRANCH" missed="2" covered="6"/>
<counter type="LINE" missed="9" covered="94"/>
<counter type="COMPLEXITY" missed="5" covered="23"/>
<counter type="METHOD" missed="3" covered="21"/>
<counter type="CLASS" missed="0" covered="1"/>
</sourcefile>
</package>
</report>"""

mocker.patch(
"xml.etree.ElementTree.parse",
return_value=ET.ElementTree(ET.fromstring(xml_str))
)

processor = CoverageProcessor(
"path/to/coverage_report.xml", "path/to/MySecondClass.java", "jacoco"
)

# Action
missed, covered = processor.parse_missed_covered_lines_jacoco_xml(
"MySecondClass"
)

# Assert
assert missed == 0
assert covered == 0

def test_parse_missed_covered_lines_jacoco_xml(self, mocker):
#, mock_xml_tree
mocker.patch(
"cover_agent.CoverageProcessor.CoverageProcessor.extract_package_and_class_java",
return_value=("com.example", "Example"),
)

xml_str = """<report>
<package name="path/to">
<sourcefile name="MyClass.java">
<counter type="INSTRUCTION" missed="53" covered="387"/>
<counter type="BRANCH" missed="2" covered="6"/>
<counter type="LINE" missed="9" covered="94"/>
<counter type="COMPLEXITY" missed="5" covered="23"/>
<counter type="METHOD" missed="3" covered="21"/>
<counter type="CLASS" missed="0" covered="1"/>
</sourcefile>
</package>
</report>"""

mocker.patch(
"xml.etree.ElementTree.parse",
return_value=ET.ElementTree(ET.fromstring(xml_str))
)

processor = CoverageProcessor(
"path/to/coverage_report.xml", "path/to/MyClass.java", "jacoco"
)

# Action
missed, covered = processor.parse_missed_covered_lines_jacoco_xml(
"MyClass"
)

# Assert
assert missed == 9
assert covered == 94

def test_get_file_extension_with_valid_file_extension(self):
processor = CoverageProcessor(
"path/to/coverage_report.xml", "path/to/MyClass.java", "jacoco"
)

file_extension = processor.get_file_extension("coverage_report.xml")

# Assert
assert file_extension == 'xml'

def test_get_file_extension_with_no_file_extension(self):
processor = CoverageProcessor(
"path/to/coverage_report", "path/to/MyClass.java", "jacoco"
)

file_extension = processor.get_file_extension("coverage_report")

# Assert
assert file_extension is ''

def test_parse_coverage_report_lcov_with_feature_flag(self, mocker):
mock_parse_lcov = mocker.patch("cover_agent.CoverageProcessor.CoverageProcessor.parse_coverage_report_lcov", return_value=([], [], 0.0))
processor = CoverageProcessor("fake_path", "app.py", "lcov", use_report_coverage_feature_flag=True)
Expand Down

0 comments on commit 5df1038

Please sign in to comment.