Skip to content

Commit c542b03

Browse files
misrasaurabh1claude
andcommitted
feat: add JaCoCo test coverage support for Java optimization
- Add JaCoCo Maven plugin management to build_tools.py: - is_jacoco_configured() to check if plugin exists - add_jacoco_plugin_to_pom() to inject plugin configuration - get_jacoco_xml_path() for coverage report location - Add JacocoCoverageUtils class to coverage_utils.py: - Parses JaCoCo XML reports into CoverageData objects - Handles method boundary detection and line/branch coverage - Update test_runner.py to support coverage collection: - run_behavioral_tests() now handles enable_coverage=True - Automatically adds JaCoCo plugin and runs jacoco:report goal - Update critic.py to enforce 60% coverage threshold for Java (previously Java was bypassed) - Add comprehensive test suite with 19 tests for coverage functionality Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 77cddec commit c542b03

5 files changed

Lines changed: 839 additions & 35 deletions

File tree

codeflash/languages/java/build_tools.py

Lines changed: 163 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
from dataclasses import dataclass
1515
from enum import Enum
1616
from pathlib import Path
17-
from typing import TYPE_CHECKING, Any
18-
19-
if TYPE_CHECKING:
20-
pass
2117

2218
logger = logging.getLogger(__name__)
2319

@@ -198,23 +194,23 @@ def _extract_java_version_from_pom(root: ET.Element, ns: dict[str, str]) -> str
198194
"""
199195
# Check properties
200196
for prop_name in ("maven.compiler.source", "java.version", "maven.compiler.release"):
201-
for props in [root.find(f"m:properties", ns), root.find("properties")]:
197+
for props in [root.find("m:properties", ns), root.find("properties")]:
202198
if props is not None:
203199
for prop in [props.find(f"m:{prop_name}", ns), props.find(prop_name)]:
204200
if prop is not None and prop.text:
205201
return prop.text
206202

207203
# Check compiler plugin configuration
208-
for build in [root.find(f"m:build", ns), root.find("build")]:
204+
for build in [root.find("m:build", ns), root.find("build")]:
209205
if build is not None:
210-
for plugins in [build.find(f"m:plugins", ns), build.find("plugins")]:
206+
for plugins in [build.find("m:plugins", ns), build.find("plugins")]:
211207
if plugins is not None:
212-
for plugin in plugins.findall(f"m:plugin", ns) + plugins.findall("plugin"):
213-
artifact_id = plugin.find(f"m:artifactId", ns) or plugin.find("artifactId")
208+
for plugin in plugins.findall("m:plugin", ns) + plugins.findall("plugin"):
209+
artifact_id = plugin.find("m:artifactId", ns) or plugin.find("artifactId")
214210
if artifact_id is not None and artifact_id.text == "maven-compiler-plugin":
215-
config = plugin.find(f"m:configuration", ns) or plugin.find("configuration")
211+
config = plugin.find("m:configuration", ns) or plugin.find("configuration")
216212
if config is not None:
217-
source = config.find(f"m:source", ns) or config.find("source")
213+
source = config.find("m:source", ns) or config.find("source")
218214
if source is not None and source.text:
219215
return source.text
220216

@@ -554,9 +550,8 @@ def install_codeflash_runtime(project_root: Path, runtime_jar_path: Path) -> boo
554550
if result.returncode == 0:
555551
logger.info("Successfully installed codeflash-runtime to local Maven repository")
556552
return True
557-
else:
558-
logger.error("Failed to install codeflash-runtime: %s", result.stderr)
559-
return False
553+
logger.error("Failed to install codeflash-runtime: %s", result.stderr)
554+
return False
560555

561556
except Exception as e:
562557
logger.exception("Failed to install codeflash-runtime: %s", e)
@@ -633,6 +628,160 @@ def add_codeflash_dependency_to_pom(pom_path: Path) -> bool:
633628
return False
634629

635630

631+
JACOCO_PLUGIN_VERSION = "0.8.11"
632+
633+
634+
def is_jacoco_configured(pom_path: Path) -> bool:
635+
"""Check if JaCoCo plugin is already configured in pom.xml.
636+
637+
Args:
638+
pom_path: Path to the pom.xml file.
639+
640+
Returns:
641+
True if JaCoCo plugin is configured, False otherwise.
642+
643+
"""
644+
if not pom_path.exists():
645+
return False
646+
647+
try:
648+
tree = ET.parse(pom_path)
649+
root = tree.getroot()
650+
651+
# Handle Maven namespace
652+
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
653+
ns_prefix = "{http://maven.apache.org/POM/4.0.0}"
654+
655+
# Check if namespace is used
656+
use_ns = root.tag.startswith("{")
657+
if not use_ns:
658+
ns_prefix = ""
659+
660+
# Find build/plugins section
661+
build = root.find(f"{ns_prefix}build" if use_ns else "build")
662+
if build is None:
663+
return False
664+
665+
plugins = build.find(f"{ns_prefix}plugins" if use_ns else "plugins")
666+
if plugins is None:
667+
return False
668+
669+
# Check for JaCoCo plugin
670+
for plugin in plugins.findall(f"{ns_prefix}plugin" if use_ns else "plugin"):
671+
group_id = plugin.find(f"{ns_prefix}groupId" if use_ns else "groupId")
672+
artifact_id = plugin.find(f"{ns_prefix}artifactId" if use_ns else "artifactId")
673+
if artifact_id is not None and artifact_id.text == "jacoco-maven-plugin":
674+
# Verify groupId if present (it's optional for org.jacoco)
675+
if group_id is None or group_id.text == "org.jacoco":
676+
return True
677+
678+
return False
679+
680+
except ET.ParseError as e:
681+
logger.warning("Failed to parse pom.xml for JaCoCo check: %s", e)
682+
return False
683+
684+
685+
def add_jacoco_plugin_to_pom(pom_path: Path) -> bool:
686+
"""Add JaCoCo Maven plugin to pom.xml for coverage collection.
687+
688+
Args:
689+
pom_path: Path to the pom.xml file.
690+
691+
Returns:
692+
True if plugin was added or already present, False on error.
693+
694+
"""
695+
if not pom_path.exists():
696+
logger.error("pom.xml not found: %s", pom_path)
697+
return False
698+
699+
# Check if already configured
700+
if is_jacoco_configured(pom_path):
701+
logger.info("JaCoCo plugin already configured in pom.xml")
702+
return True
703+
704+
try:
705+
tree = ET.parse(pom_path)
706+
root = tree.getroot()
707+
708+
# Handle Maven namespace
709+
ns_prefix = "{http://maven.apache.org/POM/4.0.0}"
710+
711+
# Check if namespace is used
712+
use_ns = root.tag.startswith("{")
713+
if not use_ns:
714+
ns_prefix = ""
715+
716+
# Find or create build section
717+
build = root.find(f"{ns_prefix}build" if use_ns else "build")
718+
if build is None:
719+
build = ET.SubElement(root, f"{ns_prefix}build" if use_ns else "build")
720+
721+
# Find or create plugins section
722+
plugins = build.find(f"{ns_prefix}plugins" if use_ns else "plugins")
723+
if plugins is None:
724+
plugins = ET.SubElement(build, f"{ns_prefix}plugins" if use_ns else "plugins")
725+
726+
# Create JaCoCo plugin element
727+
plugin = ET.SubElement(plugins, f"{ns_prefix}plugin" if use_ns else "plugin")
728+
729+
group_id = ET.SubElement(plugin, f"{ns_prefix}groupId" if use_ns else "groupId")
730+
group_id.text = "org.jacoco"
731+
732+
artifact_id = ET.SubElement(plugin, f"{ns_prefix}artifactId" if use_ns else "artifactId")
733+
artifact_id.text = "jacoco-maven-plugin"
734+
735+
version = ET.SubElement(plugin, f"{ns_prefix}version" if use_ns else "version")
736+
version.text = JACOCO_PLUGIN_VERSION
737+
738+
# Create executions section
739+
executions = ET.SubElement(plugin, f"{ns_prefix}executions" if use_ns else "executions")
740+
741+
# Add prepare-agent execution
742+
exec1 = ET.SubElement(executions, f"{ns_prefix}execution" if use_ns else "execution")
743+
exec1_id = ET.SubElement(exec1, f"{ns_prefix}id" if use_ns else "id")
744+
exec1_id.text = "prepare-agent"
745+
exec1_goals = ET.SubElement(exec1, f"{ns_prefix}goals" if use_ns else "goals")
746+
exec1_goal = ET.SubElement(exec1_goals, f"{ns_prefix}goal" if use_ns else "goal")
747+
exec1_goal.text = "prepare-agent"
748+
749+
# Add report execution
750+
exec2 = ET.SubElement(executions, f"{ns_prefix}execution" if use_ns else "execution")
751+
exec2_id = ET.SubElement(exec2, f"{ns_prefix}id" if use_ns else "id")
752+
exec2_id.text = "report"
753+
exec2_phase = ET.SubElement(exec2, f"{ns_prefix}phase" if use_ns else "phase")
754+
exec2_phase.text = "test"
755+
exec2_goals = ET.SubElement(exec2, f"{ns_prefix}goals" if use_ns else "goals")
756+
exec2_goal = ET.SubElement(exec2_goals, f"{ns_prefix}goal" if use_ns else "goal")
757+
exec2_goal.text = "report"
758+
759+
# Write back to file
760+
tree.write(pom_path, xml_declaration=True, encoding="utf-8")
761+
logger.info("Added JaCoCo plugin to pom.xml")
762+
return True
763+
764+
except ET.ParseError as e:
765+
logger.error("Failed to parse pom.xml: %s", e)
766+
return False
767+
except Exception as e:
768+
logger.exception("Failed to add JaCoCo plugin to pom.xml: %s", e)
769+
return False
770+
771+
772+
def get_jacoco_xml_path(project_root: Path) -> Path:
773+
"""Get the expected path to the JaCoCo XML report.
774+
775+
Args:
776+
project_root: Root directory of the Maven project.
777+
778+
Returns:
779+
Path to the JaCoCo XML report file.
780+
781+
"""
782+
return project_root / "target" / "site" / "jacoco" / "jacoco.xml"
783+
784+
636785
def find_test_root(project_root: Path) -> Path | None:
637786
"""Find the test root directory for a Java project.
638787

codeflash/languages/java/test_runner.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,17 @@
1515
import xml.etree.ElementTree as ET
1616
from dataclasses import dataclass
1717
from pathlib import Path
18-
from typing import TYPE_CHECKING, Any
18+
from typing import Any
1919

2020
from codeflash.code_utils.code_utils import get_run_tmp_file
2121
from codeflash.languages.base import TestResult
2222
from codeflash.languages.java.build_tools import (
23+
add_jacoco_plugin_to_pom,
2324
find_maven_executable,
24-
find_test_root,
25+
get_jacoco_xml_path,
26+
is_jacoco_configured,
2527
)
2628

27-
if TYPE_CHECKING:
28-
pass
29-
3029
logger = logging.getLogger(__name__)
3130

3231

@@ -72,7 +71,7 @@ def run_behavioral_tests(
7271
candidate_index: Index of the candidate being tested.
7372
7473
Returns:
75-
Tuple of (result_xml_path, subprocess_result, sqlite_db_path, None).
74+
Tuple of (result_xml_path, subprocess_result, sqlite_db_path, coverage_xml_path).
7675
7776
"""
7877
project_root = project_root or cwd
@@ -88,21 +87,32 @@ def run_behavioral_tests(
8887
run_env["CODEFLASH_TEST_ITERATION"] = str(candidate_index)
8988
run_env["CODEFLASH_OUTPUT_FILE"] = str(sqlite_db_path) # SQLite output path
9089

90+
# If coverage is enabled, ensure JaCoCo is configured
91+
coverage_xml_path: Path | None = None
92+
if enable_coverage:
93+
pom_path = project_root / "pom.xml"
94+
if pom_path.exists():
95+
if not is_jacoco_configured(pom_path):
96+
logger.info("Adding JaCoCo plugin to pom.xml for coverage collection")
97+
add_jacoco_plugin_to_pom(pom_path)
98+
coverage_xml_path = get_jacoco_xml_path(project_root)
99+
91100
# Run Maven tests
92101
result = _run_maven_tests(
93102
project_root,
94103
test_paths,
95104
run_env,
96105
timeout=timeout or 300,
97106
mode="behavior",
107+
enable_coverage=enable_coverage,
98108
)
99109

100110
# Find or create the JUnit XML results file
101111
surefire_dir = project_root / "target" / "surefire-reports"
102112
result_xml_path = _get_combined_junit_xml(surefire_dir, candidate_index)
103113

104-
# Return sqlite_db_path as the third element (was None before)
105-
return result_xml_path, result, sqlite_db_path, None
114+
# Return coverage_xml_path as the fourth element when coverage is enabled
115+
return result_xml_path, result, sqlite_db_path, coverage_xml_path
106116

107117

108118
def run_benchmarking_tests(
@@ -254,10 +264,10 @@ def _get_combined_junit_xml(surefire_dir: Path, candidate_index: int) -> Path:
254264

255265
def _write_empty_junit_xml(path: Path) -> None:
256266
"""Write an empty JUnit XML results file."""
257-
xml_content = '''<?xml version="1.0" encoding="UTF-8"?>
267+
xml_content = """<?xml version="1.0" encoding="UTF-8"?>
258268
<testsuite name="NoTests" tests="0" failures="0" errors="0" skipped="0" time="0">
259269
</testsuite>
260-
'''
270+
"""
261271
path.write_text(xml_content, encoding="utf-8")
262272

263273

@@ -317,6 +327,7 @@ def _run_maven_tests(
317327
env: dict[str, str],
318328
timeout: int = 300,
319329
mode: str = "behavior",
330+
enable_coverage: bool = False,
320331
) -> subprocess.CompletedProcess:
321332
"""Run Maven tests with Surefire.
322333
@@ -326,6 +337,7 @@ def _run_maven_tests(
326337
env: Environment variables.
327338
timeout: Maximum execution time in seconds.
328339
mode: Testing mode - "behavior" or "performance".
340+
enable_coverage: Whether to enable JaCoCo coverage collection.
329341
330342
Returns:
331343
CompletedProcess with test results.
@@ -345,7 +357,11 @@ def _run_maven_tests(
345357
test_filter = _build_test_filter(test_paths, mode=mode)
346358

347359
# Build Maven command
348-
cmd = [mvn, "test", "-fae"] # Fail at end to run all tests
360+
# When coverage is enabled, run both test and jacoco:report goals
361+
if enable_coverage:
362+
cmd = [mvn, "test", "jacoco:report", "-fae"] # Fail at end to run all tests
363+
else:
364+
cmd = [mvn, "test", "-fae"] # Fail at end to run all tests
349365

350366
if test_filter:
351367
cmd.append(f"-Dtest={test_filter}")
@@ -419,12 +435,11 @@ def _build_test_filter(test_paths: Any, mode: str = "behavior") -> str:
419435
class_name = _path_to_class_name(test_file.benchmarking_file_path)
420436
if class_name:
421437
filters.append(class_name)
422-
else:
423-
# For behavior mode, use instrumented_behavior_file_path
424-
if hasattr(test_file, "instrumented_behavior_file_path") and test_file.instrumented_behavior_file_path:
425-
class_name = _path_to_class_name(test_file.instrumented_behavior_file_path)
426-
if class_name:
427-
filters.append(class_name)
438+
# For behavior mode, use instrumented_behavior_file_path
439+
elif hasattr(test_file, "instrumented_behavior_file_path") and test_file.instrumented_behavior_file_path:
440+
class_name = _path_to_class_name(test_file.instrumented_behavior_file_path)
441+
if class_name:
442+
filters.append(class_name)
428443
return ",".join(filters) if filters else ""
429444

430445
return ""

codeflash/result/critic.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,14 @@ def quantity_of_tests_critic(candidate_result: OptimizedCandidateResult | Origin
206206
def coverage_critic(original_code_coverage: CoverageData | None) -> bool:
207207
"""Check if the coverage meets the threshold.
208208
209-
For languages without coverage support (like Java), returns True if no coverage data is available.
209+
For languages without coverage support (like JavaScript), returns True if no coverage data is available.
210+
Java now uses JaCoCo for coverage collection and is subject to coverage threshold checks.
210211
"""
211-
from codeflash.languages import is_java, is_javascript
212+
from codeflash.languages import is_javascript
212213

213214
if original_code_coverage:
214215
return original_code_coverage.coverage >= COVERAGE_THRESHOLD
215-
# For Java/JavaScript, coverage is not implemented yet, so skip the check
216-
if is_java() or is_javascript():
216+
# For JavaScript, coverage is not implemented yet, so skip the check
217+
if is_javascript():
217218
return True
218219
return False

0 commit comments

Comments
 (0)