diff --git a/codeflash-java-runtime/build.gradle.kts b/codeflash-java-runtime/build.gradle.kts index 69647fc35..524d8944e 100644 --- a/codeflash-java-runtime/build.gradle.kts +++ b/codeflash-java-runtime/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { implementation("org.xerial:sqlite-jdbc:3.45.0.0") implementation("org.ow2.asm:asm:9.7.1") implementation("org.ow2.asm:asm-commons:9.7.1") + implementation("org.jacoco:org.jacoco.agent:0.8.13:runtime") + implementation("org.jacoco:org.jacoco.cli:0.8.13:nodeps") testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.jar b/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..b498d2444 Binary files /dev/null and b/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.jar differ diff --git a/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.properties b/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..d6e308a63 --- /dev/null +++ b/codeflash-java-runtime/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/codeflash-java-runtime/gradlew b/codeflash-java-runtime/gradlew new file mode 100755 index 000000000..17a91706f --- /dev/null +++ b/codeflash-java-runtime/gradlew @@ -0,0 +1,176 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +if $JAVACMD --add-opens java.base/java.lang=ALL-UNNAMED -version ; then + DEFAULT_JVM_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED $DEFAULT_JVM_OPTS" +fi + +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/codeflash-java-runtime/gradlew.bat b/codeflash-java-runtime/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/codeflash-java-runtime/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/codeflash/languages/java/maven_strategy.py b/codeflash/languages/java/maven_strategy.py index 2b5ca1f35..1b398c1eb 100644 --- a/codeflash/languages/java/maven_strategy.py +++ b/codeflash/languages/java/maven_strategy.py @@ -27,8 +27,11 @@ logger = logging.getLogger(__name__) _MAVEN_VALIDATION_SKIP_FLAGS = [ "-Drat.skip=true", "-Dcheckstyle.skip=true", + "-Ddisable.checks=true", "-Dcheckstyle.failOnViolation=false", "-Dcheckstyle.failsOnError=false", + "-Dmaven-checkstyle-plugin.failsOnError=false", + "-Dmaven-checkstyle-plugin.failOnViolation=false", "-Dspotbugs.skip=true", "-Dpmd.skip=true", "-Denforcer.skip=true", @@ -154,12 +157,10 @@ def install_codeflash_runtime(project_root: Path, runtime_jar_path: Path, mvn: s return False -_VALIDATION_SKIP_PROPERTIES = [ +# Properties set to "true" to enable skipping +_VALIDATION_SKIP_PROPERTIES_TRUE = [ "checkstyle.skip", - "checkstyle.failOnViolation", - "checkstyle.failsOnError", - "maven-checkstyle-plugin.failsOnError", - "maven-checkstyle-plugin.failOnViolation", + "disable.checks", "spotbugs.skip", "pmd.skip", "rat.skip", @@ -167,6 +168,14 @@ _VALIDATION_SKIP_PROPERTIES = [ "japicmp.skip", ] +# Properties set to "false" to disable failure on violations +_VALIDATION_SKIP_PROPERTIES_FALSE = [ + "checkstyle.failOnViolation", + "checkstyle.failsOnError", + "maven-checkstyle-plugin.failsOnError", + "maven-checkstyle-plugin.failOnViolation", +] + # Plugin overrides that explicitly set true in the plugin . # This handles parent POMs with custom execution IDs that ignore skip properties. _VALIDATION_PLUGIN_OVERRIDES = """\ @@ -209,7 +218,8 @@ def inject_validation_skip_properties(pom_path: Path) -> bool: if "" in content: return True - props_lines = "".join(f" <{p}>true\n" for p in _VALIDATION_SKIP_PROPERTIES) + 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("") 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) == {}