Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: run integration #265

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/nightly_regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: Nightly Integration Tests
# Define the events that will trigger this workflow
on:
# Schedule the workflow to run nightly at midnight
pull_request:
branches:
- main
schedule:
- cron: '0 0 * * *'
# Allow manual triggering of the workflow
Expand Down
79 changes: 65 additions & 14 deletions cover_agent/coverage/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,21 @@ def parse_coverage_report(self) -> Dict[str, CoverageData]:
for cls in root.findall(".//class"):
cls_filename = cls.get("filename")
if cls_filename:
coverage[cls_filename] = self._parse_coverage_data_for_class(cls)
if cls_filename not in coverage:
coverage[cls_filename] = self._parse_coverage_data_for_class(cls)
else:
coverage[cls_filename] = self._merge_coverage_data(coverage[cls_filename], self._parse_coverage_data_for_class(cls))
return coverage

def _merge_coverage_data(self, existing_coverage: CoverageData, new_coverage: CoverageData) -> CoverageData:
covered_lines = existing_coverage.covered_lines + new_coverage.covered_lines
missed_lines = existing_coverage.missed_lines + new_coverage.missed_lines
covered = existing_coverage.covered + new_coverage.covered
missed = existing_coverage.missed + new_coverage.missed
total_lines = covered + missed
coverage_percentage = (float(covered) / total_lines) if total_lines > 0 else 0.0
return CoverageData(covered_lines, covered, missed_lines, missed, coverage_percentage)

def _parse_coverage_data_for_class(self, cls) -> CoverageData:
lines_covered, lines_missed = [], []
for line in cls.findall(".//line"):
Expand Down Expand Up @@ -223,22 +235,58 @@ class JacocoProcessor(CoverageProcessor):
"""
def parse_coverage_report(self) -> Dict[str, CoverageData]:
coverage = {}
package_name, class_name = self._extract_package_and_class_java()
source_file_extension = self._get_file_extension(self.src_file_path)

package_name, class_name = "",""
if source_file_extension == 'java':
package_name, class_name = self._extract_package_and_class_java()
elif source_file_extension == 'kt':
package_name, class_name = self._extract_package_and_class_kotlin()
else:
self.logger.warn(f"Unsupported Bytecode Language: {source_file_extension}. Using default Java logic.")
package_name, class_name = self.extract_package_and_class_java()

file_extension = self._get_file_extension(self.file_path)

if file_extension == 'xml':
missed, covered = self._parse_jacoco_xml(class_name=class_name)
lines_missed, lines_covered = self._parse_jacoco_xml(class_name=class_name)
missed, covered = len(lines_missed), len(lines_covered)
elif file_extension == 'csv':
lines_missed, lines_covered = [], []
missed, covered = self._parse_jacoco_csv(package_name=package_name, class_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.0
coverage[class_name] = CoverageData(covered_lines=[], covered=covered, missed_lines=[], missed=missed, coverage=coverage_percentage)
coverage[class_name] = CoverageData(covered_lines=lines_covered, covered=covered, missed_lines=lines_missed, missed=missed, coverage=coverage_percentage)
return coverage

def _get_file_extension(self, filename: str) -> str | None:
"""Get the file extension from a given filename."""
return os.path.splitext(filename)[1].lstrip(".")

def _extract_package_and_class_kotlin(self):
package_pattern = re.compile(r"^\s*package\s+([\w.]+)\s*(?:;)?\s*(?://.*)?$")
class_pattern = re.compile(r"^\s*(?:public|internal|abstract|data|sealed|enum|open|final|private|protected)*\s*class\s+(\w+).*")
package_name = ""
class_name = ""
try:
with open(self.src_file_path, "r") as file:
for line in file:
if not package_name: # Only match package if not already found
package_match = package_pattern.match(line)
if package_match:
package_name = package_match.group(1)
if not class_name: # Only match class if not already found
class_match = class_pattern.match(line)
if class_match:
class_name = class_match.group(1)
if package_name and class_name: # Exit loop if both are found
break
except (FileNotFoundError, IOError) as e:
self.logger.error(f"Error reading file {self.src_file_path}: {e}")
raise
return package_name, class_name

def _extract_package_and_class_java(self):
package_pattern = re.compile(r"^\s*package\s+([\w\.]+)\s*;.*$")
Expand Down Expand Up @@ -269,21 +317,24 @@ def _extract_package_and_class_java(self):

def _parse_jacoco_xml(
self, class_name: str
) -> tuple[int, int]:
) -> tuple[list, list]:
"""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']")
sourcefile = (
root.find(f".//sourcefile[@name='{class_name}.java']") or
root.find(f".//sourcefile[@name='{class_name}.kt']")
)

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 = [], []
for line in sourcefile.findall('line'):
if line.attrib.get('mi') == '0':
covered += [int(line.attrib.get('nr', 0))]
else :
missed += [int(line.attrib.get('nr', 0))]

return missed, covered
def _parse_jacoco_csv(self, package_name, class_name) -> Dict[str, CoverageData]:
Expand Down
162 changes: 155 additions & 7 deletions tests/coverage/test_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ def mock_parse(file_path):
<line number="2" hits="0"/>
</lines>
</class>
<class filename="app.py">
<lines>
<line number="3" hits="1"/>
<line number="4" hits="0"/>
</lines>
</class>
</classes>
</package>
</packages>
Expand Down Expand Up @@ -189,10 +195,10 @@ def test_parse_coverage_report_cobertura(self, mock_xml_tree, processor):
"""
coverage = processor.parse_coverage_report()
assert len(coverage) == 1, "Expected coverage data for one file"
assert coverage["app.py"].covered_lines == [1], "Should list line 1 as covered"
assert coverage["app.py"].covered == 1, "Should have 1 line as covered"
assert coverage["app.py"].missed_lines == [2], "Should list line 2 as missed"
assert coverage["app.py"].missed == 1, "Should have 1 line as missed"
assert coverage["app.py"].covered_lines == [1, 3], "Should list lines 1 and 3 as covered"
assert coverage["app.py"].covered == 2, "Should have 2 line as covered"
assert coverage["app.py"].missed_lines == [2, 4], "Should list lines 2 and 4 as missed"
assert coverage["app.py"].missed == 2, "Should have 2 line as missed"
assert coverage["app.py"].coverage == 0.5, "Coverage should be 50 percent"

class TestLcovProcessor:
Expand Down Expand Up @@ -272,9 +278,10 @@ def test_parse_xml_coverage_report_success(self, mocker):
# Assert
assert len(coverage_data) == 1
assert 'MyClass' in coverage_data
assert coverage_data['MyClass'].missed == 5
assert coverage_data['MyClass'].covered == 15
assert coverage_data['MyClass'].coverage == 0.75
# should not include <counter type="LINE" missed="5" covered="15"/>
assert coverage_data['MyClass'].missed == 0
assert coverage_data['MyClass'].covered == 0
assert coverage_data['MyClass'].coverage == 0

# Handle empty or malformed XML/CSV coverage reports
def test_parse_empty_xml_coverage_report(self, mocker):
Expand All @@ -301,6 +308,147 @@ def test_parse_empty_xml_coverage_report(self, mocker):
assert coverage_data['MyClass'].covered == 0
assert coverage_data['MyClass'].coverage == 0.0

def test_returns_empty_lists_and_float(self, mocker):
# Mocking the necessary methods
mocker.patch(
"cover_agent.coverage.processor.JacocoProcessor._extract_package_and_class_java",
return_value=("com.example", "Example"),
)
mocker.patch(
"cover_agent.coverage.processor.JacocoProcessor._parse_jacoco_xml",
return_value=([], []),
)

# Initialize the CoverageProcessor object
coverage_processor = JacocoProcessor(
file_path="path/to/coverage.xml",
src_file_path="path/to/example.java",
)

# Invoke the parse_coverage_report_jacoco method
coverageData = coverage_processor.parse_coverage_report()

# Assert the results
assert coverageData["Example"].covered_lines == [], "Expected covered_lines to be an empty list"
assert coverageData["Example"].missed_lines == [], "Expected missed_lines to be an empty list"
assert coverageData["Example"].coverage == 0, "Expected coverage percentage to be 0"

def test_parse_missed_covered_lines_jacoco_xml_no_source_file(self, mocker):
#, mock_xml_tree
mocker.patch(
"cover_agent.coverage.processor.JacocoProcessor._extract_package_and_class_java",
return_value=("com.example", "MyClass"),
)
xml_str = """<?xml version="1.0" encoding="UTF-8"?>
<report>
<package name="path/to">
<sourcefile name="MyClass.java">
<line nr="35" mi="0" ci="9" mb="0" cb="0"/>
<line nr="36" mi="0" ci="1" mb="0" cb="0"/>
<line nr="37" mi="0" ci="3" mb="0" cb="0"/>
<line nr="38" mi="0" ci="9" mb="0" cb="0"/>
<line nr="39" mi="1" ci="0" mb="0" cb="0"/>
<line nr="40" mi="5" ci="0" mb="0" cb="0"/>
<line nr="41" mi="9" ci="0" mb="0" cb="0"/>
<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 = JacocoProcessor("path/to/coverage_report.xml", "path/to/MySecondClass.java")

# Action
coverage_data = processor.parse_coverage_report()

# Assert
assert 'MySecondClass' not in coverage_data

def test_parse_missed_covered_lines_jacoco_xml(self, mocker):
#, mock_xml_tree
mocker.patch(
"cover_agent.coverage.processor.JacocoProcessor._extract_package_and_class_java",
return_value=("com.example", "MyClass"),
)
xml_str = """<report>
<package name="path/to">
<sourcefile name="MyClass.java">
<line nr="35" mi="0" ci="9" mb="0" cb="0"/>
<line nr="36" mi="0" ci="1" mb="0" cb="0"/>
<line nr="37" mi="0" ci="3" mb="0" cb="0"/>
<line nr="38" mi="0" ci="9" mb="0" cb="0"/>
<line nr="39" mi="1" ci="0" mb="0" cb="0"/>
<line nr="40" mi="5" ci="0" mb="0" cb="0"/>
<line nr="41" mi="9" ci="0" mb="0" cb="0"/>
<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 = JacocoProcessor("path/to/coverage_report.xml", "path/to/MyClass.java")

# Action
coverage_data = processor.parse_coverage_report()

# Assert
assert "MyClass" in coverage_data
assert coverage_data["MyClass"].missed_lines == [39, 40, 41]
assert coverage_data["MyClass"].covered_lines == [35, 36, 37, 38]

def test_parse_missed_covered_lines_kotlin_jacoco_xml(self, mocker):
#, mock_xml_tree
mocker.patch(
"cover_agent.coverage.processor.JacocoProcessor._extract_package_and_class_kotlin",
return_value=("com.example", "MyClass"),
)
xml_str = """<report>
<package name="path/to">
<sourcefile name="MyClass.kt">
<line nr="35" mi="0" ci="9" mb="0" cb="0"/>
<line nr="36" mi="0" ci="1" mb="0" cb="0"/>
<line nr="37" mi="0" ci="3" mb="0" cb="0"/>
<line nr="38" mi="0" ci="9" mb="0" cb="0"/>
<line nr="39" mi="1" ci="0" mb="0" cb="0"/>
<line nr="40" mi="5" ci="0" mb="0" cb="0"/>
<line nr="41" mi="9" ci="0" mb="0" cb="0"/>
<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 = JacocoProcessor("path/to/coverage_report.xml", "path/to/MyClass.kt")

# Action
coverage_data = processor.parse_coverage_report()

# Assert
assert "MyClass" in coverage_data
assert coverage_data["MyClass"].missed_lines == [39, 40, 41]
assert coverage_data["MyClass"].covered_lines == [35, 36, 37, 38]

class TestDiffCoverageProcessor:
# Successfully parse JSON diff coverage report and extract coverage data for matching file path
def test_parse_coverage_report_with_matching_file(self, mocker):
Expand Down
Loading