From 1fde200bc447ae709b24b1426184f30dae153b69 Mon Sep 17 00:00:00 2001 From: HeshamHM28 Date: Tue, 7 Apr 2026 11:08:16 +0000 Subject: [PATCH 01/14] fix: improve multi-module Gradle detection for dynamic settings.gradle.kts - Parse listOf(...) patterns in settings.gradle.kts for projects that build include lists dynamically (e.g. OpenRewrite) - Use word boundary in include regex to avoid matching variable names like 'includedProjects' - Break module voting ties using codeflash.toml module-root config, so the function's own module is preferred over cross-module tests Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/gradle_strategy.py | 76 ++++++++----- codeflash/languages/java/test_runner.py | 101 +++++++++++++++++- .../test_java/test_build_tools.py | 37 ++++--- .../test_java/test_java_test_paths.py | 29 +++++ 4 files changed, 196 insertions(+), 47 deletions(-) diff --git a/codeflash/languages/java/gradle_strategy.py b/codeflash/languages/java/gradle_strategy.py index b4481dd6e..660256fdb 100644 --- a/codeflash/languages/java/gradle_strategy.py +++ b/codeflash/languages/java/gradle_strategy.py @@ -9,7 +9,6 @@ import logging import os import re -import shutil import subprocess import tempfile import xml.etree.ElementTree as ET @@ -17,7 +16,7 @@ from typing import Any from codeflash.languages.java.build_tool_strategy import BuildToolStrategy, module_to_dir -from codeflash.languages.java.build_tools import BuildTool, JavaProjectInfo +from codeflash.languages.java.build_tools import CODEFLASH_RUNTIME_VERSION, BuildTool, JavaProjectInfo _RE_INCLUDE = re.compile(r"""include\s*\(?([^)\n]+)\)?""") @@ -205,8 +204,32 @@ def _is_multimodule_project(build_root: Path) -> bool: return False -def add_codeflash_dependency_multimodule(build_file: Path, runtime_jar_path: Path) -> bool: - """Add codeflash-runtime dependency wrapped in a subprojects block for multi-module projects. +_CODEFLASH_MAVEN_COORD = f"com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}" + + +def _ensure_maven_central_repo(build_file: Path, content: str) -> str: + """Ensure mavenCentral() is present in the repositories block. Returns updated content.""" + if "mavenCentral()" in content: + return content + + is_kts = build_file.name.endswith(".kts") + + # Try to find existing repositories block and add mavenCentral() inside it + repo_match = re.search(r"repositories\s*\{", content) + if repo_match: + insert_pos = repo_match.end() + return content[:insert_pos] + "\n mavenCentral()" + content[insert_pos:] + + # No repositories block — append one + if is_kts: + content += "\nrepositories {\n mavenCentral()\n}\n" + else: + content += "\nrepositories {\n mavenCentral()\n}\n" + return content + + +def add_codeflash_dependency_multimodule(build_file: Path) -> bool: + """Add codeflash-runtime dependency from Maven Central in a subprojects block for multi-module projects. This avoids adding testImplementation to the root build file directly, which would fail if the root project doesn't apply the java plugin. @@ -222,14 +245,16 @@ def add_codeflash_dependency_multimodule(build_file: Path, runtime_jar_path: Pat return True is_kts = build_file.name.endswith(".kts") - jar_str = str(runtime_jar_path).replace("\\", "/") if is_kts: block = ( f"\nsubprojects {{\n" f' plugins.withId("java") {{\n' + f" repositories {{\n" + f" mavenCentral()\n" + f" }}\n" f" dependencies {{\n" - f' testImplementation(files("{jar_str}")) // codeflash-runtime\n' + f' testImplementation("{_CODEFLASH_MAVEN_COORD}") // codeflash-runtime\n' f" }}\n" f" }}\n" f"}}\n" @@ -238,8 +263,11 @@ def add_codeflash_dependency_multimodule(build_file: Path, runtime_jar_path: Pat block = ( f"\nsubprojects {{\n" f" plugins.withId('java') {{\n" + f" repositories {{\n" + f" mavenCentral()\n" + f" }}\n" f" dependencies {{\n" - f" testImplementation files('{jar_str}') // codeflash-runtime\n" + f" testImplementation '{_CODEFLASH_MAVEN_COORD}' // codeflash-runtime\n" f" }}\n" f" }}\n" f"}}\n" @@ -255,7 +283,7 @@ def add_codeflash_dependency_multimodule(build_file: Path, runtime_jar_path: Pat return False -def add_codeflash_dependency(build_file: Path, runtime_jar_path: Path) -> bool: +def add_codeflash_dependency(build_file: Path) -> bool: if not build_file.exists(): return False @@ -266,13 +294,14 @@ def add_codeflash_dependency(build_file: Path, runtime_jar_path: Path) -> bool: logger.info("codeflash-runtime dependency already present in %s", build_file.name) return True + content = _ensure_maven_central_repo(build_file, content) + is_kts = build_file.name.endswith(".kts") - jar_str = str(runtime_jar_path).replace("\\", "/") if is_kts: - dep_line = f' testImplementation(files("{jar_str}")) // codeflash-runtime\n' + dep_line = f' testImplementation("{_CODEFLASH_MAVEN_COORD}") // codeflash-runtime\n' else: - dep_line = f" testImplementation files('{jar_str}') // codeflash-runtime\n" + dep_line = f" testImplementation '{_CODEFLASH_MAVEN_COORD}' // codeflash-runtime\n" # Use tree-sitter to find the top-level dependencies block insert_pos = _find_top_level_dependencies_block(build_file, content) @@ -284,9 +313,13 @@ def add_codeflash_dependency(build_file: Path, runtime_jar_path: Path) -> bool: # No existing dependencies block — append one if is_kts: - content += f'\ndependencies {{\n testImplementation(files("{jar_str}")) // codeflash-runtime\n}}\n' + content += ( + f'\ndependencies {{\n testImplementation("{_CODEFLASH_MAVEN_COORD}") // codeflash-runtime\n}}\n' + ) else: - content += f"\ndependencies {{\n testImplementation files('{jar_str}') // codeflash-runtime\n}}\n" + content += ( + f"\ndependencies {{\n testImplementation '{_CODEFLASH_MAVEN_COORD}' // codeflash-runtime\n}}\n" + ) build_file.write_text(content, encoding="utf-8") logger.info("Added codeflash-runtime dependency to %s (new block)", build_file.name) return True @@ -420,34 +453,21 @@ def find_executable(self, build_root: Path) -> str | None: return self.find_wrapper_executable(build_root, ("gradlew", "gradlew.bat"), "gradle") def ensure_runtime(self, build_root: Path, test_module: str | None) -> bool: - runtime_jar = self.find_runtime_jar() - if runtime_jar is None: - logger.error("codeflash-runtime JAR not found. Generated tests will fail to compile.") - return False - if test_module: module_root = build_root / module_to_dir(test_module) else: module_root = build_root - libs_dir = module_root / "libs" - libs_dir.mkdir(parents=True, exist_ok=True) - dest_jar = libs_dir / "codeflash-runtime-1.0.1.jar" - - if not dest_jar.exists(): - logger.info("Copying codeflash-runtime JAR to %s", dest_jar) - shutil.copy2(runtime_jar, dest_jar) - build_file = find_gradle_build_file(module_root) if build_file is None: logger.warning("No build.gradle(.kts) found at %s, cannot add codeflash-runtime dependency", module_root) return False if not test_module and _is_multimodule_project(build_root): - if not add_codeflash_dependency_multimodule(build_file, dest_jar): + if not add_codeflash_dependency_multimodule(build_file): logger.error("Failed to add codeflash-runtime dependency to %s", build_file) return False - elif not add_codeflash_dependency(build_file, dest_jar): + elif not add_codeflash_dependency(build_file): logger.error("Failed to add codeflash-runtime dependency to %s", build_file) return False diff --git a/codeflash/languages/java/test_runner.py b/codeflash/languages/java/test_runner.py index 74830d436..184aaa626 100644 --- a/codeflash/languages/java/test_runner.py +++ b/codeflash/languages/java/test_runner.py @@ -205,12 +205,25 @@ 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): + # Standard include(...) directives — word boundary avoids matching variable names + # like 'includedProjects' + for match in re.findall(r"""(?:^|(?<=\s))include\s*\(?[^)\n]*\)?""", content, re.MULTILINE): for name in re.findall(r"""['"]([^'"]+)['"]""", 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): + for match in re.findall(r"""listOf\s*\(([^)]*)\)""", content, re.DOTALL): + for name in re.findall(r"""['"]([^'"]+)['"]""", match): + stripped = name.lstrip(":") + if stripped not in modules: + modules.append(stripped) return modules @@ -269,6 +282,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. @@ -287,10 +344,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 @@ -320,7 +385,14 @@ 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, @@ -328,6 +400,31 @@ def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path, 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 diff --git a/tests/test_languages/test_java/test_build_tools.py b/tests/test_languages/test_java/test_build_tools.py index a4f01e1a6..254216a9e 100644 --- a/tests/test_languages/test_java/test_build_tools.py +++ b/tests/test_languages/test_java/test_build_tools.py @@ -588,17 +588,14 @@ def test_adds_dependency_to_correct_module_build_file(self, tmp_path): project = self._make_multi_module_project(tmp_path) strategy = GradleStrategy() - # Provide a fake runtime JAR - fake_jar = tmp_path / "fake-runtime.jar" - fake_jar.write_bytes(b"PK\x03\x04") # minimal zip header - - with patch.object(strategy, "find_runtime_jar", return_value=fake_jar): - result = strategy.ensure_runtime(project, test_module="streams") + result = strategy.ensure_runtime(project, test_module="streams") assert result is True - # Dependency should be in streams/build.gradle.kts + # Dependency should be in streams/build.gradle.kts with Maven Central coordinate streams_build = (project / "streams" / "build.gradle.kts").read_text(encoding="utf-8") assert "codeflash-runtime" in streams_build + assert "com.codeflash:codeflash-runtime:" in streams_build + assert "mavenCentral()" in streams_build # And NOT in clients/build.gradle.kts or root build.gradle.kts clients_build = (project / "clients" / "build.gradle.kts").read_text(encoding="utf-8") assert "codeflash-runtime" not in clients_build @@ -610,15 +607,13 @@ def test_adds_dependency_to_root_when_no_module(self, tmp_path): project = self._make_multi_module_project(tmp_path) strategy = GradleStrategy() - fake_jar = tmp_path / "fake-runtime.jar" - fake_jar.write_bytes(b"PK\x03\x04") - - with patch.object(strategy, "find_runtime_jar", return_value=fake_jar): - result = strategy.ensure_runtime(project, test_module=None) + result = strategy.ensure_runtime(project, test_module=None) assert result is True root_build = (project / "build.gradle.kts").read_text(encoding="utf-8") assert "codeflash-runtime" in root_build + assert "com.codeflash:codeflash-runtime:" in root_build + assert "mavenCentral()" in root_build def test_adds_dependency_to_nested_module(self, tmp_path): """When test_module='connect:runtime', the dep goes to connect/runtime/build.gradle.kts.""" @@ -632,12 +627,20 @@ def test_adds_dependency_to_nested_module(self, tmp_path): ) strategy = GradleStrategy() - fake_jar = tmp_path / "fake-runtime.jar" - fake_jar.write_bytes(b"PK\x03\x04") - - with patch.object(strategy, "find_runtime_jar", return_value=fake_jar): - result = strategy.ensure_runtime(project, test_module="connect:runtime") + result = strategy.ensure_runtime(project, test_module="connect:runtime") assert result is True nested_build = (nested / "build.gradle.kts").read_text(encoding="utf-8") assert "codeflash-runtime" in nested_build + assert "com.codeflash:codeflash-runtime:" in nested_build + assert "mavenCentral()" in nested_build + + def test_does_not_copy_jar_to_libs(self, tmp_path): + """ensure_runtime should NOT copy JARs locally — Gradle resolves from Maven Central.""" + project = self._make_multi_module_project(tmp_path) + + strategy = GradleStrategy() + strategy.ensure_runtime(project, test_module="streams") + + libs_dir = project / "streams" / "libs" + assert not libs_dir.exists() diff --git a/tests/test_languages/test_java/test_java_test_paths.py b/tests/test_languages/test_java/test_java_test_paths.py index 3a6ff95db..1120862f3 100644 --- a/tests/test_languages/test_java/test_java_test_paths.py +++ b/tests/test_languages/test_java/test_java_test_paths.py @@ -631,3 +631,32 @@ def test_project_root_is_submodule_test_outside(self, tmp_path): assert build_root == tmp_path assert test_module == "streams" + + def test_submodule_as_project_root_with_tests_inside(self, tmp_path): + """When project_root is a sub-module (e.g. rewrite-core/) and generated tests + are inside it, should walk up to find the real root and detect the module.""" + self._make_kafka_like_project(tmp_path) + submodule_root = tmp_path / "clients" + test_file = submodule_root / "src" / "test" / "java" / "com" / "ClientsTest.java" + test_file.parent.mkdir(parents=True, exist_ok=True) + test_file.touch() + + test_paths = self._make_test_paths_mock([test_file]) + build_root, test_module = _find_multi_module_root(submodule_root, test_paths) + + assert build_root == tmp_path + assert test_module == "clients" + + def test_submodule_as_project_root_nested_module(self, tmp_path): + """When project_root is a nested sub-module (connect/runtime), should detect it.""" + self._make_kafka_like_project(tmp_path) + submodule_root = tmp_path / "connect" / "runtime" + test_file = submodule_root / "src" / "test" / "java" / "com" / "RuntimeTest.java" + test_file.parent.mkdir(parents=True, exist_ok=True) + test_file.touch() + + test_paths = self._make_test_paths_mock([test_file]) + build_root, test_module = _find_multi_module_root(submodule_root, test_paths) + + assert build_root == tmp_path + assert test_module == "connect:runtime" From 2e4df0a7fe883c4aae146c763b9406276f8fd726 Mon Sep 17 00:00:00 2001 From: "codeflash-ai[bot]" <148906541+codeflash-ai[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:40:47 +0000 Subject: [PATCH 02/14] Optimize _extract_modules_from_settings_gradle The optimization pre-compiles three regex patterns at module load time (`_INCLUDE_PATTERN`, `_LISTOF_PATTERN`, `_QUOTED_PATTERN`) instead of recompiling them on every function call, eliminating the ~1 ms pattern-compilation overhead that line profiler shows dominated the original version (44.3% of total time in the first `re.findall` alone). The second major change replaces the O(n) `if stripped not in modules` list scan with a set-based `if stripped not in seen` check, which cuts the deduplication cost from ~288 ns to ~72 ns per check when the fallback listOf branch executes. Runtime improves from 2.35 ms to 1.91 ms (23% faster) with no behavioral regressions. --- codeflash/languages/java/test_runner.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/codeflash/languages/java/test_runner.py b/codeflash/languages/java/test_runner.py index 184aaa626..9bbf95753 100644 --- a/codeflash/languages/java/test_runner.py +++ b/codeflash/languages/java/test_runner.py @@ -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*\(?[^)\n]*\)?""", re.MULTILINE) + +_LISTOF_PATTERN = re.compile(r"""listOf\s*\(([^)]*)\)""", re.DOTALL) + +_QUOTED_PATTERN = re.compile(r"""['"]([^'"]+)['"]""") + _result_counter = itertools.count(1) @@ -213,17 +219,19 @@ def _extract_modules_from_settings_gradle(content: str) -> list[str]: modules: list[str] = [] # Standard include(...) directives — word boundary avoids matching variable names # like 'includedProjects' - for match in re.findall(r"""(?:^|(?<=\s))include\s*\(?[^)\n]*\)?""", content, re.MULTILINE): - for name in re.findall(r"""['"]([^'"]+)['"]""", match): + 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): - for match in re.findall(r"""listOf\s*\(([^)]*)\)""", content, re.DOTALL): - for name in re.findall(r"""['"]([^'"]+)['"]""", match): + seen = set(modules) + for match in _LISTOF_PATTERN.findall(content): + for name in _QUOTED_PATTERN.findall(match): stripped = name.lstrip(":") - if stripped not in modules: + if stripped not in seen: modules.append(stripped) + seen.add(stripped) return modules From 0ab4800f74e906382c72c019158e5423c329c6e2 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 7 Apr 2026 14:46:37 +0000 Subject: [PATCH 03/14] fix: use tree-sitter for Gradle repositories block and add version update logic - Generalize _find_top_level_dependencies_block() into _find_top_level_block(name) so it can find any top-level block (dependencies, repositories, etc.) - Rewrite _ensure_maven_central_repo() to use tree-sitter instead of regex, preventing false matches inside buildscript/subprojects/allprojects blocks - Add _update_existing_codeflash_dependency() to replace stale versions or old files() format with the current Maven Central coordinate - Wire version update into add_codeflash_dependency() and add_codeflash_dependency_multimodule() so old entries get updated instead of silently skipped Co-Authored-By: Claude Opus 4.6 --- codeflash/languages/java/gradle_strategy.py | 107 +++++++--- .../test_java/test_build_tools.py | 190 +++++++++++++++++- 2 files changed, 271 insertions(+), 26 deletions(-) diff --git a/codeflash/languages/java/gradle_strategy.py b/codeflash/languages/java/gradle_strategy.py index 660256fdb..1c527ee35 100644 --- a/codeflash/languages/java/gradle_strategy.py +++ b/codeflash/languages/java/gradle_strategy.py @@ -129,12 +129,12 @@ def find_gradle_build_file(project_root: Path) -> Path | None: return None -def _find_top_level_dependencies_block(build_file: Path, content: str) -> int | None: - """Find the insert position (before closing }) of the top-level dependencies block using tree-sitter. +def _find_top_level_block(build_file: Path, content: str, block_name: str) -> int | None: + """Find the insert position (before closing }) of a top-level block using tree-sitter. - Returns the byte offset of the closing brace, or None if no top-level dependencies block exists. - Only matches `dependencies { }` at the root level — ignores blocks nested inside - `buildscript`, `subprojects`, `allprojects`, etc. + Returns the byte offset of the closing brace, or None if no top-level block with the + given name exists. Only matches blocks at the root level — ignores blocks nested inside + ``buildscript``, ``subprojects``, ``allprojects``, etc. """ import tree_sitter as ts @@ -152,10 +152,10 @@ def _find_top_level_dependencies_block(build_file: Path, content: str) -> int | tree = parser.parse(source_bytes) - # Walk only direct children of root to find top-level `dependencies { }` + # Walk only direct children of root to find top-level ` { }` for child in tree.root_node.children: - # Groovy: expression_statement > method_invocation(identifier="dependencies", closure) - # Kotlin: call_expression(identifier="dependencies", annotated_lambda) + # Groovy: expression_statement > method_invocation(identifier, closure) + # Kotlin: call_expression(identifier, annotated_lambda) node = child if node.type == "expression_statement" and node.child_count > 0: node = node.children[0] @@ -175,7 +175,7 @@ def _find_top_level_dependencies_block(build_file: Path, content: str) -> int | continue name = source_bytes[name_node.start_byte : name_node.end_byte].decode("utf-8") - if name != "dependencies": + if name != block_name: continue # Find the closing brace of this block @@ -190,6 +190,11 @@ def _find_top_level_dependencies_block(build_file: Path, content: str) -> int | return None +def _find_top_level_dependencies_block(build_file: Path, content: str) -> int | None: + """Find the insert position (before closing }) of the top-level dependencies block.""" + return _find_top_level_block(build_file, content, "dependencies") + + def _is_multimodule_project(build_root: Path) -> bool: """Check if this is a multi-module Gradle project by looking for include directives in settings files.""" for settings_name in ("settings.gradle", "settings.gradle.kts"): @@ -207,24 +212,58 @@ def _is_multimodule_project(build_root: Path) -> bool: _CODEFLASH_MAVEN_COORD = f"com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}" -def _ensure_maven_central_repo(build_file: Path, content: str) -> str: - """Ensure mavenCentral() is present in the repositories block. Returns updated content.""" - if "mavenCentral()" in content: - return content +def _update_existing_codeflash_dependency(build_file: Path, content: str) -> str | None: + """If the codeflash-runtime dependency exists but is outdated or uses the old files() format, update it. + Returns the updated content, or None if no update was needed (already current). + """ is_kts = build_file.name.endswith(".kts") - # Try to find existing repositories block and add mavenCentral() inside it - repo_match = re.search(r"repositories\s*\{", content) - if repo_match: - insert_pos = repo_match.end() - return content[:insert_pos] + "\n mavenCentral()" + content[insert_pos:] - - # No repositories block — append one if is_kts: - content += "\nrepositories {\n mavenCentral()\n}\n" + current_dep = f'testImplementation("{_CODEFLASH_MAVEN_COORD}")' else: - content += "\nrepositories {\n mavenCentral()\n}\n" + current_dep = f"testImplementation '{_CODEFLASH_MAVEN_COORD}'" + + if current_dep in content: + return None + + # Replace the line containing "codeflash-runtime" with the current Maven Central coordinate. + # This handles both old versions (e.g. 1.0.0) and old files() format. + updated_lines: list[str] = [] + replaced = False + for line in content.splitlines(keepends=True): + if "codeflash-runtime" in line: + indent = len(line) - len(line.lstrip()) + spaces = " " * indent + if is_kts: + updated_lines.append(f'{spaces}testImplementation("{_CODEFLASH_MAVEN_COORD}") // codeflash-runtime\n') + else: + updated_lines.append(f"{spaces}testImplementation '{_CODEFLASH_MAVEN_COORD}' // codeflash-runtime\n") + replaced = True + else: + updated_lines.append(line) + + if replaced: + return "".join(updated_lines) + return None + + +def _ensure_maven_central_repo(build_file: Path, content: str) -> str: + """Ensure mavenCentral() is present in the top-level repositories block. Returns updated content. + + Uses tree-sitter to find the correct top-level ``repositories {}`` block, avoiding + false matches inside ``buildscript {}``, ``subprojects {}``, etc. + """ + if "mavenCentral()" in content: + return content + + # Use tree-sitter to find the top-level repositories block + insert_pos = _find_top_level_block(build_file, content, "repositories") + if insert_pos is not None: + return content[:insert_pos] + " mavenCentral()\n" + content[insert_pos:] + + # No top-level repositories block — append one + content += "\nrepositories {\n mavenCentral()\n}\n" return content @@ -241,7 +280,16 @@ def add_codeflash_dependency_multimodule(build_file: Path) -> bool: content = build_file.read_text(encoding="utf-8") if "codeflash-runtime" in content: - logger.info("codeflash-runtime dependency already present in %s", build_file.name) + updated = _update_existing_codeflash_dependency(build_file, content) + if updated is not None: + build_file.write_text(updated, encoding="utf-8") + logger.info( + "Updated codeflash-runtime dependency in %s to version %s", + build_file.name, + CODEFLASH_RUNTIME_VERSION, + ) + else: + logger.info("codeflash-runtime dependency already up-to-date in %s", build_file.name) return True is_kts = build_file.name.endswith(".kts") @@ -291,7 +339,18 @@ def add_codeflash_dependency(build_file: Path) -> bool: content = build_file.read_text(encoding="utf-8") if "codeflash-runtime" in content: - logger.info("codeflash-runtime dependency already present in %s", build_file.name) + updated = _update_existing_codeflash_dependency(build_file, content) + if updated is not None: + # Also ensure mavenCentral() is present (old files() format won't have it) + updated = _ensure_maven_central_repo(build_file, updated) + build_file.write_text(updated, encoding="utf-8") + logger.info( + "Updated codeflash-runtime dependency in %s to version %s", + build_file.name, + CODEFLASH_RUNTIME_VERSION, + ) + else: + logger.info("codeflash-runtime dependency already up-to-date in %s", build_file.name) return True content = _ensure_maven_central_repo(build_file, content) diff --git a/tests/test_languages/test_java/test_build_tools.py b/tests/test_languages/test_java/test_build_tools.py index 254216a9e..0b28ba64b 100644 --- a/tests/test_languages/test_java/test_build_tools.py +++ b/tests/test_languages/test_java/test_build_tools.py @@ -2,16 +2,22 @@ import os from pathlib import Path -from unittest.mock import patch from codeflash.languages.java.build_tools import ( + CODEFLASH_RUNTIME_VERSION, BuildTool, detect_build_tool, find_source_root, find_test_root, get_project_info, ) -from codeflash.languages.java.gradle_strategy import GradleStrategy +from codeflash.languages.java.gradle_strategy import ( + GradleStrategy, + _ensure_maven_central_repo, + _find_top_level_block, + _update_existing_codeflash_dependency, +) +from codeflash.languages.java.gradle_strategy import add_codeflash_dependency as gradle_add_codeflash_dependency from codeflash.languages.java.maven_strategy import MavenStrategy, add_codeflash_dependency from codeflash.languages.java.test_runner import _extract_modules_from_pom_content @@ -644,3 +650,183 @@ def test_does_not_copy_jar_to_libs(self, tmp_path): libs_dir = project / "streams" / "libs" assert not libs_dir.exists() + + +class TestEnsureMavenCentralRepo: + """Tests for _ensure_maven_central_repo with tree-sitter.""" + + def test_skips_when_maven_central_already_present(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = "repositories {\n mavenCentral()\n}\n" + build_file.write_text(content, encoding="utf-8") + result = _ensure_maven_central_repo(build_file, content) + assert result == content + + def test_inserts_into_top_level_repositories(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = "repositories {\n google()\n}\n" + build_file.write_text(content, encoding="utf-8") + result = _ensure_maven_central_repo(build_file, content) + assert "mavenCentral()" in result + assert "google()" in result + + def test_does_not_match_buildscript_repositories(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = "buildscript {\n repositories {\n google()\n }\n}\n" + build_file.write_text(content, encoding="utf-8") + result = _ensure_maven_central_repo(build_file, content) + # Should NOT insert into buildscript repositories — should append a new top-level block + assert result.endswith("\nrepositories {\n mavenCentral()\n}\n") + # The buildscript block should be unchanged + assert "buildscript {\n repositories {\n google()\n }\n}" in result + + def test_appends_new_block_when_no_repositories(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = "plugins {\n id 'java'\n}\n" + build_file.write_text(content, encoding="utf-8") + result = _ensure_maven_central_repo(build_file, content) + assert result.endswith("\nrepositories {\n mavenCentral()\n}\n") + + def test_works_with_kts(self, tmp_path): + build_file = tmp_path / "build.gradle.kts" + content = "repositories {\n google()\n}\n" + build_file.write_text(content, encoding="utf-8") + result = _ensure_maven_central_repo(build_file, content) + assert "mavenCentral()" in result + + +class TestFindTopLevelBlock: + """Tests for _find_top_level_block tree-sitter function.""" + + def test_finds_top_level_dependencies(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = 'dependencies {\n testImplementation "junit:junit:4.13"\n}\n' + build_file.write_text(content, encoding="utf-8") + pos = _find_top_level_block(build_file, content, "dependencies") + assert pos is not None + assert content[pos] == "}" + + def test_ignores_nested_dependencies(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = ( + "buildscript {\n dependencies {\n classpath 'com.android.tools.build:gradle:7.0'\n }\n}\n" + ) + build_file.write_text(content, encoding="utf-8") + pos = _find_top_level_block(build_file, content, "dependencies") + assert pos is None + + def test_finds_top_level_repositories(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = "repositories {\n google()\n}\n" + build_file.write_text(content, encoding="utf-8") + pos = _find_top_level_block(build_file, content, "repositories") + assert pos is not None + + def test_ignores_buildscript_repositories(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = "buildscript {\n repositories {\n google()\n }\n}\n" + build_file.write_text(content, encoding="utf-8") + pos = _find_top_level_block(build_file, content, "repositories") + assert pos is None + + def test_returns_none_when_block_absent(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = "plugins {\n id 'java'\n}\n" + build_file.write_text(content, encoding="utf-8") + pos = _find_top_level_block(build_file, content, "repositories") + assert pos is None + + def test_works_with_kts(self, tmp_path): + build_file = tmp_path / "build.gradle.kts" + content = 'dependencies {\n testImplementation("junit:junit:4.13")\n}\n' + build_file.write_text(content, encoding="utf-8") + pos = _find_top_level_block(build_file, content, "dependencies") + assert pos is not None + + +class TestGradleVersionUpdate: + """Tests for version update logic in Gradle dependency management.""" + + def test_updates_old_version(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = ( + "dependencies {\n testImplementation 'com.codeflash:codeflash-runtime:1.0.0' // codeflash-runtime\n}\n" + ) + build_file.write_text(content, encoding="utf-8") + result = _update_existing_codeflash_dependency(build_file, content) + assert result is not None + assert f"com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}" in result + assert "1.0.0" not in result + + def test_updates_old_files_format(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = "dependencies {\n testImplementation files('/path/to/codeflash-runtime-1.0.1.jar')\n}\n" + build_file.write_text(content, encoding="utf-8") + result = _update_existing_codeflash_dependency(build_file, content) + assert result is not None + assert f"com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}" in result + assert "files(" not in result + + def test_returns_none_when_already_current(self, tmp_path): + build_file = tmp_path / "build.gradle" + content = f"dependencies {{\n testImplementation 'com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}' // codeflash-runtime\n}}\n" + build_file.write_text(content, encoding="utf-8") + result = _update_existing_codeflash_dependency(build_file, content) + assert result is None + + def test_returns_none_when_already_current_kts(self, tmp_path): + build_file = tmp_path / "build.gradle.kts" + content = f'dependencies {{\n testImplementation("com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}")\n}}\n' + build_file.write_text(content, encoding="utf-8") + result = _update_existing_codeflash_dependency(build_file, content) + assert result is None + + def test_updates_old_version_kts(self, tmp_path): + build_file = tmp_path / "build.gradle.kts" + content = ( + 'dependencies {\n testImplementation("com.codeflash:codeflash-runtime:1.0.0") // codeflash-runtime\n}\n' + ) + build_file.write_text(content, encoding="utf-8") + result = _update_existing_codeflash_dependency(build_file, content) + assert result is not None + assert f"com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}" in result + + def test_add_codeflash_dependency_updates_old_version(self, tmp_path): + """Full integration: add_codeflash_dependency updates old version instead of skipping.""" + build_file = tmp_path / "build.gradle" + build_file.write_text( + "repositories {\n mavenCentral()\n}\n\n" + "dependencies {\n testImplementation 'com.codeflash:codeflash-runtime:1.0.0'\n}\n", + encoding="utf-8", + ) + result = gradle_add_codeflash_dependency(build_file) + assert result is True + content = build_file.read_text(encoding="utf-8") + assert f"com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}" in content + assert "1.0.0" not in content + + def test_add_codeflash_dependency_replaces_files_format(self, tmp_path): + """Full integration: add_codeflash_dependency replaces old files() with Maven Central coord.""" + build_file = tmp_path / "build.gradle" + build_file.write_text( + "dependencies {\n testImplementation files('/home/user/libs/codeflash-runtime-1.0.1.jar')\n}\n", + encoding="utf-8", + ) + result = gradle_add_codeflash_dependency(build_file) + assert result is True + content = build_file.read_text(encoding="utf-8") + assert f"com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}" in content + assert "files(" not in content + # Should also add mavenCentral() since the old format didn't need it + assert "mavenCentral()" in content + + def test_add_codeflash_dependency_preserves_indent(self, tmp_path): + build_file = tmp_path / "build.gradle" + build_file.write_text( + "dependencies {\n testImplementation 'com.codeflash:codeflash-runtime:1.0.0'\n}\n", encoding="utf-8" + ) + result = gradle_add_codeflash_dependency(build_file) + assert result is True + content = build_file.read_text(encoding="utf-8") + # Should preserve the 8-space indent + assert f" testImplementation 'com.codeflash:codeflash-runtime:{CODEFLASH_RUNTIME_VERSION}'" in content From 32bbe57867905fc2e0e8051c617a877dd0044646 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 7 Apr 2026 14:49:03 +0000 Subject: [PATCH 04/14] fix: add classpath hint to find_agent_jar for Gradle JAR resolution Gradle resolves the codeflash-runtime JAR to ~/.gradle/caches/, not ~/.m2/. Add an optional classpath parameter to find_agent_jar() that searches the resolved classpath for the JAR before falling back to the existing ~/.m2 / resources / dev-build chain. Thread the parameter through build_javaagent_arg, build_agent_env, instrument_source_for_line_profiler, and line_profiler_step so the optimization pipeline passes the resolved classpath automatically. Co-Authored-By: Claude Opus 4.6 --- .../languages/java/function_optimizer.py | 4 +++- codeflash/languages/java/line_profiler.py | 19 ++++++++++++++----- codeflash/languages/java/support.py | 5 +++-- codeflash/languages/java/tracer.py | 4 ++-- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/codeflash/languages/java/function_optimizer.py b/codeflash/languages/java/function_optimizer.py index 5700a907a..08980032e 100644 --- a/codeflash/languages/java/function_optimizer.py +++ b/codeflash/languages/java/function_optimizer.py @@ -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": ""} diff --git a/codeflash/languages/java/line_profiler.py b/codeflash/languages/java/line_profiler.py index 854a8549d..706b543a8 100644 --- a/codeflash/languages/java/line_profiler.py +++ b/codeflash/languages/java/line_profiler.py @@ -13,6 +13,7 @@ import json import logging +import os import re from pathlib import Path from typing import TYPE_CHECKING, Any @@ -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) @@ -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" diff --git a/codeflash/languages/java/support.py b/codeflash/languages/java/support.py index ab3818348..7115d2225 100644 --- a/codeflash/languages/java/support.py +++ b/codeflash/languages/java/support.py @@ -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. @@ -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. @@ -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: diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py index ab8f19514..50506797e 100644 --- a/codeflash/languages/java/tracer.py +++ b/codeflash/languages/java/tracer.py @@ -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) From 1fa01a3296701c0081bbf61582fdff7e92ca72b8 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 7 Apr 2026 14:50:45 +0000 Subject: [PATCH 05/14] fix: replace Gradle JaCoCo plugin with runtime JAR agent for coverage The Gradle JaCoCo plugin approach (jacocoTestReport task) fails on multi-module projects and adds 5-10 min overhead. Replace with: 1. Inject -javaagent:{runtime_jar}=destfile={exec} via JAVA_TOOL_OPTIONS (AgentDispatcher routes destfile= args to JaCoCo PreMain) 2. Run tests without jacocoTestReport task 3. Convert .exec to .xml via shaded JaCoCo CLI in the runtime JAR This eliminates the "jacocoTestReport not found" error on eureka and similar multi-module Gradle projects, and removes build file mutation for coverage setup. Co-Authored-By: Claude Opus 4.6 --- codeflash/languages/java/gradle_strategy.py | 156 +++++++++++--------- 1 file changed, 84 insertions(+), 72 deletions(-) diff --git a/codeflash/languages/java/gradle_strategy.py b/codeflash/languages/java/gradle_strategy.py index 1c527ee35..7589d9ca9 100644 --- a/codeflash/languages/java/gradle_strategy.py +++ b/codeflash/languages/java/gradle_strategy.py @@ -102,22 +102,6 @@ def cp = configurations.findByName('testRuntimeClasspath') } """ -# Gradle init script that applies JaCoCo plugin for coverage collection. -# Uses projectsEvaluated to avoid triggering configuration of unrelated subprojects. -_JACOCO_INIT_SCRIPT = """\ -gradle.projectsEvaluated { - allprojects { - apply plugin: 'jacoco' - jacocoTestReport { - reports { - xml.required = true - html.required = false - } - } - } -} -""" - def find_gradle_build_file(project_root: Path) -> Path | None: kts = project_root / "build.gradle.kts" @@ -736,7 +720,7 @@ def run_tests_via_build_tool( mode: str, test_module: str | None, javaagent_arg: str | None = None, - enable_coverage: bool = False, + enable_coverage: bool = False, # kept for interface compatibility; coverage now uses JAVA_TOOL_OPTIONS ) -> subprocess.CompletedProcess[str]: from codeflash.languages.java.test_runner import _build_test_filter, _run_cmd_kill_pg_on_timeout @@ -807,25 +791,12 @@ def run_tests_via_build_tool( cmd = [gradle, task, "--no-daemon", "--rerun", "--init-script", init_path] cmd.extend(["--init-script", _get_skip_validation_init_script()]) - # --continue ensures Gradle keeps going even if some tests fail. - # For coverage: needed so jacocoTestReport runs even after test failures - # (matches Maven's -Dmaven.test.failure.ignore=true). - # Note: multi-module --tests filtering is handled by - # filter.failOnNoMatchingTests = false in the init script above - # (matches Maven's -DfailIfNoTests=false). - if enable_coverage: - cmd.append("--continue") - for class_filter in test_filter.split(","): class_filter = class_filter.strip() if class_filter: cmd.extend(["--tests", class_filter]) logger.debug("Added --tests filters to Gradle command") - # Append jacocoTestReport AFTER --tests so Gradle doesn't try to apply --tests to it - if enable_coverage: - cmd.append("jacocoTestReport") - logger.debug("Running Gradle command: %s in %s", " ".join(cmd), build_root) result = _run_cmd_kill_pg_on_timeout(cmd, cwd=build_root, env=env, timeout=timeout) @@ -962,64 +933,105 @@ def run_tests_with_coverage( timeout: int, candidate_index: int, ) -> tuple[subprocess.CompletedProcess[str], Path, Path | None]: + from codeflash.languages.java.line_profiler import find_agent_jar from codeflash.languages.java.test_runner import _get_combined_junit_xml - coverage_xml_path = self.setup_coverage(build_root, test_module, build_root) + if test_module: + module_path = build_root / module_to_dir(test_module) + else: + module_path = build_root + # Locate the runtime JAR (contains shaded JaCoCo agent + CLI) + classpath = self.get_classpath(build_root, run_env, test_module) + runtime_jar = find_agent_jar(classpath=classpath) + if runtime_jar is None: + logger.warning("codeflash-runtime JAR not found, cannot collect coverage") + result = self.run_tests_via_build_tool( + build_root, test_paths, run_env, timeout=timeout, mode="behavior", test_module=test_module + ) + reports_dir = self.get_reports_dir(build_root, test_module) + result_xml_path = _get_combined_junit_xml(reports_dir, candidate_index) + return result, result_xml_path, None + + # Use the runtime JAR's built-in JaCoCo agent via AgentDispatcher + exec_path = module_path / "build" / "jacoco" / "test.exec" + exec_path.parent.mkdir(parents=True, exist_ok=True) + + jacoco_agent_arg = f"-javaagent:{runtime_jar}=destfile={exec_path}" + run_env = run_env.copy() + existing_opts = run_env.get("JAVA_TOOL_OPTIONS", "") + run_env["JAVA_TOOL_OPTIONS"] = f"{existing_opts} {jacoco_agent_arg}".strip() + + # Run tests WITHOUT enable_coverage (no jacocoTestReport task needed) result = self.run_tests_via_build_tool( - build_root, - test_paths, - run_env, - timeout=timeout, - mode="behavior", - enable_coverage=True, - test_module=test_module, + build_root, test_paths, run_env, timeout=timeout, mode="behavior", test_module=test_module ) reports_dir = self.get_reports_dir(build_root, test_module) result_xml_path = _get_combined_junit_xml(reports_dir, candidate_index) + # Convert .exec → .xml via the shaded JaCoCo CLI in the runtime JAR + coverage_xml_path = self._convert_jacoco_exec_to_xml(runtime_jar, exec_path, module_path) + return result, result_xml_path, coverage_xml_path - def setup_coverage(self, build_root: Path, test_module: str | None, project_root: Path) -> Path | None: - if test_module: - module_root = build_root / module_to_dir(test_module) - else: - module_root = project_root + def _convert_jacoco_exec_to_xml(self, runtime_jar: Path, exec_path: Path, module_path: Path) -> Path | None: + if not exec_path.exists(): + logger.warning("JaCoCo exec file not found: %s", exec_path) + return None - build_file = find_gradle_build_file(module_root) - if build_file is None: - logger.warning("No build.gradle(.kts) found at %s, cannot setup JaCoCo", module_root) + xml_path = exec_path.with_suffix(".xml") + + # Collect classfiles directories for the report + classfiles_dirs: list[str] = [] + for classes_dir in [ + module_path / "build" / "classes" / "java" / "main", + module_path / "build" / "classes" / "java" / "test", + ]: + if classes_dir.exists(): + classfiles_dirs.append(str(classes_dir)) + + if not classfiles_dirs: + logger.warning("No classfiles directories found under %s/build/classes", module_path) return None - content = build_file.read_text(encoding="utf-8") - if "jacoco" not in content.lower(): - logger.info("Adding JaCoCo plugin to %s for coverage collection", build_file.name) - is_kts = build_file.name.endswith(".kts") - if is_kts: - plugin_line = "plugins {\n jacoco\n}\n" - else: - plugin_line = "apply plugin: 'jacoco'\n" - - if "plugins {" in content or "plugins{" in content: - # Insert jacoco inside existing plugins block - plugins_idx = content.find("plugins") - brace_depth = 0 - for i in range(plugins_idx, len(content)): - if content[i] == "{": - brace_depth += 1 - elif content[i] == "}": - brace_depth -= 1 - if brace_depth == 0: - insert = " jacoco\n" if is_kts else " id 'jacoco'\n" - content = content[:i] + insert + content[i:] - break - else: - content = plugin_line + content + cmd = [ + "java", + "-cp", + str(runtime_jar), + "com.codeflash.shaded.org.jacoco.cli.internal.Main", + "report", + str(exec_path), + ] + for d in classfiles_dirs: + cmd.extend(["--classfiles", d]) + cmd.extend(["--xml", str(xml_path)]) - build_file.write_text(content, encoding="utf-8") + logger.debug("Converting JaCoCo exec to XML: %s", " ".join(cmd)) + try: + conv_result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, check=False) + if conv_result.returncode != 0: + logger.warning( + "JaCoCo exec→XML conversion failed (exit %d): %s", conv_result.returncode, conv_result.stderr + ) + return None + except Exception: + logger.exception("JaCoCo exec→XML conversion error") + return None + + if xml_path.exists(): + logger.info("JaCoCo coverage XML generated: %s", xml_path) + return xml_path + + logger.warning("JaCoCo XML not created at %s", xml_path) + return None - return module_root / "build" / "reports" / "jacoco" / "test" / "jacocoTestReport.xml" + def setup_coverage(self, build_root: Path, test_module: str | None, project_root: Path) -> Path | None: + if test_module: + module_root = build_root / module_to_dir(test_module) + else: + module_root = project_root + return module_root / "build" / "jacoco" / "test.xml" def get_test_run_command(self, project_root: Path, test_classes: list[str] | None = None) -> list[str]: from codeflash.languages.java.test_runner import _validate_java_class_name From ba8dd8bdb92aa2f865e473b24043cdcea42a01d4 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:56:15 +0000 Subject: [PATCH 06/14] fix: add project_classpath param to base LanguageSupport.instrument_source_for_line_profiler --- codeflash/languages/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeflash/languages/base.py b/codeflash/languages/base.py index e699afd2b..dea5126f7 100644 --- a/codeflash/languages/base.py +++ b/codeflash/languages/base.py @@ -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.""" ... From 217544f99ea0b04cc3d14aea23305d54e72a2220 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 7 Apr 2026 15:02:55 +0000 Subject: [PATCH 07/14] fix: handle multi-line include directives in settings.gradle The regex for extracting modules from settings.gradle only matched single-line include statements. Multi-line includes like eureka's (include 'a',\n 'b',\n 'c') only captured the first module, causing test_module to be None and breaking multi-module path resolution (e.g., classfiles lookup for JaCoCo coverage conversion). Co-Authored-By: Claude Opus 4.6 --- codeflash/languages/java/test_runner.py | 5 +++-- .../test_java/test_java_test_paths.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/codeflash/languages/java/test_runner.py b/codeflash/languages/java/test_runner.py index 9bbf95753..b333f2713 100644 --- a/codeflash/languages/java/test_runner.py +++ b/codeflash/languages/java/test_runner.py @@ -24,7 +24,7 @@ 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*\(?[^)\n]*\)?""", re.MULTILINE) +_INCLUDE_PATTERN = re.compile(r"""(?:^|(?<=\s))include\s*\(?[^)]*\)?""", re.MULTILINE | re.DOTALL) _LISTOF_PATTERN = re.compile(r"""listOf\s*\(([^)]*)\)""", re.DOTALL) @@ -218,7 +218,8 @@ def _extract_modules_from_settings_gradle(content: str) -> list[str]: """ modules: list[str] = [] # Standard include(...) directives — word boundary avoids matching variable names - # like 'includedProjects' + # 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(":")) diff --git a/tests/test_languages/test_java/test_java_test_paths.py b/tests/test_languages/test_java/test_java_test_paths.py index 1120862f3..4c4e7fa08 100644 --- a/tests/test_languages/test_java/test_java_test_paths.py +++ b/tests/test_languages/test_java/test_java_test_paths.py @@ -507,6 +507,25 @@ def test_leading_colon_stripped(self): assert "streams" in modules assert "clients" in modules + def test_multi_line_groovy_include(self): + content = """rootProject.name='eureka' +include 'eureka-client', + 'eureka-client-jersey2', + 'eureka-server', + 'eureka-core', + 'eureka-resources'""" + modules = _extract_modules_from_settings_gradle(content) + assert modules == ["eureka-client", "eureka-client-jersey2", "eureka-server", "eureka-core", "eureka-resources"] + + def test_multi_line_kotlin_dsl_include(self): + content = """include( + "module-a", + "module-b", + "module-c" +)""" + modules = _extract_modules_from_settings_gradle(content) + assert modules == ["module-a", "module-b", "module-c"] + class TestFindMultiModuleRoot: """Tests for _find_multi_module_root with Gradle multi-module projects.""" From e658fb45af5d192ffa9a83c152ff441bc76ef082 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 7 Apr 2026 15:35:55 +0000 Subject: [PATCH 08/14] fix: increase coverage timeout from 300s to 900s for Gradle builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gradle --no-daemon on multi-module projects (e.g., eureka) needs cold JVM startup + dependency resolution + compilation + test execution + JaCoCo agent overhead, which exceeds 300s. At 300s the process is killed mid-execution, producing partial results that the pipeline can't use for behavioral baseline. Ported from PR #2013 which validated 300s→600s→900s progression on eureka (build takes ~10 min, 900s provides safe headroom). Co-Authored-By: Claude Opus 4.6 --- codeflash/languages/java/test_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeflash/languages/java/test_runner.py b/codeflash/languages/java/test_runner.py index b333f2713..0b1c6aa8b 100644 --- a/codeflash/languages/java/test_runner.py +++ b/codeflash/languages/java/test_runner.py @@ -526,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 = 900 if enable_coverage else 60 effective_timeout = max(timeout or 300, min_timeout) if enable_coverage: From 389aa16f76e8c4aff89d2bce320682d21bc365bc Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 7 Apr 2026 15:55:00 +0000 Subject: [PATCH 09/14] fix: pre-compile test classes and increase compilation timeout for Gradle Two changes to prevent cold-build timeouts on large multi-module Gradle projects (e.g., eureka ~16 min cold build): 1. install_multi_module_deps now compiles testClasses instead of just classes, so the test execution timeout only covers running tests, not compilation. 2. Pre-install compilation timeout increased from 300s to 900s to accommodate cold Gradle --no-daemon builds on large projects. Combined with the coverage min_timeout of 900s (previous commit), compilation and test execution each get their own 900s budget instead of sharing one. Ported from PR #2013 experience where 300s/600s were validated as insufficient for eureka cold builds. Co-Authored-By: Claude Opus 4.6 --- codeflash/languages/java/gradle_strategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codeflash/languages/java/gradle_strategy.py b/codeflash/languages/java/gradle_strategy.py index 7589d9ca9..2eb27b8b8 100644 --- a/codeflash/languages/java/gradle_strategy.py +++ b/codeflash/languages/java/gradle_strategy.py @@ -532,14 +532,14 @@ def install_multi_module_deps(self, build_root: Path, test_module: str | None, e logger.error("Gradle not found — cannot pre-install multi-module dependencies") return False - cmd = [gradle, f":{test_module}:classes", "-x", "test", "--build-cache", "--no-daemon"] + cmd = [gradle, f":{test_module}:testClasses", "-x", "test", "--build-cache", "--no-daemon"] cmd.extend(["--init-script", _get_skip_validation_init_script()]) logger.info("Pre-installing multi-module dependencies: %s (module: %s)", build_root, test_module) logger.debug("Running: %s", " ".join(cmd)) try: - result = _run_cmd_kill_pg_on_timeout(cmd, cwd=build_root, env=env, timeout=300) + result = _run_cmd_kill_pg_on_timeout(cmd, cwd=build_root, env=env, timeout=900) if result.returncode != 0: logger.error( "Failed to pre-install multi-module deps (exit %d).\nstdout: %s\nstderr: %s", From 5e2ef37f6f73c677b789fba9af39bd14dcf29d17 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 7 Apr 2026 16:13:06 +0000 Subject: [PATCH 10/14] fix: increase coverage timeout to 1200s for large Gradle --no-daemon builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gradle --no-daemon on multi-module projects forces a full JVM cold start for every invocation. On eureka, configuration + dependency resolution alone takes ~10 min before tests even start. 900s was still getting killed at the boundary. 1200s (20 min) provides headroom for: cold Gradle startup (~10 min) + test execution (~5 min) + JaCoCo overhead + safety margin. PR #2013 iterated through 300→600→900s and found 900s sufficient only when build caches were warm from prior invocations in the same session. Co-Authored-By: Claude Opus 4.6 --- codeflash/languages/java/test_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeflash/languages/java/test_runner.py b/codeflash/languages/java/test_runner.py index 0b1c6aa8b..6a144b367 100644 --- a/codeflash/languages/java/test_runner.py +++ b/codeflash/languages/java/test_runner.py @@ -526,7 +526,7 @@ def run_behavioral_tests( if enable_coverage: coverage_xml_path = strategy.setup_coverage(build_root, test_module, project_root) - min_timeout = 900 if enable_coverage else 60 + min_timeout = 1200 if enable_coverage else 60 effective_timeout = max(timeout or 300, min_timeout) if enable_coverage: From 64790d5b609a16a7a2dddfb47e0bdb6413d96e54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:13:50 +0000 Subject: [PATCH 11/14] style: auto-format with ruff --- codeflash-benchmark/codeflash_benchmark/version.py | 2 +- codeflash/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codeflash-benchmark/codeflash_benchmark/version.py b/codeflash-benchmark/codeflash_benchmark/version.py index a421c3690..4eb66411f 100644 --- a/codeflash-benchmark/codeflash_benchmark/version.py +++ b/codeflash-benchmark/codeflash_benchmark/version.py @@ -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.post163.dev0+3f533098" diff --git a/codeflash/version.py b/codeflash/version.py index 226fdf7ad..4eb66411f 100644 --- a/codeflash/version.py +++ b/codeflash/version.py @@ -1,2 +1,2 @@ # These version placeholders will be replaced by uv-dynamic-versioning during build. -__version__ = "0.20.5" +__version__ = "0.20.5.post163.dev0+3f533098" From 44c1bcf458e9bdb21a3f4f08f1a0cf35e898a0c7 Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Thu, 9 Apr 2026 18:42:16 -0500 Subject: [PATCH 12/14] ci: retrigger CI From 11201fe7c6c95ef7216674eee7cf60fe185a6479 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:43:15 +0000 Subject: [PATCH 13/14] style: auto-format with ruff --- codeflash-benchmark/codeflash_benchmark/version.py | 2 +- codeflash/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codeflash-benchmark/codeflash_benchmark/version.py b/codeflash-benchmark/codeflash_benchmark/version.py index 4eb66411f..8605f062a 100644 --- a/codeflash-benchmark/codeflash_benchmark/version.py +++ b/codeflash-benchmark/codeflash_benchmark/version.py @@ -1,2 +1,2 @@ # These version placeholders will be replaced by uv-dynamic-versioning during build. -__version__ = "0.20.5.post163.dev0+3f533098" +__version__ = "0.20.5.post165.dev0+44c1bcf4" diff --git a/codeflash/version.py b/codeflash/version.py index 4eb66411f..8605f062a 100644 --- a/codeflash/version.py +++ b/codeflash/version.py @@ -1,2 +1,2 @@ # These version placeholders will be replaced by uv-dynamic-versioning during build. -__version__ = "0.20.5.post163.dev0+3f533098" +__version__ = "0.20.5.post165.dev0+44c1bcf4" From a6ea56bf50f408026364761990de141144b28fbc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:44:22 +0000 Subject: [PATCH 14/14] style: auto-format with ruff --- codeflash-benchmark/codeflash_benchmark/version.py | 2 +- codeflash/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codeflash-benchmark/codeflash_benchmark/version.py b/codeflash-benchmark/codeflash_benchmark/version.py index 8605f062a..a759bb9fd 100644 --- a/codeflash-benchmark/codeflash_benchmark/version.py +++ b/codeflash-benchmark/codeflash_benchmark/version.py @@ -1,2 +1,2 @@ # These version placeholders will be replaced by uv-dynamic-versioning during build. -__version__ = "0.20.5.post165.dev0+44c1bcf4" +__version__ = "0.20.5.post169.dev0+2dba3e38" diff --git a/codeflash/version.py b/codeflash/version.py index 8605f062a..a759bb9fd 100644 --- a/codeflash/version.py +++ b/codeflash/version.py @@ -1,2 +1,2 @@ # These version placeholders will be replaced by uv-dynamic-versioning during build. -__version__ = "0.20.5.post165.dev0+44c1bcf4" +__version__ = "0.20.5.post169.dev0+2dba3e38"