feat: eliminate codeflash.toml — auto-detect Java config from build files

Java projects no longer need a standalone config file. Codeflash reads
config from pom.xml <properties> or gradle.properties, and auto-detects
source/test roots from build tool conventions.

Changes:
- Add parse_java_project_config() to read codeflash.* properties from
  pom.xml and gradle.properties
- Add multi-module Maven scanning: parses each module's pom.xml for
  <sourceDirectory> and <testSourceDirectory>, picks module with most
  Java files as source root, identifies test modules by name
- Route Java projects through build-file detection in config_parser.py
  before falling back to pyproject.toml
- Detect Java language from pom.xml/build.gradle presence (no config needed)
- Fix project_root for multi-module projects (was resolving to sub-module)
- Fix JFR parser / separators (JVM uses com/example, normalized to com.example)
- Fix graceful timeout (SIGTERM before SIGKILL for JFR dump + shutdown hooks)
- Remove isRecording() check from TracingTransformer (was preventing class
  instrumentation for classes loaded during serialization)
- Delete all codeflash.toml files from fixtures and code_to_optimize
- Add 33 config detection tests
- Update docs for zero-config Java setup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
misrasaurabh1 2026-03-19 19:11:06 -07:00
parent 59031a145e
commit 3f95ff604a
19 changed files with 1078 additions and 259 deletions

View file

@ -1,4 +0,0 @@
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
formatter-cmds = []

View file

@ -1,6 +0,0 @@
# Codeflash configuration for Java project
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
formatter-cmds = []

View file

@ -22,11 +22,6 @@ public class TracingTransformer implements ClassFileTransformer {
return null;
}
// Skip instrumentation if we're inside a recording call (e.g., during Kryo serialization)
if (TraceRecorder.isRecording()) {
return null;
}
// Skip internal JDK, framework, and synthetic classes
if (className.startsWith("java/")
|| className.startsWith("javax/")

View file

@ -185,11 +185,16 @@ 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)
if is_java_project and pyproject_file_path.is_dir():
# For Java projects, pyproject_file_path IS the project root directory (not a file)
args.project_root = pyproject_file_path.resolve()
args.test_project_root = pyproject_file_path.resolve()
else:
args.project_root = project_root_from_module_root(args.module_root, pyproject_file_path)
args.test_project_root = project_root_from_module_root(args.tests_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_LSP_enabled():
args.all = None
return args
@ -208,8 +213,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()
@ -370,7 +373,7 @@ def _build_parser() -> ArgumentParser:
subparsers.add_parser("vscode-install", help="Install the Codeflash VSCode extension")
subparsers.add_parser("init-actions", help="Initialize GitHub Actions workflow")
trace_optimize = subparsers.add_parser("optimize", help="Trace and optimize your project.")
trace_optimize = subparsers.add_parser("optimize", help="Trace and optimize your project.", add_help=False)
trace_optimize.add_argument(
"--max-function-count",

View file

@ -12,8 +12,29 @@ PYPROJECT_TOML_CACHE: dict[Path, Path] = {}
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_tools 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,29 @@ 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]:
# Java projects: read config from pom.xml/gradle.properties (no standalone config file needed)
if config_file_path is None:
java_config = _try_parse_java_build_config()
if java_config is not None:
config, project_root = java_config
return config, project_root
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
# 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 +169,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"]

View file

@ -554,11 +554,13 @@ def get_all_replay_test_functions(
def _get_java_replay_test_functions(
replay_test: list[Path], test_cfg: TestConfig, project_root_path: Path
replay_test: list[Path], test_cfg: TestConfig, project_root_path: Path | str
) -> tuple[dict[Path, list[FunctionToOptimize]], Path]:
"""Parse Java replay test files to extract functions and trace file path."""
from codeflash.languages.java.replay_test import parse_replay_test_metadata
project_root_path = Path(project_root_path)
trace_file_path: Path | None = None
functions: dict[Path, list[FunctionToOptimize]] = defaultdict(list)

View file

@ -10,7 +10,8 @@ import logging
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from enum import Enum
from pathlib import Path # noqa: TC003 — used at runtime
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
@ -343,6 +344,218 @@ def _parse_surefire_reports(surefire_dir: Path) -> tuple[int, int, int, int]:
return tests_run, failures, errors, skipped
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 <properties> or gradle.properties,
then fills in defaults from auto-detected build tool conventions.
Returns None if no Java build tool is detected.
"""
build_tool = detect_build_tool(project_root)
if build_tool == BuildTool.UNKNOWN:
return None
# Read explicit codeflash properties from build files
user_config: dict[str, str] = {}
if build_tool == BuildTool.MAVEN:
user_config = _read_maven_codeflash_properties(project_root)
elif build_tool == BuildTool.GRADLE:
user_config = _read_gradle_codeflash_properties(project_root)
# Auto-detect defaults — for multi-module Maven projects, scan module pom.xml files
source_root = find_source_root(project_root)
test_root = find_test_root(project_root)
if build_tool == BuildTool.MAVEN:
source_from_modules, test_from_modules = _detect_roots_from_maven_modules(project_root)
# Module-level pom.xml declarations are more precise than directory-name heuristics
if source_from_modules is not None:
source_root = source_from_modules
if test_from_modules is not None:
test_root = test_from_modules
# Build the config dict matching the format expected by the rest of codeflash
config: dict[str, Any] = {
"language": "java",
"module_root": str(
(project_root / user_config["moduleRoot"]).resolve()
if "moduleRoot" in user_config
else (source_root or project_root / "src" / "main" / "java")
),
"tests_root": str(
(project_root / user_config["testsRoot"]).resolve()
if "testsRoot" in user_config
else (test_root or project_root / "src" / "test" / "java")
),
"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 _read_maven_codeflash_properties(project_root: Path) -> dict[str, str]:
"""Read codeflash.* properties from pom.xml <properties> section."""
pom_path = project_root / "pom.xml"
if not pom_path.exists():
return {}
try:
tree = _safe_parse_xml(pom_path)
root = tree.getroot()
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
result: dict[str, str] = {}
for props in [root.find("m:properties", ns), root.find("properties")]:
if props is None:
continue
for child in props:
tag = child.tag
# Strip Maven namespace prefix
if "}" in tag:
tag = tag.split("}", 1)[1]
if tag.startswith("codeflash.") and child.text:
key = tag[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 _read_gradle_codeflash_properties(project_root: Path) -> dict[str, str]:
"""Read codeflash.* properties from gradle.properties."""
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 _detect_roots_from_maven_modules(project_root: Path) -> tuple[Path | None, Path | None]:
"""Scan Maven module pom.xml files for custom sourceDirectory/testSourceDirectory.
For multi-module projects like aerospike (client/, test/, benchmarks/),
finds the main source module and test module by parsing each module's build config.
"""
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": "http://maven.apache.org/POM/4.0.0"}
# Find <modules> to get module names
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
# Collect candidate source and test roots with Java file counts
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
# Modules named "test" are test modules, not source modules
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
# Also check standard module layouts
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
# Pick the source root with the most Java files (likely the main library)
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
def find_test_root(project_root: Path) -> Path | None:
"""Find the test root directory for a Java project.

View file

@ -152,6 +152,8 @@ class JfrProfile:
method_name = method.get("name", "")
if not class_name or not method_name:
return None
# JFR uses / separators (JVM internal format), normalize to dots for package matching
class_name = class_name.replace("/", ".")
return f"{class_name}.{method_name}"
def _store_method_info(self, key: str, frame: dict[str, Any]) -> None:
@ -159,7 +161,7 @@ class JfrProfile:
return
method = frame.get("method", {})
self._method_info[key] = {
"class_name": method.get("type", {}).get("name", ""),
"class_name": method.get("type", {}).get("name", "").replace("/", "."),
"method_name": method.get("name", ""),
"descriptor": method.get("descriptor", ""),
"line_number": str(frame.get("lineNumber", 0)),

View file

@ -14,6 +14,39 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
GRACEFUL_SHUTDOWN_WAIT = 5 # seconds to wait after SIGTERM before SIGKILL
def _run_java_with_graceful_timeout(
java_command: list[str], env: dict[str, str], timeout: int, stage_name: str
) -> None:
"""Run a Java command with graceful timeout handling.
Sends SIGTERM first (allowing JFR dump and shutdown hooks to run),
then SIGKILL if the process doesn't exit within GRACEFUL_SHUTDOWN_WAIT seconds.
"""
if not timeout:
subprocess.run(java_command, env=env, check=False)
return
import signal
proc = subprocess.Popen(java_command, env=env)
try:
proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
logger.warning(
"%s stage timed out after %d seconds, sending SIGTERM for graceful shutdown...", stage_name, timeout
)
proc.send_signal(signal.SIGTERM)
try:
proc.wait(timeout=GRACEFUL_SHUTDOWN_WAIT)
except subprocess.TimeoutExpired:
logger.warning("%s stage did not exit after SIGTERM, sending SIGKILL", stage_name)
proc.kill()
proc.wait()
# --add-opens flags needed for Kryo serialization on Java 16+
ADD_OPENS_FLAGS = (
"--add-opens=java.base/java.util=ALL-UNNAMED "
@ -48,10 +81,7 @@ class JavaTracer:
# Stage 1: JFR Profiling
logger.info("Stage 1: Running JFR profiling...")
jfr_env = self.build_jfr_env(jfr_file)
try:
subprocess.run(java_command, env=jfr_env, check=False, timeout=timeout or None)
except subprocess.TimeoutExpired:
logger.warning("JFR profiling stage timed out after %d seconds", timeout)
_run_java_with_graceful_timeout(java_command, jfr_env, timeout, "JFR profiling")
if not jfr_file.exists():
logger.warning("JFR file was not created at %s", jfr_file)
@ -62,10 +92,7 @@ class JavaTracer:
trace_db_path, packages, project_root=project_root, max_function_count=max_function_count, timeout=timeout
)
agent_env = self.build_agent_env(config_path)
try:
subprocess.run(java_command, env=agent_env, check=False, timeout=timeout or None)
except subprocess.TimeoutExpired:
logger.warning("Argument capture stage timed out after %d seconds", timeout)
_run_java_with_graceful_timeout(java_command, agent_env, timeout, "Argument capture")
if not trace_db_path.exists():
logger.error("Trace database was not created at %s", trace_db_path)

View file

@ -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,110 @@ def _write_codeflash_toml(project_root: Path, config: CodeflashConfig) -> tuple[
Tuple of (success, message).
"""
codeflash_toml_path = project_root / "codeflash.toml"
config_dict = config.to_pyproject_dict()
# 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)}
# Remove empty lists and False booleans
non_default = {k: v for k, v in non_default.items() if v not in ([], False, "", None)}
if not non_default:
return True, "Standard Maven/Gradle layout detected — no config needed"
pom_path = project_root / "pom.xml"
if pom_path.exists():
return _write_maven_properties(pom_path, non_default)
gradle_props_path = project_root / "gradle.properties"
return _write_gradle_properties(gradle_props_path, non_default)
def _write_maven_properties(pom_path: Path, config: dict) -> tuple[bool, str]:
"""Add codeflash.* properties to pom.xml <properties> section."""
import xml.etree.ElementTree as ET
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()
tree = ET.parse(str(pom_path))
root = tree.getroot()
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
# Ensure [tool] section exists
if "tool" not in doc:
doc["tool"] = tomlkit.table()
# Find or create <properties>
properties = root.find("m:properties", ns) or root.find("properties")
if properties is None:
properties = ET.SubElement(root, "properties")
# Create codeflash section
codeflash_table = tomlkit.table()
codeflash_table.add(tomlkit.comment("Codeflash configuration for Java - https://docs.codeflash.ai"))
# Convert kebab-case keys to camelCase for Maven convention
key_map = {
"module-root": "moduleRoot",
"tests-root": "testsRoot",
"git-remote": "gitRemote",
"disable-telemetry": "disableTelemetry",
"ignore-paths": "ignorePaths",
"formatter-cmds": "formatterCmds",
}
# Add config values
config_dict = config.to_pyproject_dict()
for key, value in config_dict.items():
codeflash_table[key] = value
for key, value in config.items():
maven_key = f"codeflash.{key_map.get(key, key)}"
if isinstance(value, list):
value = ",".join(str(v) for v in value)
elif isinstance(value, bool):
value = str(value).lower()
else:
value = str(value)
# Update the document
doc["tool"]["codeflash"] = codeflash_table
existing = properties.find(maven_key)
if existing is None:
elem = ET.SubElement(properties, maven_key)
elem.text = value
else:
existing.text = value
# Write back
with codeflash_toml_path.open("w", encoding="utf8") as f:
f.write(tomlkit.dumps(doc))
return True, f"Config saved to {codeflash_toml_path}"
tree.write(str(pom_path), xml_declaration=True, encoding="UTF-8")
return True, f"Config saved to {pom_path} <properties>"
except Exception as e:
return False, f"Failed to write codeflash.toml: {e}"
return False, f"Failed to write Maven properties: {e}"
def _write_gradle_properties(props_path: Path, config: dict) -> tuple[bool, str]:
"""Add codeflash.* entries to gradle.properties."""
key_map = {
"module-root": "moduleRoot",
"tests-root": "testsRoot",
"git-remote": "gitRemote",
"disable-telemetry": "disableTelemetry",
"ignore-paths": "ignorePaths",
"formatter-cmds": "formatterCmds",
}
try:
lines = []
if props_path.exists():
lines = props_path.read_text(encoding="utf-8").splitlines()
# Remove existing codeflash.* lines
lines = [line for line in lines if not line.strip().startswith("codeflash.")]
# Add new config
if lines and lines[-1].strip():
lines.append("")
lines.append("# Codeflash configuration — https://docs.codeflash.ai")
for key, value in config.items():
gradle_key = f"codeflash.{key_map.get(key, key)}"
if isinstance(value, list):
value = ",".join(str(v) for v in value)
elif isinstance(value, bool):
value = str(value).lower()
else:
value = str(value)
lines.append(f"{gradle_key}={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 _write_package_json(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]:
@ -206,7 +276,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 +305,45 @@ 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"
def _remove_java_build_config(project_root: Path) -> tuple[bool, str]:
"""Remove codeflash.* properties from pom.xml or gradle.properties."""
# Try gradle.properties first (simpler)
gradle_props = project_root / "gradle.properties"
if gradle_props.exists():
try:
lines = gradle_props.read_text(encoding="utf-8").splitlines()
filtered = [
line
for line in lines
if not line.strip().startswith("codeflash.")
and line.strip() != "# Codeflash configuration — https://docs.codeflash.ai"
]
gradle_props.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}"
if not codeflash_toml_path.exists():
return True, "No codeflash.toml found"
# Try pom.xml
pom_path = project_root / "pom.xml"
if pom_path.exists():
try:
import xml.etree.ElementTree as ET
try:
with codeflash_toml_path.open("rb") as f:
doc = tomlkit.parse(f.read())
tree = ET.parse(str(pom_path))
root = tree.getroot()
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
for properties in [root.find("m:properties", ns), root.find("properties")]:
if properties is None:
continue
to_remove = [child for child in properties if child.tag.split("}")[-1].startswith("codeflash.")]
for elem in to_remove:
properties.remove(elem)
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}"
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}"
return True, "No Java build config found"
def _remove_from_package_json(project_root: Path) -> tuple[bool, str]:

View file

@ -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 — Java projects store config in pom.xml properties or gradle.properties
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"

View file

@ -38,7 +38,7 @@ logger = logging.getLogger(__name__)
def _detect_non_python_language(args: Namespace | None) -> Language | None:
"""Detect if the project uses a non-Python language from --file or config.
"""Detect if the project uses a non-Python language from --file or build files.
Returns a Language enum value if non-Python detected, None otherwise.
"""
@ -66,15 +66,23 @@ def _detect_non_python_language(args: Namespace | None) -> Language | None:
except Exception:
pass
# Method 2: Check project config for language field
# Method 2: Detect Java from build files (pom.xml / build.gradle)
try:
from codeflash.languages.java.build_tools import BuildTool, detect_build_tool
cwd = Path.cwd()
if detect_build_tool(cwd) != BuildTool.UNKNOWN:
return Language.JAVA
except Exception:
pass
# Method 3: Check config file for language field (JS/TS via package.json)
try:
from codeflash.code_utils.config_parser import parse_config_file
config_file = getattr(args, "config_file_path", None) if args else None
config, _ = parse_config_file(config_file)
lang_str = config.get("language", "")
if lang_str == "java":
return Language.JAVA
if lang_str in ("javascript", "typescript"):
return Language(lang_str)
except Exception:
@ -336,8 +344,12 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser:
max_function_count = getattr(config, "max_function_count", 256)
timeout = int(getattr(config, "timeout", None) or getattr(config, "tracer_timeout", 0) or 0)
console.print("[bold]Java project detected[/]")
console.print(f" Project root: {project_root}")
console.print(f" Module root: {getattr(config, 'module_root', '?')}")
console.print(f" Tests root: {getattr(config, 'tests_root', '?')}")
from codeflash.code_utils.code_utils import get_run_tmp_file
from codeflash.languages.java.build_tools import find_test_root
from codeflash.languages.java.tracer import JavaTracer, run_java_tracer
tracer = JavaTracer()
@ -347,12 +359,16 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser:
trace_db_path = get_run_tmp_file(Path("java_trace.db"))
# Place replay tests in the project's test source tree so Maven/Gradle can compile them
test_root = find_test_root(project_root)
if test_root:
output_dir = test_root / "codeflash" / "replay"
# Place replay tests in the project's test source tree so Maven/Gradle can compile them.
# Use the config's tests_root (correctly resolved for multi-module projects) not find_test_root().
tests_root = Path(getattr(config, "tests_root", ""))
if tests_root.is_dir():
output_dir = tests_root / "codeflash" / "replay"
else:
output_dir = project_root / "src" / "test" / "java" / "codeflash" / "replay"
from codeflash.languages.java.build_tools import find_test_root
test_root = find_test_root(project_root)
output_dir = (test_root or project_root / "src" / "test" / "java") / "codeflash" / "replay"
output_dir.mkdir(parents=True, exist_ok=True)
# Remaining args after our flags are the Java command

View file

@ -1,101 +1,112 @@
---
title: "Java Configuration"
description: "Configure Codeflash for Java projects using codeflash.toml"
description: "Configure Codeflash for Java projects — zero config for standard layouts"
icon: "java"
sidebarTitle: "Java (codeflash.toml)"
sidebarTitle: "Java (pom.xml / Gradle)"
keywords:
[
"configuration",
"codeflash.toml",
"java",
"maven",
"gradle",
"junit",
"pom.xml",
"gradle.properties",
"zero-config",
]
---
# Java Configuration
Codeflash stores its configuration in `codeflash.toml` under the `[tool.codeflash]` section.
**Standard Maven/Gradle projects need zero configuration.** Codeflash auto-detects your project structure from `pom.xml` or `build.gradle` — no config file is required.
## Full Reference
```toml
[tool.codeflash]
# Required
module-root = "src/main/java"
tests-root = "src/test/java"
language = "java"
# Optional
test-framework = "junit5" # "junit5", "junit4", or "testng"
disable-telemetry = false
git-remote = "origin"
ignore-paths = ["src/main/java/generated/"]
```
All file paths are relative to the directory containing `codeflash.toml`.
<Info>
Codeflash auto-detects most settings from your project structure. Running `codeflash init` will set up the correct config — manual configuration is usually not needed.
</Info>
For projects with non-standard layouts, you can add `codeflash.*` properties to your existing `pom.xml` or `gradle.properties`.
## Auto-Detection
When you run `codeflash init`, Codeflash inspects your project and auto-detects:
Codeflash inspects your build files and auto-detects:
| Setting | Detection logic |
|---------|----------------|
| `module-root` | Looks for `src/main/java` (Maven/Gradle standard layout) |
| `tests-root` | Looks for `src/test/java`, `test/`, `tests/` |
| `language` | Detected from build files (`pom.xml`, `build.gradle`) and `.java` files |
| `test-framework` | Checks build file dependencies for JUnit 5, JUnit 4, or TestNG |
| **Language** | Presence of `pom.xml` or `build.gradle` / `build.gradle.kts` |
| **Source root** | `src/main/java` (standard), or `<sourceDirectory>` in `pom.xml`, or Gradle `sourceSets` |
| **Test root** | `src/test/java` (standard), or `<testSourceDirectory>` in `pom.xml` |
| **Test framework** | Checks build file dependencies for JUnit 5, JUnit 4, or TestNG |
| **Java version** | `<maven.compiler.source>`, `<java.version>` in `pom.xml` |
## Required Options
### Multi-module Maven projects
- **`module-root`**: The source directory to optimize. Only code under this directory is discovered for optimization. For standard Maven/Gradle projects, this is `src/main/java`.
- **`tests-root`**: The directory where your tests are located. Codeflash discovers existing tests and places generated replay tests here.
- **`language`**: Must be set to `"java"` for Java projects.
For multi-module projects, Codeflash scans each module's `pom.xml` for `<sourceDirectory>` and `<testSourceDirectory>` declarations. It picks the module with the most Java source files as the main source root, and identifies test modules by name.
## Optional Options
- **`test-framework`**: Test framework. Auto-detected from build dependencies. Supported values: `"junit5"` (default), `"junit4"`, `"testng"`.
- **`disable-telemetry`**: Disable anonymized telemetry. Defaults to `false`.
- **`git-remote`**: Git remote for pull requests. Defaults to `"origin"`.
- **`ignore-paths`**: Paths within `module-root` to skip during optimization.
## Multi-Module Projects
For multi-module Maven/Gradle projects, place `codeflash.toml` at the project root and set `module-root` to the module you want to optimize:
For example, with this layout:
```text
my-project/
|- client/
| |- src/main/java/com/example/client/
| |- src/test/java/com/example/client/
|- server/
| |- src/main/java/com/example/server/
|- pom.xml
|- codeflash.toml
|- client/ ← main library (most .java files)
| |- src/com/example/
| |- pom.xml ← <sourceDirectory>${project.basedir}/src</sourceDirectory>
|- test/ ← test module
| |- src/com/example/
| |- pom.xml ← <testSourceDirectory>${project.basedir}/src</testSourceDirectory>
|- benchmarks/ ← skipped (benchmark module)
|- pom.xml ← <modules>client, test, benchmarks</modules>
```
```toml
[tool.codeflash]
module-root = "client/src/main/java"
tests-root = "client/src/test/java"
language = "java"
Codeflash auto-detects `client/src` as the source root and `test/src` as the test root — no manual configuration needed.
## Custom Configuration
If auto-detection doesn't match your project layout, add `codeflash.*` properties to your build files.
<Tabs>
<Tab title="Maven (pom.xml)">
Add properties to your `pom.xml` `<properties>` section:
```xml
<properties>
<!-- Only set values that differ from auto-detected defaults -->
<codeflash.moduleRoot>client/src</codeflash.moduleRoot>
<codeflash.testsRoot>test/src</codeflash.testsRoot>
<codeflash.disableTelemetry>true</codeflash.disableTelemetry>
<codeflash.gitRemote>upstream</codeflash.gitRemote>
<codeflash.ignorePaths>src/main/java/generated/,src/main/java/proto/</codeflash.ignorePaths>
</properties>
```
For non-standard layouts (like the Aerospike client where source is under `client/src/`), adjust paths accordingly:
This follows the same pattern as SonarQube (`sonar.sources`), JaCoCo, and other Java tools — config lives in the build file, not a separate tool-specific file.
```toml
[tool.codeflash]
module-root = "client/src"
tests-root = "test/src"
language = "java"
</Tab>
<Tab title="Gradle (gradle.properties)">
Add properties to `gradle.properties`:
```properties
# Only set values that differ from auto-detected defaults
codeflash.moduleRoot=lib/src/main/java
codeflash.testsRoot=lib/src/test/java
codeflash.disableTelemetry=true
codeflash.gitRemote=upstream
codeflash.ignorePaths=src/main/java/generated/
```
## Tracer Options
</Tab>
</Tabs>
## Available Properties
All properties are optional — only set values that differ from auto-detected defaults.
| Property | Description | Default |
|----------|------------|---------|
| `codeflash.moduleRoot` | Source directory to optimize | Auto-detected from `<sourceDirectory>` or `src/main/java` |
| `codeflash.testsRoot` | Test directory | Auto-detected from `<testSourceDirectory>` or `src/test/java` |
| `codeflash.disableTelemetry` | Disable anonymized telemetry | `false` |
| `codeflash.gitRemote` | Git remote for pull requests | `origin` |
| `codeflash.ignorePaths` | Comma-separated paths to skip during optimization | Empty |
| `codeflash.formatterCmds` | Comma-separated formatter commands (`$file` = file path) | Empty |
## Tracer CLI Options
When using `codeflash optimize` to trace a Java program, these CLI options are available:
@ -111,9 +122,9 @@ Example with timeout:
codeflash optimize --timeout 30 java -jar target/my-app.jar --app-args
```
## Example
## Examples
### Standard Maven project
### Standard Maven project (zero config)
```text
my-app/
@ -124,17 +135,14 @@ my-app/
| |- test/java/com/example/
| |- AppTest.java
|- pom.xml
|- codeflash.toml
```
```toml
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
language = "java"
Just run:
```bash
codeflash optimize java -jar target/my-app.jar
```
### Gradle project
### Standard Gradle project (zero config)
```text
my-lib/
@ -142,12 +150,55 @@ my-lib/
| |- main/java/com/example/
| |- test/java/com/example/
|- build.gradle
|- codeflash.toml
```
```toml
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
language = "java"
Just run:
```bash
codeflash optimize java -cp build/classes/java/main com.example.Main
```
### Non-standard layout (with config)
```text
aerospike-client-java/
|- client/
| |- src/com/aerospike/client/ ← source here (not src/main/java)
| |- pom.xml
|- test/
| |- src/com/aerospike/test/ ← tests here
| |- pom.xml
|- pom.xml
```
If auto-detection doesn't pick up the right modules, add to the root `pom.xml`:
```xml
<properties>
<codeflash.moduleRoot>client/src</codeflash.moduleRoot>
<codeflash.testsRoot>test/src</codeflash.testsRoot>
</properties>
```
<Info>
In most cases, even non-standard multi-module layouts are auto-detected correctly from `<sourceDirectory>` and `<testSourceDirectory>` in each module's `pom.xml`. Only add manual config if auto-detection gets it wrong.
</Info>
## FAQ
<AccordionGroup>
<Accordion title="Do I need to run codeflash init for Java projects?">
No. Codeflash auto-detects Java projects from `pom.xml` or `build.gradle`. No initialization step or config file is needed for standard layouts.
</Accordion>
<Accordion title="Where does codeflash store its config for Java?">
Codeflash reads config from your existing build files — `pom.xml` `<properties>` for Maven, `gradle.properties` for Gradle. No separate config file is created.
</Accordion>
<Accordion title="What if codeflash detects the wrong source/test directories?">
Add `<codeflash.moduleRoot>` and `<codeflash.testsRoot>` properties to your `pom.xml` or `gradle.properties`. These override auto-detection.
</Accordion>
<Accordion title="How does codeflash handle multi-module Maven projects?">
Codeflash scans each module's `pom.xml` for `<sourceDirectory>` and `<testSourceDirectory>`. It picks the module with the most Java files as the source root (skipping modules named `examples`, `benchmarks`, etc.) and identifies `test` modules for the test root.
</Accordion>
</AccordionGroup>

View file

@ -12,10 +12,11 @@ keywords:
"junit",
"junit5",
"tracing",
"zero-config",
]
---
Codeflash supports Java projects using Maven or Gradle build systems. It uses a two-stage tracing approach to capture method arguments and profiling data from running Java programs, then optimizes the hottest functions.
Codeflash supports Java projects using Maven or Gradle build systems. **No configuration file is needed** — Codeflash auto-detects your project structure from `pom.xml` or `build.gradle`.
### Prerequisites
@ -23,7 +24,7 @@ Before installing Codeflash, ensure you have:
1. **Java 11 or above** installed
2. **Maven or Gradle** as your build tool
3. **A Java project** with source code under a standard directory layout
3. **A Java project** with source code
Good to have (optional):
@ -44,52 +45,16 @@ Or with uv:
uv pip install codeflash
```
</Step>
<Step title="Initialize your project">
Navigate to your Java project root (where `pom.xml` or `build.gradle` is) and run:
```bash
codeflash init
```
This will:
- Detect your build tool (Maven/Gradle)
- Find your source and test directories
- Create a `codeflash.toml` configuration file
</Step>
<Step title="Verify setup">
Check that the configuration looks correct:
```bash
cat codeflash.toml
```
You should see something like:
```toml
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
language = "java"
```
</Step>
<Step title="Run your first optimization">
Trace and optimize a running Java program:
Navigate to your Java project root (where `pom.xml` or `build.gradle` is) and run:
```bash
codeflash optimize java -jar target/my-app.jar
```
Or with Maven:
```bash
codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main"
```
That's it — no `init` step, no config file. Codeflash detects Maven/Gradle automatically and infers source and test directories from your build files.
Codeflash will:
1. Profile your program using JFR (Java Flight Recorder)
@ -101,6 +66,29 @@ Codeflash will:
</Step>
</Steps>
<Info>
**Zero config for standard projects.** If your project uses the standard Maven/Gradle layout (`src/main/java`, `src/test/java`), everything is auto-detected. For non-standard layouts, see the [configuration guide](/configuration/java).
</Info>
## Usage examples
**Trace and optimize a JAR application:**
```bash
codeflash optimize java -jar target/my-app.jar --app-args
```
**Optimize a specific file and function:**
```bash
codeflash --file src/main/java/com/example/Utils.java --function computeHash
```
**Trace a long-running program with a timeout:**
```bash
codeflash optimize --timeout 30 java -jar target/my-server.jar
```
Each tracing stage runs for at most 30 seconds, then the captured data is processed.
## How it works
Codeflash uses a **two-stage tracing** approach for Java:

View file

@ -149,8 +149,8 @@ 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():

View file

@ -1,5 +0,0 @@
# Codeflash configuration for Java project
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"

View file

@ -1,6 +0,0 @@
# Codeflash configuration for Java project
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
language = "java"

View file

@ -0,0 +1,444 @@
"""Tests for Java project auto-detection from Maven/Gradle build files.
Tests that codeflash can detect Java projects and infer module-root,
tests-root, and other config from pom.xml / build.gradle / gradle.properties
without requiring a standalone codeflash.toml config file.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from codeflash.languages.java.build_tools import (
BuildTool,
detect_build_tool,
find_source_root,
find_test_root,
parse_java_project_config,
)
# ---------------------------------------------------------------------------
# Build tool detection
# ---------------------------------------------------------------------------
class TestDetectBuildTool:
def test_detect_maven(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
assert detect_build_tool(tmp_path) == BuildTool.MAVEN
def test_detect_gradle(self, tmp_path: Path) -> None:
(tmp_path / "build.gradle").write_text("", encoding="utf-8")
assert detect_build_tool(tmp_path) == BuildTool.GRADLE
def test_detect_gradle_kts(self, tmp_path: Path) -> None:
(tmp_path / "build.gradle.kts").write_text("", encoding="utf-8")
assert detect_build_tool(tmp_path) == BuildTool.GRADLE
def test_maven_takes_priority_over_gradle(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
(tmp_path / "build.gradle").write_text("", encoding="utf-8")
assert detect_build_tool(tmp_path) == BuildTool.MAVEN
def test_unknown_when_no_build_file(self, tmp_path: Path) -> None:
assert detect_build_tool(tmp_path) == BuildTool.UNKNOWN
def test_detect_maven_in_parent(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
child = tmp_path / "module"
child.mkdir()
assert detect_build_tool(child) == BuildTool.MAVEN
# ---------------------------------------------------------------------------
# Source / test root detection (standard layouts)
# ---------------------------------------------------------------------------
class TestFindSourceRoot:
def test_standard_maven_layout(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
src = tmp_path / "src" / "main" / "java"
src.mkdir(parents=True)
assert find_source_root(tmp_path) == src
def test_fallback_to_src_with_java_files(self, tmp_path: Path) -> None:
src = tmp_path / "src"
src.mkdir()
(src / "App.java").write_text("class App {}", encoding="utf-8")
assert find_source_root(tmp_path) == src
def test_returns_none_when_no_source(self, tmp_path: Path) -> None:
assert find_source_root(tmp_path) is None
class TestFindTestRoot:
def test_standard_maven_layout(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
test = tmp_path / "src" / "test" / "java"
test.mkdir(parents=True)
assert find_test_root(tmp_path) == test
def test_fallback_to_test_dir(self, tmp_path: Path) -> None:
test = tmp_path / "test"
test.mkdir()
assert find_test_root(tmp_path) == test
def test_fallback_to_tests_dir(self, tmp_path: Path) -> None:
tests = tmp_path / "tests"
tests.mkdir()
assert find_test_root(tmp_path) == tests
def test_returns_none_when_no_test_dir(self, tmp_path: Path) -> None:
assert find_test_root(tmp_path) is None
# ---------------------------------------------------------------------------
# parse_java_project_config — standard layouts
# ---------------------------------------------------------------------------
class TestParseJavaProjectConfigStandard:
def test_standard_maven_project(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text("<project/>", 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"
assert config["module_root"] == str(src)
assert config["tests_root"] == str(test)
def test_returns_none_for_non_java_project(self, tmp_path: Path) -> None:
assert parse_java_project_config(tmp_path) is None
def test_defaults_when_dirs_missing(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
config = parse_java_project_config(tmp_path)
assert config is not None
# Falls back to default paths even if they don't exist
assert "src/main/java" in config["module_root"]
assert config["language"] == "java"
# ---------------------------------------------------------------------------
# parse_java_project_config — Maven properties (codeflash.*)
# ---------------------------------------------------------------------------
MAVEN_POM_WITH_PROPERTIES = """\
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
<properties>
<codeflash.moduleRoot>custom/src</codeflash.moduleRoot>
<codeflash.testsRoot>custom/test</codeflash.testsRoot>
<codeflash.disableTelemetry>true</codeflash.disableTelemetry>
<codeflash.gitRemote>upstream</codeflash.gitRemote>
<codeflash.ignorePaths>gen/,build/</codeflash.ignorePaths>
</properties>
</project>
"""
class TestMavenCodeflashProperties:
def test_reads_custom_properties(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text(MAVEN_POM_WITH_PROPERTIES, 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
assert config["git_remote"] == "upstream"
assert len(config["ignore_paths"]) == 2
def test_properties_override_auto_detection(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text(MAVEN_POM_WITH_PROPERTIES, encoding="utf-8")
# Create standard dirs AND custom dirs
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
(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
# Should use custom paths from properties, not auto-detected standard paths
assert "custom/src" in config["module_root"]
def test_no_properties_uses_defaults(self, tmp_path: Path) -> None:
(tmp_path / "pom.xml").write_text(
'<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion></project>',
encoding="utf-8",
)
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
config = parse_java_project_config(tmp_path)
assert config is not None
assert config["disable_telemetry"] is False
assert config["git_remote"] == "origin"
# ---------------------------------------------------------------------------
# parse_java_project_config — Gradle properties
# ---------------------------------------------------------------------------
class TestGradleCodeflashProperties:
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(
"codeflash.moduleRoot=lib/src\ncodeflash.testsRoot=lib/test\ncodeflash.disableTelemetry=true\n",
encoding="utf-8",
)
(tmp_path / "lib" / "src").mkdir(parents=True)
(tmp_path / "lib" / "test").mkdir(parents=True)
config = parse_java_project_config(tmp_path)
assert config is not None
assert config["module_root"] == str((tmp_path / "lib" / "src").resolve())
assert config["tests_root"] == str((tmp_path / "lib" / "test").resolve())
assert config["disable_telemetry"] is True
def test_ignores_non_codeflash_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.gitRemote=upstream\n",
encoding="utf-8",
)
config = parse_java_project_config(tmp_path)
assert config is not None
assert config["git_remote"] == "upstream"
def test_no_gradle_properties_uses_defaults(self, tmp_path: Path) -> None:
(tmp_path / "build.gradle").write_text("", encoding="utf-8")
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
(tmp_path / "src" / "test" / "java").mkdir(parents=True)
config = parse_java_project_config(tmp_path)
assert config is not None
assert config["git_remote"] == "origin"
assert config["disable_telemetry"] is False
# ---------------------------------------------------------------------------
# Multi-module Maven projects
# ---------------------------------------------------------------------------
PARENT_POM = """\
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>client</module>
<module>test</module>
<module>examples</module>
</modules>
</project>
"""
CLIENT_POM = """\
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>client</artifactId>
<build>
<sourceDirectory>${project.basedir}/src</sourceDirectory>
</build>
</project>
"""
TEST_POM = """\
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>test</artifactId>
<build>
<testSourceDirectory>${project.basedir}/src</testSourceDirectory>
</build>
</project>
"""
EXAMPLES_POM = """\
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>examples</artifactId>
<build>
<sourceDirectory>${project.basedir}/src</sourceDirectory>
</build>
</project>
"""
class TestMultiModuleMaven:
@pytest.fixture
def multi_module_project(self, tmp_path: Path) -> Path:
"""Create a multi-module Maven project mimicking aerospike's layout."""
(tmp_path / "pom.xml").write_text(PARENT_POM, encoding="utf-8")
# Client module — main library with the most Java files
client = tmp_path / "client"
client.mkdir()
(client / "pom.xml").write_text(CLIENT_POM, encoding="utf-8")
client_src = client / "src" / "com" / "example" / "client"
client_src.mkdir(parents=True)
for i in range(10):
(client_src / f"Class{i}.java").write_text(f"class Class{i} {{}}", encoding="utf-8")
# Test module — test code
test = tmp_path / "test"
test.mkdir()
(test / "pom.xml").write_text(TEST_POM, encoding="utf-8")
test_src = test / "src" / "com" / "example" / "test"
test_src.mkdir(parents=True)
(test_src / "ClientTest.java").write_text("class ClientTest {}", encoding="utf-8")
# Examples module — should be skipped
examples = tmp_path / "examples"
examples.mkdir()
(examples / "pom.xml").write_text(EXAMPLES_POM, encoding="utf-8")
examples_src = examples / "src" / "com" / "example"
examples_src.mkdir(parents=True)
(examples_src / "Example.java").write_text("class Example {}", encoding="utf-8")
return tmp_path
def test_detects_client_as_source_root(self, multi_module_project: Path) -> None:
config = parse_java_project_config(multi_module_project)
assert config is not None
assert config["module_root"] == str(multi_module_project / "client" / "src")
def test_detects_test_module_as_test_root(self, multi_module_project: Path) -> None:
config = parse_java_project_config(multi_module_project)
assert config is not None
assert config["tests_root"] == str(multi_module_project / "test" / "src")
def test_skips_examples_module(self, multi_module_project: Path) -> None:
config = parse_java_project_config(multi_module_project)
assert config is not None
# The module_root should be client/src, not examples/src
assert config["module_root"] == str(multi_module_project / "client" / "src")
def test_picks_module_with_most_java_files(self, multi_module_project: Path) -> None:
"""Client has 10 .java files, examples has 1 — client should win."""
config = parse_java_project_config(multi_module_project)
assert config is not None
assert "client" in config["module_root"]
# ---------------------------------------------------------------------------
# Language detection from config_parser
# ---------------------------------------------------------------------------
class TestLanguageDetectionViaConfigParser:
def test_java_detected_from_pom_xml(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
(tmp_path / "src" / "test" / "java").mkdir(parents=True)
monkeypatch.chdir(tmp_path)
from codeflash.code_utils.config_parser import _try_parse_java_build_config
result = _try_parse_java_build_config()
assert result is not None
config, project_root = result
assert config["language"] == "java"
assert project_root == tmp_path
def test_java_detected_from_build_gradle(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
(tmp_path / "build.gradle").write_text("", encoding="utf-8")
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
monkeypatch.chdir(tmp_path)
from codeflash.code_utils.config_parser import _try_parse_java_build_config
result = _try_parse_java_build_config()
assert result is not None
config, _ = result
assert config["language"] == "java"
def test_no_java_detected_for_python_project(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
(tmp_path / "pyproject.toml").write_text("[tool.codeflash]\nmodule-root='src'\ntests-root='tests'\n", encoding="utf-8")
monkeypatch.chdir(tmp_path)
from codeflash.code_utils.config_parser import _try_parse_java_build_config
result = _try_parse_java_build_config()
assert result is None
# ---------------------------------------------------------------------------
# Language detection from tracer
# ---------------------------------------------------------------------------
class TestTracerLanguageDetection:
def test_detects_java_from_build_files(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
monkeypatch.chdir(tmp_path)
from codeflash.languages.base import Language
from codeflash.tracer import _detect_non_python_language
result = _detect_non_python_language(None)
assert result == Language.JAVA
def test_no_detection_without_build_files(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
from codeflash.tracer import _detect_non_python_language
result = _detect_non_python_language(None)
assert result is None
def test_detects_java_from_file_extension(self, tmp_path: Path) -> None:
java_file = tmp_path / "App.java"
java_file.write_text("class App {}", encoding="utf-8")
from argparse import Namespace
from codeflash.languages.base import Language
from codeflash.tracer import _detect_non_python_language
args = Namespace(file=str(java_file))
result = _detect_non_python_language(args)
assert result == Language.JAVA