Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1fde200
fix: improve multi-module Gradle detection for dynamic settings.gradl…
HeshamHM28 Apr 7, 2026
2e4df0a
Optimize _extract_modules_from_settings_gradle
codeflash-ai[bot] Apr 7, 2026
0ab4800
fix: use tree-sitter for Gradle repositories block and add version up…
mohamedashrraf222 Apr 7, 2026
32bbe57
fix: add classpath hint to find_agent_jar for Gradle JAR resolution
mohamedashrraf222 Apr 7, 2026
1fa01a3
fix: replace Gradle JaCoCo plugin with runtime JAR agent for coverage
mohamedashrraf222 Apr 7, 2026
ba8dd8b
fix: add project_classpath param to base LanguageSupport.instrument_s…
github-actions[bot] Apr 7, 2026
2a2125b
Merge pull request #2017 from codeflash-ai/codeflash/optimize-pr2015-…
claude[bot] Apr 7, 2026
217544f
fix: handle multi-line include directives in settings.gradle
mohamedashrraf222 Apr 7, 2026
e658fb4
fix: increase coverage timeout from 300s to 900s for Gradle builds
mohamedashrraf222 Apr 7, 2026
389aa16
fix: pre-compile test classes and increase compilation timeout for Gr…
mohamedashrraf222 Apr 7, 2026
5e2ef37
fix: increase coverage timeout to 1200s for large Gradle --no-daemon …
mohamedashrraf222 Apr 7, 2026
ebd72ac
merge: resolve conflict with main in test_build_tools.py
mohamedashrraf222 Apr 9, 2026
32bb1cb
Merge remote-tracking branch 'origin/main' into fix/gradle-maven-cent…
mohamedashrraf222 Apr 9, 2026
3f53309
Merge branch 'main' into fix/gradle-maven-central-dependency
KRRT7 Apr 9, 2026
64790d5
style: auto-format with ruff
github-actions[bot] Apr 9, 2026
44c1bcf
ci: retrigger CI
KRRT7 Apr 9, 2026
11201fe
style: auto-format with ruff
github-actions[bot] Apr 9, 2026
2dba3e3
Merge branch 'main' into fix/gradle-maven-central-dependency
KRRT7 Apr 9, 2026
a6ea56b
style: auto-format with ruff
github-actions[bot] Apr 9, 2026
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
2 changes: 1 addition & 1 deletion codeflash-benchmark/codeflash_benchmark/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# These version placeholders will be replaced by uv-dynamic-versioning during build.
__version__ = "0.20.5.post146.dev0+5ff38597"
__version__ = "0.20.5.post169.dev0+2dba3e38"
2 changes: 1 addition & 1 deletion codeflash/languages/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ def instrument_existing_test(
...

def instrument_source_for_line_profiler(
self, func_info: FunctionToOptimize, line_profiler_output_file: Path
self, func_info: FunctionToOptimize, line_profiler_output_file: Path, project_classpath: str | None = None
) -> bool:
"""Instrument source code before line profiling."""
...
Expand Down
4 changes: 3 additions & 1 deletion codeflash/languages/java/function_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,9 @@ def line_profiler_step(
line_profiler_output_path = get_run_tmp_file(Path("line_profiler_output.json"))

success = self.language_support.instrument_source_for_line_profiler(
func_info=self.function_to_optimize, line_profiler_output_file=line_profiler_output_path
func_info=self.function_to_optimize,
line_profiler_output_file=line_profiler_output_path,
project_classpath=self._get_project_classpath(),
)
if not success:
return {"timings": {}, "unit": 0, "str_out": ""}
Expand Down
317 changes: 204 additions & 113 deletions codeflash/languages/java/gradle_strategy.py

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions codeflash/languages/java/line_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import json
import logging
import os
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any
Expand Down Expand Up @@ -130,9 +131,9 @@ class name, then writes a config JSON that the agent uses to know which
config_output_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
return config_output_path

def build_javaagent_arg(self, config_path: Path) -> str:
def build_javaagent_arg(self, config_path: Path, classpath: str | None = None) -> str:
"""Return the -javaagent JVM argument string."""
agent_jar = find_agent_jar()
agent_jar = find_agent_jar(classpath=classpath)
if agent_jar is None:
msg = f"{AGENT_JAR_NAME} not found in resources or dev build directory"
raise FileNotFoundError(msg)
Expand Down Expand Up @@ -565,12 +566,20 @@ def find_method_for_line(
return Path(file_path).name, line_num


def find_agent_jar() -> Path | None:
def find_agent_jar(classpath: str | None = None) -> Path | None:
"""Locate the profiler agent JAR file (now bundled in codeflash-runtime).

Checks local Maven repo, package resources, and development build directory.
Checks the resolved classpath (if provided), local Maven repo, package resources,
and development build directory.
"""
# Check local Maven repository first (fastest)
# Check resolved classpath first (Gradle projects resolve here, not ~/.m2)
if classpath:
for entry in classpath.split(os.pathsep):
jar_path = Path(entry)
if "codeflash-runtime" in jar_path.name and jar_path.suffix == ".jar" and jar_path.exists():
return jar_path

# Check local Maven repository (Maven projects resolve here)
m2_jar = (
Path.home()
/ ".m2"
Expand Down
5 changes: 3 additions & 2 deletions codeflash/languages/java/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ def instrument_existing_test(
)

def instrument_source_for_line_profiler(
self, func_info: FunctionToOptimize, line_profiler_output_file: Path
self, func_info: FunctionToOptimize, line_profiler_output_file: Path, project_classpath: str | None = None
) -> bool:
"""Prepare line profiling via the bytecode-instrumentation agent.

Expand All @@ -602,6 +602,7 @@ def instrument_source_for_line_profiler(
Args:
func_info: Function to profile.
line_profiler_output_file: Path where profiling results will be written by the agent.
project_classpath: Resolved classpath from the build tool, used to locate the agent JAR.

Returns:
True if preparation succeeded, False otherwise.
Expand All @@ -619,7 +620,7 @@ def instrument_source_for_line_profiler(
source=source, file_path=func_info.file_path, functions=[func_info], config_output_path=config_path
)

self.line_profiler_agent_arg = profiler.build_javaagent_arg(config_path)
self.line_profiler_agent_arg = profiler.build_javaagent_arg(config_path, classpath=project_classpath)
self.line_profiler_warmup_iterations = profiler.warmup_iterations
return True
except Exception:
Expand Down
114 changes: 110 additions & 4 deletions codeflash/languages/java/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
from codeflash.code_utils.code_utils import get_run_tmp_file
from codeflash.languages.base import TestResult

_INCLUDE_PATTERN = re.compile(r"""(?:^|(?<=\s))include\s*\(?[^)]*\)?""", re.MULTILINE | re.DOTALL)

_LISTOF_PATTERN = re.compile(r"""listOf\s*\(([^)]*)\)""", re.DOTALL)

_QUOTED_PATTERN = re.compile(r"""['"]([^'"]+)['"]""")

_result_counter = itertools.count(1)


Expand Down Expand Up @@ -205,12 +211,28 @@ def _extract_modules_from_settings_gradle(content: str) -> list[str]:
Looks for include directives like:
include("module-a", "module-b") // Kotlin DSL
include 'module-a', 'module-b' // Groovy DSL
Also handles dynamic Kotlin DSL patterns like:
val allProjects = listOf("module-a", "module-b")
include(*(allProjects + ...).toTypedArray())
Module names may be prefixed with ':' which is stripped.
"""
modules: list[str] = []
for match in re.findall(r"""include\s*\(?[^)\n]*\)?""", content):
for name in re.findall(r"""['"]([^'"]+)['"]""", match):
# Standard include(...) directives — word boundary avoids matching variable names
# like 'includedProjects'. Pattern allows multi-line includes (e.g., eureka's
# include 'module-a',\n 'module-b',\n 'module-c')
for match in _INCLUDE_PATTERN.findall(content):
for name in _QUOTED_PATTERN.findall(match):
modules.append(name.lstrip(":"))
# Kotlin DSL: val ... = listOf("module-a", "module-b", ...) spanning multiple lines.
# Used when settings.gradle.kts builds the include list dynamically.
if not modules or not any("/" not in m and "." not in m for m in modules):
seen = set(modules)
for match in _LISTOF_PATTERN.findall(content):
for name in _QUOTED_PATTERN.findall(match):
stripped = name.lstrip(":")
if stripped not in seen:
modules.append(stripped)
seen.add(stripped)
return modules


Expand Down Expand Up @@ -269,6 +291,50 @@ def _match_module_from_rel_path(rel_path: Path, modules: list[str]) -> str | Non
return None


def _read_config_module_root(project_root: Path) -> str | None:
"""Read module-root from codeflash.toml or pyproject.toml."""
for cfg_name in ("codeflash.toml", "pyproject.toml"):
cfg_path = project_root / cfg_name
if cfg_path.exists():
try:
cfg_text = cfg_path.read_text(encoding="utf-8")
m = re.search(r'module-root\s*=\s*["\']([^"\']+)["\']', cfg_text)
if m:
return m.group(1).strip().strip("/")
except Exception:
pass
return None


def _infer_module_from_config(project_root: Path) -> str | None:
"""Infer the target Gradle module from codeflash config in gradle.properties.

Reads codeflash.moduleRoot or codeflash.testsRoot and extracts the first
path component as the module name. Verifies the module directory has a
build.gradle(.kts) file.
"""
props_file = project_root / "gradle.properties"
if not props_file.exists():
return None
try:
content = props_file.read_text(encoding="utf-8")
except Exception:
return None

for key in ("codeflash.moduleRoot", "codeflash.testsRoot"):
for line in content.splitlines():
line = line.strip()
if line.startswith(key + "="):
value = line.split("=", 1)[1].strip()
# Extract first path component (e.g. "rewrite-core/src/main/java" → "rewrite-core")
candidate = Path(value).parts[0] if Path(value).parts else None
if candidate:
module_dir = project_root / candidate
if (module_dir / "build.gradle.kts").exists() or (module_dir / "build.gradle").exists():
return candidate
return None


def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path, str | None]:
"""Find the multi-module parent root if tests are in a different module.

Expand All @@ -287,10 +353,18 @@ def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path,
test_file_paths.append(test_file.benchmarking_file_path)
elif hasattr(test_file, "instrumented_behavior_file_path") and test_file.instrumented_behavior_file_path:
test_file_paths.append(test_file.instrumented_behavior_file_path)
elif hasattr(test_file, "original_file_path") and test_file.original_file_path:
test_file_paths.append(test_file.original_file_path)
elif isinstance(test_paths, (list, tuple)):
test_file_paths = [Path(p) if isinstance(p, str) else p for p in test_paths]

if not test_file_paths:
# No test file paths available — try to infer the module from codeflash config
# in gradle.properties (e.g. codeflash.moduleRoot=rewrite-core/src/main/java).
module = _infer_module_from_config(project_root)
if module:
logger.info("Inferred module '%s' from codeflash config (no test file paths)", module)
return project_root, module
return project_root, None

test_outside_project = False
Expand Down Expand Up @@ -320,14 +394,46 @@ def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path,
module_counts[matched] = module_counts.get(matched, 0) + 1

if module_counts:
best_module = max(module_counts, key=lambda m: module_counts[m])
# On ties, prefer the module matching codeflash.toml module-root
config_module = _read_config_module_root(project_root)
max_count = max(module_counts.values())
tied = [m for m, c in module_counts.items() if c == max_count]
if config_module and config_module in tied:
best_module = config_module
else:
best_module = max(module_counts, key=lambda m: module_counts[m])
logger.debug(
"Detected multi-module project. Root: %s, Module votes: %s, Selected: %s",
project_root,
module_counts,
best_module,
)
return project_root, best_module

# project_root has no sub-modules — check if it is itself a sub-module
# of a parent multi-module project (e.g. rewrite-core/ inside rewrite/).
parent = project_root.parent
while parent != parent.parent:
if _is_build_root(parent):
parent_modules = _detect_modules(parent)
if parent_modules:
try:
rel_path = project_root.relative_to(parent)
matched = _match_module_from_rel_path(rel_path, parent_modules)
if matched:
logger.debug("Detected project_root as sub-module. Root: %s, Module: %s", parent, matched)
return parent, matched
except ValueError:
pass
parent = parent.parent

# Last resort: settings.gradle may use dynamic includes that _detect_modules
# can't parse. Fall back to codeflash config in gradle.properties.
module = _infer_module_from_config(project_root)
if module:
logger.info("Inferred module '%s' from codeflash config (dynamic settings.gradle)", module)
return project_root, module

return project_root, None

current = project_root.parent
Expand Down Expand Up @@ -420,7 +526,7 @@ def run_behavioral_tests(
if enable_coverage:
coverage_xml_path = strategy.setup_coverage(build_root, test_module, project_root)

min_timeout = 300 if enable_coverage else 60
min_timeout = 1200 if enable_coverage else 60
effective_timeout = max(timeout or 300, min_timeout)

if enable_coverage:
Expand Down
4 changes: 2 additions & 2 deletions codeflash/languages/java/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ def build_jfr_env(self, jfr_file: Path) -> dict[str, str]:
env["JAVA_TOOL_OPTIONS"] = f"{existing} {jfr_opts}".strip()
return env

def build_agent_env(self, config_path: Path) -> dict[str, str]:
def build_agent_env(self, config_path: Path, classpath: str | None = None) -> dict[str, str]:
env = os.environ.copy()
agent_jar = find_agent_jar()
agent_jar = find_agent_jar(classpath=classpath)
if agent_jar is None:
msg = "codeflash-runtime JAR not found, cannot run tracing agent"
raise FileNotFoundError(msg)
Expand Down
2 changes: 1 addition & 1 deletion codeflash/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# These version placeholders will be replaced by uv-dynamic-versioning during build.
__version__ = "0.20.5"
__version__ = "0.20.5.post169.dev0+2dba3e38"
Loading