diff --git a/codeflash/languages/java/gradle_strategy.py b/codeflash/languages/java/gradle_strategy.py index b4481dd6e..3b9a18903 100644 --- a/codeflash/languages/java/gradle_strategy.py +++ b/codeflash/languages/java/gradle_strategy.py @@ -17,7 +17,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_JAR_NAME, BuildTool, JavaProjectInfo _RE_INCLUDE = re.compile(r"""include\s*\(?([^)\n]+)\)?""") @@ -78,6 +78,10 @@ def _get_skip_validation_init_script() -> str: return _skip_validation_init_path +# JaCoCo version used for agent and CLI JARs. +_JACOCO_VERSION = "0.8.11" + + # Cache for classpath strings — keyed on (gradle_root, test_module). _classpath_cache: dict[tuple[Path, str | None], str] = {} @@ -103,22 +107,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" @@ -421,6 +409,10 @@ def find_executable(self, build_root: Path) -> str | None: def ensure_runtime(self, build_root: Path, test_module: str | None) -> bool: runtime_jar = self.find_runtime_jar() + if runtime_jar is None: + from codeflash.languages.java.maven_strategy import download_from_maven_central_http + + runtime_jar = download_from_maven_central_http() if runtime_jar is None: logger.error("codeflash-runtime JAR not found. Generated tests will fail to compile.") return False @@ -432,7 +424,7 @@ def ensure_runtime(self, build_root: Path, test_module: str | None) -> bool: libs_dir = module_root / "libs" libs_dir.mkdir(parents=True, exist_ok=True) - dest_jar = libs_dir / "codeflash-runtime-1.0.1.jar" + dest_jar = libs_dir / CODEFLASH_RUNTIME_JAR_NAME if not dest_jar.exists(): logger.info("Copying codeflash-runtime JAR to %s", dest_jar) @@ -728,24 +720,17 @@ 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") + # JaCoCo coverage is collected via -javaagent (set up by run_tests_with_coverage), + # not via a Gradle task. No extra tasks or init scripts needed here. logger.debug("Running Gradle command: %s in %s", " ".join(cmd), build_root) @@ -885,7 +870,26 @@ def run_tests_with_coverage( ) -> tuple[subprocess.CompletedProcess[str], Path, Path | None]: from codeflash.languages.java.test_runner import _get_combined_junit_xml - coverage_xml_path = self.setup_coverage(build_root, test_module, build_root) + # Determine coverage paths + if test_module: + module_path = build_root / module_to_dir(test_module) + else: + module_path = build_root + exec_path = module_path / "build" / "jacoco" / "test.exec" + exec_path.parent.mkdir(parents=True, exist_ok=True) + xml_path: Path | None = exec_path.with_suffix(".xml") + + # Inject JaCoCo agent via JAVA_TOOL_OPTIONS — collects coverage during test execution + # without requiring any Gradle plugin or jacocoTestReport task. + try: + agent_jar = get_jacoco_agent_jar() + agent_opts = f"-javaagent:{agent_jar.absolute()}=destfile={exec_path.absolute()}" + existing = run_env.get("JAVA_TOOL_OPTIONS", "") + run_env["JAVA_TOOL_OPTIONS"] = f"{existing} {agent_opts}".strip() if existing else agent_opts + logger.info("JaCoCo agent enabled: %s", agent_opts) + except Exception: + logger.exception("Failed to configure JaCoCo agent — coverage will be unavailable") + xml_path = None result = self.run_tests_via_build_tool( build_root, @@ -897,50 +901,31 @@ def run_tests_with_coverage( test_module=test_module, ) + # Convert .exec → .xml using JaCoCo CLI (fast, ~2s even on large projects) + if xml_path and exec_path.exists(): + classes_dirs = [ + module_path / "build" / "classes" / "java" / "main", + module_path / "build" / "classes" / "java" / "test", + ] + sources_dirs = [module_path / "src" / "main" / "java", module_path / "src" / "test" / "java"] + convert_jacoco_exec_to_xml(exec_path, classes_dirs, sources_dirs, xml_path) + elif xml_path: + logger.warning("JaCoCo .exec not found at %s — agent may not have run", exec_path) + xml_path = None + 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, coverage_xml_path + return result, result_xml_path, xml_path def setup_coverage(self, build_root: Path, test_module: str | None, project_root: Path) -> Path | None: + # Coverage is collected via JaCoCo agent (injected in run_tests_with_coverage). + # Return the expected XML path so callers know where to look. if test_module: module_root = build_root / module_to_dir(test_module) else: module_root = project_root - - 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) - 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 - - build_file.write_text(content, encoding="utf-8") - - return module_root / "build" / "reports" / "jacoco" / "test" / "jacocoTestReport.xml" + 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 @@ -957,3 +942,85 @@ def get_test_run_command(self, project_root: Path, test_classes: list[str] | Non for cls in test_classes: cmd.extend(["--tests", cls]) return cmd + + +def get_jacoco_agent_jar(codeflash_home: Path | None = None) -> Path: + if codeflash_home is None: + codeflash_home = Path.home() / ".codeflash" + + agent_dir = codeflash_home / "java_agents" + agent_dir.mkdir(parents=True, exist_ok=True) + agent_jar = agent_dir / "jacocoagent.jar" + + if not agent_jar.exists(): + import urllib.request + + url = ( + f"https://repo1.maven.org/maven2/org/jacoco/org.jacoco.agent/{_JACOCO_VERSION}/" + f"org.jacoco.agent-{_JACOCO_VERSION}-runtime.jar" + ) + logger.info("Downloading JaCoCo agent from %s", url) + urllib.request.urlretrieve(url, agent_jar) # noqa: S310 + logger.info("Downloaded JaCoCo agent to %s", agent_jar) + + return agent_jar + + +def get_jacoco_cli_jar(codeflash_home: Path | None = None) -> Path: + if codeflash_home is None: + codeflash_home = Path.home() / ".codeflash" + + cli_dir = codeflash_home / "java_agents" + cli_dir.mkdir(parents=True, exist_ok=True) + cli_jar = cli_dir / "jacococli.jar" + + if not cli_jar.exists(): + import urllib.request + + url = ( + f"https://repo1.maven.org/maven2/org/jacoco/org.jacoco.cli/{_JACOCO_VERSION}/" + f"org.jacoco.cli-{_JACOCO_VERSION}-nodeps.jar" + ) + logger.info("Downloading JaCoCo CLI from %s", url) + urllib.request.urlretrieve(url, cli_jar) # noqa: S310 + logger.info("Downloaded JaCoCo CLI to %s", cli_jar) + + return cli_jar + + +def convert_jacoco_exec_to_xml( + exec_path: Path, classes_dirs: list[Path], sources_dirs: list[Path], xml_path: Path +) -> bool: + if not exec_path.exists(): + logger.error("JaCoCo .exec file not found: %s", exec_path) + return False + + try: + cli_jar = get_jacoco_cli_jar() + except Exception: + logger.exception("Failed to get JaCoCo CLI") + return False + + cmd: list[str] = ["java", "-jar", str(cli_jar), "report", str(exec_path)] + for d in classes_dirs: + if d.exists(): + cmd.extend(["--classfiles", str(d)]) + for d in sources_dirs: + if d.exists(): + cmd.extend(["--sourcefiles", str(d)]) + cmd.extend(["--xml", str(xml_path)]) + + logger.info("Converting JaCoCo .exec to XML: %s", " ".join(cmd)) + try: + result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=60) + if result.returncode == 0: + logger.info("JaCoCo coverage XML generated: %s", xml_path) + return True + logger.error("Failed to convert .exec to XML: %s", result.stderr) + return False + except subprocess.TimeoutExpired: + logger.exception("JaCoCo CLI conversion timed out") + return False + except Exception: + logger.exception("Error converting .exec to XML") + return False diff --git a/codeflash/languages/java/line_profiler.py b/codeflash/languages/java/line_profiler.py index 854a8549d..291e6a2b4 100644 --- a/codeflash/languages/java/line_profiler.py +++ b/codeflash/languages/java/line_profiler.py @@ -568,7 +568,8 @@ def find_method_for_line( def find_agent_jar() -> Path | None: """Locate the profiler agent JAR file (now bundled in codeflash-runtime). - Checks local Maven repo, package resources, and development build directory. + Checks local Maven repo, package resources, development build directory, + and falls back to downloading from Maven Central via HTTP if not found locally. """ # Check local Maven repository first (fastest) m2_jar = ( @@ -594,7 +595,10 @@ def find_agent_jar() -> Path | None: if dev_jar.exists(): return dev_jar - return None + # Download from Maven Central as last resort (no mvn binary needed) + from codeflash.languages.java.maven_strategy import download_from_maven_central_http + + return download_from_maven_central_http() def resolve_internal_class_name(file_path: Path, source: str) -> str: diff --git a/codeflash/languages/java/maven_strategy.py b/codeflash/languages/java/maven_strategy.py index 95dd310c4..86ebdaceb 100644 --- a/codeflash/languages/java/maven_strategy.py +++ b/codeflash/languages/java/maven_strategy.py @@ -72,6 +72,45 @@ """ +MAVEN_CENTRAL_URL = ( + "https://repo1.maven.org/maven2/com/codeflash/codeflash-runtime" + f"/{CODEFLASH_RUNTIME_VERSION}/{CODEFLASH_RUNTIME_JAR_NAME}" +) + +M2_JAR_PATH = ( + Path.home() + / ".m2" + / "repository" + / "com" + / "codeflash" + / "codeflash-runtime" + / CODEFLASH_RUNTIME_VERSION + / CODEFLASH_RUNTIME_JAR_NAME +) + + +def download_from_maven_central_http() -> Path | None: + """Download codeflash-runtime JAR directly from Maven Central via HTTP. + + No `mvn` binary required — works for Gradle-only users and the tracer flow. + Downloads to ~/.m2/repository/ so all resolution paths find it. + Returns the path to the JAR, or None if the download fails. + """ + if M2_JAR_PATH.exists(): + return M2_JAR_PATH + + try: + M2_JAR_PATH.parent.mkdir(parents=True, exist_ok=True) + logger.info("Downloading codeflash-runtime from Maven Central: %s", MAVEN_CENTRAL_URL) + urllib.request.urlretrieve(MAVEN_CENTRAL_URL, M2_JAR_PATH) # noqa: S310 + logger.info("Downloaded codeflash-runtime to %s", M2_JAR_PATH) + return M2_JAR_PATH + except Exception as e: + logger.debug("Maven Central HTTP download failed: %s", e) + M2_JAR_PATH.unlink(missing_ok=True) + return None + + def download_from_github_releases() -> Path | None: """Download codeflash-runtime JAR from GitHub Releases. diff --git a/codeflash/languages/java/test_runner.py b/codeflash/languages/java/test_runner.py index 74830d436..40d920def 100644 --- a/codeflash/languages/java/test_runner.py +++ b/codeflash/languages/java/test_runner.py @@ -205,10 +205,13 @@ 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 + Handles multi-line include statements (comma-continued across lines). Module names may be prefixed with ':' which is stripped. """ modules: list[str] = [] - for match in re.findall(r"""include\s*\(?[^)\n]*\)?""", content): + # Match include blocks that may span multiple lines (comma-separated entries). + # The block ends at a line that doesn't end with a comma (after stripping comments/whitespace). + for match in re.findall(r"include\s*\(?((?:[^)\n]*,\s*\n)*[^)\n]*)\)?", content): for name in re.findall(r"""['"]([^'"]+)['"]""", match): modules.append(name.lstrip(":")) return modules @@ -420,11 +423,13 @@ 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 + # Coverage runs use the build tool (not direct JVM), so Gradle --no-daemon cold startup + + # compilation + test execution can take 10+ min on large multi-module projects. + min_timeout = 900 if enable_coverage else 60 effective_timeout = max(timeout or 300, min_timeout) if enable_coverage: - # Coverage MUST use build tool — JaCoCo runs as a plugin during the verify phase + # Coverage uses the build tool with JaCoCo agent injected via JAVA_TOOL_OPTIONS result, result_xml_path, coverage_xml_path = strategy.run_tests_with_coverage( build_root, test_module, test_paths, run_env, effective_timeout, candidate_index ) @@ -452,20 +457,13 @@ def run_behavioral_tests( ) if enable_coverage and coverage_xml_path: - target_dir = strategy.get_build_output_dir(build_root, test_module) - jacoco_exec_path = target_dir / "jacoco.exec" - logger.info("Coverage paths - target_dir: %s, coverage_xml_path: %s", target_dir, coverage_xml_path) - if jacoco_exec_path.exists(): - logger.info("JaCoCo exec file exists: %s (%s bytes)", jacoco_exec_path, jacoco_exec_path.stat().st_size) - else: - logger.warning("JaCoCo exec file not found: %s - JaCoCo agent may not have run", jacoco_exec_path) if coverage_xml_path.exists(): file_size = coverage_xml_path.stat().st_size - logger.info("JaCoCo XML report exists: %s (%s bytes)", coverage_xml_path, file_size) + logger.info("JaCoCo coverage XML: %s (%s bytes)", coverage_xml_path, file_size) if file_size == 0: - logger.warning("JaCoCo XML report is empty - report generation may have failed") + logger.warning("JaCoCo XML report is empty — report generation may have failed") else: - logger.warning("JaCoCo XML report not found: %s - verify phase may not have completed", coverage_xml_path) + logger.warning("JaCoCo XML report not found: %s", coverage_xml_path) return result_xml_path, result, coverage_xml_path, None diff --git a/tests/test_languages/test_java/test_build_tools.py b/tests/test_languages/test_java/test_build_tools.py index a4f01e1a6..d711f871b 100644 --- a/tests/test_languages/test_java/test_build_tools.py +++ b/tests/test_languages/test_java/test_build_tools.py @@ -12,7 +12,12 @@ get_project_info, ) from codeflash.languages.java.gradle_strategy import GradleStrategy -from codeflash.languages.java.maven_strategy import MavenStrategy, add_codeflash_dependency +from codeflash.languages.java.line_profiler import find_agent_jar +from codeflash.languages.java.maven_strategy import ( + MavenStrategy, + add_codeflash_dependency, + download_from_maven_central_http, +) from codeflash.languages.java.test_runner import _extract_modules_from_pom_content @@ -641,3 +646,167 @@ def test_adds_dependency_to_nested_module(self, tmp_path): assert result is True nested_build = (nested / "build.gradle.kts").read_text(encoding="utf-8") assert "codeflash-runtime" in nested_build + + +class TestDownloadFromMavenCentralHttp: + """Tests for the direct HTTP download from Maven Central.""" + + def test_returns_existing_m2_jar(self, tmp_path): + """If the JAR already exists in ~/.m2, return it without downloading.""" + fake_m2 = tmp_path / "m2" / "codeflash-runtime" / "1.0.1" / "codeflash-runtime-1.0.1.jar" + fake_m2.parent.mkdir(parents=True) + fake_m2.write_bytes(b"PK\x03\x04") + + with patch("codeflash.languages.java.maven_strategy.M2_JAR_PATH", fake_m2): + result = download_from_maven_central_http() + + assert result == fake_m2 + + def test_downloads_jar_when_not_cached(self, tmp_path): + """Downloads the JAR to ~/.m2 when it doesn't exist locally.""" + fake_m2 = tmp_path / "m2" / "codeflash-runtime" / "1.0.1" / "codeflash-runtime-1.0.1.jar" + + with ( + patch("codeflash.languages.java.maven_strategy.M2_JAR_PATH", fake_m2), + patch("urllib.request.urlretrieve") as mock_download, + ): + mock_download.side_effect = lambda _url, path: Path(path).write_bytes(b"PK\x03\x04") + result = download_from_maven_central_http() + + assert result == fake_m2 + assert "repo1.maven.org" in mock_download.call_args[0][0] + + def test_returns_none_on_network_failure(self, tmp_path): + """Returns None when the download fails.""" + fake_m2 = tmp_path / "m2" / "codeflash-runtime" / "1.0.1" / "codeflash-runtime-1.0.1.jar" + + with ( + patch("codeflash.languages.java.maven_strategy.M2_JAR_PATH", fake_m2), + patch("urllib.request.urlretrieve", side_effect=OSError("Network unreachable")), + ): + result = download_from_maven_central_http() + + assert result is None + assert not fake_m2.exists() + + +class TestFindAgentJarFallback: + """Tests that find_agent_jar falls back to Maven Central HTTP download.""" + + def test_falls_back_to_maven_central_when_no_local_jar(self, tmp_path): + """When no local JAR exists, find_agent_jar tries Maven Central HTTP download.""" + fake_jar = tmp_path / "downloaded.jar" + fake_jar.write_bytes(b"PK\x03\x04") + + with ( + patch("codeflash.languages.java.line_profiler.CODEFLASH_RUNTIME_VERSION", "99.99.99"), + patch("codeflash.languages.java.line_profiler.AGENT_JAR_NAME", "codeflash-runtime-99.99.99.jar"), + patch( + "codeflash.languages.java.maven_strategy.download_from_maven_central_http", return_value=fake_jar + ) as mock_download, + ): + result = find_agent_jar() + + assert result == fake_jar + assert mock_download.called + + +class TestGradleEnsureRuntimeFallback: + """Tests that Gradle ensure_runtime falls back to Maven Central HTTP download.""" + + def test_falls_back_to_http_download_when_find_runtime_jar_returns_none(self, tmp_path): + """When find_runtime_jar returns None, ensure_runtime tries HTTP download.""" + project = tmp_path / "project" + project.mkdir() + (project / "build.gradle.kts").write_text( + 'plugins {\n java\n}\n\ndependencies {\n testImplementation("junit:junit:4.13.2")\n}\n', + encoding="utf-8", + ) + (project / "gradlew").write_text("#!/bin/sh\necho gradle", encoding="utf-8") + (project / "gradlew").chmod(0o755) + + fake_jar = tmp_path / "downloaded.jar" + fake_jar.write_bytes(b"PK\x03\x04") + + strategy = GradleStrategy() + with ( + patch.object(strategy, "find_runtime_jar", return_value=None), + patch( + "codeflash.languages.java.maven_strategy.download_from_maven_central_http", return_value=fake_jar + ) as mock_download, + ): + result = strategy.ensure_runtime(project, test_module=None) + + assert result is True + assert mock_download.called + build_content = (project / "build.gradle.kts").read_text(encoding="utf-8") + assert "codeflash-runtime" in build_content + + def test_fails_when_both_local_and_http_return_none(self, tmp_path): + """When both find_runtime_jar and HTTP download return None, ensure_runtime fails.""" + project = tmp_path / "project" + project.mkdir() + (project / "build.gradle.kts").write_text("plugins { java }\n", encoding="utf-8") + + strategy = GradleStrategy() + with ( + patch.object(strategy, "find_runtime_jar", return_value=None), + patch("codeflash.languages.java.maven_strategy.download_from_maven_central_http", return_value=None), + ): + result = strategy.ensure_runtime(project, test_module=None) + + assert result is False + + +class TestGradleSetupCoverage: + """Tests for GradleStrategy.setup_coverage — returns expected XML path for JaCoCo agent output.""" + + def test_returns_report_path_for_module(self, tmp_path): + strategy = GradleStrategy() + path = strategy.setup_coverage(tmp_path, test_module="eureka-core", project_root=tmp_path) + assert path == tmp_path / "eureka-core" / "build" / "jacoco" / "test.xml" + + def test_returns_report_path_without_module(self, tmp_path): + strategy = GradleStrategy() + path = strategy.setup_coverage(tmp_path, test_module=None, project_root=tmp_path) + assert path == tmp_path / "build" / "jacoco" / "test.xml" + + def test_does_not_modify_build_files(self, tmp_path): + """setup_coverage must NOT modify build.gradle — coverage is collected via JaCoCo agent.""" + build_file = tmp_path / "build.gradle" + original_content = "plugins { id 'java' }\n" + build_file.write_text(original_content, encoding="utf-8") + + strategy = GradleStrategy() + strategy.setup_coverage(tmp_path, test_module=None, project_root=tmp_path) + assert build_file.read_text(encoding="utf-8") == original_content + + +class TestJacocoAgentDownload: + """Tests for JaCoCo agent and CLI JAR download helpers.""" + + def test_get_jacoco_agent_jar_downloads_to_codeflash_home(self, tmp_path): + from codeflash.languages.java.gradle_strategy import get_jacoco_agent_jar + + jar = get_jacoco_agent_jar(codeflash_home=tmp_path) + assert jar == tmp_path / "java_agents" / "jacocoagent.jar" + # JAR is downloaded from Maven Central — verify it exists and is non-empty + assert jar.exists() + assert jar.stat().st_size > 0 + + def test_get_jacoco_agent_jar_is_cached(self, tmp_path): + from codeflash.languages.java.gradle_strategy import get_jacoco_agent_jar + + jar1 = get_jacoco_agent_jar(codeflash_home=tmp_path) + size1 = jar1.stat().st_size + jar2 = get_jacoco_agent_jar(codeflash_home=tmp_path) + assert jar1 == jar2 + assert jar2.stat().st_size == size1 + + def test_get_jacoco_cli_jar_downloads(self, tmp_path): + from codeflash.languages.java.gradle_strategy import get_jacoco_cli_jar + + jar = get_jacoco_cli_jar(codeflash_home=tmp_path) + assert jar == tmp_path / "java_agents" / "jacococli.jar" + assert jar.exists() + assert jar.stat().st_size > 0 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..523f14729 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,23 @@ def test_leading_colon_stripped(self): assert "streams" in modules assert "clients" in modules + def test_multiline_groovy_include(self): + """Multi-line include with comma continuation (eureka-style settings.gradle).""" + content = ( + "rootProject.name='eureka'\n" + "include 'eureka-client',\n" + " 'eureka-server',\n" + " 'eureka-core'\n" + ) + modules = _extract_modules_from_settings_gradle(content) + assert modules == ["eureka-client", "eureka-server", "eureka-core"] + + def test_multiple_separate_includes(self): + """Multiple separate include statements on different lines.""" + content = "include 'module-a'\ninclude 'module-b'\n" + modules = _extract_modules_from_settings_gradle(content) + assert modules == ["module-a", "module-b"] + class TestFindMultiModuleRoot: """Tests for _find_multi_module_root with Gradle multi-module projects."""