feat: add JaCoCo deps, fix checkstyle skip properties, and add auto-config integration tests

Add JaCoCo runtime and CLI dependencies to Gradle build. Split Maven validation
skip properties into true/false groups so failOnViolation flags are set to false
instead of true. Add Gradle wrapper and integration tests for Java auto-config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-03-26 07:31:53 +00:00
parent 10f50b391c
commit d2e99e2f51
7 changed files with 1028 additions and 6 deletions

View file

@ -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")

Binary file not shown.

View file

@ -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

176
codeflash-java-runtime/gradlew vendored Executable file
View file

@ -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" "$@"

84
codeflash-java-runtime/gradlew.bat vendored Normal file
View file

@ -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

View file

@ -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 <skip>true</skip> in the plugin <configuration>.
# 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 "<!-- codeflash-validation-skip -->" in content:
return True
props_lines = "".join(f" <{p}>true</{p}>\n" for p in _VALIDATION_SKIP_PROPERTIES)
props_lines = "".join(f" <{p}>true</{p}>\n" for p in _VALIDATION_SKIP_PROPERTIES_TRUE)
props_lines += "".join(f" <{p}>false</{p}>\n" for p in _VALIDATION_SKIP_PROPERTIES_FALSE)
# 1. Inject properties
closing_idx = content.find("</properties>")

View file

@ -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'<?xml version="1.0" encoding="UTF-8"?>\n'
f"<project{ns}>\n"
f" <modelVersion>4.0.0</modelVersion>\n"
f" <groupId>com.example</groupId>\n"
f" <artifactId>demo-app</artifactId>\n"
f" <version>1.0.0</version>\n"
f" <properties>\n"
f" <maven.compiler.source>{java_version}</maven.compiler.source>\n"
f" <maven.compiler.target>{java_version}</maven.compiler.target>\n"
f" </properties>\n"
f"</project>\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(
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
" <modelVersion>4.0.0</modelVersion>\n"
" <groupId>com.example</groupId>\n"
" <artifactId>parent</artifactId>\n"
" <version>1.0.0</version>\n"
" <packaging>pom</packaging>\n"
" <modules>\n"
" <module>core</module>\n"
" <module>api</module>\n"
" <module>tests</module>\n"
" </modules>\n"
"</project>\n",
encoding="utf-8",
)
# core module — main source
core = root / "core"
core.mkdir()
(core / "pom.xml").write_text(
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
" <modelVersion>4.0.0</modelVersion>\n"
" <parent>\n"
" <groupId>com.example</groupId>\n"
" <artifactId>parent</artifactId>\n"
" <version>1.0.0</version>\n"
" </parent>\n"
" <artifactId>core</artifactId>\n"
"</project>\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(
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
" <modelVersion>4.0.0</modelVersion>\n"
" <parent>\n"
" <groupId>com.example</groupId>\n"
" <artifactId>parent</artifactId>\n"
" <version>1.0.0</version>\n"
" </parent>\n"
" <artifactId>api</artifactId>\n"
"</project>\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(
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
" <modelVersion>4.0.0</modelVersion>\n"
" <parent>\n"
" <groupId>com.example</groupId>\n"
" <artifactId>parent</artifactId>\n"
" <version>1.0.0</version>\n"
" </parent>\n"
" <artifactId>tests</artifactId>\n"
"</project>\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 "<module>core</module>" in pom_text
assert "<module>api</module>" 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(
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
" <modelVersion>4.0.0</modelVersion>\n"
" <parent>\n"
" <groupId>com.example</groupId>\n"
" <artifactId>parent</artifactId>\n"
" <version>1.0.0</version>\n"
" </parent>\n"
" <artifactId>core</artifactId>\n"
" <build>\n"
" <sourceDirectory>src/main/custom</sourceDirectory>\n"
" </build>\n"
"</project>\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) == {}