Skip to content

Commit 5df1038

Browse files
leandrogianottiLeandro GianottiEmbeddedDevops1
authored
Added support for Jacoco XML parsing (#132)
* 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]>
1 parent 32b2058 commit 5df1038

File tree

2 files changed

+152
-8
lines changed

2 files changed

+152
-8
lines changed

cover_agent/CoverageProcessor.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,16 +208,46 @@ def parse_coverage_report_jacoco(self) -> Tuple[list, list, float]:
208208
lines_covered, lines_missed = [], []
209209

210210
package_name, class_name = self.extract_package_and_class_java()
211-
missed, covered = self.parse_missed_covered_lines_jacoco(
212-
package_name, class_name
213-
)
211+
file_extension = self.get_file_extension(self.file_path)
212+
213+
missed, covered = 0, 0
214+
if file_extension == 'xml':
215+
missed, covered = self.parse_missed_covered_lines_jacoco_xml(
216+
class_name
217+
)
218+
elif file_extension == 'csv':
219+
missed, covered = self.parse_missed_covered_lines_jacoco_csv(
220+
package_name, class_name
221+
)
222+
else:
223+
raise ValueError(f"Unsupported JaCoCo code coverage report format: {file_extension}")
214224

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

218228
return lines_covered, lines_missed, coverage_percentage
219229

220-
def parse_missed_covered_lines_jacoco(
230+
def parse_missed_covered_lines_jacoco_xml(
231+
self, class_name: str
232+
) -> tuple[int, int]:
233+
"""Parses a JaCoCo XML code coverage report to extract covered and missed line numbers for a specific file."""
234+
tree = ET.parse(self.file_path)
235+
root = tree.getroot()
236+
sourcefile = root.find(f".//sourcefile[@name='{class_name}.java']")
237+
238+
if sourcefile is None:
239+
return 0, 0
240+
241+
missed, covered = 0, 0
242+
for counter in sourcefile.findall('counter'):
243+
if counter.attrib.get('type') == 'LINE':
244+
missed += int(counter.attrib.get('missed', 0))
245+
covered += int(counter.attrib.get('covered', 0))
246+
break
247+
248+
return missed, covered
249+
250+
def parse_missed_covered_lines_jacoco_csv(
221251
self, package_name: str, class_name: str
222252
) -> tuple[int, int]:
223253
with open(self.file_path, "r") as file:
@@ -261,3 +291,7 @@ def extract_package_and_class_java(self):
261291
raise
262292

263293
return package_name, class_name
294+
295+
def get_file_extension(self, filename: str) -> str | None:
296+
"""Get the file extension from a given filename."""
297+
return os.path.splitext(filename)[1].lstrip(".")

tests/test_CoverageProcessor.py

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_correct_parsing_for_matching_package_and_class(self, mocker):
7171
)
7272

7373
# Action
74-
missed, covered = processor.parse_missed_covered_lines_jacoco(
74+
missed, covered = processor.parse_missed_covered_lines_jacoco_csv(
7575
"com.example", "MyClass"
7676
)
7777

@@ -86,7 +86,7 @@ def test_returns_empty_lists_and_float(self, mocker):
8686
return_value=("com.example", "Example"),
8787
)
8888
mocker.patch(
89-
"cover_agent.CoverageProcessor.CoverageProcessor.parse_missed_covered_lines_jacoco",
89+
"cover_agent.CoverageProcessor.CoverageProcessor.parse_missed_covered_lines_jacoco_xml",
9090
return_value=(0, 0),
9191
)
9292

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

176-
def test_parse_missed_covered_lines_jacoco_key_error(self, mocker):
176+
def test_parse_missed_covered_lines_jacoco_csv_key_error(self, mocker):
177177
mock_open = mocker.patch(
178178
"builtins.open",
179179
mocker.mock_open(
@@ -192,7 +192,7 @@ def test_parse_missed_covered_lines_jacoco_key_error(self, mocker):
192192
)
193193

194194
with pytest.raises(KeyError):
195-
processor.parse_missed_covered_lines_jacoco("com.example", "MyClass")
195+
processor.parse_missed_covered_lines_jacoco_csv("com.example", "MyClass")
196196

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

251+
def test_parse_coverage_report_unsupported_type(self, mocker):
252+
mocker.patch(
253+
"cover_agent.CoverageProcessor.CoverageProcessor.extract_package_and_class_java",
254+
return_value=("com.example", "Example"),
255+
)
256+
257+
processor = CoverageProcessor(
258+
"path/to/coverage_report.html", "path/to/MyClass.java", "jacoco"
259+
)
260+
with pytest.raises(
261+
ValueError, match="Unsupported JaCoCo code coverage report format: html"
262+
):
263+
processor.parse_coverage_report_jacoco()
264+
265+
def test_parse_missed_covered_lines_jacoco_xml_no_source_file(self, mocker):
266+
#, mock_xml_tree
267+
mocker.patch(
268+
"cover_agent.CoverageProcessor.CoverageProcessor.extract_package_and_class_java",
269+
return_value=("com.example", "Example"),
270+
)
271+
272+
xml_str = """<report>
273+
<package name="path/to">
274+
<sourcefile name="MyClass.java">
275+
<counter type="INSTRUCTION" missed="53" covered="387"/>
276+
<counter type="BRANCH" missed="2" covered="6"/>
277+
<counter type="LINE" missed="9" covered="94"/>
278+
<counter type="COMPLEXITY" missed="5" covered="23"/>
279+
<counter type="METHOD" missed="3" covered="21"/>
280+
<counter type="CLASS" missed="0" covered="1"/>
281+
</sourcefile>
282+
</package>
283+
</report>"""
284+
285+
mocker.patch(
286+
"xml.etree.ElementTree.parse",
287+
return_value=ET.ElementTree(ET.fromstring(xml_str))
288+
)
289+
290+
processor = CoverageProcessor(
291+
"path/to/coverage_report.xml", "path/to/MySecondClass.java", "jacoco"
292+
)
293+
294+
# Action
295+
missed, covered = processor.parse_missed_covered_lines_jacoco_xml(
296+
"MySecondClass"
297+
)
298+
299+
# Assert
300+
assert missed == 0
301+
assert covered == 0
302+
303+
def test_parse_missed_covered_lines_jacoco_xml(self, mocker):
304+
#, mock_xml_tree
305+
mocker.patch(
306+
"cover_agent.CoverageProcessor.CoverageProcessor.extract_package_and_class_java",
307+
return_value=("com.example", "Example"),
308+
)
309+
310+
xml_str = """<report>
311+
<package name="path/to">
312+
<sourcefile name="MyClass.java">
313+
<counter type="INSTRUCTION" missed="53" covered="387"/>
314+
<counter type="BRANCH" missed="2" covered="6"/>
315+
<counter type="LINE" missed="9" covered="94"/>
316+
<counter type="COMPLEXITY" missed="5" covered="23"/>
317+
<counter type="METHOD" missed="3" covered="21"/>
318+
<counter type="CLASS" missed="0" covered="1"/>
319+
</sourcefile>
320+
</package>
321+
</report>"""
322+
323+
mocker.patch(
324+
"xml.etree.ElementTree.parse",
325+
return_value=ET.ElementTree(ET.fromstring(xml_str))
326+
)
327+
328+
processor = CoverageProcessor(
329+
"path/to/coverage_report.xml", "path/to/MyClass.java", "jacoco"
330+
)
331+
332+
# Action
333+
missed, covered = processor.parse_missed_covered_lines_jacoco_xml(
334+
"MyClass"
335+
)
336+
337+
# Assert
338+
assert missed == 9
339+
assert covered == 94
340+
341+
def test_get_file_extension_with_valid_file_extension(self):
342+
processor = CoverageProcessor(
343+
"path/to/coverage_report.xml", "path/to/MyClass.java", "jacoco"
344+
)
345+
346+
file_extension = processor.get_file_extension("coverage_report.xml")
347+
348+
# Assert
349+
assert file_extension == 'xml'
350+
351+
def test_get_file_extension_with_no_file_extension(self):
352+
processor = CoverageProcessor(
353+
"path/to/coverage_report", "path/to/MyClass.java", "jacoco"
354+
)
355+
356+
file_extension = processor.get_file_extension("coverage_report")
357+
358+
# Assert
359+
assert file_extension is ''
360+
251361
def test_parse_coverage_report_lcov_with_feature_flag(self, mocker):
252362
mock_parse_lcov = mocker.patch("cover_agent.CoverageProcessor.CoverageProcessor.parse_coverage_report_lcov", return_value=([], [], 0.0))
253363
processor = CoverageProcessor("fake_path", "app.py", "lcov", use_report_coverage_feature_flag=True)

0 commit comments

Comments
 (0)