refactor: remove zero-config logic from java-config-redesign branch

Zero-config Java support (auto-detection from build files, codeflash.toml
elimination) is handled separately in cf-java-zero-config-strategy. This
commit strips those changes, keeping only bug fixes:
- JFR parser, ReplayHelper, instrumentation, replay tests
- Multi-module test root resolution
- JUnit 4/5 test framework detection
- add_help=False for optimize subparser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-03-26 07:39:29 +00:00
parent 970c9f86da
commit c5b3687d52
17 changed files with 234 additions and 1305 deletions

View file

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

View file

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

View file

@ -190,12 +190,6 @@ def process_pyproject_config(args: Namespace) -> Namespace:
if args.benchmarks_root: if args.benchmarks_root:
args.benchmarks_root = Path(args.benchmarks_root).resolve() args.benchmarks_root = Path(args.benchmarks_root).resolve()
args.test_project_root = project_root_from_module_root(args.tests_root, pyproject_file_path) args.test_project_root = project_root_from_module_root(args.tests_root, pyproject_file_path)
if is_java_project and pyproject_file_path.is_dir():
# For Java projects, pyproject_file_path IS the project root directory (not a file).
# Override project_root which may have resolved to a sub-module.
args.project_root = pyproject_file_path.resolve()
args.test_project_root = pyproject_file_path.resolve()
if is_LSP_enabled(): if is_LSP_enabled():
args.all = None args.all = None
return args return args
@ -214,6 +208,8 @@ def project_root_from_module_root(module_root: Path, pyproject_file_path: Path)
return current.resolve() return current.resolve()
if (current / "build.gradle").exists() or (current / "build.gradle.kts").exists(): if (current / "build.gradle").exists() or (current / "build.gradle.kts").exists():
return current.resolve() return current.resolve()
if (current / "codeflash.toml").exists():
return current.resolve()
current = current.parent current = current.parent
return module_root.parent.resolve() return module_root.parent.resolve()

View file

@ -12,29 +12,8 @@ PYPROJECT_TOML_CACHE: dict[Path, Path] = {}
ALL_CONFIG_FILES: dict[Path, dict[str, 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: def find_pyproject_toml(config_file: Path | None = None) -> Path:
# Find the pyproject.toml file on the root of the project # Find the pyproject.toml or codeflash.toml file on the root of the project
if config_file is not None: if config_file is not None:
config_file = Path(config_file) config_file = Path(config_file)
@ -50,13 +29,21 @@ def find_pyproject_toml(config_file: Path | None = None) -> Path:
# see if it was encountered before in search # see if it was encountered before in search
if cur_path in PYPROJECT_TOML_CACHE: if cur_path in PYPROJECT_TOML_CACHE:
return PYPROJECT_TOML_CACHE[cur_path] 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: while dir_path != dir_path.parent:
# First check pyproject.toml (Python projects)
config_file = dir_path / "pyproject.toml" config_file = dir_path / "pyproject.toml"
if config_file.exists(): if config_file.exists():
PYPROJECT_TOML_CACHE[cur_path] = config_file PYPROJECT_TOML_CACHE[cur_path] = config_file
return 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 dir_path = dir_path.parent
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." 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."
raise ValueError(msg) from None raise ValueError(msg) from None
@ -103,34 +90,33 @@ def find_conftest_files(test_paths: list[Path]) -> list[Path]:
return list(list_of_conftest_files) 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( def parse_config_file(
config_file_path: Path | None = None, override_formatter_check: bool = False config_file_path: Path | None = None, override_formatter_check: bool = False
) -> tuple[dict[str, Any], Path]: ) -> tuple[dict[str, Any], Path]:
# Detect all config sources — Java, package.json, pyproject.toml
java_result = _try_parse_java_build_config() if config_file_path is None else None
package_json_path = find_package_json(config_file_path) 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 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
# Use Java config only if no closer JS/Python config exists (monorepo support). # Pick the closest toml config (pyproject.toml or codeflash.toml).
# In a monorepo with a parent pom.xml and a child package.json, the closer config wins. # Java projects use codeflash.toml; Python projects use pyproject.toml.
if java_result is not None: closest_toml_path = None
java_depth = len(java_result[1].parts) if pyproject_toml_path and codeflash_toml_path:
has_closer = (package_json_path is not None and len(package_json_path.parent.parts) >= java_depth) or ( closest_toml_path = max(pyproject_toml_path, codeflash_toml_path, key=lambda p: len(p.parent.parts))
pyproject_toml_path is not None and len(pyproject_toml_path.parent.parts) >= java_depth else:
) closest_toml_path = pyproject_toml_path or codeflash_toml_path
if not has_closer:
return java_result
# When both config files exist, prefer the one closer to CWD. # When both config files exist, prefer the one closer to CWD.
# This prevents a parent-directory package.json (e.g., monorepo root) # This prevents a parent-directory package.json (e.g., monorepo root)
# from overriding a closer pyproject.toml. # from overriding a closer pyproject.toml or codeflash.toml.
use_package_json = False use_package_json = False
if package_json_path: if package_json_path:
if pyproject_toml_path is None: if closest_toml_path is None:
use_package_json = True use_package_json = True
else: else:
# Compare depth: more path parts = closer to CWD = more specific
package_json_depth = len(package_json_path.parent.parts) package_json_depth = len(package_json_path.parent.parts)
toml_depth = len(pyproject_toml_path.parent.parts) toml_depth = len(closest_toml_path.parent.parts)
use_package_json = package_json_depth >= toml_depth use_package_json = package_json_depth >= toml_depth
if use_package_json: if use_package_json:
@ -174,7 +160,7 @@ def parse_config_file(
if config == {} and lsp_mode: if config == {} and lsp_mode:
return {}, config_file_path return {}, config_file_path
# Preserve language field if present (important for JS/TS projects) # Preserve language field if present (important for Java/JS projects using codeflash.toml)
# default values: # default values:
path_keys = ["module-root", "tests-root", "benchmarks-root"] path_keys = ["module-root", "tests-root", "benchmarks-root"]
path_list_keys = ["ignore-paths"] path_list_keys = ["ignore-paths"]

View file

@ -10,8 +10,7 @@ import logging
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path # noqa: TC003 — used at runtime
from typing import Any
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -344,218 +343,6 @@ def _parse_surefire_reports(surefire_dir: Path) -> tuple[int, int, int, int]:
return tests_run, failures, errors, skipped 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: def find_test_root(project_root: Path) -> Path | None:
"""Find the test root directory for a Java project. """Find the test root directory for a Java project.

View file

@ -8,7 +8,7 @@ This module writes Codeflash configuration to native config files:
from __future__ import annotations from __future__ import annotations
import json import json
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING
import tomlkit import tomlkit
@ -38,7 +38,7 @@ def write_config(detected: DetectedProject, config: CodeflashConfig | None = Non
if detected.language == "python": if detected.language == "python":
return _write_pyproject_toml(detected.project_root, config) return _write_pyproject_toml(detected.project_root, config)
if detected.language == "java": if detected.language == "java":
return _write_java_build_config(detected.project_root, config) return _write_codeflash_toml(detected.project_root, config)
return _write_package_json(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}" return False, f"Failed to write pyproject.toml: {e}"
def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: def _write_codeflash_toml(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]:
"""Write codeflash config to pom.xml properties or gradle.properties. """Write config to codeflash.toml [tool.codeflash] section for Java projects.
Only writes non-default values. Standard Maven/Gradle layouts need no config. Creates codeflash.toml if it doesn't exist.
Args: Args:
project_root: Project root directory. project_root: Project root directory.
@ -105,141 +105,40 @@ def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tup
Tuple of (success, message). Tuple of (success, message).
""" """
config_dict = config.to_pyproject_dict() codeflash_toml_path = project_root / "codeflash.toml"
# 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)
_MAVEN_KEY_MAP: dict[str, str] = {
"module-root": "moduleRoot",
"tests-root": "testsRoot",
"git-remote": "gitRemote",
"disable-telemetry": "disableTelemetry",
"ignore-paths": "ignorePaths",
"formatter-cmds": "formatterCmds",
}
def _write_maven_properties(pom_path: Path, config: dict[str, Any]) -> tuple[bool, str]:
"""Add codeflash.* properties to pom.xml <properties> section.
Uses text-based manipulation to preserve comments, formatting, and namespace declarations.
"""
import re
try: try:
content = pom_path.read_text(encoding="utf-8") # Load existing or create new
if codeflash_toml_path.exists():
# Remove existing codeflash.* property lines (with surrounding whitespace) with codeflash_toml_path.open("rb") as f:
content = re.sub(r"\n[ \t]*<codeflash\.[^>]*>[^<]*</codeflash\.[^>]*>", "", content) doc = tomlkit.parse(f.read())
# Detect child indentation from existing properties or fall back to </properties> indent + 4 spaces
props_close = re.search(r"([ \t]*)</properties>", content)
if props_close:
parent_indent = props_close.group(1)
# Try to detect child indent from an existing property element
child_match = re.search(
r"\n([ \t]+)<[a-zA-Z]",
content[content.find("<properties>") : props_close.start()] if "<properties>" in content else "",
)
child_indent = child_match.group(1) if child_match else parent_indent + " "
else: else:
parent_indent = "" doc = tomlkit.document()
child_indent = " "
# Build new property lines with detected indentation # Ensure [tool] section exists
new_lines = [] if "tool" not in doc:
for key, value in config.items(): doc["tool"] = tomlkit.table()
maven_key = f"codeflash.{_MAVEN_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)
new_lines.append(f"{child_indent}<{maven_key}>{value}</{maven_key}>")
properties_block = "\n".join(new_lines) # Create codeflash section
codeflash_table = tomlkit.table()
codeflash_table.add(tomlkit.comment("Codeflash configuration for Java - https://docs.codeflash.ai"))
# Insert before </properties> # Add config values
if props_close: config_dict = config.to_pyproject_dict()
content = ( for key, value in config_dict.items():
content[: props_close.start()] codeflash_table[key] = value
+ properties_block
+ "\n"
+ parent_indent
+ "</properties>"
+ content[props_close.end() :]
)
else:
# No <properties> section — create one before </project>
project_close = re.search(r"([ \t]*)</project>", content)
if project_close:
indent = project_close.group(1)
inner = " " + indent
props_section = (
f"{inner}<properties>\n"
+ "\n".join(f" {line}" for line in new_lines)
+ f"\n{inner}</properties>\n"
)
content = (
content[: project_close.start()]
+ props_section
+ indent
+ "</project>"
+ content[project_close.end() :]
)
pom_path.write_text(content, encoding="utf-8") # Update the document
return True, f"Config saved to {pom_path} <properties>" doc["tool"]["codeflash"] = codeflash_table
# 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}"
except Exception as e: except Exception as e:
return False, f"Failed to write Maven properties: {e}" return False, f"Failed to write codeflash.toml: {e}"
def _write_gradle_properties(props_path: Path, config: dict[str, Any]) -> tuple[bool, str]:
"""Add codeflash.* entries to gradle.properties."""
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.{_MAVEN_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]: def _write_package_json(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]:
@ -307,7 +206,7 @@ def remove_config(project_root: Path, language: str) -> tuple[bool, str]:
if language == "python": if language == "python":
return _remove_from_pyproject(project_root) return _remove_from_pyproject(project_root)
if language == "java": if language == "java":
return _remove_java_build_config(project_root) return _remove_from_codeflash_toml(project_root)
return _remove_from_package_json(project_root) return _remove_from_package_json(project_root)
@ -336,42 +235,29 @@ def _remove_from_pyproject(project_root: Path) -> tuple[bool, str]:
return False, f"Failed to remove config: {e}" return False, f"Failed to remove config: {e}"
def _remove_java_build_config(project_root: Path) -> tuple[bool, str]: def _remove_from_codeflash_toml(project_root: Path) -> tuple[bool, str]:
"""Remove codeflash.* properties from pom.xml or gradle.properties. """Remove [tool.codeflash] section from codeflash.toml."""
codeflash_toml_path = project_root / "codeflash.toml"
Priority matches _write_java_build_config: pom.xml first, then gradle.properties. if not codeflash_toml_path.exists():
""" return True, "No codeflash.toml found"
# Try pom.xml first (matches write priority) — text-based removal preserves formatting
pom_path = project_root / "pom.xml"
if pom_path.exists():
try:
import re
content = pom_path.read_text(encoding="utf-8") try:
updated = re.sub(r"\n[ \t]*<codeflash\.[^>]*>[^<]*</codeflash\.[^>]*>", "", content) with codeflash_toml_path.open("rb") as f:
if updated != content: doc = tomlkit.parse(f.read())
pom_path.write_text(updated, 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}"
# Try gradle.properties if "tool" in doc and "codeflash" in doc["tool"]:
gradle_props = project_root / "gradle.properties" del doc["tool"]["codeflash"]
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 \u2014 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}"
return True, "No Java build config found" 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}"
def _remove_from_package_json(project_root: Path) -> tuple[bool, str]: def _remove_from_package_json(project_root: Path) -> tuple[bool, str]:

View file

@ -886,25 +886,20 @@ def has_existing_config(project_root: Path) -> tuple[bool, str | None]:
Returns: Returns:
Tuple of (has_config, config_file_type). Tuple of (has_config, config_file_type).
config_file_type is "pyproject.toml", "pom.xml", "build.gradle", "package.json", or None. config_file_type is "pyproject.toml", "codeflash.toml", "package.json", or None.
""" """
# Check pyproject.toml (Python projects) # Check TOML config files (pyproject.toml, codeflash.toml)
pyproject_path = project_root / "pyproject.toml" for toml_filename in ("pyproject.toml", "codeflash.toml"):
if pyproject_path.exists(): toml_path = project_root / toml_filename
try: if toml_path.exists():
with pyproject_path.open("rb") as f: try:
data = tomlkit.parse(f.read()) with toml_path.open("rb") as f:
if "tool" in data and "codeflash" in data["tool"]: data = tomlkit.parse(f.read())
return True, "pyproject.toml" if "tool" in data and "codeflash" in data["tool"]:
except Exception: return True, toml_filename
pass except Exception:
pass
# Check Java build files — for zero-config Java, any build file means "configured"
# because Java config is auto-detected from build files without explicit codeflash.* 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 # Check package.json
package_json_path = project_root / "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: def _detect_non_python_language(args: Namespace | None) -> Language | None:
"""Detect if the project uses a non-Python language from --file or build files. """Detect if the project uses a non-Python language from --file or config.
Returns a Language enum value if non-Python detected, None otherwise. Returns a Language enum value if non-Python detected, None otherwise.
""" """
@ -66,23 +66,15 @@ def _detect_non_python_language(args: Namespace | None) -> Language | None:
except Exception: except Exception:
pass pass
# Method 2: Detect Java from build files (pom.xml / build.gradle) # Method 2: Check project config for language field
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: try:
from codeflash.code_utils.config_parser import parse_config_file from codeflash.code_utils.config_parser import parse_config_file
config_file = getattr(args, "config_file_path", None) if args else None config_file = getattr(args, "config_file_path", None) if args else None
config, _ = parse_config_file(config_file) config, _ = parse_config_file(config_file)
lang_str = config.get("language", "") lang_str = config.get("language", "")
if lang_str == "java":
return Language.JAVA
if lang_str in ("javascript", "typescript"): if lang_str in ("javascript", "typescript"):
return Language(lang_str) return Language(lang_str)
except Exception: except Exception:

View file

@ -1,112 +1,101 @@
--- ---
title: "Java Configuration" title: "Java Configuration"
description: "Configure Codeflash for Java projects — zero config for standard layouts" description: "Configure Codeflash for Java projects using codeflash.toml"
icon: "java" icon: "java"
sidebarTitle: "Java (pom.xml / Gradle)" sidebarTitle: "Java (codeflash.toml)"
keywords: keywords:
[ [
"configuration", "configuration",
"codeflash.toml",
"java", "java",
"maven", "maven",
"gradle", "gradle",
"junit", "junit",
"pom.xml",
"gradle.properties",
"zero-config",
] ]
--- ---
# Java Configuration # Java Configuration
**Standard Maven/Gradle projects need zero configuration.** Codeflash auto-detects your project structure from `pom.xml` or `build.gradle` — no config file is required. Codeflash stores its configuration in `codeflash.toml` under the `[tool.codeflash]` section.
For projects with non-standard layouts, you can add `codeflash.*` properties to your existing `pom.xml` or `gradle.properties`. ## 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>
## Auto-Detection ## Auto-Detection
Codeflash inspects your build files and auto-detects: When you run `codeflash init`, Codeflash inspects your project and auto-detects:
| Setting | Detection logic | | Setting | Detection logic |
|---------|----------------| |---------|----------------|
| **Language** | Presence of `pom.xml` or `build.gradle` / `build.gradle.kts` | | `module-root` | Looks for `src/main/java` (Maven/Gradle standard layout) |
| **Source root** | `src/main/java` (standard), or `<sourceDirectory>` in `pom.xml`, or Gradle `sourceSets` | | `tests-root` | Looks for `src/test/java`, `test/`, `tests/` |
| **Test root** | `src/test/java` (standard), or `<testSourceDirectory>` in `pom.xml` | | `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 | | `test-framework` | Checks build file dependencies for JUnit 5, JUnit 4, or TestNG |
| **Java version** | `<maven.compiler.source>`, `<java.version>` in `pom.xml` |
### Multi-module Maven projects ## Required Options
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. - **`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 example, with this layout: ## 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:
```text ```text
my-project/ my-project/
|- client/ ← main library (most .java files) |- client/
| |- src/com/example/ | |- src/main/java/com/example/client/
| |- pom.xml ← <sourceDirectory>${project.basedir}/src</sourceDirectory> | |- src/test/java/com/example/client/
|- test/ ← test module |- server/
| |- src/com/example/ | |- src/main/java/com/example/server/
| |- pom.xml ← <testSourceDirectory>${project.basedir}/src</testSourceDirectory> |- pom.xml
|- benchmarks/ ← skipped (benchmark module) |- codeflash.toml
|- pom.xml ← <modules>client, test, benchmarks</modules>
``` ```
Codeflash auto-detects `client/src` as the source root and `test/src` as the test root — no manual configuration needed. ```toml
[tool.codeflash]
## Custom Configuration module-root = "client/src/main/java"
tests-root = "client/src/test/java"
If auto-detection doesn't match your project layout, add `codeflash.*` properties to your build files. language = "java"
<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>
``` ```
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. For non-standard layouts (like the Aerospike client where source is under `client/src/`), adjust paths accordingly:
</Tab> ```toml
<Tab title="Gradle (gradle.properties)"> [tool.codeflash]
module-root = "client/src"
Add properties to `gradle.properties`: tests-root = "test/src"
language = "java"
```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/
``` ```
</Tab> ## Tracer Options
</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: When using `codeflash optimize` to trace a Java program, these CLI options are available:
@ -122,9 +111,9 @@ Example with timeout:
codeflash optimize --timeout 30 java -jar target/my-app.jar --app-args codeflash optimize --timeout 30 java -jar target/my-app.jar --app-args
``` ```
## Examples ## Example
### Standard Maven project (zero config) ### Standard Maven project
```text ```text
my-app/ my-app/
@ -135,14 +124,17 @@ my-app/
| |- test/java/com/example/ | |- test/java/com/example/
| |- AppTest.java | |- AppTest.java
|- pom.xml |- pom.xml
|- codeflash.toml
``` ```
Just run: ```toml
```bash [tool.codeflash]
codeflash optimize java -jar target/my-app.jar module-root = "src/main/java"
tests-root = "src/test/java"
language = "java"
``` ```
### Standard Gradle project (zero config) ### Gradle project
```text ```text
my-lib/ my-lib/
@ -150,55 +142,12 @@ my-lib/
| |- main/java/com/example/ | |- main/java/com/example/
| |- test/java/com/example/ | |- test/java/com/example/
|- build.gradle |- build.gradle
|- codeflash.toml
``` ```
Just run: ```toml
```bash [tool.codeflash]
codeflash optimize java -cp build/classes/java/main com.example.Main module-root = "src/main/java"
tests-root = "src/test/java"
language = "java"
``` ```
### 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,11 +12,10 @@ keywords:
"junit", "junit",
"junit5", "junit5",
"tracing", "tracing",
"zero-config",
] ]
--- ---
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`. 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.
### Prerequisites ### Prerequisites
@ -24,7 +23,7 @@ Before installing Codeflash, ensure you have:
1. **Java 11 or above** installed 1. **Java 11 or above** installed
2. **Maven or Gradle** as your build tool 2. **Maven or Gradle** as your build tool
3. **A Java project** with source code 3. **A Java project** with source code under a standard directory layout
Good to have (optional): Good to have (optional):
@ -46,15 +45,51 @@ uv pip install codeflash
``` ```
</Step> </Step>
<Step title="Run your first optimization"> <Step title="Initialize your project">
Navigate to your Java project root (where `pom.xml` or `build.gradle` is) and run: 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:
```bash ```bash
codeflash optimize java -jar target/my-app.jar codeflash optimize java -jar target/my-app.jar
``` ```
That's it — no `init` step, no config file. Codeflash detects Maven/Gradle automatically and infers source and test directories from your build files. Or with Maven:
```bash
codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main"
```
Codeflash will: Codeflash will:
1. Profile your program using JFR (Java Flight Recorder) 1. Profile your program using JFR (Java Flight Recorder)
@ -66,29 +101,6 @@ Codeflash will:
</Step> </Step>
</Steps> </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 ## How it works
Codeflash uses a **two-stage tracing** approach for Java: Codeflash uses a **two-stage tracing** approach for Java:

View file

@ -1,87 +0,0 @@
"""Tests for config_parser.py — monorepo language detection priority."""
from __future__ import annotations
import json
import os
from pathlib import Path
from unittest.mock import patch
import pytest
from codeflash.code_utils.config_parser import parse_config_file
class TestMonorepoConfigPriority:
"""Verify that closer config files win over parent Java build files in monorepos."""
def test_closer_package_json_wins_over_parent_pom_xml(self, tmp_path: Path) -> None:
"""In monorepo/frontend/, a local package.json should win over a parent pom.xml."""
# Parent Java project
(tmp_path / "pom.xml").write_text("<project></project>", encoding="utf-8")
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
# Child JS project
frontend = tmp_path / "frontend"
frontend.mkdir()
(frontend / "package.json").write_text(
json.dumps({"name": "frontend", "codeflash": {"moduleRoot": "src"}}),
encoding="utf-8",
)
(frontend / "src").mkdir()
with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls:
mock_path_cls.cwd.return_value = frontend
# find_package_json also uses Path.cwd; mock it at the source
with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls:
mock_js_path_cls.cwd.return_value = frontend
# Also need to let normal Path operations work
mock_path_cls.side_effect = Path
mock_path_cls.cwd.return_value = frontend
mock_js_path_cls.side_effect = Path
mock_js_path_cls.cwd.return_value = frontend
config, root = parse_config_file()
# Should detect JS, not Java
assert config.get("language") != "java", (
"Closer package.json should take priority over parent pom.xml"
)
def test_java_wins_when_no_closer_js_config(self, tmp_path: Path) -> None:
"""When only a pom.xml exists (no package.json/pyproject.toml closer), Java config wins."""
(tmp_path / "pom.xml").write_text("<project></project>", encoding="utf-8")
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls:
mock_path_cls.side_effect = Path
mock_path_cls.cwd.return_value = tmp_path
with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls:
mock_js_path_cls.side_effect = Path
mock_js_path_cls.cwd.return_value = tmp_path
config, root = parse_config_file()
assert config.get("language") == "java"
def test_same_level_package_json_wins_over_pom_xml(self, tmp_path: Path) -> None:
"""When pom.xml and package.json are at the same level, package.json wins (more specific)."""
(tmp_path / "pom.xml").write_text("<project></project>", encoding="utf-8")
(tmp_path / "src" / "main" / "java").mkdir(parents=True)
(tmp_path / "package.json").write_text(
json.dumps({"name": "mixed-project", "codeflash": {"moduleRoot": "src"}}),
encoding="utf-8",
)
with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls:
mock_path_cls.side_effect = Path
mock_path_cls.cwd.return_value = tmp_path
with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls:
mock_js_path_cls.side_effect = Path
mock_js_path_cls.cwd.return_value = tmp_path
config, root = parse_config_file()
assert config.get("language") != "java", (
"Same-level package.json should take priority over pom.xml"
)

View file

@ -149,8 +149,8 @@ def build_command(
if config.function_name: if config.function_name:
base_command.extend(["--function", config.function_name]) base_command.extend(["--function", config.function_name])
# Check if config exists (pyproject.toml, pom.xml, build.gradle) - if so, don't override it # Check if config exists (pyproject.toml or codeflash.toml) - if so, don't override it
has_codeflash_config = (cwd / "pom.xml").exists() or (cwd / "build.gradle").exists() or (cwd / "build.gradle.kts").exists() has_codeflash_config = (cwd / "codeflash.toml").exists()
if not has_codeflash_config: if not has_codeflash_config:
pyproject_path = cwd / "pyproject.toml" pyproject_path = cwd / "pyproject.toml"
if pyproject_path.exists(): if pyproject_path.exists():

View file

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

View file

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

View file

@ -1,444 +0,0 @@
"""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 str(tmp_path / "src" / "main" / "java") == 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 config["module_root"] == str((tmp_path / "custom" / "src").resolve())
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

View file

@ -1,148 +0,0 @@
"""Tests for config_writer module — Java pom.xml formatting preservation."""
from pathlib import Path
class TestWriteMavenProperties:
"""Tests for _write_maven_properties — text-based pom.xml editing."""
def test_preserves_comments(self, tmp_path: Path) -> None:
pom = tmp_path / "pom.xml"
pom.write_text(
'<?xml version="1.0" encoding="UTF-8"?>\n'
"<project>\n"
" <!-- Important comment -->\n"
" <properties>\n"
" <maven.compiler.source>17</maven.compiler.source>\n"
" </properties>\n"
"</project>\n",
encoding="utf-8",
)
from codeflash.setup.config_writer import _write_maven_properties
ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"})
result = pom.read_text(encoding="utf-8")
assert ok
assert "<!-- Important comment -->" in result
assert "<codeflash.moduleRoot>src/main/java</codeflash.moduleRoot>" in result
def test_preserves_namespace(self, tmp_path: Path) -> None:
pom = tmp_path / "pom.xml"
pom.write_text(
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
" <properties>\n"
" <maven.compiler.source>17</maven.compiler.source>\n"
" </properties>\n"
"</project>\n",
encoding="utf-8",
)
from codeflash.setup.config_writer import _write_maven_properties
ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"})
result = pom.read_text(encoding="utf-8")
assert ok
assert 'xmlns="http://maven.apache.org/POM/4.0.0"' in result
# Must NOT have ns0: prefix (ElementTree bug)
assert "ns0:" not in result
def test_updates_existing_codeflash_properties(self, tmp_path: Path) -> None:
pom = tmp_path / "pom.xml"
pom.write_text(
"<project>\n"
" <properties>\n"
" <codeflash.moduleRoot>old/path</codeflash.moduleRoot>\n"
" </properties>\n"
"</project>\n",
encoding="utf-8",
)
from codeflash.setup.config_writer import _write_maven_properties
ok, _ = _write_maven_properties(pom, {"module-root": "new/path"})
result = pom.read_text(encoding="utf-8")
assert ok
assert "old/path" not in result
assert "<codeflash.moduleRoot>new/path</codeflash.moduleRoot>" in result
def test_creates_properties_section(self, tmp_path: Path) -> None:
pom = tmp_path / "pom.xml"
pom.write_text(
"<project>\n" " <modelVersion>4.0.0</modelVersion>\n" "</project>\n",
encoding="utf-8",
)
from codeflash.setup.config_writer import _write_maven_properties
ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"})
result = pom.read_text(encoding="utf-8")
assert ok
assert "<properties>" in result
assert "<codeflash.moduleRoot>src/main/java</codeflash.moduleRoot>" in result
def test_converts_kebab_to_camelcase(self, tmp_path: Path) -> None:
pom = tmp_path / "pom.xml"
pom.write_text(
"<project>\n <properties>\n </properties>\n</project>\n",
encoding="utf-8",
)
from codeflash.setup.config_writer import _write_maven_properties
ok, _ = _write_maven_properties(pom, {"ignore-paths": ["target", "build"]})
result = pom.read_text(encoding="utf-8")
assert ok
assert "<codeflash.ignorePaths>target,build</codeflash.ignorePaths>" in result
class TestRemoveJavaBuildConfig:
"""Tests for _remove_java_build_config — preserves formatting during removal."""
def test_removes_codeflash_from_pom_preserving_others(self, tmp_path: Path) -> None:
pom = tmp_path / "pom.xml"
pom.write_text(
"<project>\n"
" <!-- Keep me -->\n"
" <properties>\n"
" <maven.compiler.source>17</maven.compiler.source>\n"
" <codeflash.moduleRoot>src/main/java</codeflash.moduleRoot>\n"
" </properties>\n"
"</project>\n",
encoding="utf-8",
)
from codeflash.setup.config_writer import _remove_java_build_config
ok, _ = _remove_java_build_config(tmp_path)
result = pom.read_text(encoding="utf-8")
assert ok
assert "<!-- Keep me -->" in result
assert "<maven.compiler.source>17</maven.compiler.source>" in result
assert "codeflash.moduleRoot" not in result
def test_removes_codeflash_from_gradle_properties(self, tmp_path: Path) -> None:
gradle = tmp_path / "gradle.properties"
gradle.write_text(
"org.gradle.jvmargs=-Xmx2g\n"
"# Codeflash configuration \u2014 https://docs.codeflash.ai\n"
"codeflash.moduleRoot=src/main/java\n"
"codeflash.testsRoot=src/test/java\n",
encoding="utf-8",
)
from codeflash.setup.config_writer import _remove_java_build_config
ok, _ = _remove_java_build_config(tmp_path)
result = gradle.read_text(encoding="utf-8")
assert ok
assert "org.gradle.jvmargs=-Xmx2g" in result
assert "codeflash." not in result

View file

@ -558,22 +558,6 @@ class TestHasExistingConfig:
assert has_config is False assert has_config is False
assert config_type is None assert config_type is None
def test_java_pom_xml_is_zero_config(self, tmp_path):
"""Java projects with pom.xml are zero-config — build file presence means configured."""
(tmp_path / "pom.xml").write_text("<project><modelVersion>4.0.0</modelVersion></project>")
has_config, config_type = has_existing_config(tmp_path)
assert has_config is True
assert config_type == "pom.xml"
def test_java_build_gradle_is_zero_config(self, tmp_path):
"""Java projects with build.gradle are zero-config — build file presence means configured."""
(tmp_path / "build.gradle").write_text('plugins { id "java" }')
has_config, config_type = has_existing_config(tmp_path)
assert has_config is True
assert config_type == "build.gradle"
def test_returns_false_for_empty_directory(self, tmp_path): def test_returns_false_for_empty_directory(self, tmp_path):
"""Should return False for empty directory.""" """Should return False for empty directory."""
has_config, config_type = has_existing_config(tmp_path) has_config, config_type = has_existing_config(tmp_path)