diff --git a/codeflash-java-runtime/build.gradle.kts b/codeflash-java-runtime/build.gradle.kts
index 69647fc35..524d8944e 100644
--- a/codeflash-java-runtime/build.gradle.kts
+++ b/codeflash-java-runtime/build.gradle.kts
@@ -22,6 +22,8 @@ dependencies {
implementation("org.xerial:sqlite-jdbc:3.45.0.0")
implementation("org.ow2.asm:asm:9.7.1")
implementation("org.ow2.asm:asm-commons:9.7.1")
+ implementation("org.jacoco:org.jacoco.agent:0.8.13:runtime")
+ implementation("org.jacoco:org.jacoco.cli:0.8.13:nodeps")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
diff --git a/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.jar b/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..b498d2444
Binary files /dev/null and b/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.properties b/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..d6e308a63
--- /dev/null
+++ b/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/codeflash-java-runtime/gradlew b/codeflash-java-runtime/gradlew
new file mode 100755
index 000000000..17a91706f
--- /dev/null
+++ b/codeflash-java-runtime/gradlew
@@ -0,0 +1,176 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+if $JAVACMD --add-opens java.base/java.lang=ALL-UNNAMED -version ; then
+ DEFAULT_JVM_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED $DEFAULT_JVM_OPTS"
+fi
+
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/codeflash-java-runtime/gradlew.bat b/codeflash-java-runtime/gradlew.bat
new file mode 100644
index 000000000..e95643d6a
--- /dev/null
+++ b/codeflash-java-runtime/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/codeflash/languages/java/maven_strategy.py b/codeflash/languages/java/maven_strategy.py
index 2b5ca1f35..1b398c1eb 100644
--- a/codeflash/languages/java/maven_strategy.py
+++ b/codeflash/languages/java/maven_strategy.py
@@ -27,8 +27,11 @@ logger = logging.getLogger(__name__)
_MAVEN_VALIDATION_SKIP_FLAGS = [
"-Drat.skip=true",
"-Dcheckstyle.skip=true",
+ "-Ddisable.checks=true",
"-Dcheckstyle.failOnViolation=false",
"-Dcheckstyle.failsOnError=false",
+ "-Dmaven-checkstyle-plugin.failsOnError=false",
+ "-Dmaven-checkstyle-plugin.failOnViolation=false",
"-Dspotbugs.skip=true",
"-Dpmd.skip=true",
"-Denforcer.skip=true",
@@ -154,12 +157,10 @@ def install_codeflash_runtime(project_root: Path, runtime_jar_path: Path, mvn: s
return False
-_VALIDATION_SKIP_PROPERTIES = [
+# Properties set to "true" to enable skipping
+_VALIDATION_SKIP_PROPERTIES_TRUE = [
"checkstyle.skip",
- "checkstyle.failOnViolation",
- "checkstyle.failsOnError",
- "maven-checkstyle-plugin.failsOnError",
- "maven-checkstyle-plugin.failOnViolation",
+ "disable.checks",
"spotbugs.skip",
"pmd.skip",
"rat.skip",
@@ -167,6 +168,14 @@ _VALIDATION_SKIP_PROPERTIES = [
"japicmp.skip",
]
+# Properties set to "false" to disable failure on violations
+_VALIDATION_SKIP_PROPERTIES_FALSE = [
+ "checkstyle.failOnViolation",
+ "checkstyle.failsOnError",
+ "maven-checkstyle-plugin.failsOnError",
+ "maven-checkstyle-plugin.failOnViolation",
+]
+
# Plugin overrides that explicitly set true in the plugin .
# This handles parent POMs with custom execution IDs that ignore skip properties.
_VALIDATION_PLUGIN_OVERRIDES = """\
@@ -209,7 +218,8 @@ def inject_validation_skip_properties(pom_path: Path) -> bool:
if "" in content:
return True
- props_lines = "".join(f" <{p}>true{p}>\n" for p in _VALIDATION_SKIP_PROPERTIES)
+ props_lines = "".join(f" <{p}>true{p}>\n" for p in _VALIDATION_SKIP_PROPERTIES_TRUE)
+ props_lines += "".join(f" <{p}>false{p}>\n" for p in _VALIDATION_SKIP_PROPERTIES_FALSE)
# 1. Inject properties
closing_idx = content.find("")
diff --git a/tests/test_languages/test_java/test_auto_config_integration.py b/tests/test_languages/test_java/test_auto_config_integration.py
new file mode 100644
index 000000000..54d15da20
--- /dev/null
+++ b/tests/test_languages/test_java/test_auto_config_integration.py
@@ -0,0 +1,745 @@
+"""Integration tests for Java auto-config logic across Gradle and Maven projects.
+
+Tests the end-to-end flow: build tool detection → strategy selection →
+config parsing → write → read → remove, using realistic project layouts.
+"""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from codeflash.languages.java.build_config_strategy import (
+ GradleConfigStrategy,
+ MavenConfigStrategy,
+ get_config_strategy,
+ parse_java_project_config,
+)
+from codeflash.languages.java.build_tools import (
+ BuildTool,
+ detect_build_tool,
+ find_source_root,
+ find_test_root,
+ get_project_info,
+)
+
+
+# ---------------------------------------------------------------------------
+# Helpers — create realistic project layouts in tmp_path
+# ---------------------------------------------------------------------------
+
+
+def _make_maven_project(root: Path, *, with_namespace: bool = True, java_version: str = "17") -> Path:
+ ns = ' xmlns="http://maven.apache.org/POM/4.0.0"' if with_namespace else ""
+ pom = root / "pom.xml"
+ pom.write_text(
+ f'\n'
+ f"\n"
+ f" 4.0.0\n"
+ f" com.example\n"
+ f" demo-app\n"
+ f" 1.0.0\n"
+ f" \n"
+ f" {java_version}\n"
+ f" {java_version}\n"
+ f" \n"
+ f"\n",
+ encoding="utf-8",
+ )
+ src = root / "src" / "main" / "java" / "com" / "example"
+ src.mkdir(parents=True)
+ (src / "App.java").write_text("package com.example;\npublic class App {}\n", encoding="utf-8")
+ test = root / "src" / "test" / "java" / "com" / "example"
+ test.mkdir(parents=True)
+ (test / "AppTest.java").write_text(
+ "package com.example;\nimport org.junit.jupiter.api.Test;\nclass AppTest {\n"
+ " @Test void works() {}\n}\n",
+ encoding="utf-8",
+ )
+ return root
+
+
+def _make_gradle_project(root: Path, *, kotlin_dsl: bool = False) -> Path:
+ ext = ".kts" if kotlin_dsl else ""
+ build_file = root / f"build.gradle{ext}"
+ build_file.write_text(
+ "plugins {\n id 'java'\n}\ngroup = 'com.example'\nversion = '1.0.0'\n",
+ encoding="utf-8",
+ )
+ (root / f"settings.gradle{ext}").write_text(f"rootProject.name = 'demo'\n", encoding="utf-8")
+ src = root / "src" / "main" / "java" / "com" / "example"
+ src.mkdir(parents=True)
+ (src / "App.java").write_text("package com.example;\npublic class App {}\n", encoding="utf-8")
+ test = root / "src" / "test" / "java" / "com" / "example"
+ test.mkdir(parents=True)
+ (test / "AppTest.java").write_text(
+ "package com.example;\nimport org.junit.jupiter.api.Test;\nclass AppTest {\n"
+ " @Test void works() {}\n}\n",
+ encoding="utf-8",
+ )
+ return root
+
+
+def _make_maven_multimodule(root: Path) -> Path:
+ # Parent pom with modules
+ (root / "pom.xml").write_text(
+ '\n'
+ '\n'
+ " 4.0.0\n"
+ " com.example\n"
+ " parent\n"
+ " 1.0.0\n"
+ " pom\n"
+ " \n"
+ " core\n"
+ " api\n"
+ " tests\n"
+ " \n"
+ "\n",
+ encoding="utf-8",
+ )
+
+ # core module — main source
+ core = root / "core"
+ core.mkdir()
+ (core / "pom.xml").write_text(
+ '\n'
+ " 4.0.0\n"
+ " \n"
+ " com.example\n"
+ " parent\n"
+ " 1.0.0\n"
+ " \n"
+ " core\n"
+ "\n",
+ encoding="utf-8",
+ )
+ core_src = core / "src" / "main" / "java" / "com" / "example"
+ core_src.mkdir(parents=True)
+ (core_src / "Core.java").write_text("package com.example;\npublic class Core {}\n", encoding="utf-8")
+ (core_src / "Utils.java").write_text("package com.example;\npublic class Utils {}\n", encoding="utf-8")
+
+ # api module — fewer source files
+ api = root / "api"
+ api.mkdir()
+ (api / "pom.xml").write_text(
+ '\n'
+ " 4.0.0\n"
+ " \n"
+ " com.example\n"
+ " parent\n"
+ " 1.0.0\n"
+ " \n"
+ " api\n"
+ "\n",
+ encoding="utf-8",
+ )
+ api_src = api / "src" / "main" / "java" / "com" / "example"
+ api_src.mkdir(parents=True)
+ (api_src / "Api.java").write_text("package com.example;\npublic class Api {}\n", encoding="utf-8")
+
+ # tests module — integration tests
+ tests = root / "tests"
+ tests.mkdir()
+ (tests / "pom.xml").write_text(
+ '\n'
+ " 4.0.0\n"
+ " \n"
+ " com.example\n"
+ " parent\n"
+ " 1.0.0\n"
+ " \n"
+ " tests\n"
+ "\n",
+ encoding="utf-8",
+ )
+ test_src = tests / "src" / "test" / "java" / "com" / "example"
+ test_src.mkdir(parents=True)
+ (test_src / "IntegrationTest.java").write_text(
+ "package com.example;\npublic class IntegrationTest {}\n", encoding="utf-8"
+ )
+
+ return root
+
+
+def _make_gradle_multimodule(root: Path, *, kotlin_dsl: bool = False) -> Path:
+ ext = ".kts" if kotlin_dsl else ""
+
+ (root / f"build.gradle{ext}").write_text("// root build\n", encoding="utf-8")
+ (root / f"settings.gradle{ext}").write_text(
+ "rootProject.name = 'multi'\ninclude 'core', 'api'\n", encoding="utf-8"
+ )
+
+ for module_name in ["core", "api"]:
+ mod = root / module_name
+ mod.mkdir()
+ (mod / f"build.gradle{ext}").write_text(
+ f"plugins {{\n id 'java'\n}}\n", encoding="utf-8"
+ )
+ src = mod / "src" / "main" / "java" / "com" / "example"
+ src.mkdir(parents=True)
+ (src / f"{module_name.capitalize()}.java").write_text(
+ f"package com.example;\npublic class {module_name.capitalize()} {{}}\n", encoding="utf-8"
+ )
+ test_dir = mod / "src" / "test" / "java" / "com" / "example"
+ test_dir.mkdir(parents=True)
+ (test_dir / f"{module_name.capitalize()}Test.java").write_text(
+ f"package com.example;\nclass {module_name.capitalize()}Test {{}}\n", encoding="utf-8"
+ )
+
+ return root
+
+
+# ===================================================================
+# Integration: Maven — detection through full config lifecycle
+# ===================================================================
+
+
+class TestMavenAutoConfigIntegration:
+ """End-to-end: detect Maven → get strategy → parse config → write → read → remove."""
+
+ def test_standard_maven_detection_to_config(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path)
+
+ assert detect_build_tool(project) == BuildTool.MAVEN
+ assert find_source_root(project) == project / "src" / "main" / "java"
+ assert find_test_root(project) == project / "src" / "test" / "java"
+
+ strategy = get_config_strategy(project)
+ assert isinstance(strategy, MavenConfigStrategy)
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ assert config["language"] == "java"
+ assert config["module_root"] == str(project / "src" / "main" / "java")
+ assert config["tests_root"] == str(project / "src" / "test" / "java")
+ assert config["git_remote"] == "origin"
+ assert config["disable_telemetry"] is False
+
+ def test_maven_full_lifecycle_write_read_remove(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ # Write config
+ ok, msg = strategy.write_codeflash_properties(project, {
+ "module-root": "custom/src",
+ "tests-root": "custom/test",
+ "git-remote": "upstream",
+ "disable-telemetry": True,
+ "ignore-paths": ["target", ".idea"],
+ "formatter-cmds": ["spotless:apply"],
+ })
+ assert ok, msg
+
+ # Read back
+ props = strategy.read_codeflash_properties(project)
+ assert props["moduleRoot"] == "custom/src"
+ assert props["testsRoot"] == "custom/test"
+ assert props["gitRemote"] == "upstream"
+ assert props["disableTelemetry"] == "true"
+ assert props["ignorePaths"] == "target,.idea"
+ assert props["formatterCmds"] == "spotless:apply"
+
+ # Verify non-codeflash properties are preserved
+ pom_text = (project / "pom.xml").read_text(encoding="utf-8")
+ assert "maven.compiler.source" in pom_text
+ assert "maven.compiler.target" in pom_text
+
+ # Remove
+ ok, msg = strategy.remove_codeflash_properties(project)
+ assert ok, msg
+
+ # Verify removed
+ props_after = strategy.read_codeflash_properties(project)
+ assert props_after == {}
+
+ # Verify non-codeflash properties still preserved
+ pom_after = (project / "pom.xml").read_text(encoding="utf-8")
+ assert "maven.compiler.source" in pom_after
+
+ def test_maven_with_namespace_full_lifecycle(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path, with_namespace=True)
+ strategy = get_config_strategy(project)
+
+ ok, _ = strategy.write_codeflash_properties(project, {"module-root": "lib/main"})
+ assert ok
+
+ props = strategy.read_codeflash_properties(project)
+ assert props["moduleRoot"] == "lib/main"
+
+ # Verify namespace preserved, no ns0: prefix
+ pom_text = (project / "pom.xml").read_text(encoding="utf-8")
+ assert 'xmlns="http://maven.apache.org/POM/4.0.0"' in pom_text
+ assert "ns0:" not in pom_text
+
+ def test_maven_without_namespace_full_lifecycle(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path, with_namespace=False)
+ strategy = get_config_strategy(project)
+
+ ok, _ = strategy.write_codeflash_properties(project, {"module-root": "src/main/java"})
+ assert ok
+
+ props = strategy.read_codeflash_properties(project)
+ assert props["moduleRoot"] == "src/main/java"
+
+ def test_maven_user_overrides_take_precedence(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ # Write user overrides to pom.xml
+ ok, _ = strategy.write_codeflash_properties(project, {
+ "module-root": "custom/src",
+ "tests-root": "custom/test",
+ "disable-telemetry": True,
+ })
+ assert ok
+
+ # Create the custom directories
+ (project / "custom" / "src").mkdir(parents=True)
+ (project / "custom" / "test").mkdir(parents=True)
+
+ # parse_java_project_config should use user overrides, not auto-detected paths
+ config = parse_java_project_config(project)
+ assert config is not None
+ assert config["module_root"] == str((project / "custom" / "src").resolve())
+ assert config["tests_root"] == str((project / "custom" / "test").resolve())
+ assert config["disable_telemetry"] is True
+
+ def test_maven_project_info_extraction(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path, java_version="11")
+
+ info = get_project_info(project)
+ assert info is not None
+ assert info.build_tool == BuildTool.MAVEN
+ assert info.group_id == "com.example"
+ assert info.artifact_id == "demo-app"
+ assert info.version == "1.0.0"
+ assert info.java_version == "11"
+ assert len(info.source_roots) == 1
+ assert len(info.test_roots) == 1
+
+ def test_maven_overwrite_then_overwrite(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ # First write
+ ok, _ = strategy.write_codeflash_properties(project, {"module-root": "v1"})
+ assert ok
+ assert strategy.read_codeflash_properties(project)["moduleRoot"] == "v1"
+
+ # Second write replaces previous values
+ ok, _ = strategy.write_codeflash_properties(project, {"module-root": "v2", "git-remote": "upstream"})
+ assert ok
+ props = strategy.read_codeflash_properties(project)
+ assert props["moduleRoot"] == "v2"
+ assert props["gitRemote"] == "upstream"
+
+
+# ===================================================================
+# Integration: Maven multi-module
+# ===================================================================
+
+
+class TestMavenMultiModuleIntegration:
+ """End-to-end auto-config for Maven multi-module projects."""
+
+ def test_multimodule_detects_source_from_largest_module(self, tmp_path: Path) -> None:
+ project = _make_maven_multimodule(tmp_path)
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ # core has 2 java files, api has 1 → core should be chosen as source root
+ assert "core" in config["module_root"]
+ assert config["module_root"].endswith(str(Path("src") / "main" / "java"))
+
+ def test_multimodule_detects_test_module(self, tmp_path: Path) -> None:
+ project = _make_maven_multimodule(tmp_path)
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ # "tests" module has "test" in its name → should be detected as test root
+ assert "tests" in config["tests_root"]
+
+ def test_multimodule_build_tool_detection(self, tmp_path: Path) -> None:
+ project = _make_maven_multimodule(tmp_path)
+
+ assert detect_build_tool(project) == BuildTool.MAVEN
+ strategy = get_config_strategy(project)
+ assert isinstance(strategy, MavenConfigStrategy)
+
+ def test_multimodule_config_write_read_on_parent(self, tmp_path: Path) -> None:
+ project = _make_maven_multimodule(tmp_path)
+ strategy = get_config_strategy(project)
+
+ ok, _ = strategy.write_codeflash_properties(project, {"git-remote": "upstream"})
+ assert ok
+
+ props = strategy.read_codeflash_properties(project)
+ assert props["gitRemote"] == "upstream"
+
+ # Verify the parent pom still has modules
+ pom_text = (project / "pom.xml").read_text(encoding="utf-8")
+ assert "core" in pom_text
+ assert "api" in pom_text
+
+ def test_multimodule_with_custom_source_directory(self, tmp_path: Path) -> None:
+ project = _make_maven_multimodule(tmp_path)
+
+ # Modify core module to use a custom source directory
+ core_pom = project / "core" / "pom.xml"
+ core_pom.write_text(
+ '\n'
+ " 4.0.0\n"
+ " \n"
+ " com.example\n"
+ " parent\n"
+ " 1.0.0\n"
+ " \n"
+ " core\n"
+ " \n"
+ " src/main/custom\n"
+ " \n"
+ "\n",
+ encoding="utf-8",
+ )
+ custom_src = project / "core" / "src" / "main" / "custom"
+ custom_src.mkdir(parents=True)
+ (custom_src / "Main.java").write_text("public class Main {}\n", encoding="utf-8")
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ # Should detect the custom source directory from the module pom
+ # The exact path depends on which module has more java files
+ assert config["module_root"] is not None
+
+
+# ===================================================================
+# Integration: Gradle — detection through full config lifecycle
+# ===================================================================
+
+
+class TestGradleAutoConfigIntegration:
+ """End-to-end: detect Gradle → get strategy → parse config → write → read → remove."""
+
+ def test_standard_gradle_detection_to_config(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+
+ assert detect_build_tool(project) == BuildTool.GRADLE
+ assert find_source_root(project) == project / "src" / "main" / "java"
+ assert find_test_root(project) == project / "src" / "test" / "java"
+
+ strategy = get_config_strategy(project)
+ assert isinstance(strategy, GradleConfigStrategy)
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ assert config["language"] == "java"
+ assert config["module_root"] == str(project / "src" / "main" / "java")
+ assert config["tests_root"] == str(project / "src" / "test" / "java")
+
+ def test_gradle_kotlin_dsl_detection_to_config(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path, kotlin_dsl=True)
+
+ assert detect_build_tool(project) == BuildTool.GRADLE
+
+ strategy = get_config_strategy(project)
+ assert isinstance(strategy, GradleConfigStrategy)
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ assert config["language"] == "java"
+
+ def test_gradle_full_lifecycle_write_read_remove(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ # Write config
+ ok, msg = strategy.write_codeflash_properties(project, {
+ "module-root": "custom/src",
+ "tests-root": "custom/test",
+ "git-remote": "upstream",
+ "disable-telemetry": True,
+ "ignore-paths": ["build", ".gradle"],
+ "formatter-cmds": ["spotlessApply"],
+ })
+ assert ok, msg
+
+ # Read back
+ props = strategy.read_codeflash_properties(project)
+ assert props["moduleRoot"] == "custom/src"
+ assert props["testsRoot"] == "custom/test"
+ assert props["gitRemote"] == "upstream"
+ assert props["disableTelemetry"] == "true"
+ assert props["ignorePaths"] == "build,.gradle"
+ assert props["formatterCmds"] == "spotlessApply"
+
+ # Verify gradle.properties has the codeflash header comment
+ gp_text = (project / "gradle.properties").read_text(encoding="utf-8")
+ assert "# Codeflash configuration" in gp_text
+
+ # Remove
+ ok, msg = strategy.remove_codeflash_properties(project)
+ assert ok, msg
+
+ # Verify removed
+ props_after = strategy.read_codeflash_properties(project)
+ assert props_after == {}
+
+ # Verify header comment also removed
+ gp_after = (project / "gradle.properties").read_text(encoding="utf-8")
+ assert "Codeflash" not in gp_after
+
+ def test_gradle_preserves_existing_properties(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+
+ # Pre-existing gradle.properties with user settings
+ (project / "gradle.properties").write_text(
+ "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m\n"
+ "org.gradle.parallel=true\n"
+ "org.gradle.caching=true\n",
+ encoding="utf-8",
+ )
+
+ strategy = get_config_strategy(project)
+ ok, _ = strategy.write_codeflash_properties(project, {"module-root": "lib/src"})
+ assert ok
+
+ gp_text = (project / "gradle.properties").read_text(encoding="utf-8")
+ assert "org.gradle.jvmargs=-Xmx4g" in gp_text
+ assert "org.gradle.parallel=true" in gp_text
+ assert "org.gradle.caching=true" in gp_text
+ assert "codeflash.moduleRoot=lib/src" in gp_text
+
+ def test_gradle_user_overrides_take_precedence(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ ok, _ = strategy.write_codeflash_properties(project, {
+ "module-root": "custom/src",
+ "tests-root": "custom/test",
+ })
+ assert ok
+
+ (project / "custom" / "src").mkdir(parents=True)
+ (project / "custom" / "test").mkdir(parents=True)
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ assert config["module_root"] == str((project / "custom" / "src").resolve())
+ assert config["tests_root"] == str((project / "custom" / "test").resolve())
+
+ def test_gradle_overwrite_then_overwrite(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ ok, _ = strategy.write_codeflash_properties(project, {"module-root": "v1"})
+ assert ok
+ assert strategy.read_codeflash_properties(project)["moduleRoot"] == "v1"
+
+ ok, _ = strategy.write_codeflash_properties(project, {"module-root": "v2", "git-remote": "upstream"})
+ assert ok
+ props = strategy.read_codeflash_properties(project)
+ assert props["moduleRoot"] == "v2"
+ assert props["gitRemote"] == "upstream"
+ # Old values should not persist
+ gp_text = (project / "gradle.properties").read_text(encoding="utf-8")
+ assert gp_text.count("codeflash.moduleRoot") == 1
+
+ def test_gradle_project_info_extraction(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+
+ info = get_project_info(project)
+ assert info is not None
+ assert info.build_tool == BuildTool.GRADLE
+ assert len(info.source_roots) == 1
+ assert len(info.test_roots) == 1
+
+
+# ===================================================================
+# Integration: Gradle multi-module
+# ===================================================================
+
+
+class TestGradleMultiModuleIntegration:
+ """End-to-end auto-config for Gradle multi-module projects."""
+
+ def test_multimodule_root_detection(self, tmp_path: Path) -> None:
+ project = _make_gradle_multimodule(tmp_path)
+
+ assert detect_build_tool(project) == BuildTool.GRADLE
+ strategy = get_config_strategy(project)
+ assert isinstance(strategy, GradleConfigStrategy)
+
+ def test_multimodule_config_write_read_at_root(self, tmp_path: Path) -> None:
+ project = _make_gradle_multimodule(tmp_path)
+ strategy = get_config_strategy(project)
+
+ ok, _ = strategy.write_codeflash_properties(project, {
+ "module-root": "core/src/main/java",
+ "tests-root": "core/src/test/java",
+ })
+ assert ok
+
+ props = strategy.read_codeflash_properties(project)
+ assert props["moduleRoot"] == "core/src/main/java"
+ assert props["testsRoot"] == "core/src/test/java"
+
+ def test_multimodule_kotlin_dsl(self, tmp_path: Path) -> None:
+ project = _make_gradle_multimodule(tmp_path, kotlin_dsl=True)
+
+ assert detect_build_tool(project) == BuildTool.GRADLE
+ config = parse_java_project_config(project)
+ assert config is not None
+ assert config["language"] == "java"
+
+
+# ===================================================================
+# Integration: cross-cutting scenarios
+# ===================================================================
+
+
+class TestCrossCuttingIntegration:
+ """Scenarios that test across both build tools or edge conditions."""
+
+ def test_maven_takes_precedence_over_gradle(self, tmp_path: Path) -> None:
+ # Create both Maven and Gradle files
+ _make_maven_project(tmp_path)
+ (tmp_path / "build.gradle").write_text("plugins { id 'java' }\n", encoding="utf-8")
+
+ assert detect_build_tool(tmp_path) == BuildTool.MAVEN
+ strategy = get_config_strategy(tmp_path)
+ assert isinstance(strategy, MavenConfigStrategy)
+
+ def test_empty_directory_returns_unknown(self, tmp_path: Path) -> None:
+ assert detect_build_tool(tmp_path) == BuildTool.UNKNOWN
+ assert parse_java_project_config(tmp_path) is None
+ with pytest.raises(ValueError, match="No supported Java build tool"):
+ get_config_strategy(tmp_path)
+
+ def test_maven_config_with_all_properties(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ full_config = {
+ "module-root": "src/main/java",
+ "tests-root": "src/test/java",
+ "git-remote": "upstream",
+ "disable-telemetry": True,
+ "ignore-paths": ["target", ".idea", "*.iml"],
+ "formatter-cmds": ["mvn spotless:apply", "mvn formatter:format"],
+ }
+
+ ok, _ = strategy.write_codeflash_properties(project, full_config)
+ assert ok
+
+ props = strategy.read_codeflash_properties(project)
+ assert len(props) == 6
+ assert props["moduleRoot"] == "src/main/java"
+ assert props["testsRoot"] == "src/test/java"
+ assert props["gitRemote"] == "upstream"
+ assert props["disableTelemetry"] == "true"
+ assert props["ignorePaths"] == "target,.idea,*.iml"
+ assert props["formatterCmds"] == "mvn spotless:apply,mvn formatter:format"
+
+ def test_gradle_config_with_all_properties(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ full_config = {
+ "module-root": "lib/src/main/java",
+ "tests-root": "lib/src/test/java",
+ "git-remote": "upstream",
+ "disable-telemetry": False,
+ "ignore-paths": ["build", ".gradle"],
+ "formatter-cmds": ["./gradlew spotlessApply"],
+ }
+
+ ok, _ = strategy.write_codeflash_properties(project, full_config)
+ assert ok
+
+ props = strategy.read_codeflash_properties(project)
+ assert len(props) == 6
+ assert props["moduleRoot"] == "lib/src/main/java"
+ assert props["disableTelemetry"] == "false"
+
+ def test_parse_config_feeds_ignore_paths_and_formatter_cmds(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ ok, _ = strategy.write_codeflash_properties(project, {
+ "ignore-paths": ["target", "generated"],
+ "formatter-cmds": ["mvn fmt:format"],
+ })
+ assert ok
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ assert len(config["ignore_paths"]) == 2
+ assert any("target" in p for p in config["ignore_paths"])
+ assert any("generated" in p for p in config["ignore_paths"])
+ assert config["formatter_cmds"] == ["mvn fmt:format"]
+
+ def test_parse_config_defaults_when_no_user_overrides(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path)
+
+ config = parse_java_project_config(project)
+ assert config is not None
+ assert config["git_remote"] == "origin"
+ assert config["disable_telemetry"] is False
+ assert config["ignore_paths"] == []
+ assert config["formatter_cmds"] == []
+
+ def test_subdir_detection_from_child(self, tmp_path: Path) -> None:
+ """Build tool detection works from a subdirectory (multi-module child)."""
+ project = _make_maven_project(tmp_path)
+ child = project / "src" / "main" / "java"
+
+ # detect_build_tool should find pom.xml in parent directories
+ assert detect_build_tool(child) == BuildTool.MAVEN
+
+ def test_gradle_write_creates_properties_file_if_missing(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+ props_path = project / "gradle.properties"
+
+ # Ensure no gradle.properties exists
+ if props_path.exists():
+ props_path.unlink()
+ assert not props_path.exists()
+
+ strategy = get_config_strategy(project)
+ ok, _ = strategy.write_codeflash_properties(project, {"module-root": "src/main/java"})
+ assert ok
+ assert props_path.exists()
+
+ props = strategy.read_codeflash_properties(project)
+ assert props["moduleRoot"] == "src/main/java"
+
+ def test_maven_remove_idempotent(self, tmp_path: Path) -> None:
+ project = _make_maven_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ # Remove when nothing was written
+ ok1, _ = strategy.remove_codeflash_properties(project)
+ assert ok1
+
+ # Write then remove twice
+ strategy.write_codeflash_properties(project, {"module-root": "src"})
+ ok2, _ = strategy.remove_codeflash_properties(project)
+ assert ok2
+ ok3, _ = strategy.remove_codeflash_properties(project)
+ assert ok3
+
+ assert strategy.read_codeflash_properties(project) == {}
+
+ def test_gradle_remove_idempotent(self, tmp_path: Path) -> None:
+ project = _make_gradle_project(tmp_path)
+ strategy = get_config_strategy(project)
+
+ strategy.write_codeflash_properties(project, {"module-root": "src"})
+ ok1, _ = strategy.remove_codeflash_properties(project)
+ assert ok1
+ ok2, _ = strategy.remove_codeflash_properties(project)
+ assert ok2
+
+ assert strategy.read_codeflash_properties(project) == {}