diff --git a/code_to_optimize/java-gradle/build.gradle.kts b/code_to_optimize/java-gradle/build.gradle.kts index 034648035..876d02e5c 100644 --- a/code_to_optimize/java-gradle/build.gradle.kts +++ b/code_to_optimize/java-gradle/build.gradle.kts @@ -21,7 +21,6 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0") testImplementation("org.xerial:sqlite-jdbc:3.42.0.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation(files("/Users/heshammohamed/Work/codeflash/code_to_optimize/java-gradle/libs/codeflash-runtime-1.0.0.jar")) // codeflash-runtime } tasks.test { diff --git a/code_to_optimize/java-gradle/codeflash.toml b/code_to_optimize/java-gradle/codeflash.toml deleted file mode 100644 index bf6e45279..000000000 --- a/code_to_optimize/java-gradle/codeflash.toml +++ /dev/null @@ -1,4 +0,0 @@ -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" -formatter-cmds = [] diff --git a/code_to_optimize/java-gradle/gradle/wrapper/gradle-wrapper.jar b/code_to_optimize/java-gradle/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..b498d2444 Binary files /dev/null and b/code_to_optimize/java-gradle/gradle/wrapper/gradle-wrapper.jar differ diff --git a/code_to_optimize/java-gradle/gradle/wrapper/gradle-wrapper.properties b/code_to_optimize/java-gradle/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..d6e308a63 --- /dev/null +++ b/code_to_optimize/java-gradle/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/code_to_optimize/java-gradle/gradlew b/code_to_optimize/java-gradle/gradlew new file mode 100755 index 000000000..17a91706f --- /dev/null +++ b/code_to_optimize/java-gradle/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/code_to_optimize/java-gradle/gradlew.bat b/code_to_optimize/java-gradle/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/code_to_optimize/java-gradle/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/code_to_optimize/java-gradle/libs/codeflash-runtime-1.0.0.jar b/code_to_optimize/java-gradle/libs/codeflash-runtime-1.0.0.jar deleted file mode 100644 index 92ad8be00..000000000 Binary files a/code_to_optimize/java-gradle/libs/codeflash-runtime-1.0.0.jar and /dev/null differ diff --git a/code_to_optimize/java/codeflash.toml b/code_to_optimize/java/codeflash.toml deleted file mode 100644 index 4016df28a..000000000 --- a/code_to_optimize/java/codeflash.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Codeflash configuration for Java project - -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" -formatter-cmds = [] 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/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index d76e60a11..a2bbdc413 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -185,11 +185,17 @@ def process_pyproject_config(args: Namespace) -> Namespace: args.ignore_paths = normalize_ignore_paths(args.ignore_paths, base_path=args.module_root) # If module-root is "." then all imports are relatives to it. # in this case, the ".." becomes outside project scope, causing issues with un-importable paths - args.project_root = project_root_from_module_root(args.module_root, pyproject_file_path) + args.project_root = project_root_from_module_root(Path(args.module_root), pyproject_file_path) args.tests_root = Path(args.tests_root).resolve() if args.benchmarks_root: args.benchmarks_root = Path(args.benchmarks_root).resolve() args.test_project_root = project_root_from_module_root(args.tests_root, pyproject_file_path) + + if is_java_project and pyproject_file_path.is_dir(): + # For Java projects, pyproject_file_path IS the project root directory (not a file). + # Override project_root which may have resolved to a sub-module. + args.project_root = pyproject_file_path.resolve() + args.test_project_root = pyproject_file_path.resolve() if is_LSP_enabled(): args.all = None return args @@ -208,8 +214,6 @@ def project_root_from_module_root(module_root: Path, pyproject_file_path: Path) return current.resolve() if (current / "build.gradle").exists() or (current / "build.gradle.kts").exists(): return current.resolve() - if (current / "codeflash.toml").exists(): - return current.resolve() current = current.parent return module_root.parent.resolve() diff --git a/codeflash/cli_cmds/init_java.py b/codeflash/cli_cmds/init_java.py index a2d1a139b..eb01002fa 100644 --- a/codeflash/cli_cmds/init_java.py +++ b/codeflash/cli_cmds/init_java.py @@ -26,6 +26,8 @@ from codeflash.code_utils.shell_utils import get_shell_rc_path, is_powershell from codeflash.telemetry.posthog_cf import ph +_MAVEN_NS = {"m": "http://maven.apache.org/POM/4.0.0"} + class JavaBuildTool(Enum): """Java build tools.""" @@ -75,23 +77,15 @@ def detect_java_build_tool(project_root: Path) -> JavaBuildTool: def detect_java_source_root(project_root: Path) -> str: """Detect the Java source root directory.""" # Standard Maven/Gradle layout - standard_src = project_root / "src" / "main" / "java" - if standard_src.is_dir(): + if (project_root / "src" / "main" / "java").is_dir(): return "src/main/java" # Try to detect from pom.xml - pom_path = project_root / "pom.xml" - if pom_path.exists(): - try: - tree = ET.parse(pom_path) - root = tree.getroot() - # Handle Maven namespace - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - source_dir = root.find(".//m:sourceDirectory", ns) - if source_dir is not None and source_dir.text: - return source_dir.text - except ET.ParseError: - pass + root = _get_pom_root_cached(project_root) + if root is not None: + source_dir = root.find(".//m:sourceDirectory", _MAVEN_NS) + if source_dir is not None and source_dir.text: + return source_dir.text # Fallback to src directory if (project_root / "src").is_dir(): @@ -103,22 +97,15 @@ def detect_java_source_root(project_root: Path) -> str: def detect_java_test_root(project_root: Path) -> str: """Detect the Java test root directory.""" # Standard Maven/Gradle layout - standard_test = project_root / "src" / "test" / "java" - if standard_test.is_dir(): + if (project_root / "src" / "test" / "java").is_dir(): return "src/test/java" # Try to detect from pom.xml - pom_path = project_root / "pom.xml" - if pom_path.exists(): - try: - tree = ET.parse(pom_path) - root = tree.getroot() - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - test_source_dir = root.find(".//m:testSourceDirectory", ns) - if test_source_dir is not None and test_source_dir.text: - return test_source_dir.text - except ET.ParseError: - pass + root = _get_pom_root_cached(project_root) + if root is not None: + test_source_dir = root.find(".//m:testSourceDirectory", _MAVEN_NS) + if test_source_dir is not None and test_source_dir.text: + return test_source_dir.text # Fallback patterns if (project_root / "test").is_dir(): @@ -232,12 +219,18 @@ def should_modify_java_config() -> tuple[bool, dict[str, Any] | None]: project_root = Path.cwd() - # Check for existing codeflash config in pom.xml or a separate config file - codeflash_config_path = project_root / "codeflash.toml" - if codeflash_config_path.exists(): - return Confirm.ask( - "A Codeflash config already exists. Do you want to re-configure it?", default=False, show_default=True - ), None + # Check for existing codeflash config in pom.xml properties or gradle.properties + from codeflash.languages.java.build_config_strategy import get_config_strategy + + try: + strategy = get_config_strategy(project_root) + existing = strategy.read_codeflash_properties(project_root) + if existing: + return Confirm.ask( + "A Codeflash config already exists. Do you want to re-configure it?", default=False, show_default=True + ), None + except ValueError: + pass return True, None @@ -436,42 +429,36 @@ def get_java_formatter_cmd(formatter: str, build_tool: JavaBuildTool) -> list[st if formatter == "other": global formatter_warning_shown if not formatter_warning_shown: - click.echo("In codeflash.toml, please replace 'your-formatter' with your formatter command.") + click.echo("In your build config, please replace 'your-formatter' with your formatter command.") formatter_warning_shown = True return ["your-formatter $file"] return ["disabled"] def configure_java_project(setup_info: JavaSetupInfo) -> bool: - """Configure codeflash.toml for Java projects.""" - import tomlkit + """Configure codeflash in pom.xml properties or gradle.properties.""" + from codeflash.languages.java.build_config_strategy import get_config_strategy - codeflash_config_path = Path.cwd() / "codeflash.toml" + curdir = Path.cwd() - # Build config + # Build config dict with only non-default overrides config: dict[str, Any] = {} - # Detect values - curdir = Path.cwd() source_root = setup_info.module_root_override or detect_java_source_root(curdir) test_root = setup_info.test_root_override or detect_java_test_root(curdir) - config["language"] = "java" - config["module-root"] = source_root - config["tests-root"] = test_root + # Only include non-default values + if source_root != "src/main/java": + config["module-root"] = source_root + if test_root != "src/test/java": + config["tests-root"] = test_root - # Formatter - if setup_info.formatter_override is not None: - if setup_info.formatter_override != ["disabled"]: - config["formatter-cmds"] = setup_info.formatter_override - else: - config["formatter-cmds"] = [] + if setup_info.formatter_override is not None and setup_info.formatter_override != ["disabled"]: + config["formatter-cmds"] = setup_info.formatter_override - # Git remote if setup_info.git_remote and setup_info.git_remote not in ("", "origin"): config["git-remote"] = setup_info.git_remote - # User preferences if setup_info.disable_telemetry: config["disable-telemetry"] = True @@ -481,27 +468,19 @@ def configure_java_project(setup_info: JavaSetupInfo) -> bool: if setup_info.benchmarks_root: config["benchmarks-root"] = setup_info.benchmarks_root - try: - # Create TOML document - doc = tomlkit.document() - doc.add(tomlkit.comment("Codeflash configuration for Java project")) - doc.add(tomlkit.nl()) - - codeflash_table = tomlkit.table() - for key, value in config.items(): - codeflash_table.add(key, value) - - doc.add("tool", tomlkit.table()) - doc["tool"]["codeflash"] = codeflash_table - - with codeflash_config_path.open("w", encoding="utf-8") as f: - f.write(tomlkit.dumps(doc)) - - click.echo(f"Created Codeflash configuration in {codeflash_config_path}") + if not config: + click.echo("Standard Maven/Gradle layout detected — no config needed") click.echo() return True - except OSError as e: - click.echo(f"Failed to create codeflash.toml: {e}") + + try: + strategy = get_config_strategy(curdir) + ok, msg = strategy.write_codeflash_properties(curdir, config) + click.echo(msg) + click.echo() + return ok + except ValueError as e: + click.echo(f"Failed to write config: {e}") return False @@ -546,6 +525,18 @@ def get_java_test_command(build_tool: JavaBuildTool) -> str: return "mvn test" +@lru_cache(maxsize=8) +def _get_pom_root_cached(project_root: Path) -> Union[ET.Element, None]: + pom_path = project_root / "pom.xml" + if not pom_path.exists(): + return None + try: + tree = ET.parse(pom_path) + return tree.getroot() + except ET.ParseError: + return None + + formatter_warning_shown = False _SPOTLESS_COMMANDS = { diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index ef21ce051..196779589 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -12,8 +12,29 @@ ALL_CONFIG_FILES: dict[Path, dict[str, Path]] = {} +def _try_parse_java_build_config() -> tuple[dict[str, Any], Path] | None: + """Detect Java project from build files and parse config from pom.xml/gradle.properties. + + Returns (config_dict, project_root) if a Java project is found, None otherwise. + """ + dir_path = Path.cwd() + while dir_path != dir_path.parent: + if ( + (dir_path / "pom.xml").exists() + or (dir_path / "build.gradle").exists() + or (dir_path / "build.gradle.kts").exists() + ): + from codeflash.languages.java.build_config_strategy import parse_java_project_config + + config = parse_java_project_config(dir_path) + if config is not None: + return config, dir_path + dir_path = dir_path.parent + return None + + def find_pyproject_toml(config_file: Path | None = None) -> Path: - # Find the pyproject.toml or codeflash.toml file on the root of the project + # Find the pyproject.toml file on the root of the project if config_file is not None: config_file = Path(config_file) @@ -29,21 +50,13 @@ def find_pyproject_toml(config_file: Path | None = None) -> Path: # see if it was encountered before in search if cur_path in PYPROJECT_TOML_CACHE: return PYPROJECT_TOML_CACHE[cur_path] - # map current path to closest file - check both pyproject.toml and codeflash.toml while dir_path != dir_path.parent: - # First check pyproject.toml (Python projects) config_file = dir_path / "pyproject.toml" if config_file.exists(): PYPROJECT_TOML_CACHE[cur_path] = config_file return config_file - # Then check codeflash.toml (Java/other projects) - config_file = dir_path / "codeflash.toml" - if config_file.exists(): - PYPROJECT_TOML_CACHE[cur_path] = config_file - return config_file - # Search in parent directories dir_path = dir_path.parent - msg = f"Could not find pyproject.toml or codeflash.toml in the current directory {Path.cwd()} or any of the parent directories. Please create it by running `codeflash init`, or pass the path to the config file with the --config-file argument." + msg = f"Could not find pyproject.toml in the current directory {Path.cwd()} or any of the parent directories. Please create it by running `codeflash init`, or pass the path to the config file with the --config-file argument." raise ValueError(msg) from None @@ -90,33 +103,34 @@ def find_conftest_files(test_paths: list[Path]) -> list[Path]: return list(list_of_conftest_files) -# TODO for claude: There should be different functions to parse it per language, which should be chosen during runtime def parse_config_file( config_file_path: Path | None = None, override_formatter_check: bool = False ) -> tuple[dict[str, Any], Path]: + # Detect all config sources — Java build files, package.json, pyproject.toml + java_result = _try_parse_java_build_config() if config_file_path is None else None package_json_path = find_package_json(config_file_path) pyproject_toml_path = find_closest_config_file("pyproject.toml") if config_file_path is None else None - codeflash_toml_path = find_closest_config_file("codeflash.toml") if config_file_path is None else None - # Pick the closest toml config (pyproject.toml or codeflash.toml). - # Java projects use codeflash.toml; Python projects use pyproject.toml. - closest_toml_path = None - if pyproject_toml_path and codeflash_toml_path: - closest_toml_path = max(pyproject_toml_path, codeflash_toml_path, key=lambda p: len(p.parent.parts)) - else: - closest_toml_path = pyproject_toml_path or codeflash_toml_path + # Use Java config only if no closer JS/Python config exists (monorepo support). + # In a monorepo with a parent pom.xml and a child package.json, the closer config wins. + if java_result is not None: + java_depth = len(java_result[1].parts) + has_closer = (package_json_path is not None and len(package_json_path.parent.parts) >= java_depth) or ( + pyproject_toml_path is not None and len(pyproject_toml_path.parent.parts) >= java_depth + ) + if not has_closer: + return java_result # When both config files exist, prefer the one closer to CWD. # This prevents a parent-directory package.json (e.g., monorepo root) - # from overriding a closer pyproject.toml or codeflash.toml. + # from overriding a closer pyproject.toml. use_package_json = False if package_json_path: - if closest_toml_path is None: + if pyproject_toml_path is None: use_package_json = True else: - # Compare depth: more path parts = closer to CWD = more specific package_json_depth = len(package_json_path.parent.parts) - toml_depth = len(closest_toml_path.parent.parts) + toml_depth = len(pyproject_toml_path.parent.parts) use_package_json = package_json_depth >= toml_depth if use_package_json: @@ -160,7 +174,7 @@ def parse_config_file( if config == {} and lsp_mode: return {}, config_file_path - # Preserve language field if present (important for Java/JS projects using codeflash.toml) + # Preserve language field if present (important for JS/TS projects) # default values: path_keys = ["module-root", "tests-root", "benchmarks-root"] path_list_keys = ["ignore-paths"] diff --git a/codeflash/languages/java/__init__.py b/codeflash/languages/java/__init__.py index 5095a4847..99d1adc9e 100644 --- a/codeflash/languages/java/__init__.py +++ b/codeflash/languages/java/__init__.py @@ -5,6 +5,12 @@ Maven/Gradle for build operations. """ +from codeflash.languages.java.build_config_strategy import ( + BuildConfigStrategy, + GradleConfigStrategy, + MavenConfigStrategy, + get_config_strategy, +) from codeflash.languages.java.build_tool_strategy import BuildToolStrategy, get_strategy from codeflash.languages.java.build_tools import ( BuildTool, @@ -96,9 +102,12 @@ ) __all__ = [ + # Build config strategy + "BuildConfigStrategy", # Build tools "BuildTool", "BuildToolStrategy", + "GradleConfigStrategy", # Parser "JavaAnalyzer", # Assertion removal @@ -118,6 +127,7 @@ "JavaSupport", # Test runner "JavaTestRunResult", + "MavenConfigStrategy", "MavenTestResult", "ResolvedImport", "add_codeflash_dependency", @@ -151,6 +161,7 @@ "format_java_code", "format_java_file", "get_class_methods", + "get_config_strategy", "get_java_analyzer", "get_java_support", "get_method_by_name", diff --git a/codeflash/languages/java/build_config_strategy.py b/codeflash/languages/java/build_config_strategy.py new file mode 100644 index 000000000..793c75162 --- /dev/null +++ b/codeflash/languages/java/build_config_strategy.py @@ -0,0 +1,487 @@ +"""Strategy pattern for Java build-config read/write/remove operations. + +Defines BuildConfigStrategy ABC with MavenConfigStrategy (lxml-based pom.xml) +and GradleConfigStrategy (line-based gradle.properties) implementations. +""" + +from __future__ import annotations + +import logging +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any + +from lxml import etree + +logger = logging.getLogger(__name__) + +MAVEN_NS = "http://maven.apache.org/POM/4.0.0" + +# Maps kebab-case config keys to camelCase Maven/Gradle property names +_KEY_MAP: dict[str, str] = { + "module-root": "moduleRoot", + "tests-root": "testsRoot", + "git-remote": "gitRemote", + "disable-telemetry": "disableTelemetry", + "ignore-paths": "ignorePaths", + "formatter-cmds": "formatterCmds", +} + + +class BuildConfigStrategy(ABC): + """Strategy interface for Java build-config read/write/remove operations.""" + + @property + @abstractmethod + def name(self) -> str: ... + + @abstractmethod + def read_codeflash_properties(self, project_root: Path) -> dict[str, str]: + """Read codeflash.* properties from the build file. + + Returns a dict mapping property suffix to value, e.g. {"moduleRoot": "src/main/java"}. + """ + ... + + @abstractmethod + def write_codeflash_properties(self, project_root: Path, config: dict[str, Any]) -> tuple[bool, str]: + """Write codeflash.* properties to the build file. Only writes non-default overrides.""" + ... + + @abstractmethod + def remove_codeflash_properties(self, project_root: Path) -> tuple[bool, str]: + """Remove all codeflash.* properties from the build file.""" + ... + + +def _local_tag(element: etree._Element) -> str: + """Strip namespace prefix from an lxml element tag to get the local name.""" + tag = element.tag + if isinstance(tag, str) and tag.startswith("{"): + return tag.split("}", 1)[1] + return str(tag) + + +def _make_tag(root: etree._Element, local_name: str) -> str: + """Create a tag name respecting the document's default namespace.""" + ns = root.nsmap.get(None) + if ns: + return f"{{{ns}}}{local_name}" + return local_name + + +def _find_element(parent: etree._Element, local_name: str) -> etree._Element | None: + """Find a direct child element by local name, handling namespaces.""" + ns = parent.nsmap.get(None) + if ns: + return parent.find(f"{{{ns}}}{local_name}") + return parent.find(local_name) + + +def _detect_child_indent(properties_elem: etree._Element) -> str: + """Detect indentation used for children of a element.""" + for child in properties_elem: + if isinstance(child.tag, str) and child.tail and "\n" in child.tail: + # Indent is whitespace after the last newline in tail + lines = child.tail.split("\n") + if len(lines) > 1 and lines[-1].strip() == "": + return lines[-1] + # Try the element's own text (whitespace before first child) + if properties_elem.text and "\n" in properties_elem.text: + lines = properties_elem.text.split("\n") + if len(lines) > 1: + return lines[-1] + return " " # 8-space default (typical Maven indent) + + +def _format_value(value: Any) -> str: + """Convert a config value to a string suitable for build file properties.""" + if isinstance(value, list): + return ",".join(str(v) for v in value) + if isinstance(value, bool): + return str(value).lower() + return str(value) + + +class MavenConfigStrategy(BuildConfigStrategy): + """Read/write/remove codeflash.* properties in pom.xml using lxml.""" + + @property + def name(self) -> str: + return "Maven" + + def read_codeflash_properties(self, project_root: Path) -> dict[str, str]: + pom_path = project_root / "pom.xml" + if not pom_path.exists(): + return {} + try: + _tree, root = self._parse_pom(pom_path) + props = _find_element(root, "properties") + if props is None: + return {} + result: dict[str, str] = {} + for child in props: + if not isinstance(child.tag, str): + continue + local = _local_tag(child) + if local.startswith("codeflash.") and child.text: + key = local[len("codeflash.") :] + result[key] = child.text.strip() + return result + except Exception: + logger.debug("Failed to read codeflash properties from pom.xml", exc_info=True) + return {} + + def write_codeflash_properties(self, project_root: Path, config: dict[str, Any]) -> tuple[bool, str]: + pom_path = project_root / "pom.xml" + if not pom_path.exists(): + return False, f"No pom.xml found at {project_root}" + try: + tree, root = self._parse_pom(pom_path) + props = _find_element(root, "properties") + + if props is None: + # Create section + props = etree.SubElement(root, _make_tag(root, "properties")) + props.text = "\n " + props.tail = "\n" + else: + # Remove existing codeflash.* elements + for child in list(props): + if isinstance(child.tag, str) and _local_tag(child).startswith("codeflash."): + self._remove_preserving_whitespace(props, child) + + indent = _detect_child_indent(props) + + # Add new codeflash.* elements + for key, value in config.items(): + prop_name = f"codeflash.{_KEY_MAP.get(key, key)}" + tag = _make_tag(root, prop_name) + elem = etree.SubElement(props, tag) + elem.text = _format_value(value) + elem.tail = "\n" + indent + + # Fix the last element's tail to align with the closing + last = props[-1] if len(props) > 0 else None + if last is not None: + # Closing tag indent = one level less than child indent + parent_indent = indent[:-4] if len(indent) >= 4 else indent[:-2] if len(indent) >= 2 else "" + last.tail = "\n" + parent_indent + + # Ensure props.text has proper indent for first child + if props.text is None or props.text.strip() == "": + props.text = "\n" + indent + + tree.write(str(pom_path), xml_declaration=True, encoding="UTF-8") + return True, f"Config saved to {pom_path} " + except Exception as e: + return False, f"Failed to write Maven properties: {e}" + + def remove_codeflash_properties(self, project_root: Path) -> tuple[bool, str]: + pom_path = project_root / "pom.xml" + if not pom_path.exists(): + return True, "No pom.xml found" + try: + tree, root = self._parse_pom(pom_path) + props = _find_element(root, "properties") + if props is None: + return True, "No codeflash properties found in pom.xml" + + removed = False + for child in list(props): + if isinstance(child.tag, str) and _local_tag(child).startswith("codeflash."): + self._remove_preserving_whitespace(props, child) + removed = True + + if removed: + tree.write(str(pom_path), xml_declaration=True, encoding="UTF-8") + return True, "Removed codeflash properties from pom.xml" + except Exception as e: + return False, f"Failed to remove config from pom.xml: {e}" + + @staticmethod + def _parse_pom(pom_path: Path) -> tuple[etree._ElementTree, etree._Element]: + parser = etree.XMLParser(remove_blank_text=False, strip_cdata=False) + tree = etree.parse(str(pom_path), parser) + return tree, tree.getroot() + + @staticmethod + def _remove_preserving_whitespace(parent: etree._Element, child: etree._Element) -> None: + """Remove a child element, merging its tail whitespace into the previous sibling or parent text.""" + prev = child.getprevious() + if prev is not None: + # Merge child's tail into previous sibling's tail + prev.tail = ( + (prev.tail or "") if child.tail is None else (prev.tail or "").rstrip(" \t") + (child.tail or "") + ) + # First child — merge tail into parent's text + elif child.tail is not None: + parent.text = (parent.text or "").rstrip(" \t") + child.tail + parent.remove(child) + + +class GradleConfigStrategy(BuildConfigStrategy): + """Read/write/remove codeflash.* properties in gradle.properties.""" + + @property + def name(self) -> str: + return "Gradle" + + def read_codeflash_properties(self, project_root: Path) -> dict[str, str]: + props_path = project_root / "gradle.properties" + if not props_path.exists(): + return {} + result: dict[str, str] = {} + try: + with props_path.open("r", encoding="utf-8") as f: + for line in f: + stripped = line.strip() + if stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + key = key.strip() + if key.startswith("codeflash."): + result[key[len("codeflash.") :]] = value.strip() + return result + except Exception: + logger.debug("Failed to read codeflash properties from gradle.properties", exc_info=True) + return {} + + def write_codeflash_properties(self, project_root: Path, config: dict[str, Any]) -> tuple[bool, str]: + props_path = project_root / "gradle.properties" + try: + lines: list[str] = [] + if props_path.exists(): + lines = props_path.read_text(encoding="utf-8").splitlines() + + # Remove existing codeflash.* lines and our comment header + lines = [ + line + for line in lines + if not line.strip().startswith("codeflash.") + and line.strip() != "# Codeflash configuration \u2014 https://docs.codeflash.ai" + ] + + # Add blank line separator if needed + if lines and lines[-1].strip(): + lines.append("") + lines.append("# Codeflash configuration \u2014 https://docs.codeflash.ai") + for key, value in config.items(): + gradle_key = f"codeflash.{_KEY_MAP.get(key, key)}" + lines.append(f"{gradle_key}={_format_value(value)}") + + props_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + return True, f"Config saved to {props_path}" + except Exception as e: + return False, f"Failed to write gradle.properties: {e}" + + def remove_codeflash_properties(self, project_root: Path) -> tuple[bool, str]: + props_path = project_root / "gradle.properties" + if not props_path.exists(): + return True, "No gradle.properties found" + try: + lines = props_path.read_text(encoding="utf-8").splitlines() + filtered = [ + line + for line in lines + if not line.strip().startswith("codeflash.") + and line.strip() != "# Codeflash configuration \u2014 https://docs.codeflash.ai" + ] + props_path.write_text("\n".join(filtered) + "\n", encoding="utf-8") + return True, "Removed codeflash properties from gradle.properties" + except Exception as e: + return False, f"Failed to remove config from gradle.properties: {e}" + + +def get_config_strategy(project_root: Path) -> BuildConfigStrategy: + """Detect build tool and return the appropriate config strategy.""" + from codeflash.languages.java.build_tools import BuildTool, detect_build_tool + + build_tool = detect_build_tool(project_root) + if build_tool == BuildTool.MAVEN: + return MavenConfigStrategy() + if build_tool == BuildTool.GRADLE: + return GradleConfigStrategy() + msg = f"No supported Java build tool found in {project_root}" + raise ValueError(msg) + + +def _is_multi_module_project(project_root: Path) -> bool: + """Check if the project is a multi-module Maven or Gradle project.""" + # Maven: check for in pom.xml + pom_path = project_root / "pom.xml" + if pom_path.exists(): + try: + content = pom_path.read_text(encoding="utf-8") + if "" in content: + return True + except Exception: + pass + + # Gradle: check for include directives in settings.gradle(.kts) + for name in ("settings.gradle.kts", "settings.gradle"): + settings_path = project_root / name + if settings_path.exists(): + try: + content = settings_path.read_text(encoding="utf-8") + if "include" in content: + return True + except Exception: + pass + + return False + + +def parse_java_project_config(project_root: Path) -> dict[str, Any] | None: + """Parse codeflash config from Maven/Gradle build files. + + Reads codeflash.* properties from pom.xml or gradle.properties, + then fills in defaults from auto-detected build tool conventions. + + For multi-module projects, scans submodule pom.xml files to detect the + source root (largest module by Java file count) and test root (module with + "test" in its name). The per-function _get_java_sources_root() further + derives the correct module-specific test directory at runtime. + + Returns None if no Java build tool is detected. + """ + from codeflash.languages.java.build_tools import BuildTool, detect_build_tool, find_source_root, find_test_root + + build_tool = detect_build_tool(project_root) + if build_tool == BuildTool.UNKNOWN: + return None + + try: + strategy = get_config_strategy(project_root) + user_config = strategy.read_codeflash_properties(project_root) + except ValueError: + user_config = {} + + is_multimodule = _is_multi_module_project(project_root) + + if is_multimodule: + source_root, test_root = _detect_roots_from_maven_modules(project_root) + else: + source_root = find_source_root(project_root) + test_root = find_test_root(project_root) + + default_source = project_root / "src" / "main" / "java" + default_test = project_root / "src" / "test" / "java" + config: dict[str, Any] = { + "language": "java", + "module_root": str( + (project_root / user_config["moduleRoot"]).resolve() + if "moduleRoot" in user_config + else (source_root or (default_source if default_source.is_dir() else project_root)) + ), + "tests_root": str( + (project_root / user_config["testsRoot"]).resolve() + if "testsRoot" in user_config + else (test_root or (default_test if default_test.is_dir() else project_root)) + ), + "pytest_cmd": "pytest", + "git_remote": user_config.get("gitRemote", "origin"), + "disable_telemetry": user_config.get("disableTelemetry", "false").lower() == "true", + "disable_imports_sorting": False, + "override_fixtures": False, + "benchmark": False, + "formatter_cmds": [], + "ignore_paths": [], + } + + if "ignorePaths" in user_config: + config["ignore_paths"] = [ + str((project_root / p.strip()).resolve()) for p in user_config["ignorePaths"].split(",") if p.strip() + ] + + if "formatterCmds" in user_config: + config["formatter_cmds"] = [cmd.strip() for cmd in user_config["formatterCmds"].split(",") if cmd.strip()] + + return config + + +def _detect_roots_from_maven_modules(project_root: Path) -> tuple[Path | None, Path | None]: + """Scan Maven module pom.xml files for custom sourceDirectory/testSourceDirectory.""" + from codeflash.languages.java.build_tools import _safe_parse_xml + + pom_path = project_root / "pom.xml" + if not pom_path.exists(): + return None, None + + try: + tree = _safe_parse_xml(pom_path) + root = tree.getroot() + ns = {"m": MAVEN_NS} + + modules: list[str] = [] + for modules_elem in [root.find("m:modules", ns), root.find("modules")]: + if modules_elem is not None: + for mod in modules_elem: + if mod.text: + modules.append(mod.text.strip()) + + if not modules: + return None, None + + source_candidates: list[tuple[Path, int]] = [] + test_root: Path | None = None + skip_modules = {"example", "examples", "benchmark", "benchmarks", "demo", "sample", "samples"} + + for module_name in modules: + module_pom = project_root / module_name / "pom.xml" + if not module_pom.exists(): + continue + + is_test_module = "test" in module_name.lower() + + try: + mod_tree = _safe_parse_xml(module_pom) + mod_root = mod_tree.getroot() + + for build in [mod_root.find("m:build", ns), mod_root.find("build")]: + if build is None: + continue + + for src_elem in [build.find("m:sourceDirectory", ns), build.find("sourceDirectory")]: + if src_elem is not None and src_elem.text: + src_text = src_elem.text.replace("${project.basedir}", str(project_root / module_name)) + src_path = Path(src_text) + if not src_path.is_absolute(): + src_path = project_root / module_name / src_path + if src_path.exists(): + if is_test_module and test_root is None: + test_root = src_path + elif module_name.lower() not in skip_modules: + java_count = sum(1 for _ in src_path.rglob("*.java")) + if java_count > 0: + source_candidates.append((src_path, java_count)) + + for test_elem in [build.find("m:testSourceDirectory", ns), build.find("testSourceDirectory")]: + if test_elem is not None and test_elem.text: + test_text = test_elem.text.replace("${project.basedir}", str(project_root / module_name)) + test_path = Path(test_text) + if not test_path.is_absolute(): + test_path = project_root / module_name / test_path + if test_path.exists() and test_root is None: + test_root = test_path + + if module_name.lower() not in skip_modules and not is_test_module: + std_src = project_root / module_name / "src" / "main" / "java" + if std_src.exists(): + java_count = sum(1 for _ in std_src.rglob("*.java")) + if java_count > 0: + source_candidates.append((std_src, java_count)) + + if test_root is None: + std_test = project_root / module_name / "src" / "test" / "java" + if std_test.exists() and any(std_test.rglob("*.java")): + test_root = std_test + + except Exception: + continue + + source_root = max(source_candidates, key=lambda x: x[1])[0] if source_candidates else None + return source_root, test_root + + except Exception: + return None, None diff --git a/codeflash/languages/java/build_tool_strategy.py b/codeflash/languages/java/build_tool_strategy.py index 4cd503fcd..b63def8c4 100644 --- a/codeflash/languages/java/build_tool_strategy.py +++ b/codeflash/languages/java/build_tool_strategy.py @@ -16,6 +16,8 @@ if TYPE_CHECKING: import subprocess + from codeflash.languages.java.build_tools import JavaProjectInfo + logger = logging.getLogger(__name__) _RUNTIME_JAR_NAME = "codeflash-runtime-1.0.0.jar" @@ -45,6 +47,11 @@ def name(self) -> str: """Human-readable name for log messages (e.g. 'Maven', 'Gradle').""" ... + @abstractmethod + def get_project_info(self, project_root: Path) -> JavaProjectInfo | None: + """Extract project metadata (source roots, versions, etc.) from the build configuration.""" + ... + def find_runtime_jar(self) -> Path | None: """Find the codeflash-runtime JAR file. diff --git a/codeflash/languages/java/build_tools.py b/codeflash/languages/java/build_tools.py index 28db2c9aa..a1203ac4a 100644 --- a/codeflash/languages/java/build_tools.py +++ b/codeflash/languages/java/build_tools.py @@ -126,171 +126,16 @@ def get_project_info(project_root: Path) -> JavaProjectInfo | None: """ build_tool = detect_build_tool(project_root) - - if build_tool == BuildTool.MAVEN: - return _get_maven_project_info(project_root) - if build_tool == BuildTool.GRADLE: - return _get_gradle_project_info(project_root) - - return None - - -def _get_maven_project_info(project_root: Path) -> JavaProjectInfo | None: - """Get project info from Maven pom.xml. - - Args: - project_root: Root directory of the Maven project. - - Returns: - JavaProjectInfo extracted from pom.xml. - - """ - pom_path = project_root / "pom.xml" - if not pom_path.exists(): + if build_tool == BuildTool.UNKNOWN: return None + from codeflash.languages.java.build_tool_strategy import get_strategy + try: - tree = _safe_parse_xml(pom_path) - root = tree.getroot() - - # Handle Maven namespace - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - - def get_text(xpath: str, default: str | None = None) -> str | None: - # Try with namespace first - elem = root.find(f"m:{xpath}", ns) - if elem is None: - # Try without namespace - elem = root.find(xpath) - return elem.text if elem is not None else default - - group_id = get_text("groupId") - artifact_id = get_text("artifactId") - version = get_text("version") - - # Get Java version from properties or compiler plugin - java_version = _extract_java_version_from_pom(root, ns) - - # Standard Maven directory structure - source_roots = [] - test_roots = [] - - main_src = project_root / "src" / "main" / "java" - if main_src.exists(): - source_roots.append(main_src) - - test_src = project_root / "src" / "test" / "java" - if test_src.exists(): - test_roots.append(test_src) - - # Check for custom source directories in pom.xml section - for build in [root.find("m:build", ns), root.find("build")]: - if build is not None: - for tag, roots_list in [("sourceDirectory", source_roots), ("testSourceDirectory", test_roots)]: - for elem in [build.find(f"m:{tag}", ns), build.find(tag)]: - if elem is not None and elem.text: - custom_dir = project_root / elem.text.strip() - if custom_dir.exists() and custom_dir not in roots_list: - roots_list.append(custom_dir) - - target_dir = project_root / "target" - - return JavaProjectInfo( - project_root=project_root, - build_tool=BuildTool.MAVEN, - source_roots=source_roots, - test_roots=test_roots, - target_dir=target_dir, - group_id=group_id, - artifact_id=artifact_id, - version=version, - java_version=java_version, - ) - - except ET.ParseError as e: - logger.warning("Failed to parse pom.xml: %s", e) + strategy = get_strategy(project_root) + except ValueError: return None - - -def _extract_java_version_from_pom(root: ET.Element, ns: dict[str, str]) -> str | None: - """Extract Java version from Maven pom.xml. - - Checks multiple locations: - 1. properties/maven.compiler.source - 2. properties/java.version - 3. build/plugins/plugin[compiler]/configuration/source - - Args: - root: Root element of the pom.xml. - ns: XML namespace mapping. - - Returns: - Java version string or None. - - """ - # Check properties - for prop_name in ("maven.compiler.source", "java.version", "maven.compiler.release"): - for props in [root.find("m:properties", ns), root.find("properties")]: - if props is not None: - for prop in [props.find(f"m:{prop_name}", ns), props.find(prop_name)]: - if prop is not None and prop.text: - return prop.text - - # Check compiler plugin configuration - for build in [root.find("m:build", ns), root.find("build")]: - if build is not None: - for plugins in [build.find("m:plugins", ns), build.find("plugins")]: - if plugins is not None: - for plugin in plugins.findall("m:plugin", ns) + plugins.findall("plugin"): - artifact_id = plugin.find("m:artifactId", ns) or plugin.find("artifactId") - if artifact_id is not None and artifact_id.text == "maven-compiler-plugin": - config = plugin.find("m:configuration", ns) or plugin.find("configuration") - if config is not None: - source = config.find("m:source", ns) or config.find("source") - if source is not None and source.text: - return source.text - - return None - - -def _get_gradle_project_info(project_root: Path) -> JavaProjectInfo | None: - """Get project info from Gradle build file. - - Note: This is a basic implementation. Full Gradle parsing would require - running Gradle with a custom task or using the Gradle tooling API. - - Args: - project_root: Root directory of the Gradle project. - - Returns: - JavaProjectInfo with basic Gradle project structure. - - """ - # Standard Gradle directory structure - source_roots = [] - test_roots = [] - - main_src = project_root / "src" / "main" / "java" - if main_src.exists(): - source_roots.append(main_src) - - test_src = project_root / "src" / "test" / "java" - if test_src.exists(): - test_roots.append(test_src) - - build_dir = project_root / "build" - - return JavaProjectInfo( - project_root=project_root, - build_tool=BuildTool.GRADLE, - source_roots=source_roots, - test_roots=test_roots, - target_dir=build_dir, - group_id=None, # Would need to parse build.gradle - artifact_id=None, - version=None, - java_version=None, - ) + return strategy.get_project_info(project_root) def _parse_surefire_reports(surefire_dir: Path) -> tuple[int, int, int, int]: diff --git a/codeflash/languages/java/function_optimizer.py b/codeflash/languages/java/function_optimizer.py index c24091c9a..5700a907a 100644 --- a/codeflash/languages/java/function_optimizer.py +++ b/codeflash/languages/java/function_optimizer.py @@ -248,10 +248,14 @@ def _fix_java_test_paths( package_name = new_package logger.debug(f"[JPMS] Remapped package: {old_decl} -> {new_decl}") - class_match = re.search(r"^(?:public\s+)?class\s+(\w+)", behavior_source, re.MULTILINE) + class_match = re.search( + r"^\s*(?:(?:public|static|final|abstract)\s+)*class\s+(\w+)", behavior_source, re.MULTILINE + ) behavior_class = class_match.group(1) if class_match else "GeneratedTest" - perf_class_match = re.search(r"^(?:public\s+)?class\s+(\w+)", perf_source, re.MULTILINE) + perf_class_match = re.search( + r"^\s*(?:(?:public|static|final|abstract)\s+)*class\s+(\w+)", perf_source, re.MULTILINE + ) perf_class = perf_class_match.group(1) if perf_class_match else "GeneratedPerfTest" test_dir = self._get_java_sources_root() diff --git a/codeflash/languages/java/gradle_strategy.py b/codeflash/languages/java/gradle_strategy.py index a132e61c9..095ecc956 100644 --- a/codeflash/languages/java/gradle_strategy.py +++ b/codeflash/languages/java/gradle_strategy.py @@ -17,6 +17,11 @@ 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 + +_RE_INCLUDE = re.compile(r"""include\s*\(?([^)\n]+)\)?""") + +_RE_QUOTED = re.compile(r"""['"]([^'"]+)['"]""") _BUILD = "build" @@ -29,13 +34,13 @@ _GRADLE_SKIP_VALIDATION_INIT_SCRIPT = """\ gradle.projectsEvaluated { allprojects { + // Disable checkstyle, spotbugs, pmd by type (catches all source sets, not just Main/Test) + try { tasks.withType(Checkstyle) { enabled = false } } catch (e) {} + try { tasks.withType(Class.forName('com.github.spotbugs.snom.SpotBugsTask')) { enabled = false } } catch (e) {} + try { tasks.withType(Pmd) { enabled = false } } catch (e) {} + // Disable remaining validation tasks by name tasks.matching { task -> - task.name in [ - 'checkstyleMain', 'checkstyleTest', - 'spotbugsMain', 'spotbugsTest', - 'pmdMain', 'pmdTest', - 'rat', 'japicmp' - ] + task.name in ['rat', 'japicmp'] }.configureEach { enabled = false } @@ -180,6 +185,70 @@ def _find_top_level_dependencies_block(build_file: Path, content: str) -> int | return None +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"): + settings_file = build_root / settings_name + if settings_file.exists(): + try: + content = settings_file.read_text(encoding="utf-8") + if re.search(r'include\s*[\(\'"]', content): + return True + except Exception: + pass + 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. + + This avoids adding testImplementation to the root build file directly, which would fail + if the root project doesn't apply the java plugin. + """ + if not build_file.exists(): + return False + + try: + 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) + 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" dependencies {{\n" + f' testImplementation(files("{jar_str}")) // codeflash-runtime\n' + f" }}\n" + f" }}\n" + f"}}\n" + ) + else: + block = ( + f"\nsubprojects {{\n" + f" plugins.withId('java') {{\n" + f" dependencies {{\n" + f" testImplementation files('{jar_str}') // codeflash-runtime\n" + f" }}\n" + f" }}\n" + f"}}\n" + ) + + content += block + build_file.write_text(content, encoding="utf-8") + logger.info("Added codeflash-runtime dependency to %s (subprojects block)", build_file.name) + return True + + except Exception as e: + logger.exception("Failed to add dependency to %s: %s", build_file.name, e) + return False + + def add_codeflash_dependency(build_file: Path, runtime_jar_path: Path) -> bool: if not build_file.exists(): return False @@ -254,6 +323,54 @@ def _normalize_gradle_xml_reports(reports_dir: Path) -> None: logger.debug("Failed to normalize Gradle XML report %s", xml_file) +def _extract_gradle_include_modules(content: str) -> list[str]: + """Extract module names from include() directives in settings.gradle.""" + modules: list[str] = [] + for match in _RE_INCLUDE.finditer(content): + args = match.group(1) + for quoted in _RE_QUOTED.findall(args): + module = quoted.lstrip(":") + if module: + modules.append(module) + return modules + + +def _parse_gradle_settings_modules(project_root: Path) -> list[str]: + """Parse settings.gradle(.kts) to find included modules.""" + for settings_name in ["settings.gradle", "settings.gradle.kts"]: + settings_path = project_root / settings_name + if settings_path.exists(): + try: + content = settings_path.read_text(encoding="utf-8") + return _extract_gradle_include_modules(content) + except Exception: + continue + return [] + + +def _discover_gradle_submodule_roots(project_root: Path) -> tuple[list[Path], list[Path]]: + """Discover source and test roots from Gradle submodules.""" + source_roots: list[Path] = [] + test_roots: list[Path] = [] + + modules = _parse_gradle_settings_modules(project_root) + for module_name in modules: + module_path = module_name.replace(":", "/") + module_dir = project_root / module_path + if not module_dir.is_dir(): + continue + + std_src = module_dir / "src" / "main" / "java" + if std_src.exists(): + source_roots.append(std_src) + + std_test = module_dir / "src" / "test" / "java" + if std_test.exists(): + test_roots.append(std_test) + + return source_roots, test_roots + + class GradleStrategy(BuildToolStrategy): """Gradle-specific build tool operations.""" @@ -261,6 +378,38 @@ class GradleStrategy(BuildToolStrategy): def name(self) -> str: return "Gradle" + def get_project_info(self, project_root: Path) -> JavaProjectInfo | None: + source_roots: list[Path] = [] + test_roots: list[Path] = [] + + main_src = project_root / "src" / "main" / "java" + if main_src.exists(): + source_roots.append(main_src) + + test_src = project_root / "src" / "test" / "java" + if test_src.exists(): + test_roots.append(test_src) + + sub_sources, sub_tests = _discover_gradle_submodule_roots(project_root) + for root_path in sub_sources: + if root_path not in source_roots: + source_roots.append(root_path) + for root_path in sub_tests: + if root_path not in test_roots: + test_roots.append(root_path) + + return JavaProjectInfo( + project_root=project_root, + build_tool=BuildTool.GRADLE, + source_roots=source_roots, + test_roots=test_roots, + target_dir=project_root / "build", + group_id=None, + artifact_id=None, + version=None, + java_version=None, + ) + def find_executable(self, build_root: Path) -> str | None: # Walk up from build_root to find gradlew — for multi-module projects # the wrapper lives at the repo root, which may be a parent of build_root. @@ -303,7 +452,11 @@ def ensure_runtime(self, build_root: Path, test_module: str | None) -> bool: logger.warning("No build.gradle(.kts) found at %s, cannot add codeflash-runtime dependency", module_root) return False - if not add_codeflash_dependency(build_file, dest_jar): + if not test_module and _is_multimodule_project(build_root): + if not add_codeflash_dependency_multimodule(build_file, dest_jar): + logger.error("Failed to add codeflash-runtime dependency to %s", build_file) + return False + elif not add_codeflash_dependency(build_file, dest_jar): logger.error("Failed to add codeflash-runtime dependency to %s", build_file) return False diff --git a/codeflash/languages/java/import_resolver.py b/codeflash/languages/java/import_resolver.py index cf87146aa..a1f495555 100644 --- a/codeflash/languages/java/import_resolver.py +++ b/codeflash/languages/java/import_resolver.py @@ -111,7 +111,20 @@ def resolve_import(self, import_info: JavaImportInfo) -> ResolvedImport: class_name=self._extract_class_name(import_path), ) - # Check if it's a known external library + # Try to resolve within the project FIRST — catches project-internal imports + # whose package prefixes (e.g. org.apache, com.google) overlap with + # COMMON_EXTERNAL_PREFIXES + resolved_path = self._resolve_to_file(import_path) + if resolved_path is not None: + return ResolvedImport( + import_path=import_path, + file_path=resolved_path, + is_external=False, + is_wildcard=import_info.is_wildcard, + class_name=self._extract_class_name(import_path), + ) + + # Only check external prefixes after failing to find the file in the project if self._is_external_library(import_path): return ResolvedImport( import_path=import_path, @@ -121,13 +134,11 @@ def resolve_import(self, import_info: JavaImportInfo) -> ResolvedImport: class_name=self._extract_class_name(import_path), ) - # Try to resolve within the project - resolved_path = self._resolve_to_file(import_path) - + # Not found in project and not a known external — mark as unresolved return ResolvedImport( import_path=import_path, - file_path=resolved_path, - is_external=resolved_path is None, + file_path=None, + is_external=True, is_wildcard=import_info.is_wildcard, class_name=self._extract_class_name(import_path), ) diff --git a/codeflash/languages/java/instrumentation.py b/codeflash/languages/java/instrumentation.py index 9ecbd613e..10ae9f057 100644 --- a/codeflash/languages/java/instrumentation.py +++ b/codeflash/languages/java/instrumentation.py @@ -1381,7 +1381,7 @@ def instrument_generated_java_test( # Extract class name from the test code # Use pattern that starts at beginning of line to avoid matching words in comments - class_match = re.search(r"^(?:public\s+)?class\s+(\w+)", test_code, re.MULTILINE) + class_match = re.search(r"^\s*(?:(?:public|static|final|abstract)\s+)*class\s+(\w+)", test_code, re.MULTILINE) if not class_match: logger.warning("Could not find class name in generated test") return test_code diff --git a/codeflash/languages/java/maven_strategy.py b/codeflash/languages/java/maven_strategy.py index fe30e4c25..7f1f64ae6 100644 --- a/codeflash/languages/java/maven_strategy.py +++ b/codeflash/languages/java/maven_strategy.py @@ -17,7 +17,13 @@ from typing import Any from codeflash.languages.java.build_tool_strategy import BuildToolStrategy, module_to_dir -from codeflash.languages.java.build_tools import CODEFLASH_RUNTIME_JAR_NAME, CODEFLASH_RUNTIME_VERSION +from codeflash.languages.java.build_tools import ( + CODEFLASH_RUNTIME_JAR_NAME, + CODEFLASH_RUNTIME_VERSION, + BuildTool, + JavaProjectInfo, + _safe_parse_xml, +) _TARGET = "target" @@ -27,6 +33,11 @@ _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", @@ -112,12 +123,6 @@ def resolve_from_maven_central(maven_root: Path) -> bool: return False -def _safe_parse_xml(file_path: Path) -> ET.ElementTree: - content = file_path.read_text(encoding="utf-8") - root = ET.fromstring(content) - return ET.ElementTree(root) - - def install_codeflash_runtime(project_root: Path, runtime_jar_path: Path, mvn: str | None = None) -> bool: if not mvn: mvn = shutil.which("mvn") @@ -152,6 +157,129 @@ def install_codeflash_runtime(project_root: Path, runtime_jar_path: Path, mvn: s return False +# Properties set to "true" to enable skipping +_VALIDATION_SKIP_PROPERTIES_TRUE = [ + "checkstyle.skip", + "disable.checks", + "spotbugs.skip", + "pmd.skip", + "rat.skip", + "enforcer.skip", + "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 = """\ + + org.apache.maven.plugins + maven-checkstyle-plugin + + true + false + false + + + + com.github.spotbugs + spotbugs-maven-plugin + true + + + org.apache.maven.plugins + maven-pmd-plugin + true + +""" + + +def inject_validation_skip_properties(pom_path: Path) -> bool: + """Inject validation skip properties and plugin overrides into the POM. + + Two-layer approach: + 1. Properties — works when the plugin reads from standard property names. + 2. Plugin configuration overrides — handles parent POMs with custom execution + bindings that ignore the standard skip properties. + """ + if not pom_path.exists(): + return False + + try: + content = pom_path.read_text(encoding="utf-8") + + if "" in content: + return True + + props_lines = "".join(f" <{p}>true\n" for p in _VALIDATION_SKIP_PROPERTIES_TRUE) + props_lines += "".join(f" <{p}>false\n" for p in _VALIDATION_SKIP_PROPERTIES_FALSE) + + # 1. Inject properties + closing_idx = content.find("") + if closing_idx != -1: + content = content[:closing_idx] + props_lines + content[closing_idx:] + else: + project_close = content.rfind("") + if project_close == -1: + logger.warning("No tag found in %s", pom_path) + return False + content = ( + content[:project_close] + + " \n" + + props_lines + + " \n" + + content[project_close:] + ) + + # 2. Inject plugin configuration overrides + plugins_block = ( + " \n" + " \n" + " \n" + _VALIDATION_PLUGIN_OVERRIDES + " \n" + " \n" + ) + + build_close = content.find("") + if build_close != -1: + # Insert plugins before existing + plugins_close = content.find("", 0, build_close) + if plugins_close != -1: + content = ( + content[:plugins_close] + + "\n" + + _VALIDATION_PLUGIN_OVERRIDES + + content[plugins_close:] + ) + else: + content = ( + content[:build_close] + + " \n" + + " \n" + + _VALIDATION_PLUGIN_OVERRIDES + + " \n" + + content[build_close:] + ) + else: + project_close = content.rfind("") + if project_close != -1: + content = content[:project_close] + plugins_block + content[project_close:] + + pom_path.write_text(content, encoding="utf-8") + logger.info("Injected validation skip properties and plugin overrides into %s", pom_path) + return True + + except Exception: + logger.debug("Failed to inject validation skip properties into %s", pom_path, exc_info=True) + return False + + def add_codeflash_dependency(pom_path: Path) -> bool: if not pom_path.exists(): return False @@ -360,6 +488,79 @@ def get_jacoco_report_path(project_root: Path) -> Path: return project_root / "target" / "site" / "jacoco" / "jacoco.xml" +def _extract_java_version_from_pom(root: ET.Element, ns: dict[str, str]) -> str | None: + """Extract Java version from Maven pom.xml properties or compiler plugin.""" + for prop_name in ("maven.compiler.source", "java.version", "maven.compiler.release"): + for props in [root.find("m:properties", ns), root.find("properties")]: + if props is not None: + for prop in [props.find(f"m:{prop_name}", ns), props.find(prop_name)]: + if prop is not None and prop.text: + return prop.text + + for build in [root.find("m:build", ns), root.find("build")]: + if build is not None: + for plugins in [build.find("m:plugins", ns), build.find("plugins")]: + if plugins is not None: + for plugin in plugins.findall("m:plugin", ns) + plugins.findall("plugin"): + artifact_id = plugin.find("m:artifactId", ns) or plugin.find("artifactId") + if artifact_id is not None and artifact_id.text == "maven-compiler-plugin": + config = plugin.find("m:configuration", ns) or plugin.find("configuration") + if config is not None: + source = config.find("m:source", ns) or config.find("source") + if source is not None and source.text: + return source.text + + return None + + +def _discover_maven_submodule_roots( + project_root: Path, root: ET.Element, ns: dict[str, str] +) -> tuple[list[Path], list[Path]]: + """Discover source and test roots from Maven submodules.""" + source_roots: list[Path] = [] + test_roots: list[Path] = [] + + modules: list[str] = [] + for modules_elem in [root.find("m:modules", ns), root.find("modules")]: + if modules_elem is not None: + for mod in modules_elem: + if mod.text: + modules.append(mod.text.strip()) + + for module_name in modules: + module_dir = project_root / module_name + if not module_dir.is_dir(): + continue + + std_src = module_dir / "src" / "main" / "java" + if std_src.exists(): + source_roots.append(std_src) + + std_test = module_dir / "src" / "test" / "java" + if std_test.exists(): + test_roots.append(std_test) + + module_pom = module_dir / "pom.xml" + if module_pom.exists(): + try: + mod_tree = _safe_parse_xml(module_pom) + mod_root = mod_tree.getroot() + if mod_root is None: + continue + for build in [mod_root.find("m:build", ns), mod_root.find("build")]: + if build is not None: + for tag, roots_list in [("sourceDirectory", source_roots), ("testSourceDirectory", test_roots)]: + for elem in [build.find(f"m:{tag}", ns), build.find(tag)]: + if elem is not None and elem.text: + custom_dir = module_dir / elem.text.strip() + if custom_dir.exists() and custom_dir not in roots_list: + roots_list.append(custom_dir) + except Exception: + continue + + return source_roots, test_roots + + class MavenStrategy(BuildToolStrategy): """Maven-specific build tool operations.""" @@ -378,6 +579,73 @@ class MavenStrategy(BuildToolStrategy): def name(self) -> str: return "Maven" + def get_project_info(self, project_root: Path) -> JavaProjectInfo | None: + pom_path = project_root / "pom.xml" + if not pom_path.exists(): + return None + + try: + tree = _safe_parse_xml(pom_path) + root = tree.getroot() + if root is None: + return None + ns = {"m": "http://maven.apache.org/POM/4.0.0"} + + def get_text(xpath: str, default: str | None = None) -> str | None: + elem = root.find(f"m:{xpath}", ns) + if elem is None: + elem = root.find(xpath) + return elem.text if elem is not None else default + + group_id = get_text("groupId") + artifact_id = get_text("artifactId") + version = get_text("version") + java_version = _extract_java_version_from_pom(root, ns) + + source_roots: list[Path] = [] + test_roots: list[Path] = [] + + main_src = project_root / "src" / "main" / "java" + if main_src.exists(): + source_roots.append(main_src) + + test_src = project_root / "src" / "test" / "java" + if test_src.exists(): + test_roots.append(test_src) + + for build in [root.find("m:build", ns), root.find("build")]: + if build is not None: + for tag, roots_list in [("sourceDirectory", source_roots), ("testSourceDirectory", test_roots)]: + for elem in [build.find(f"m:{tag}", ns), build.find(tag)]: + if elem is not None and elem.text: + custom_dir = project_root / elem.text.strip() + if custom_dir.exists() and custom_dir not in roots_list: + roots_list.append(custom_dir) + + sub_sources, sub_tests = _discover_maven_submodule_roots(project_root, root, ns) + for root_path in sub_sources: + if root_path not in source_roots: + source_roots.append(root_path) + for root_path in sub_tests: + if root_path not in test_roots: + test_roots.append(root_path) + + return JavaProjectInfo( + project_root=project_root, + build_tool=BuildTool.MAVEN, + source_roots=source_roots, + test_roots=test_roots, + target_dir=project_root / "target", + group_id=group_id, + artifact_id=artifact_id, + version=version, + java_version=java_version, + ) + + except ET.ParseError as e: + logger.warning("Failed to parse pom.xml: %s", e) + return None + def find_executable(self, build_root: Path) -> str | None: mvnw_path = build_root / "mvnw" if mvnw_path.exists(): @@ -424,6 +692,7 @@ def ensure_runtime(self, build_root: Path, test_module: str | None) -> bool: if not add_codeflash_dependency(pom_path): logger.error("Failed to add codeflash-runtime dependency to %s", pom_path) return False + inject_validation_skip_properties(pom_path) else: logger.warning("pom.xml not found at %s, cannot add codeflash-runtime dependency", pom_path) return False diff --git a/codeflash/optimization/optimizer.py b/codeflash/optimization/optimizer.py index 8e9c08ac2..e36a3dda1 100644 --- a/codeflash/optimization/optimizer.py +++ b/codeflash/optimization/optimizer.py @@ -503,6 +503,16 @@ def run(self) -> None: cleanup_paths(Optimizer.find_leftover_instrumented_test_files(self.test_cfg.tests_root)) + # For multi-module Java projects, generated test files are placed in module-specific + # test dirs (e.g. spring-ai-core/src/test/java/...) which may differ from tests_root. + # Maven's test-compile phase compiles ALL .java files in src/test/java/, so leftover + # instrumented files from previous runs poison the build. Search from project root. + if self.args.project_root.resolve() != self.test_cfg.tests_root.resolve(): + java_leftovers = Optimizer.find_leftover_java_test_files(self.args.project_root) + if java_leftovers: + logger.debug(f"Cleaning up {len(java_leftovers)} leftover Java test file(s) from submodules") + cleanup_paths(java_leftovers) + function_optimizer = None file_to_funcs_to_optimize, num_optimizable_functions, trace_file_path = self.get_optimizable_functions() @@ -736,6 +746,18 @@ def find_leftover_instrumented_test_files(test_root: Path) -> list[Path]: file_path for file_path in test_root.rglob("*") if file_path.is_file() and pattern.match(file_path.name) ] + @staticmethod + def find_leftover_java_test_files(project_root: Path) -> list[Path]: + """Search all directories under project_root for leftover instrumented Java test files. + + Uses targeted glob patterns (much faster than rglob('*') + regex on large project roots) + to find instrumented test files that may have been left behind in submodule test directories. + """ + results: list[Path] = [] + for pattern in ("*__perfinstrumented*.java", "*__perfonlyinstrumented*.java"): + results.extend(f for f in project_root.rglob(pattern) if f.is_file()) + return results + def cleanup_replay_tests(self) -> None: paths_to_cleanup = [] if self.replay_tests_dir and self.replay_tests_dir.exists(): diff --git a/codeflash/setup/config_writer.py b/codeflash/setup/config_writer.py index 0889690d5..b0598dcb0 100644 --- a/codeflash/setup/config_writer.py +++ b/codeflash/setup/config_writer.py @@ -38,7 +38,7 @@ def write_config(detected: DetectedProject, config: CodeflashConfig | None = Non if detected.language == "python": return _write_pyproject_toml(detected.project_root, config) if detected.language == "java": - return _write_codeflash_toml(detected.project_root, config) + return _write_java_build_config(detected.project_root, config) return _write_package_json(detected.project_root, config) @@ -92,10 +92,10 @@ def _write_pyproject_toml(project_root: Path, config: CodeflashConfig) -> tuple[ return False, f"Failed to write pyproject.toml: {e}" -def _write_codeflash_toml(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: - """Write config to codeflash.toml [tool.codeflash] section for Java projects. +def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: + """Write codeflash config to pom.xml properties or gradle.properties. - Creates codeflash.toml if it doesn't exist. + Only writes non-default values. Standard Maven/Gradle layouts need no config. Args: project_root: Project root directory. @@ -105,40 +105,23 @@ def _write_codeflash_toml(project_root: Path, config: CodeflashConfig) -> tuple[ Tuple of (success, message). """ - codeflash_toml_path = project_root / "codeflash.toml" + from codeflash.languages.java.build_config_strategy import get_config_strategy - try: - # Load existing or create new - if codeflash_toml_path.exists(): - with codeflash_toml_path.open("rb") as f: - doc = tomlkit.parse(f.read()) - else: - doc = tomlkit.document() - - # Ensure [tool] section exists - if "tool" not in doc: - doc["tool"] = tomlkit.table() - - # Create codeflash section - codeflash_table = tomlkit.table() - codeflash_table.add(tomlkit.comment("Codeflash configuration for Java - https://docs.codeflash.ai")) - - # Add config values - config_dict = config.to_pyproject_dict() - for key, value in config_dict.items(): - codeflash_table[key] = value - - # Update the document - doc["tool"]["codeflash"] = codeflash_table + config_dict = config.to_pyproject_dict() - # Write back - with codeflash_toml_path.open("w", encoding="utf8") as f: - f.write(tomlkit.dumps(doc)) + # Filter out default values — only write overrides + defaults = {"module-root": "src/main/java", "tests-root": "src/test/java", "language": "java"} + non_default = {k: v for k, v in config_dict.items() if k not in defaults or str(v) != defaults.get(k)} + non_default = {k: v for k, v in non_default.items() if v not in ([], False, "", None)} - return True, f"Config saved to {codeflash_toml_path}" + if not non_default: + return True, "Standard Maven/Gradle layout detected \u2014 no config needed" - except Exception as e: - return False, f"Failed to write codeflash.toml: {e}" + try: + strategy = get_config_strategy(project_root) + return strategy.write_codeflash_properties(project_root, non_default) + except ValueError as e: + return False, str(e) def _write_package_json(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: @@ -206,7 +189,7 @@ def remove_config(project_root: Path, language: str) -> tuple[bool, str]: if language == "python": return _remove_from_pyproject(project_root) if language == "java": - return _remove_from_codeflash_toml(project_root) + return _remove_java_build_config(project_root) return _remove_from_package_json(project_root) @@ -235,29 +218,15 @@ def _remove_from_pyproject(project_root: Path) -> tuple[bool, str]: return False, f"Failed to remove config: {e}" -def _remove_from_codeflash_toml(project_root: Path) -> tuple[bool, str]: - """Remove [tool.codeflash] section from codeflash.toml.""" - codeflash_toml_path = project_root / "codeflash.toml" - - if not codeflash_toml_path.exists(): - return True, "No codeflash.toml found" +def _remove_java_build_config(project_root: Path) -> tuple[bool, str]: + """Remove codeflash.* properties from pom.xml or gradle.properties.""" + from codeflash.languages.java.build_config_strategy import get_config_strategy try: - with codeflash_toml_path.open("rb") as f: - doc = tomlkit.parse(f.read()) - - if "tool" in doc and "codeflash" in doc["tool"]: - del doc["tool"]["codeflash"] - - with codeflash_toml_path.open("w", encoding="utf8") as f: - f.write(tomlkit.dumps(doc)) - - return True, "Removed [tool.codeflash] section from codeflash.toml" - - return True, "No codeflash config found in codeflash.toml" - - except Exception as e: - return False, f"Failed to remove config: {e}" + strategy = get_config_strategy(project_root) + return strategy.remove_codeflash_properties(project_root) + except ValueError: + return True, "No Java build config found" def _remove_from_package_json(project_root: Path) -> tuple[bool, str]: diff --git a/codeflash/setup/detector.py b/codeflash/setup/detector.py index defe1a22d..216dd669d 100644 --- a/codeflash/setup/detector.py +++ b/codeflash/setup/detector.py @@ -886,20 +886,24 @@ def has_existing_config(project_root: Path) -> tuple[bool, str | None]: Returns: Tuple of (has_config, config_file_type). - config_file_type is "pyproject.toml", "codeflash.toml", "package.json", or None. + config_file_type is "pyproject.toml", "pom.xml", "build.gradle", "package.json", or None. """ - # Check TOML config files (pyproject.toml, codeflash.toml) - for toml_filename in ("pyproject.toml", "codeflash.toml"): - toml_path = project_root / toml_filename - if toml_path.exists(): - try: - with toml_path.open("rb") as f: - data = tomlkit.parse(f.read()) - if "tool" in data and "codeflash" in data["tool"]: - return True, toml_filename - except Exception: - pass + # Check pyproject.toml (Python projects) + pyproject_path = project_root / "pyproject.toml" + if pyproject_path.exists(): + try: + with pyproject_path.open("rb") as f: + data = tomlkit.parse(f.read()) + if "tool" in data and "codeflash" in data["tool"]: + return True, "pyproject.toml" + except Exception: + pass + + # Check Java build files — zero-config: build file presence means "configured" + for build_file in ("pom.xml", "build.gradle", "build.gradle.kts"): + if (project_root / build_file).exists(): + return True, build_file # Check package.json package_json_path = project_root / "package.json" diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 84f58e9da..26dbb25c2 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -66,7 +66,19 @@ def _detect_non_python_language(args: Namespace | None) -> Language | None: except Exception: pass - # Method 2: Check project config for language field + # Method 2: Check for Java build files (pom.xml, build.gradle, build.gradle.kts) + cwd = Path.cwd() + search = cwd + while search != search.parent: + if ( + (search / "pom.xml").exists() + or (search / "build.gradle").exists() + or (search / "build.gradle.kts").exists() + ): + return Language.JAVA + search = search.parent + + # Method 3: Check project config for language field try: from codeflash.code_utils.config_parser import parse_config_file @@ -90,7 +102,8 @@ def main(args: Namespace | None = None) -> ArgumentParser: # # Detection methods (in priority order): # 1. --file pointing to a .java/.js/.ts file - # 2. language field in project config (codeflash.toml or pyproject.toml) + # 2. Java build files (pom.xml, build.gradle, build.gradle.kts) + # 3. language field in project config (pyproject.toml, package.json) detected_language = _detect_non_python_language(args) if detected_language is not None: from codeflash.languages import Language diff --git a/tests/scripts/end_to_end_test_utilities.py b/tests/scripts/end_to_end_test_utilities.py index 12259b339..321f6e673 100644 --- a/tests/scripts/end_to_end_test_utilities.py +++ b/tests/scripts/end_to_end_test_utilities.py @@ -149,8 +149,10 @@ def build_command( if config.function_name: base_command.extend(["--function", config.function_name]) - # Check if config exists (pyproject.toml or codeflash.toml) - if so, don't override it - has_codeflash_config = (cwd / "codeflash.toml").exists() + # Check if config exists (pyproject.toml, pom.xml, build.gradle) - if so, don't override it + has_codeflash_config = ( + (cwd / "pom.xml").exists() or (cwd / "build.gradle").exists() or (cwd / "build.gradle.kts").exists() + ) if not has_codeflash_config: pyproject_path = cwd / "pyproject.toml" if pyproject_path.exists(): diff --git a/tests/test_languages/fixtures/java_maven/codeflash.toml b/tests/test_languages/fixtures/java_maven/codeflash.toml deleted file mode 100644 index ecd20a562..000000000 --- a/tests/test_languages/fixtures/java_maven/codeflash.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Codeflash configuration for Java project - -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" diff --git a/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml b/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml deleted file mode 100644 index a501ef8cb..000000000 --- a/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Codeflash configuration for Java project - -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" -language = "java" 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) == {} diff --git a/tests/test_languages/test_java/test_build_config_strategy.py b/tests/test_languages/test_java/test_build_config_strategy.py new file mode 100644 index 000000000..15effd60b --- /dev/null +++ b/tests/test_languages/test_java/test_build_config_strategy.py @@ -0,0 +1,440 @@ +"""Tests for BuildConfigStrategy — Maven (lxml) and Gradle config read/write/remove.""" + +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, +) + + +# --------------------------------------------------------------------------- +# MavenConfigStrategy — read +# --------------------------------------------------------------------------- + + +class TestMavenRead: + def test_reads_codeflash_properties_with_namespace(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + '\n' + '\n' + " \n" + " 17\n" + " custom/src\n" + " custom/test\n" + " \n" + "\n", + encoding="utf-8", + ) + result = MavenConfigStrategy().read_codeflash_properties(tmp_path) + assert result == {"moduleRoot": "custom/src", "testsRoot": "custom/test"} + + def test_reads_codeflash_properties_without_namespace(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" + " \n" + " upstream\n" + " \n" + "\n", + encoding="utf-8", + ) + result = MavenConfigStrategy().read_codeflash_properties(tmp_path) + assert result == {"gitRemote": "upstream"} + + def test_returns_empty_when_no_properties(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text("\n", encoding="utf-8") + assert MavenConfigStrategy().read_codeflash_properties(tmp_path) == {} + + def test_returns_empty_when_no_pom(self, tmp_path: Path) -> None: + assert MavenConfigStrategy().read_codeflash_properties(tmp_path) == {} + + def test_ignores_non_codeflash_properties(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" + " \n" + " 17\n" + " src\n" + " \n" + "\n", + encoding="utf-8", + ) + result = MavenConfigStrategy().read_codeflash_properties(tmp_path) + assert "maven.compiler.source" not in result + assert result == {"moduleRoot": "src"} + + +# --------------------------------------------------------------------------- +# MavenConfigStrategy — write +# --------------------------------------------------------------------------- + + +class TestMavenWrite: + def test_preserves_comments(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + '\n' + "\n" + " \n" + " \n" + " 17\n" + " \n" + "\n", + encoding="utf-8", + ) + ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src/main/java"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "" in result + assert "codeflash.moduleRoot" in result + + def test_preserves_namespace(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + '\n' + '\n' + " \n" + " 17\n" + " \n" + "\n", + encoding="utf-8", + ) + ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src/main/java"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert 'xmlns="http://maven.apache.org/POM/4.0.0"' in result + # Must NOT have ns0: prefix (ElementTree bug — lxml avoids this) + assert "ns0:" not in result + + def test_preserves_existing_properties(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" + " \n" + " 17\n" + " \n" + "\n", + encoding="utf-8", + ) + ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "17" in result + assert "codeflash.moduleRoot" in result + + def test_updates_existing_codeflash_properties(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" + " \n" + " old/path\n" + " \n" + "\n", + encoding="utf-8", + ) + ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "new/path"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "old/path" not in result + assert "new/path" in result + + def test_creates_properties_section(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text("\n 4.0.0\n\n", encoding="utf-8") + + ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src/main/java"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "properties" in result + assert "codeflash.moduleRoot" in result + + def test_converts_kebab_to_camelcase(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text("\n \n \n\n", encoding="utf-8") + + ok, _ = MavenConfigStrategy().write_codeflash_properties( + tmp_path, {"ignore-paths": ["target", "build"]} + ) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "target,build" in result + + def test_handles_boolean_values(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text("\n \n \n\n", encoding="utf-8") + + ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"disable-telemetry": True}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "true" in result + + def test_returns_error_when_no_pom(self, tmp_path: Path) -> None: + ok, msg = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src"}) + assert not ok + assert "No pom.xml" in msg + + +# --------------------------------------------------------------------------- +# MavenConfigStrategy — remove +# --------------------------------------------------------------------------- + + +class TestMavenRemove: + def test_removes_only_codeflash_properties(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" + " \n" + " \n" + " 17\n" + " src/main/java\n" + " \n" + "\n", + encoding="utf-8", + ) + ok, _ = MavenConfigStrategy().remove_codeflash_properties(tmp_path) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "" in result + assert "17" in result + assert "codeflash.moduleRoot" not in result + + def test_preserves_comments_after_removal(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + '\n' + "\n" + " \n" + " \n" + " \n" + " src\n" + " \n" + "\n", + encoding="utf-8", + ) + ok, _ = MavenConfigStrategy().remove_codeflash_properties(tmp_path) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "" in result + assert "" in result + assert "codeflash" not in result + + def test_noop_when_no_codeflash_properties(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n \n bar\n \n\n", + encoding="utf-8", + ) + ok, _ = MavenConfigStrategy().remove_codeflash_properties(tmp_path) + assert ok + + +# --------------------------------------------------------------------------- +# MavenConfigStrategy — roundtrip +# --------------------------------------------------------------------------- + + +class TestMavenRoundtrip: + def test_write_then_read_roundtrip(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n \n \n\n", + encoding="utf-8", + ) + strategy = MavenConfigStrategy() + strategy.write_codeflash_properties( + tmp_path, + {"module-root": "client/src", "git-remote": "upstream", "disable-telemetry": True}, + ) + result = strategy.read_codeflash_properties(tmp_path) + assert result["moduleRoot"] == "client/src" + assert result["gitRemote"] == "upstream" + assert result["disableTelemetry"] == "true" + + +# --------------------------------------------------------------------------- +# GradleConfigStrategy +# --------------------------------------------------------------------------- + + +class TestGradleRead: + def test_reads_gradle_properties(self, tmp_path: Path) -> None: + (tmp_path / "build.gradle").write_text("", encoding="utf-8") + (tmp_path / "gradle.properties").write_text( + "org.gradle.jvmargs=-Xmx2g\ncodeflash.moduleRoot=lib/src\ncodeflash.disableTelemetry=true\n", + encoding="utf-8", + ) + result = GradleConfigStrategy().read_codeflash_properties(tmp_path) + assert result == {"moduleRoot": "lib/src", "disableTelemetry": "true"} + + def test_ignores_non_codeflash(self, tmp_path: Path) -> None: + (tmp_path / "gradle.properties").write_text( + "org.gradle.jvmargs=-Xmx2g\ncodeflash.gitRemote=upstream\n", + encoding="utf-8", + ) + result = GradleConfigStrategy().read_codeflash_properties(tmp_path) + assert "org.gradle.jvmargs" not in result + assert result == {"gitRemote": "upstream"} + + def test_returns_empty_when_no_file(self, tmp_path: Path) -> None: + assert GradleConfigStrategy().read_codeflash_properties(tmp_path) == {} + + +class TestGradleWrite: + def test_writes_gradle_properties(self, tmp_path: Path) -> None: + (tmp_path / "gradle.properties").write_text("org.gradle.jvmargs=-Xmx2g\n", encoding="utf-8") + ok, _ = GradleConfigStrategy().write_codeflash_properties( + tmp_path, {"module-root": "lib/src", "disable-telemetry": True} + ) + result = (tmp_path / "gradle.properties").read_text(encoding="utf-8") + + assert ok + assert "org.gradle.jvmargs=-Xmx2g" in result + assert "codeflash.moduleRoot=lib/src" in result + assert "codeflash.disableTelemetry=true" in result + + def test_creates_file_if_missing(self, tmp_path: Path) -> None: + ok, _ = GradleConfigStrategy().write_codeflash_properties(tmp_path, {"git-remote": "upstream"}) + result = (tmp_path / "gradle.properties").read_text(encoding="utf-8") + + assert ok + assert "codeflash.gitRemote=upstream" in result + + def test_updates_existing_codeflash_properties(self, tmp_path: Path) -> None: + (tmp_path / "gradle.properties").write_text( + "codeflash.moduleRoot=old\ncodeflash.gitRemote=origin\n", + encoding="utf-8", + ) + ok, _ = GradleConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "new"}) + result = (tmp_path / "gradle.properties").read_text(encoding="utf-8") + + assert ok + assert "codeflash.moduleRoot=new" in result + assert "old" not in result + + +class TestGradleRemove: + def test_removes_codeflash_from_gradle_properties(self, tmp_path: Path) -> None: + (tmp_path / "gradle.properties").write_text( + "org.gradle.jvmargs=-Xmx2g\n" + "# Codeflash configuration \u2014 https://docs.codeflash.ai\n" + "codeflash.moduleRoot=src/main/java\n", + encoding="utf-8", + ) + ok, _ = GradleConfigStrategy().remove_codeflash_properties(tmp_path) + result = (tmp_path / "gradle.properties").read_text(encoding="utf-8") + + assert ok + assert "org.gradle.jvmargs=-Xmx2g" in result + assert "codeflash." not in result + + def test_noop_when_no_file(self, tmp_path: Path) -> None: + ok, _ = GradleConfigStrategy().remove_codeflash_properties(tmp_path) + assert ok + + +class TestGradleRoundtrip: + def test_write_then_read_roundtrip(self, tmp_path: Path) -> None: + strategy = GradleConfigStrategy() + strategy.write_codeflash_properties(tmp_path, {"module-root": "lib/src", "git-remote": "upstream"}) + result = strategy.read_codeflash_properties(tmp_path) + assert result["moduleRoot"] == "lib/src" + assert result["gitRemote"] == "upstream" + + +# --------------------------------------------------------------------------- +# Factory +# --------------------------------------------------------------------------- + + +class TestGetConfigStrategy: + def test_returns_maven_for_pom(self, tmp_path: Path) -> None: + (tmp_path / "pom.xml").write_text("", encoding="utf-8") + assert isinstance(get_config_strategy(tmp_path), MavenConfigStrategy) + + def test_returns_gradle_for_build_gradle(self, tmp_path: Path) -> None: + (tmp_path / "build.gradle").write_text("", encoding="utf-8") + assert isinstance(get_config_strategy(tmp_path), GradleConfigStrategy) + + def test_raises_for_unknown(self, tmp_path: Path) -> None: + with pytest.raises(ValueError, match="No supported Java build tool"): + get_config_strategy(tmp_path) + + +# --------------------------------------------------------------------------- +# parse_java_project_config +# --------------------------------------------------------------------------- + + +class TestParseJavaProjectConfig: + def test_standard_maven_project(self, tmp_path: Path) -> None: + (tmp_path / "pom.xml").write_text("", encoding="utf-8") + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + test = tmp_path / "src" / "test" / "java" + test.mkdir(parents=True) + + config = parse_java_project_config(tmp_path) + assert config is not None + assert config["language"] == "java" + assert config["module_root"] == str(src) + assert config["tests_root"] == str(test) + + def test_standard_gradle_project(self, tmp_path: Path) -> None: + (tmp_path / "build.gradle").write_text("", encoding="utf-8") + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + test = tmp_path / "src" / "test" / "java" + test.mkdir(parents=True) + + config = parse_java_project_config(tmp_path) + assert config is not None + assert config["language"] == "java" + + def test_returns_none_for_non_java(self, tmp_path: Path) -> None: + assert parse_java_project_config(tmp_path) is None + + def test_maven_with_custom_properties(self, tmp_path: Path) -> None: + (tmp_path / "pom.xml").write_text( + '\n' + " \n" + " custom/src\n" + " custom/test\n" + " true\n" + " \n" + "\n", + encoding="utf-8", + ) + (tmp_path / "custom" / "src").mkdir(parents=True) + (tmp_path / "custom" / "test").mkdir(parents=True) + + config = parse_java_project_config(tmp_path) + assert config is not None + assert config["module_root"] == str((tmp_path / "custom" / "src").resolve()) + assert config["tests_root"] == str((tmp_path / "custom" / "test").resolve()) + assert config["disable_telemetry"] is True + + def test_defaults_when_dirs_missing(self, tmp_path: Path) -> None: + (tmp_path / "pom.xml").write_text("", encoding="utf-8") + config = parse_java_project_config(tmp_path) + assert config is not None + # When src/main/java doesn't exist, should fall back to project root + assert config["module_root"] == str(tmp_path)