Skip to content

Commit 153097b

Browse files
authored
Merge pull request #2015 from codeflash-ai/fix/gradle-maven-central-dependency
fix: improve multi-module Gradle detection for dynamic settings.gradle.kts
2 parents 8e60f25 + a6ea56b commit 153097b

11 files changed

Lines changed: 595 additions & 149 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# These version placeholders will be replaced by uv-dynamic-versioning during build.
2-
__version__ = "0.20.5.post146.dev0+5ff38597"
2+
__version__ = "0.20.5.post169.dev0+2dba3e38"

codeflash/languages/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ def instrument_existing_test(
897897
...
898898

899899
def instrument_source_for_line_profiler(
900-
self, func_info: FunctionToOptimize, line_profiler_output_file: Path
900+
self, func_info: FunctionToOptimize, line_profiler_output_file: Path, project_classpath: str | None = None
901901
) -> bool:
902902
"""Instrument source code before line profiling."""
903903
...

codeflash/languages/java/function_optimizer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,9 @@ def line_profiler_step(
404404
line_profiler_output_path = get_run_tmp_file(Path("line_profiler_output.json"))
405405

406406
success = self.language_support.instrument_source_for_line_profiler(
407-
func_info=self.function_to_optimize, line_profiler_output_file=line_profiler_output_path
407+
func_info=self.function_to_optimize,
408+
line_profiler_output_file=line_profiler_output_path,
409+
project_classpath=self._get_project_classpath(),
408410
)
409411
if not success:
410412
return {"timings": {}, "unit": 0, "str_out": ""}

codeflash/languages/java/gradle_strategy.py

Lines changed: 204 additions & 113 deletions
Large diffs are not rendered by default.

codeflash/languages/java/line_profiler.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import json
1515
import logging
16+
import os
1617
import re
1718
from pathlib import Path
1819
from typing import TYPE_CHECKING, Any
@@ -130,9 +131,9 @@ class name, then writes a config JSON that the agent uses to know which
130131
config_output_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
131132
return config_output_path
132133

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

567568

568-
def find_agent_jar() -> Path | None:
569+
def find_agent_jar(classpath: str | None = None) -> Path | None:
569570
"""Locate the profiler agent JAR file (now bundled in codeflash-runtime).
570571
571-
Checks local Maven repo, package resources, and development build directory.
572+
Checks the resolved classpath (if provided), local Maven repo, package resources,
573+
and development build directory.
572574
"""
573-
# Check local Maven repository first (fastest)
575+
# Check resolved classpath first (Gradle projects resolve here, not ~/.m2)
576+
if classpath:
577+
for entry in classpath.split(os.pathsep):
578+
jar_path = Path(entry)
579+
if "codeflash-runtime" in jar_path.name and jar_path.suffix == ".jar" and jar_path.exists():
580+
return jar_path
581+
582+
# Check local Maven repository (Maven projects resolve here)
574583
m2_jar = (
575584
Path.home()
576585
/ ".m2"

codeflash/languages/java/support.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ def instrument_existing_test(
590590
)
591591

592592
def instrument_source_for_line_profiler(
593-
self, func_info: FunctionToOptimize, line_profiler_output_file: Path
593+
self, func_info: FunctionToOptimize, line_profiler_output_file: Path, project_classpath: str | None = None
594594
) -> bool:
595595
"""Prepare line profiling via the bytecode-instrumentation agent.
596596
@@ -602,6 +602,7 @@ def instrument_source_for_line_profiler(
602602
Args:
603603
func_info: Function to profile.
604604
line_profiler_output_file: Path where profiling results will be written by the agent.
605+
project_classpath: Resolved classpath from the build tool, used to locate the agent JAR.
605606
606607
Returns:
607608
True if preparation succeeded, False otherwise.
@@ -619,7 +620,7 @@ def instrument_source_for_line_profiler(
619620
source=source, file_path=func_info.file_path, functions=[func_info], config_output_path=config_path
620621
)
621622

622-
self.line_profiler_agent_arg = profiler.build_javaagent_arg(config_path)
623+
self.line_profiler_agent_arg = profiler.build_javaagent_arg(config_path, classpath=project_classpath)
623624
self.line_profiler_warmup_iterations = profiler.warmup_iterations
624625
return True
625626
except Exception:

codeflash/languages/java/test_runner.py

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
from codeflash.code_utils.code_utils import get_run_tmp_file
2525
from codeflash.languages.base import TestResult
2626

27+
_INCLUDE_PATTERN = re.compile(r"""(?:^|(?<=\s))include\s*\(?[^)]*\)?""", re.MULTILINE | re.DOTALL)
28+
29+
_LISTOF_PATTERN = re.compile(r"""listOf\s*\(([^)]*)\)""", re.DOTALL)
30+
31+
_QUOTED_PATTERN = re.compile(r"""['"]([^'"]+)['"]""")
32+
2733
_result_counter = itertools.count(1)
2834

2935

@@ -205,12 +211,28 @@ def _extract_modules_from_settings_gradle(content: str) -> list[str]:
205211
Looks for include directives like:
206212
include("module-a", "module-b") // Kotlin DSL
207213
include 'module-a', 'module-b' // Groovy DSL
214+
Also handles dynamic Kotlin DSL patterns like:
215+
val allProjects = listOf("module-a", "module-b")
216+
include(*(allProjects + ...).toTypedArray())
208217
Module names may be prefixed with ':' which is stripped.
209218
"""
210219
modules: list[str] = []
211-
for match in re.findall(r"""include\s*\(?[^)\n]*\)?""", content):
212-
for name in re.findall(r"""['"]([^'"]+)['"]""", match):
220+
# Standard include(...) directives — word boundary avoids matching variable names
221+
# like 'includedProjects'. Pattern allows multi-line includes (e.g., eureka's
222+
# include 'module-a',\n 'module-b',\n 'module-c')
223+
for match in _INCLUDE_PATTERN.findall(content):
224+
for name in _QUOTED_PATTERN.findall(match):
213225
modules.append(name.lstrip(":"))
226+
# Kotlin DSL: val ... = listOf("module-a", "module-b", ...) spanning multiple lines.
227+
# Used when settings.gradle.kts builds the include list dynamically.
228+
if not modules or not any("/" not in m and "." not in m for m in modules):
229+
seen = set(modules)
230+
for match in _LISTOF_PATTERN.findall(content):
231+
for name in _QUOTED_PATTERN.findall(match):
232+
stripped = name.lstrip(":")
233+
if stripped not in seen:
234+
modules.append(stripped)
235+
seen.add(stripped)
214236
return modules
215237

216238

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

271293

294+
def _read_config_module_root(project_root: Path) -> str | None:
295+
"""Read module-root from codeflash.toml or pyproject.toml."""
296+
for cfg_name in ("codeflash.toml", "pyproject.toml"):
297+
cfg_path = project_root / cfg_name
298+
if cfg_path.exists():
299+
try:
300+
cfg_text = cfg_path.read_text(encoding="utf-8")
301+
m = re.search(r'module-root\s*=\s*["\']([^"\']+)["\']', cfg_text)
302+
if m:
303+
return m.group(1).strip().strip("/")
304+
except Exception:
305+
pass
306+
return None
307+
308+
309+
def _infer_module_from_config(project_root: Path) -> str | None:
310+
"""Infer the target Gradle module from codeflash config in gradle.properties.
311+
312+
Reads codeflash.moduleRoot or codeflash.testsRoot and extracts the first
313+
path component as the module name. Verifies the module directory has a
314+
build.gradle(.kts) file.
315+
"""
316+
props_file = project_root / "gradle.properties"
317+
if not props_file.exists():
318+
return None
319+
try:
320+
content = props_file.read_text(encoding="utf-8")
321+
except Exception:
322+
return None
323+
324+
for key in ("codeflash.moduleRoot", "codeflash.testsRoot"):
325+
for line in content.splitlines():
326+
line = line.strip()
327+
if line.startswith(key + "="):
328+
value = line.split("=", 1)[1].strip()
329+
# Extract first path component (e.g. "rewrite-core/src/main/java" → "rewrite-core")
330+
candidate = Path(value).parts[0] if Path(value).parts else None
331+
if candidate:
332+
module_dir = project_root / candidate
333+
if (module_dir / "build.gradle.kts").exists() or (module_dir / "build.gradle").exists():
334+
return candidate
335+
return None
336+
337+
272338
def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path, str | None]:
273339
"""Find the multi-module parent root if tests are in a different module.
274340
@@ -287,10 +353,18 @@ def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path,
287353
test_file_paths.append(test_file.benchmarking_file_path)
288354
elif hasattr(test_file, "instrumented_behavior_file_path") and test_file.instrumented_behavior_file_path:
289355
test_file_paths.append(test_file.instrumented_behavior_file_path)
356+
elif hasattr(test_file, "original_file_path") and test_file.original_file_path:
357+
test_file_paths.append(test_file.original_file_path)
290358
elif isinstance(test_paths, (list, tuple)):
291359
test_file_paths = [Path(p) if isinstance(p, str) else p for p in test_paths]
292360

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

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

322396
if module_counts:
323-
best_module = max(module_counts, key=lambda m: module_counts[m])
397+
# On ties, prefer the module matching codeflash.toml module-root
398+
config_module = _read_config_module_root(project_root)
399+
max_count = max(module_counts.values())
400+
tied = [m for m, c in module_counts.items() if c == max_count]
401+
if config_module and config_module in tied:
402+
best_module = config_module
403+
else:
404+
best_module = max(module_counts, key=lambda m: module_counts[m])
324405
logger.debug(
325406
"Detected multi-module project. Root: %s, Module votes: %s, Selected: %s",
326407
project_root,
327408
module_counts,
328409
best_module,
329410
)
330411
return project_root, best_module
412+
413+
# project_root has no sub-modules — check if it is itself a sub-module
414+
# of a parent multi-module project (e.g. rewrite-core/ inside rewrite/).
415+
parent = project_root.parent
416+
while parent != parent.parent:
417+
if _is_build_root(parent):
418+
parent_modules = _detect_modules(parent)
419+
if parent_modules:
420+
try:
421+
rel_path = project_root.relative_to(parent)
422+
matched = _match_module_from_rel_path(rel_path, parent_modules)
423+
if matched:
424+
logger.debug("Detected project_root as sub-module. Root: %s, Module: %s", parent, matched)
425+
return parent, matched
426+
except ValueError:
427+
pass
428+
parent = parent.parent
429+
430+
# Last resort: settings.gradle may use dynamic includes that _detect_modules
431+
# can't parse. Fall back to codeflash config in gradle.properties.
432+
module = _infer_module_from_config(project_root)
433+
if module:
434+
logger.info("Inferred module '%s' from codeflash config (dynamic settings.gradle)", module)
435+
return project_root, module
436+
331437
return project_root, None
332438

333439
current = project_root.parent
@@ -420,7 +526,7 @@ def run_behavioral_tests(
420526
if enable_coverage:
421527
coverage_xml_path = strategy.setup_coverage(build_root, test_module, project_root)
422528

423-
min_timeout = 300 if enable_coverage else 60
529+
min_timeout = 1200 if enable_coverage else 60
424530
effective_timeout = max(timeout or 300, min_timeout)
425531

426532
if enable_coverage:

codeflash/languages/java/tracer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,9 @@ def build_jfr_env(self, jfr_file: Path) -> dict[str, str]:
132132
env["JAVA_TOOL_OPTIONS"] = f"{existing} {jfr_opts}".strip()
133133
return env
134134

135-
def build_agent_env(self, config_path: Path) -> dict[str, str]:
135+
def build_agent_env(self, config_path: Path, classpath: str | None = None) -> dict[str, str]:
136136
env = os.environ.copy()
137-
agent_jar = find_agent_jar()
137+
agent_jar = find_agent_jar(classpath=classpath)
138138
if agent_jar is None:
139139
msg = "codeflash-runtime JAR not found, cannot run tracing agent"
140140
raise FileNotFoundError(msg)

codeflash/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# These version placeholders will be replaced by uv-dynamic-versioning during build.
2-
__version__ = "0.20.5"
2+
__version__ = "0.20.5.post169.dev0+2dba3e38"

0 commit comments

Comments
 (0)