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:
parent
59031a145e
commit
3f95ff604a
19 changed files with 1078 additions and 259 deletions
|
|
@ -1,4 +0,0 @@
|
|||
[tool.codeflash]
|
||||
module-root = "src/main/java"
|
||||
tests-root = "src/test/java"
|
||||
formatter-cmds = []
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Codeflash configuration for Java project
|
||||
|
||||
[tool.codeflash]
|
||||
module-root = "src/main/java"
|
||||
tests-root = "src/test/java"
|
||||
formatter-cmds = []
|
||||
|
|
@ -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/")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
# Codeflash configuration for Java project
|
||||
|
||||
[tool.codeflash]
|
||||
module-root = "src/main/java"
|
||||
tests-root = "src/test/java"
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Codeflash configuration for Java project
|
||||
|
||||
[tool.codeflash]
|
||||
module-root = "src/main/java"
|
||||
tests-root = "src/test/java"
|
||||
language = "java"
|
||||
444
tests/test_languages/test_java/test_java_config_detection.py
Normal file
444
tests/test_languages/test_java/test_java_config_detection.py
Normal 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
|
||||
Loading…
Reference in a new issue