From 3f95ff604abbec93b9cddc0e0665be45e5415310 Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 19:11:06 -0700 Subject: [PATCH 01/26] =?UTF-8?q?feat:=20eliminate=20codeflash.toml=20?= =?UTF-8?q?=E2=80=94=20auto-detect=20Java=20config=20from=20build=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Java projects no longer need a standalone config file. Codeflash reads config from pom.xml 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 and , 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) --- code_to_optimize/java-gradle/codeflash.toml | 4 - code_to_optimize/java/codeflash.toml | 6 - .../codeflash/tracer/TracingTransformer.java | 5 - codeflash/cli_cmds/cli.py | 13 +- codeflash/code_utils/config_parser.py | 59 ++- codeflash/discovery/functions_to_optimize.py | 4 +- codeflash/languages/java/build_tools.py | 215 ++++++++- codeflash/languages/java/jfr_parser.py | 4 +- .../resources/codeflash-runtime-1.0.0.jar | Bin 15974015 -> 15973968 bytes codeflash/languages/java/tracer.py | 43 +- codeflash/setup/config_writer.py | 192 +++++--- codeflash/setup/detector.py | 28 +- codeflash/tracer.py | 36 +- docs/configuration/java.mdx | 207 +++++--- docs/getting-started/java-installation.mdx | 64 +-- tests/scripts/end_to_end_test_utilities.py | 4 +- .../fixtures/java_maven/codeflash.toml | 5 - .../fixtures/java_tracer_e2e/codeflash.toml | 6 - .../test_java/test_java_config_detection.py | 444 ++++++++++++++++++ 19 files changed, 1079 insertions(+), 260 deletions(-) delete mode 100644 code_to_optimize/java-gradle/codeflash.toml delete mode 100644 code_to_optimize/java/codeflash.toml delete mode 100644 tests/test_languages/fixtures/java_maven/codeflash.toml delete mode 100644 tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml create mode 100644 tests/test_languages/test_java/test_java_config_detection.py diff --git a/code_to_optimize/java-gradle/codeflash.toml b/code_to_optimize/java-gradle/codeflash.toml deleted file mode 100644 index bf6e45279..000000000 --- a/code_to_optimize/java-gradle/codeflash.toml +++ /dev/null @@ -1,4 +0,0 @@ -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" -formatter-cmds = [] diff --git a/code_to_optimize/java/codeflash.toml b/code_to_optimize/java/codeflash.toml deleted file mode 100644 index 4016df28a..000000000 --- a/code_to_optimize/java/codeflash.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Codeflash configuration for Java project - -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" -formatter-cmds = [] diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java index 974c767a9..75c61de3a 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java @@ -22,11 +22,6 @@ public byte[] transform(ClassLoader loader, String className, 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/") diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index d76e60a11..f27817a39 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -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", diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index ef21ce051..1d0f13df5 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -12,8 +12,29 @@ 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"] diff --git a/codeflash/discovery/functions_to_optimize.py b/codeflash/discovery/functions_to_optimize.py index 5780f4def..ec58a747d 100644 --- a/codeflash/discovery/functions_to_optimize.py +++ b/codeflash/discovery/functions_to_optimize.py @@ -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) diff --git a/codeflash/languages/java/build_tools.py b/codeflash/languages/java/build_tools.py index 28db2c9aa..f8a19c693 100644 --- a/codeflash/languages/java/build_tools.py +++ b/codeflash/languages/java/build_tools.py @@ -10,7 +10,8 @@ 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 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 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 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. diff --git a/codeflash/languages/java/jfr_parser.py b/codeflash/languages/java/jfr_parser.py index 7775378e6..7f3816856 100644 --- a/codeflash/languages/java/jfr_parser.py +++ b/codeflash/languages/java/jfr_parser.py @@ -152,6 +152,8 @@ def _frame_to_key(self, frame: dict[str, Any]) -> str | None: 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 @@ def _store_method_info(self, key: str, frame: dict[str, Any]) -> None: 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)), diff --git a/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar b/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar index cfcee9390d6529e7aa1d8639566228ce132a72d0..10a03b3cc7b53fd7a6a87708b3cc0b412332f38a 100644 GIT binary patch delta 39272 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
)I3AcV5xA#mPC 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 P?bEuo)7_oLn7IBQqQLhD diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py index 7b5a30421..91b06eab7 100644 --- a/codeflash/languages/java/tracer.py +++ b/codeflash/languages/java/tracer.py @@ -14,6 +14,39 @@ 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 @@ def trace( # 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 @@ def trace( 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) diff --git a/codeflash/setup/config_writer.py b/codeflash/setup/config_writer.py index 0889690d5..e872cfeba 100644 --- a/codeflash/setup/config_writer.py +++ b/codeflash/setup/config_writer.py @@ -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() - 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() + # 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)} - # Ensure [tool] section exists - if "tool" not in doc: - doc["tool"] = tomlkit.table() + if not non_default: + return True, "Standard Maven/Gradle layout detected — no config needed" - # Create codeflash section - codeflash_table = tomlkit.table() - codeflash_table.add(tomlkit.comment("Codeflash configuration for Java - https://docs.codeflash.ai")) + pom_path = project_root / "pom.xml" + if pom_path.exists(): + return _write_maven_properties(pom_path, non_default) - # Add config values - config_dict = config.to_pyproject_dict() - for key, value in config_dict.items(): - codeflash_table[key] = value + gradle_props_path = project_root / "gradle.properties" + return _write_gradle_properties(gradle_props_path, non_default) - # Update the document - doc["tool"]["codeflash"] = codeflash_table - # Write back - with codeflash_toml_path.open("w", encoding="utf8") as f: - f.write(tomlkit.dumps(doc)) +def _write_maven_properties(pom_path: Path, config: dict) -> tuple[bool, str]: + """Add codeflash.* properties to pom.xml section.""" + import xml.etree.ElementTree as ET - return True, f"Config saved to {codeflash_toml_path}" + try: + tree = ET.parse(str(pom_path)) + root = tree.getroot() + ns = {"m": "http://maven.apache.org/POM/4.0.0"} + + # Find or create + properties = root.find("m:properties", ns) or root.find("properties") + if properties is None: + properties = ET.SubElement(root, "properties") + + # 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", + } + + 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) + + existing = properties.find(maven_key) + if existing is None: + elem = ET.SubElement(properties, maven_key) + elem.text = value + else: + existing.text = value + + tree.write(str(pom_path), xml_declaration=True, encoding="UTF-8") + return True, f"Config saved to {pom_path} " 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" - - if not codeflash_toml_path.exists(): - return True, "No codeflash.toml found" - - try: - with codeflash_toml_path.open("rb") as f: - doc = tomlkit.parse(f.read()) - - 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}" +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}" + + # Try pom.xml + pom_path = project_root / "pom.xml" + if pom_path.exists(): + try: + import xml.etree.ElementTree as ET + + 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}" + + return True, "No Java build config found" def _remove_from_package_json(project_root: Path) -> tuple[bool, str]: diff --git a/codeflash/setup/detector.py b/codeflash/setup/detector.py index defe1a22d..06d690190 100644 --- a/codeflash/setup/detector.py +++ b/codeflash/setup/detector.py @@ -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" diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 84f58e9da..892a2a694 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -38,7 +38,7 @@ 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 diff --git a/docs/configuration/java.mdx b/docs/configuration/java.mdx index 9d110fc55..720e5e091 100644 --- a/docs/configuration/java.mdx +++ b/docs/configuration/java.mdx @@ -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`. - - -Codeflash auto-detects most settings from your project structure. Running `codeflash init` will set up the correct config — manual configuration is usually not needed. - +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 | - -## Required Options - -- **`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. +| **Language** | Presence of `pom.xml` or `build.gradle` / `build.gradle.kts` | +| **Source root** | `src/main/java` (standard), or `` in `pom.xml`, or Gradle `sourceSets` | +| **Test root** | `src/test/java` (standard), or `` in `pom.xml` | +| **Test framework** | Checks build file dependencies for JUnit 5, JUnit 4, or TestNG | +| **Java version** | ``, `` in `pom.xml` | -## Optional Options +### Multi-module Maven projects -- **`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. +For multi-module projects, Codeflash scans each module's `pom.xml` for `` and `` declarations. It picks the module with the most Java source files as the main source root, and identifies test modules by name. -## 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 ← ${project.basedir}/src +|- test/ ← test module +| |- src/com/example/ +| |- pom.xml ← ${project.basedir}/src +|- benchmarks/ ← skipped (benchmark module) +|- pom.xml ← client, test, benchmarks ``` -```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. + + + + +Add properties to your `pom.xml` `` section: + +```xml + + + client/src + test/src + true + upstream + src/main/java/generated/,src/main/java/proto/ + ``` -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. + + + + +Add properties to `gradle.properties`: -```toml -[tool.codeflash] -module-root = "client/src" -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/ ``` -## Tracer Options + + + +## 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 `` or `src/main/java` | +| `codeflash.testsRoot` | Test directory | Auto-detected from `` 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 + + client/src + test/src + +``` + + +In most cases, even non-standard multi-module layouts are auto-detected correctly from `` and `` in each module's `pom.xml`. Only add manual config if auto-detection gets it wrong. + + +## FAQ + + + + No. Codeflash auto-detects Java projects from `pom.xml` or `build.gradle`. No initialization step or config file is needed for standard layouts. + + + + Codeflash reads config from your existing build files — `pom.xml` `` for Maven, `gradle.properties` for Gradle. No separate config file is created. + + + + Add `` and `` properties to your `pom.xml` or `gradle.properties`. These override auto-detection. + + + + Codeflash scans each module's `pom.xml` for `` and ``. 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. + + diff --git a/docs/getting-started/java-installation.mdx b/docs/getting-started/java-installation.mdx index a75e1f0b7..fb2a88ef2 100644 --- a/docs/getting-started/java-installation.mdx +++ b/docs/getting-started/java-installation.mdx @@ -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): @@ -45,61 +46,48 @@ uv pip install codeflash ``` - + Navigate to your Java project root (where `pom.xml` or `build.gradle` is) and run: ```bash -codeflash init +codeflash optimize java -jar target/my-app.jar ``` -This will: -- Detect your build tool (Maven/Gradle) -- Find your source and test directories -- Create a `codeflash.toml` configuration file +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) +2. Capture method arguments using a bytecode instrumentation agent +3. Generate JUnit replay tests from the captured data +4. Rank functions by performance impact +5. Optimize the most impactful functions -Check that the configuration looks correct: + + -```bash -cat codeflash.toml -``` + +**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). + -You should see something like: +## Usage examples -```toml -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" -language = "java" +**Trace and optimize a JAR application:** +```bash +codeflash optimize java -jar target/my-app.jar --app-args ``` - - - -Trace and optimize a running Java program: - +**Optimize a specific file and function:** ```bash -codeflash optimize java -jar target/my-app.jar +codeflash --file src/main/java/com/example/Utils.java --function computeHash ``` -Or with Maven: - +**Trace a long-running program with a timeout:** ```bash -codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main" +codeflash optimize --timeout 30 java -jar target/my-server.jar ``` -Codeflash will: -1. Profile your program using JFR (Java Flight Recorder) -2. Capture method arguments using a bytecode instrumentation agent -3. Generate JUnit replay tests from the captured data -4. Rank functions by performance impact -5. Optimize the most impactful functions - - - +Each tracing stage runs for at most 30 seconds, then the captured data is processed. ## How it works diff --git a/tests/scripts/end_to_end_test_utilities.py b/tests/scripts/end_to_end_test_utilities.py index 12259b339..33825db4d 100644 --- a/tests/scripts/end_to_end_test_utilities.py +++ b/tests/scripts/end_to_end_test_utilities.py @@ -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(): diff --git a/tests/test_languages/fixtures/java_maven/codeflash.toml b/tests/test_languages/fixtures/java_maven/codeflash.toml deleted file mode 100644 index ecd20a562..000000000 --- a/tests/test_languages/fixtures/java_maven/codeflash.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Codeflash configuration for Java project - -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" diff --git a/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml b/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml deleted file mode 100644 index a501ef8cb..000000000 --- a/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Codeflash configuration for Java project - -[tool.codeflash] -module-root = "src/main/java" -tests-root = "src/test/java" -language = "java" diff --git a/tests/test_languages/test_java/test_java_config_detection.py b/tests/test_languages/test_java/test_java_config_detection.py new file mode 100644 index 000000000..fc5565ffb --- /dev/null +++ b/tests/test_languages/test_java/test_java_config_detection.py @@ -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("", 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("", 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("", 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("", 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("", 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("", 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("", 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 = """\ + + 4.0.0 + com.example + test + 1.0 + + custom/src + custom/test + true + upstream + gen/,build/ + + +""" + + +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( + '4.0.0', + 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 = """\ + + 4.0.0 + com.example + parent + 1.0 + pom + + client + test + examples + + +""" + +CLIENT_POM = """\ + + 4.0.0 + + com.example + parent + 1.0 + + client + + ${project.basedir}/src + + +""" + +TEST_POM = """\ + + 4.0.0 + + com.example + parent + 1.0 + + test + + ${project.basedir}/src + + +""" + +EXAMPLES_POM = """\ + + 4.0.0 + + com.example + parent + 1.0 + + examples + + ${project.basedir}/src + + +""" + + +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("", 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("", 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 From 9d01710d85d8b983b992a713b58992a223ec6eef Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 19:20:48 -0700 Subject: [PATCH 02/26] fix: skip behavior instrumentation for replay test files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replay tests call helper.replay() via reflection, not the target function directly. The behavior instrumentation can't wrap indirect calls and produces malformed output (code emitted outside class body) for large replay test files. For replay tests, just rename the class without adding instrumentation — JUnit pass/fail results verify correctness. Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/support.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/codeflash/languages/java/support.py b/codeflash/languages/java/support.py index 825c7e7da..31959426f 100644 --- a/codeflash/languages/java/support.py +++ b/codeflash/languages/java/support.py @@ -582,8 +582,27 @@ def instrument_existing_test( tests_project_root: Path, mode: str, ) -> tuple[bool, str | None]: - """Inject profiling code into an existing test file.""" + """Inject profiling code into an existing test file. + + For replay test files (generated by the tracer), skip instrumentation — + they call helper.replay() via reflection, not the target function directly. + The behavior instrumentation can't wrap indirect calls and produces + malformed output for large replay test files. + """ test_string = test_path.read_text(encoding="utf-8") + + # Skip instrumentation for replay tests — just rename the class + if test_string.lstrip().startswith("// codeflash:"): + import re + + original_class = test_path.stem + if mode == "behavior": + new_class = f"{original_class}__perfinstrumented" + else: + new_class = f"{original_class}__perfonlyinstrumented" + modified = re.sub(rf"\b{re.escape(original_class)}\b", new_class, test_string) + return True, modified + return instrument_existing_test( test_string=test_string, function_to_optimize=function_to_optimize, mode=mode, test_path=test_path ) From df55e74fdfb0173e4a54933e0d0467b3616d91dd Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 19:29:01 -0700 Subject: [PATCH 03/26] fix: support JUnit 4 in replay test generation Detect test framework from project build config and generate replay tests with appropriate imports (org.junit.Test for JUnit 4, org.junit.jupiter.api.Test for JUnit 5). Fixes compilation failures on projects using JUnit 4 (like aerospike-client-java). Also passes test_framework through run_java_tracer to generate_replay_tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/replay_test.py | 45 ++++++++++++++++++------- codeflash/languages/java/tracer.py | 7 +++- codeflash/tracer.py | 7 ++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/codeflash/languages/java/replay_test.py b/codeflash/languages/java/replay_test.py index c753bf4fa..457cfe711 100644 --- a/codeflash/languages/java/replay_test.py +++ b/codeflash/languages/java/replay_test.py @@ -12,9 +12,12 @@ logger = logging.getLogger(__name__) -def generate_replay_tests(trace_db_path: Path, output_dir: Path, project_root: Path, max_run_count: int = 256) -> int: - """Generate JUnit 5 replay test files from a trace SQLite database. +def generate_replay_tests( + trace_db_path: Path, output_dir: Path, project_root: Path, max_run_count: int = 256, test_framework: str = "junit5" +) -> int: + """Generate JUnit replay test files from a trace SQLite database. + Supports both JUnit 5 (default) and JUnit 4. Returns the number of test files generated. """ if not trace_db_path.exists(): @@ -58,29 +61,47 @@ def generate_replay_tests(trace_db_path: Path, output_dir: Path, project_root: P for i in range(invocation_count): escaped_descriptor = descriptor.replace('"', '\\"') - test_methods_code.append( - f" @Test void replay_{safe_method}_{i}() throws Exception {{\n" - f' helper.replay("{classname}", "{method_name}", ' - f'"{escaped_descriptor}", {i});\n' - f" }}" - ) + if test_framework == "junit4": + test_methods_code.append( + f" @Test public void replay_{safe_method}_{i}() throws Exception {{\n" + f' helper.replay("{classname}", "{method_name}", ' + f'"{escaped_descriptor}", {i});\n' + f" }}" + ) + else: + test_methods_code.append( + f" @Test void replay_{safe_method}_{i}() throws Exception {{\n" + f' helper.replay("{classname}", "{method_name}", ' + f'"{escaped_descriptor}", {i});\n' + f" }}" + ) all_function_names.extend(class_function_names) # Generate the test file functions_comment = ",".join(class_function_names) + if test_framework == "junit4": + test_imports = "import org.junit.Test;\nimport org.junit.AfterClass;\n" + cleanup_annotation = "@AfterClass" + class_modifier = "public " + else: + test_imports = "import org.junit.jupiter.api.Test;\nimport org.junit.jupiter.api.AfterAll;\n" + cleanup_annotation = "@AfterAll" + class_modifier = "" + test_content = ( f"// codeflash:functions={functions_comment}\n" f"// codeflash:trace_file={trace_db_path.as_posix()}\n" f"// codeflash:classname={classname}\n" f"package codeflash.replay;\n\n" - f"import org.junit.jupiter.api.Test;\n" - f"import org.junit.jupiter.api.AfterAll;\n" + f"{test_imports}" f"import com.codeflash.ReplayHelper;\n\n" - f"class {test_class_name} {{\n" + f"{class_modifier}class {test_class_name} {{\n" f" private static final ReplayHelper helper =\n" f' new ReplayHelper("{trace_db_path.as_posix()}");\n\n' - f" @AfterAll static void cleanup() {{ helper.close(); }}\n\n" + "\n\n".join(test_methods_code) + "\n" + f" {cleanup_annotation} public static void cleanup() {{ helper.close(); }}\n\n" + + "\n\n".join(test_methods_code) + + "\n" "}\n" ) diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py index 91b06eab7..5ad449088 100644 --- a/codeflash/languages/java/tracer.py +++ b/codeflash/languages/java/tracer.py @@ -180,6 +180,7 @@ def run_java_tracer( max_function_count: int = 256, timeout: int = 0, max_run_count: int = 256, + test_framework: str = "junit5", ) -> tuple[Path, Path, int]: """High-level entry point: trace a Java command and generate replay tests. @@ -196,7 +197,11 @@ def run_java_tracer( ) test_count = generate_replay_tests( - trace_db_path=trace_db, output_dir=output_dir, project_root=project_root, max_run_count=max_run_count + trace_db_path=trace_db, + output_dir=output_dir, + project_root=project_root, + max_run_count=max_run_count, + test_framework=test_framework, ) return trace_db, jfr_file, test_count diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 892a2a694..5f8a1a4ab 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -380,6 +380,12 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser: sys.exit(1) java_command = remaining + # Detect test framework for replay test generation + from codeflash.languages.java.config import detect_java_project + + java_config = detect_java_project(project_root) + test_framework = java_config.test_framework if java_config else "junit5" + trace_db, jfr_file, test_count = run_java_tracer( java_command=java_command, trace_db_path=trace_db_path, @@ -388,6 +394,7 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser: output_dir=output_dir, max_function_count=max_function_count, timeout=timeout, + test_framework=test_framework, ) console.print(f"[bold green]Java tracing complete:[/] {test_count} replay test files generated") From 9b1fc1461de45ccdaf8178ae721ad70c4e2e806d Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 19:37:54 -0700 Subject: [PATCH 04/26] fix: avoid duplicate method names for overloaded Java methods in replay tests Use a global counter per method name across all descriptors to generate unique test method names. Previously, overloaded methods (same name, different descriptor) would generate duplicate replay_methodName_N methods, causing compilation errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/replay_test.py | 28 ++++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/codeflash/languages/java/replay_test.py b/codeflash/languages/java/replay_test.py index 457cfe711..415b7a34e 100644 --- a/codeflash/languages/java/replay_test.py +++ b/codeflash/languages/java/replay_test.py @@ -47,9 +47,10 @@ def generate_replay_tests( test_methods_code: list[str] = [] class_function_names: list[str] = [] + # Global test counter to avoid duplicate method names for overloaded Java methods + method_name_counters: dict[str, int] = {} for method_name, descriptor in method_list: - # Count invocations for this method count_result = conn.execute( "SELECT COUNT(*) FROM function_calls WHERE classname = ? AND function = ? AND descriptor = ?", (classname, method_name, descriptor), @@ -60,21 +61,18 @@ def generate_replay_tests( safe_method = _sanitize_identifier(method_name) for i in range(invocation_count): + # Use a global counter per method name to avoid collisions on overloaded methods + test_idx = method_name_counters.get(safe_method, 0) + method_name_counters[safe_method] = test_idx + 1 + escaped_descriptor = descriptor.replace('"', '\\"') - if test_framework == "junit4": - test_methods_code.append( - f" @Test public void replay_{safe_method}_{i}() throws Exception {{\n" - f' helper.replay("{classname}", "{method_name}", ' - f'"{escaped_descriptor}", {i});\n' - f" }}" - ) - else: - test_methods_code.append( - f" @Test void replay_{safe_method}_{i}() throws Exception {{\n" - f' helper.replay("{classname}", "{method_name}", ' - f'"{escaped_descriptor}", {i});\n' - f" }}" - ) + access = "public " if test_framework == "junit4" else "" + test_methods_code.append( + f" @Test {access}void replay_{safe_method}_{test_idx}() throws Exception {{\n" + f' helper.replay("{classname}", "{method_name}", ' + f'"{escaped_descriptor}", {i});\n' + f" }}" + ) all_function_names.extend(class_function_names) From 721655fdd149204dd8a23d72e5d4a9616623bda0 Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 19:39:25 -0700 Subject: [PATCH 05/26] test: add tests for JUnit 4 support, overload handling, instrumentation skip 10 new tests covering: - JUnit 5 replay test generation (imports, class visibility) - JUnit 4 replay test generation (imports, public methods, @AfterClass) - Overloaded method handling (no duplicate test method names) - Instrumentation skip for replay tests (behavior + perf mode) - Regular tests still get instrumented normally Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test_java/test_replay_test_generation.py | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 tests/test_languages/test_java/test_replay_test_generation.py diff --git a/tests/test_languages/test_java/test_replay_test_generation.py b/tests/test_languages/test_java/test_replay_test_generation.py new file mode 100644 index 000000000..5b40c6f9c --- /dev/null +++ b/tests/test_languages/test_java/test_replay_test_generation.py @@ -0,0 +1,246 @@ +"""Tests for Java replay test generation — JUnit 4/5 support, overload handling, instrumentation skip.""" + +from __future__ import annotations + +import sqlite3 +from pathlib import Path + +import pytest + +from codeflash.languages.java.replay_test import generate_replay_tests, parse_replay_test_metadata + + +@pytest.fixture +def trace_db(tmp_path: Path) -> Path: + """Create a trace database with sample function calls.""" + db_path = tmp_path / "trace.db" + conn = sqlite3.connect(str(db_path)) + conn.execute( + "CREATE TABLE function_calls(" + "type TEXT, function TEXT, classname TEXT, filename TEXT, " + "line_number INTEGER, descriptor TEXT, time_ns INTEGER, args BLOB)" + ) + conn.execute("CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT)") + conn.execute( + "INSERT INTO function_calls VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + ("call", "add", "com.example.Calculator", "Calculator.java", 10, "(II)I", 1000, b"\x00"), + ) + conn.execute( + "INSERT INTO function_calls VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + ("call", "add", "com.example.Calculator", "Calculator.java", 10, "(II)I", 2000, b"\x00"), + ) + conn.execute( + "INSERT INTO function_calls VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + ("call", "multiply", "com.example.Calculator", "Calculator.java", 20, "(II)I", 3000, b"\x00"), + ) + conn.commit() + conn.close() + return db_path + + +@pytest.fixture +def trace_db_overloaded(tmp_path: Path) -> Path: + """Create a trace database with overloaded methods (same name, different descriptors).""" + db_path = tmp_path / "trace_overloaded.db" + conn = sqlite3.connect(str(db_path)) + conn.execute( + "CREATE TABLE function_calls(" + "type TEXT, function TEXT, classname TEXT, filename TEXT, " + "line_number INTEGER, descriptor TEXT, time_ns INTEGER, args BLOB)" + ) + conn.execute("CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT)") + # Two overloads of estimateKeySize with different descriptors + for i in range(3): + conn.execute( + "INSERT INTO function_calls VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + ("call", "estimateKeySize", "com.example.Command", "Command.java", 10, "(I)I", i * 1000, b"\x00"), + ) + for i in range(2): + conn.execute( + "INSERT INTO function_calls VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + ( + "call", + "estimateKeySize", + "com.example.Command", + "Command.java", + 15, + "(Ljava/lang/String;)I", + (i + 10) * 1000, + b"\x00", + ), + ) + conn.commit() + conn.close() + return db_path + + +class TestGenerateReplayTestsJunit5: + def test_generates_junit5_by_default(self, trace_db: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + count = generate_replay_tests(trace_db, output_dir, tmp_path) + assert count == 1 + + test_file = list(output_dir.glob("*.java"))[0] + content = test_file.read_text(encoding="utf-8") + assert "import org.junit.jupiter.api.Test;" in content + assert "import org.junit.jupiter.api.AfterAll;" in content + assert "@Test void replay_add_0()" in content + + def test_junit5_class_is_package_private(self, trace_db: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + generate_replay_tests(trace_db, output_dir, tmp_path) + + test_file = list(output_dir.glob("*.java"))[0] + content = test_file.read_text(encoding="utf-8") + assert "class ReplayTest_" in content + assert "public class ReplayTest_" not in content + + +class TestGenerateReplayTestsJunit4: + def test_generates_junit4_imports(self, trace_db: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + count = generate_replay_tests(trace_db, output_dir, tmp_path, test_framework="junit4") + assert count == 1 + + test_file = list(output_dir.glob("*.java"))[0] + content = test_file.read_text(encoding="utf-8") + assert "import org.junit.Test;" in content + assert "import org.junit.AfterClass;" in content + assert "org.junit.jupiter" not in content + + def test_junit4_methods_are_public(self, trace_db: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + generate_replay_tests(trace_db, output_dir, tmp_path, test_framework="junit4") + + test_file = list(output_dir.glob("*.java"))[0] + content = test_file.read_text(encoding="utf-8") + assert "@Test public void replay_add_0()" in content + + def test_junit4_class_is_public(self, trace_db: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + generate_replay_tests(trace_db, output_dir, tmp_path, test_framework="junit4") + + test_file = list(output_dir.glob("*.java"))[0] + content = test_file.read_text(encoding="utf-8") + assert "public class ReplayTest_" in content + + def test_junit4_cleanup_uses_afterclass(self, trace_db: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + generate_replay_tests(trace_db, output_dir, tmp_path, test_framework="junit4") + + test_file = list(output_dir.glob("*.java"))[0] + content = test_file.read_text(encoding="utf-8") + assert "@AfterClass" in content + assert "@AfterAll" not in content + + +class TestOverloadedMethods: + def test_no_duplicate_method_names(self, trace_db_overloaded: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + count = generate_replay_tests(trace_db_overloaded, output_dir, tmp_path) + assert count == 1 + + test_file = list(output_dir.glob("*.java"))[0] + content = test_file.read_text(encoding="utf-8") + + # Should have 5 unique methods (3 from first overload + 2 from second) + assert "replay_estimateKeySize_0" in content + assert "replay_estimateKeySize_1" in content + assert "replay_estimateKeySize_2" in content + assert "replay_estimateKeySize_3" in content + assert "replay_estimateKeySize_4" in content + + # Verify no duplicates by counting occurrences + lines = content.splitlines() + method_lines = [l for l in lines if "void replay_estimateKeySize_" in l] + method_names = [l.split("void ")[1].split("(")[0] for l in method_lines] + assert len(method_names) == len(set(method_names)), f"Duplicate methods: {method_names}" + + +class TestReplayTestInstrumentationSkip: + def test_skip_instrumentation_for_replay_tests(self, trace_db: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + generate_replay_tests(trace_db, output_dir, tmp_path) + + test_file = list(output_dir.glob("*.java"))[0] + + from codeflash.languages.java.support import JavaSupport + + support = JavaSupport() + + # Instrument in behavior mode + success, instrumented = support.instrument_existing_test( + test_path=test_file, + call_positions=[], + function_to_optimize=None, + tests_project_root=tmp_path, + mode="behavior", + ) + assert success + assert instrumented is not None + + # Should just rename the class, no behavior setup code + assert "__perfinstrumented" in instrumented + assert "CODEFLASH_LOOP_INDEX" not in instrumented + assert "// Codeflash behavior instrumentation" not in instrumented + + def test_skip_instrumentation_for_perf_mode(self, trace_db: Path, tmp_path: Path) -> None: + output_dir = tmp_path / "output" + generate_replay_tests(trace_db, output_dir, tmp_path) + + test_file = list(output_dir.glob("*.java"))[0] + + from codeflash.languages.java.support import JavaSupport + + support = JavaSupport() + + success, instrumented = support.instrument_existing_test( + test_path=test_file, + call_positions=[], + function_to_optimize=None, + tests_project_root=tmp_path, + mode="performance", + ) + assert success + assert "__perfonlyinstrumented" in instrumented + + def test_regular_tests_still_get_instrumented(self, tmp_path: Path) -> None: + """Non-replay test files should still be instrumented normally.""" + from codeflash.languages.java.discovery import discover_functions_from_source + + src = """ +public class Calculator { + public int add(int a, int b) { return a + b; } +} +""" + funcs = discover_functions_from_source(src, tmp_path / "Calculator.java") + target = funcs[0] + + test_file = tmp_path / "CalculatorTest.java" + test_file.write_text( + """ +import org.junit.jupiter.api.Test; +public class CalculatorTest { + @Test + public void testAdd() { + Calculator calc = new Calculator(); + calc.add(1, 2); + } +} +""", + encoding="utf-8", + ) + + from codeflash.languages.java.support import JavaSupport + + support = JavaSupport() + success, instrumented = support.instrument_existing_test( + test_path=test_file, + call_positions=[], + function_to_optimize=target, + tests_project_root=tmp_path, + mode="behavior", + ) + assert success + # Regular tests should have behavior instrumentation + assert "CODEFLASH_LOOP_INDEX" in instrumented From b0d4a5e8bfced5bb7d1b2e848e921c7d1f77ce7d Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 19:43:31 -0700 Subject: [PATCH 06/26] test: add tests for JFR parser, graceful timeout, and project root resolution 13 new tests covering: - JFR class name normalization (/ to . conversion) - Package-based sample filtering - Addressable time calculation from JFR samples - Method ranking order and format - Graceful timeout (SIGTERM before SIGKILL) - Multi-module project root detection (Path not str) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test_java/test_jfr_parser.py | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 tests/test_languages/test_java/test_jfr_parser.py diff --git a/tests/test_languages/test_java/test_jfr_parser.py b/tests/test_languages/test_java/test_jfr_parser.py new file mode 100644 index 000000000..8c883c0f2 --- /dev/null +++ b/tests/test_languages/test_java/test_jfr_parser.py @@ -0,0 +1,301 @@ +"""Tests for JFR parser — class name normalization, package filtering, addressable time.""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path +from unittest.mock import patch + +import pytest + +from codeflash.languages.java.jfr_parser import JfrProfile + + +def _make_jfr_json(events: list[dict]) -> str: + """Create fake JFR JSON output matching the jfr print format.""" + return json.dumps({"recording": {"events": events}}) + + +def _make_execution_sample(class_name: str, method_name: str, start_time: str = "2026-01-01T00:00:00Z") -> dict: + return { + "type": "jdk.ExecutionSample", + "values": { + "startTime": start_time, + "stackTrace": { + "frames": [ + { + "method": { + "type": {"name": class_name}, + "name": method_name, + "descriptor": "()V", + }, + "lineNumber": 42, + } + ], + }, + }, + } + + +class TestClassNameNormalization: + """Test that JVM internal class names (com/example/Foo) are normalized to dots (com.example.Foo).""" + + def test_slash_separators_normalized_to_dots(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + jfr_json = _make_jfr_json( + [ + _make_execution_sample("com/aerospike/client/command/Buffer", "bytesToInt"), + _make_execution_sample("com/aerospike/client/command/Buffer", "bytesToInt"), + _make_execution_sample("com/aerospike/client/util/Utf8", "encodedLength"), + ] + ) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, ["com.aerospike"]) + + assert profile._total_samples == 3 + assert len(profile._method_samples) == 2 + + # Keys should use dots, not slashes + assert "com.aerospike.client.command.Buffer.bytesToInt" in profile._method_samples + assert "com.aerospike.client.util.Utf8.encodedLength" in profile._method_samples + + def test_method_info_uses_dot_class_names(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + jfr_json = _make_jfr_json( + [_make_execution_sample("com/example/MyClass", "myMethod")] + ) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, ["com.example"]) + + info = profile._method_info.get("com.example.MyClass.myMethod") + assert info is not None + assert info["class_name"] == "com.example.MyClass" + assert info["method_name"] == "myMethod" + + +class TestPackageFiltering: + def test_filters_by_package_prefix(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + jfr_json = _make_jfr_json( + [ + _make_execution_sample("com/aerospike/client/Value", "get"), + _make_execution_sample("java/util/HashMap", "put"), + _make_execution_sample("com/aerospike/benchmarks/Main", "main"), + ] + ) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, ["com.aerospike"]) + + # Only com.aerospike classes should be in samples + assert len(profile._method_samples) == 2 + assert "com.aerospike.client.Value.get" in profile._method_samples + assert "com.aerospike.benchmarks.Main.main" in profile._method_samples + assert "java.util.HashMap.put" not in profile._method_samples + + def test_empty_packages_includes_all(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + jfr_json = _make_jfr_json( + [ + _make_execution_sample("com/example/Foo", "bar"), + _make_execution_sample("java/lang/String", "length"), + ] + ) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, []) + + assert len(profile._method_samples) == 2 + + +class TestAddressableTime: + def test_addressable_time_proportional_to_samples(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + # 3 samples for methodA, 1 for methodB, spanning 10 seconds + jfr_json = _make_jfr_json( + [ + _make_execution_sample("com/example/Foo", "methodA", "2026-01-01T00:00:00Z"), + _make_execution_sample("com/example/Foo", "methodA", "2026-01-01T00:00:03Z"), + _make_execution_sample("com/example/Foo", "methodA", "2026-01-01T00:00:06Z"), + _make_execution_sample("com/example/Foo", "methodB", "2026-01-01T00:00:10Z"), + ] + ) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, ["com.example"]) + + time_a = profile.get_addressable_time_ns("com.example.Foo", "methodA") + time_b = profile.get_addressable_time_ns("com.example.Foo", "methodB") + + # methodA has 3x the samples of methodB, so 3x the addressable time + assert time_a > 0 + assert time_b > 0 + assert time_a == pytest.approx(time_b * 3, rel=0.01) + + def test_addressable_time_zero_for_unknown_method(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + jfr_json = _make_jfr_json( + [_make_execution_sample("com/example/Foo", "bar")] + ) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, ["com.example"]) + + assert profile.get_addressable_time_ns("com.example.Foo", "nonExistent") == 0.0 + + +class TestMethodRanking: + def test_ranking_ordered_by_sample_count(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + jfr_json = _make_jfr_json( + [ + _make_execution_sample("com/example/A", "hot"), + _make_execution_sample("com/example/A", "hot"), + _make_execution_sample("com/example/A", "hot"), + _make_execution_sample("com/example/B", "warm"), + _make_execution_sample("com/example/B", "warm"), + _make_execution_sample("com/example/C", "cold"), + ] + ) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, ["com.example"]) + + ranking = profile.get_method_ranking() + assert len(ranking) == 3 + assert ranking[0]["method_name"] == "hot" + assert ranking[0]["sample_count"] == 3 + assert ranking[1]["method_name"] == "warm" + assert ranking[1]["sample_count"] == 2 + assert ranking[2]["method_name"] == "cold" + assert ranking[2]["sample_count"] == 1 + + def test_empty_ranking_when_no_samples(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + jfr_json = _make_jfr_json([]) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, ["com.example"]) + + assert profile.get_method_ranking() == [] + + def test_ranking_uses_dot_class_names(self, tmp_path: Path) -> None: + jfr_file = tmp_path / "test.jfr" + jfr_file.write_text("dummy", encoding="utf-8") + + jfr_json = _make_jfr_json( + [_make_execution_sample("com/example/nested/Deep", "method")] + ) + + with patch("subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=jfr_json, stderr="") + profile = JfrProfile(jfr_file, ["com.example"]) + + ranking = profile.get_method_ranking() + assert len(ranking) == 1 + assert ranking[0]["class_name"] == "com.example.nested.Deep" + + +class TestGracefulTimeout: + """Test that _run_java_with_graceful_timeout sends SIGTERM before SIGKILL.""" + + def test_sends_sigterm_on_timeout(self) -> None: + import signal + + from codeflash.languages.java.tracer import _run_java_with_graceful_timeout + + # Run a sleep command with a 1s timeout — should get SIGTERM'd + import os + + env = os.environ.copy() + _run_java_with_graceful_timeout(["sleep", "60"], env, timeout=1, stage_name="test") + # If we get here, the process was killed (didn't hang for 60s) + + def test_no_timeout_runs_normally(self) -> None: + import os + + from codeflash.languages.java.tracer import _run_java_with_graceful_timeout + + env = os.environ.copy() + _run_java_with_graceful_timeout(["echo", "hello"], env, timeout=0, stage_name="test") + # Should complete without error + + +class TestProjectRootResolution: + """Test that project_root is correctly set for Java multi-module projects.""" + + def test_java_project_root_is_build_root_not_module(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """For multi-module Maven, project_root should be the root with , not a sub-module.""" + # Create a multi-module project + (tmp_path / "pom.xml").write_text( + 'client', + encoding="utf-8", + ) + client = tmp_path / "client" + client.mkdir() + (client / "pom.xml").write_text("", encoding="utf-8") + src = client / "src" / "main" / "java" + src.mkdir(parents=True) + test = tmp_path / "src" / "test" / "java" + test.mkdir(parents=True) + monkeypatch.chdir(tmp_path) + + from codeflash.code_utils.config_parser import parse_config_file + + config, config_path = parse_config_file() + assert config["language"] == "java" + + # config_path should be the project root directory + assert config_path == tmp_path + + def test_project_root_is_path_not_string(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """project_root from process_pyproject_config should be a Path for Java projects.""" + (tmp_path / "pom.xml").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) + monkeypatch.chdir(tmp_path) + + import sys + from argparse import Namespace + + sys.argv = ["codeflash", "optimize", "java", "-jar", "app.jar"] + from codeflash.cli_cmds.cli import parse_args, process_pyproject_config + + from codeflash.cli_cmds.cli import _build_parser + _build_parser.cache_clear() + + args = parse_args() + args = process_pyproject_config(args) + + assert hasattr(args, "project_root") + assert isinstance(args.project_root, Path) + assert args.project_root == tmp_path From d441bb9761f5b0e681a3312593dd7039d92a551c Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 20:10:20 -0700 Subject: [PATCH 07/26] fix: properly instrument replay tests instead of skipping them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The behavior instrumentation was producing malformed output for compact @Test lines (annotation + method signature on same line, common in replay tests). The method signature collection loop would skip past the opening brace and consume subsequent methods' content. Fix: detect when the @Test annotation line already contains { and treat it as both annotation and method signature, avoiding the separate signature search that was over-consuming lines. Reverted the instrumentation skip for replay tests — they now get properly instrumented for both behavior capture and performance timing. Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/instrumentation.py | 41 ++++++++++------ codeflash/languages/java/support.py | 21 +------- .../test_java/test_replay_test_generation.py | 49 +++++++++++-------- 3 files changed, 55 insertions(+), 56 deletions(-) diff --git a/codeflash/languages/java/instrumentation.py b/codeflash/languages/java/instrumentation.py index 9ecbd613e..914fe7a70 100644 --- a/codeflash/languages/java/instrumentation.py +++ b/codeflash/languages/java/instrumentation.py @@ -785,26 +785,35 @@ def _add_behavior_instrumentation(source: str, class_name: str, func_name: str, if _is_test_annotation(stripped): if not helper_added: helper_added = True - result.append(line) - i += 1 - # Collect any additional annotations - while i < len(lines) and lines[i].strip().startswith("@"): - result.append(lines[i]) + # Check if the @Test line already contains the method signature and opening brace + # (common in compact test styles like replay tests: @Test void replay_foo_0() throws Exception {) + if "{" in line: + # The annotation line IS the method signature — don't look for a separate one + result.append(line) i += 1 - - # Now find the method signature and opening brace - method_lines = [] - while i < len(lines): - method_lines.append(lines[i]) - if "{" in lines[i]: - break + method_lines = [line] + else: + result.append(line) i += 1 - # Add the method signature lines - for ml in method_lines: - result.append(ml) - i += 1 + # Collect any additional annotations + while i < len(lines) and lines[i].strip().startswith("@"): + result.append(lines[i]) + i += 1 + + # Now find the method signature and opening brace + method_lines = [] + while i < len(lines): + method_lines.append(lines[i]) + if "{" in lines[i]: + break + i += 1 + + # Add the method signature lines + for ml in method_lines: + result.append(ml) + i += 1 # Extract the test method name from the method signature test_method_name = _extract_test_method_name(method_lines) diff --git a/codeflash/languages/java/support.py b/codeflash/languages/java/support.py index 31959426f..825c7e7da 100644 --- a/codeflash/languages/java/support.py +++ b/codeflash/languages/java/support.py @@ -582,27 +582,8 @@ def instrument_existing_test( tests_project_root: Path, mode: str, ) -> tuple[bool, str | None]: - """Inject profiling code into an existing test file. - - For replay test files (generated by the tracer), skip instrumentation — - they call helper.replay() via reflection, not the target function directly. - The behavior instrumentation can't wrap indirect calls and produces - malformed output for large replay test files. - """ + """Inject profiling code into an existing test file.""" test_string = test_path.read_text(encoding="utf-8") - - # Skip instrumentation for replay tests — just rename the class - if test_string.lstrip().startswith("// codeflash:"): - import re - - original_class = test_path.stem - if mode == "behavior": - new_class = f"{original_class}__perfinstrumented" - else: - new_class = f"{original_class}__perfonlyinstrumented" - modified = re.sub(rf"\b{re.escape(original_class)}\b", new_class, test_string) - return True, modified - return instrument_existing_test( test_string=test_string, function_to_optimize=function_to_optimize, mode=mode, test_path=test_path ) diff --git a/tests/test_languages/test_java/test_replay_test_generation.py b/tests/test_languages/test_java/test_replay_test_generation.py index 5b40c6f9c..da7138114 100644 --- a/tests/test_languages/test_java/test_replay_test_generation.py +++ b/tests/test_languages/test_java/test_replay_test_generation.py @@ -157,62 +157,72 @@ def test_no_duplicate_method_names(self, trace_db_overloaded: Path, tmp_path: Pa assert len(method_names) == len(set(method_names)), f"Duplicate methods: {method_names}" -class TestReplayTestInstrumentationSkip: - def test_skip_instrumentation_for_replay_tests(self, trace_db: Path, tmp_path: Path) -> None: +class TestReplayTestInstrumentation: + def test_replay_tests_instrumented_correctly(self, trace_db: Path, tmp_path: Path) -> None: + """Replay tests with compact @Test lines should be instrumented without orphaned code.""" + from codeflash.languages.java.discovery import discover_functions_from_source + output_dir = tmp_path / "output" generate_replay_tests(trace_db, output_dir, tmp_path) test_file = list(output_dir.glob("*.java"))[0] + src = "public class Calculator { public int add(int a, int b) { return a + b; } }" + funcs = discover_functions_from_source(src, tmp_path / "Calculator.java") + target = funcs[0] + from codeflash.languages.java.support import JavaSupport support = JavaSupport() - - # Instrument in behavior mode success, instrumented = support.instrument_existing_test( test_path=test_file, call_positions=[], - function_to_optimize=None, + function_to_optimize=target, tests_project_root=tmp_path, mode="behavior", ) assert success assert instrumented is not None - - # Should just rename the class, no behavior setup code assert "__perfinstrumented" in instrumented - assert "CODEFLASH_LOOP_INDEX" not in instrumented - assert "// Codeflash behavior instrumentation" not in instrumented - def test_skip_instrumentation_for_perf_mode(self, trace_db: Path, tmp_path: Path) -> None: + # Verify no code outside class body + lines = instrumented.splitlines() + class_closed = False + for line in lines: + if line.strip() == "}" and not line.startswith(" "): + class_closed = True + elif class_closed and line.strip() and not line.strip().startswith("//"): + pytest.fail(f"Orphaned code outside class: {line}") + + def test_replay_tests_perf_instrumented(self, trace_db: Path, tmp_path: Path) -> None: + from codeflash.languages.java.discovery import discover_functions_from_source + output_dir = tmp_path / "output" generate_replay_tests(trace_db, output_dir, tmp_path) test_file = list(output_dir.glob("*.java"))[0] + src = "public class Calculator { public int add(int a, int b) { return a + b; } }" + funcs = discover_functions_from_source(src, tmp_path / "Calculator.java") + target = funcs[0] + from codeflash.languages.java.support import JavaSupport support = JavaSupport() - success, instrumented = support.instrument_existing_test( test_path=test_file, call_positions=[], - function_to_optimize=None, + function_to_optimize=target, tests_project_root=tmp_path, mode="performance", ) assert success assert "__perfonlyinstrumented" in instrumented - def test_regular_tests_still_get_instrumented(self, tmp_path: Path) -> None: - """Non-replay test files should still be instrumented normally.""" + def test_regular_tests_still_instrumented(self, tmp_path: Path) -> None: from codeflash.languages.java.discovery import discover_functions_from_source - src = """ -public class Calculator { - public int add(int a, int b) { return a + b; } -} -""" + src = "public class Calculator { public int add(int a, int b) { return a + b; } }" funcs = discover_functions_from_source(src, tmp_path / "Calculator.java") target = funcs[0] @@ -242,5 +252,4 @@ def test_regular_tests_still_get_instrumented(self, tmp_path: Path) -> None: mode="behavior", ) assert success - # Regular tests should have behavior instrumentation assert "CODEFLASH_LOOP_INDEX" in instrumented From c087d0d82e2d5da92059b32d061df47cf23f39ff Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 20:34:49 -0700 Subject: [PATCH 08/26] feat: smart ReplayHelper with behavior capture and performance timing ReplayHelper now reads CODEFLASH_MODE env var and produces the same output as the existing test instrumentation: - Behavior mode: captures return value via Kryo serialization, writes to SQLite (test_results table) for correctness comparison, prints start/end timing markers - Performance mode: runs inner loop for JIT warmup, prints timing markers for each iteration matching the expected format - No mode: just invokes the method (trace-only or manual testing) This achieves feature parity with the existing test instrumentation for replay tests, which call functions via reflection and can't be wrapped by text-level instrumentation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/com/codeflash/ReplayHelper.java | 236 +++++++++++++++--- .../resources/codeflash-runtime-1.0.0.jar | Bin 15973968 -> 15976923 bytes 2 files changed, 198 insertions(+), 38 deletions(-) diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java b/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java index f4b9ec453..3a73038c1 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java @@ -12,20 +12,179 @@ public class ReplayHelper { - private final Connection db; + private final Connection traceDb; + + // Codeflash instrumentation state — read from environment variables once + private final String mode; // "behavior", "performance", or null + private final int loopIndex; + private final String testIteration; + private final String outputFile; // SQLite path for behavior capture + private final int innerIterations; // for performance looping + + // Behavior mode: lazily opened SQLite connection for writing results + private Connection behaviorDb; + private boolean behaviorDbInitialized; public ReplayHelper(String traceDbPath) { try { - this.db = DriverManager.getConnection("jdbc:sqlite:" + traceDbPath); + this.traceDb = DriverManager.getConnection("jdbc:sqlite:" + traceDbPath); } catch (SQLException e) { throw new RuntimeException("Failed to open trace database: " + traceDbPath, e); } + + // Read codeflash instrumentation env vars (set by the test runner) + this.mode = System.getenv("CODEFLASH_MODE"); + this.loopIndex = parseIntEnv("CODEFLASH_LOOP_INDEX", 1); + this.testIteration = getEnvOrDefault("CODEFLASH_TEST_ITERATION", "0"); + this.outputFile = System.getenv("CODEFLASH_OUTPUT_FILE"); + this.innerIterations = parseIntEnv("CODEFLASH_INNER_ITERATIONS", 10); } public void replay(String className, String methodName, String descriptor, int invocationIndex) throws Exception { - // Query the function_calls table for this method at the given index + // Deserialize args and resolve method (done once, outside timing) + Object[] allArgs = loadArgs(className, methodName, descriptor, invocationIndex); + Class targetClass = Class.forName(className); + + Type[] paramTypes = Type.getArgumentTypes(descriptor); + Class[] paramClasses = new Class[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + paramClasses[i] = typeToClass(paramTypes[i]); + } + + Method method = targetClass.getDeclaredMethod(methodName, paramClasses); + method.setAccessible(true); + boolean isStatic = Modifier.isStatic(method.getModifiers()); + + Object instance = null; + if (!isStatic) { + try { + java.lang.reflect.Constructor ctor = targetClass.getDeclaredConstructor(); + ctor.setAccessible(true); + instance = ctor.newInstance(); + } catch (NoSuchMethodException e) { + instance = new org.objenesis.ObjenesisStd().newInstance(targetClass); + } + } + + // Get the calling test method name from the stack trace + String testMethodName = getCallingTestMethodName(); + // Module name = the test class that called us + String testClassName = getCallingTestClassName(); + + if ("behavior".equals(mode)) { + replayBehavior(method, instance, allArgs, className, methodName, testClassName, testMethodName); + } else if ("performance".equals(mode)) { + replayPerformance(method, instance, allArgs, className, methodName, testClassName, testMethodName); + } else { + // No codeflash mode — just invoke (trace-only or manual testing) + method.invoke(instance, allArgs); + } + } + + private void replayBehavior(Method method, Object instance, Object[] args, + String className, String methodName, + String testClassName, String testMethodName) throws Exception { + String invId = testIteration + "_" + testMethodName; + + // Print start marker (same format as behavior instrumentation) + System.out.println("!$######" + testClassName + ":" + testClassName + "." + testMethodName + + ":" + methodName + ":" + loopIndex + ":" + invId + "######$!"); + + long startNs = System.nanoTime(); + Object result; + try { + result = method.invoke(instance, args); + } catch (java.lang.reflect.InvocationTargetException e) { + throw (Exception) e.getCause(); + } + long durationNs = System.nanoTime() - startNs; + + // Print end marker + System.out.println("!######" + testClassName + ":" + testClassName + "." + testMethodName + + ":" + methodName + ":" + loopIndex + ":" + invId + ":" + durationNs + "######!"); + + // Write return value to SQLite for correctness comparison + if (outputFile != null && !outputFile.isEmpty()) { + writeBehaviorResult(testClassName, testMethodName, methodName, invId, durationNs, result); + } + } + + private void replayPerformance(Method method, Object instance, Object[] args, + String className, String methodName, + String testClassName, String testMethodName) throws Exception { + // Performance mode: run inner loop for JIT warmup, print timing for each iteration + int maxInner = innerIterations; + for (int inner = 0; inner < maxInner; inner++) { + int loopId = (loopIndex - 1) * maxInner + inner; + String invId = testMethodName; + + // Print start marker + System.out.println("!$######" + testClassName + ":" + testClassName + "." + testMethodName + + ":" + methodName + ":" + loopId + ":" + invId + "######$!"); + + long startNs = System.nanoTime(); + try { + method.invoke(instance, args); + } catch (java.lang.reflect.InvocationTargetException e) { + // Swallow — performance mode doesn't check correctness + } + long durationNs = System.nanoTime() - startNs; + + // Print end marker + System.out.println("!######" + testClassName + ":" + testClassName + "." + testMethodName + + ":" + methodName + ":" + loopId + ":" + invId + ":" + durationNs + "######!"); + } + } + + private void writeBehaviorResult(String testClassName, String testMethodName, + String functionName, String invId, + long durationNs, Object result) { + try { + ensureBehaviorDb(); + String sql = "INSERT INTO test_results VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = behaviorDb.prepareStatement(sql)) { + ps.setString(1, testClassName); // test_module_path + ps.setString(2, testClassName); // test_class_name + ps.setString(3, testMethodName); // test_function_name + ps.setString(4, functionName); // function_getting_tested + ps.setInt(5, loopIndex); // loop_index + ps.setString(6, invId); // iteration_id + ps.setLong(7, durationNs); // runtime + ps.setBytes(8, serializeResult(result)); // return_value + ps.setString(9, "function_call"); // verification_type + ps.executeUpdate(); + } + } catch (Exception e) { + System.err.println("ReplayHelper: SQLite behavior write error: " + e.getMessage()); + } + } + + private void ensureBehaviorDb() throws SQLException { + if (behaviorDbInitialized) return; + behaviorDbInitialized = true; + behaviorDb = DriverManager.getConnection("jdbc:sqlite:" + outputFile); + try (java.sql.Statement stmt = behaviorDb.createStatement()) { + stmt.execute("CREATE TABLE IF NOT EXISTS test_results (" + + "test_module_path TEXT, test_class_name TEXT, test_function_name TEXT, " + + "function_getting_tested TEXT, loop_index INTEGER, iteration_id TEXT, " + + "runtime INTEGER, return_value BLOB, verification_type TEXT)"); + } + } + + private byte[] serializeResult(Object result) { + if (result == null) return null; + try { + return Serializer.serialize(result); + } catch (Exception e) { + // Fall back to String.valueOf if Kryo fails + return String.valueOf(result).getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + } + + private Object[] loadArgs(String className, String methodName, String descriptor, int invocationIndex) + throws SQLException { byte[] argsBlob; - try (PreparedStatement stmt = db.prepareStatement( + try (PreparedStatement stmt = traceDb.prepareStatement( "SELECT args FROM function_calls " + "WHERE classname = ? AND function = ? AND descriptor = ? " + "ORDER BY time_ns LIMIT 1 OFFSET ?")) { @@ -43,46 +202,35 @@ public void replay(String className, String methodName, String descriptor, int i } } - // Deserialize args Object deserialized = Serializer.deserialize(argsBlob); if (!(deserialized instanceof Object[])) { throw new RuntimeException("Deserialized args is not Object[], got: " + (deserialized == null ? "null" : deserialized.getClass().getName())); } - Object[] allArgs = (Object[]) deserialized; - - // Load the target class - Class targetClass = Class.forName(className); + return (Object[]) deserialized; + } - // Parse descriptor to find parameter types - Type[] paramTypes = Type.getArgumentTypes(descriptor); - Class[] paramClasses = new Class[paramTypes.length]; - for (int i = 0; i < paramTypes.length; i++) { - paramClasses[i] = typeToClass(paramTypes[i]); + private static String getCallingTestMethodName() { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + // Walk up: [0]=getStackTrace, [1]=this method, [2]=replay(), [3]=calling test method + for (int i = 3; i < stack.length; i++) { + String method = stack[i].getMethodName(); + if (method.startsWith("replay_")) { + return method; + } } + return stack.length > 3 ? stack[3].getMethodName() : "unknown"; + } - // Find the method - Method method = targetClass.getDeclaredMethod(methodName, paramClasses); - method.setAccessible(true); - - boolean isStatic = Modifier.isStatic(method.getModifiers()); - - if (isStatic) { - method.invoke(null, allArgs); - } else { - // Args contain only explicit parameters (no 'this'). - // Create a default instance via no-arg constructor or Kryo. - Object instance; - try { - java.lang.reflect.Constructor ctor = targetClass.getDeclaredConstructor(); - ctor.setAccessible(true); - instance = ctor.newInstance(); - } catch (NoSuchMethodException e) { - // Fall back to Objenesis instantiation (no constructor needed) - instance = new org.objenesis.ObjenesisStd().newInstance(targetClass); + private static String getCallingTestClassName() { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (int i = 3; i < stack.length; i++) { + String cls = stack[i].getClassName(); + if (cls.contains("ReplayTest") || cls.contains("replay")) { + return cls; } - method.invoke(instance, allArgs); } + return stack.length > 3 ? stack[3].getClassName() : "unknown"; } private static Class typeToClass(Type type) throws ClassNotFoundException { @@ -106,11 +254,23 @@ private static Class typeToClass(Type type) throws ClassNotFoundException { } } + private static int parseIntEnv(String name, int defaultValue) { + String val = System.getenv(name); + if (val == null || val.isEmpty()) return defaultValue; + try { return Integer.parseInt(val); } catch (NumberFormatException e) { return defaultValue; } + } + + private static String getEnvOrDefault(String name, String defaultValue) { + String val = System.getenv(name); + return (val != null && !val.isEmpty()) ? val : defaultValue; + } + public void close() { - try { - if (db != null) db.close(); - } catch (SQLException e) { - System.err.println("Error closing ReplayHelper: " + e.getMessage()); + try { if (traceDb != null) traceDb.close(); } catch (SQLException e) { + System.err.println("Error closing ReplayHelper trace db: " + e.getMessage()); + } + try { if (behaviorDb != null) behaviorDb.close(); } catch (SQLException e) { + System.err.println("Error closing ReplayHelper behavior db: " + e.getMessage()); } } } diff --git a/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar b/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar index 10a03b3cc7b53fd7a6a87708b3cc0b412332f38a..48ebc0a96623f477df4640b0eeb733f5ab82b477 100644 GIT binary patch delta 45011 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 R`>EI|t%^gV1KF56|35)Usg?i$ delta 42432 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

^Os)`0B^neq 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

oJL#~t&}D>U><)7=Otu#s@@*MNIre;N)F98{oA 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 Date: Thu, 19 Mar 2026 22:34:56 -0700 Subject: [PATCH 09/26] fix: e2e java tracer runs on all codeflash changes and validates replay tests + speedups - Trigger on any codeflash/** or tests/** changes (not just java subset) - Validate replay test files are discovered per-function - Already validates: replay test generation, global discovery count, optimization success, and minimum speedup percentage Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/e2e-java-tracer.yaml | 12 ++---------- tests/scripts/end_to_end_test_java_tracer.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/e2e-java-tracer.yaml b/.github/workflows/e2e-java-tracer.yaml index 7e92e9eee..6ed17ce90 100644 --- a/.github/workflows/e2e-java-tracer.yaml +++ b/.github/workflows/e2e-java-tracer.yaml @@ -3,17 +3,9 @@ name: E2E - Java Tracer on: pull_request: paths: - - 'codeflash/languages/java/**' - - 'codeflash/languages/base.py' - - 'codeflash/languages/registry.py' - - 'codeflash/tracer.py' - - 'codeflash/benchmarking/function_ranker.py' - - 'codeflash/discovery/functions_to_optimize.py' - - 'codeflash/optimization/**' - - 'codeflash/verification/**' + - 'codeflash/**' - 'codeflash-java-runtime/**' - - 'tests/test_languages/fixtures/java_tracer_e2e/**' - - 'tests/scripts/end_to_end_test_java_tracer.py' + - 'tests/**' - '.github/workflows/e2e-java-tracer.yaml' workflow_dispatch: diff --git a/tests/scripts/end_to_end_test_java_tracer.py b/tests/scripts/end_to_end_test_java_tracer.py index e904a4e98..3f68d02d4 100644 --- a/tests/scripts/end_to_end_test_java_tracer.py +++ b/tests/scripts/end_to_end_test_java_tracer.py @@ -90,7 +90,7 @@ def run_test(expected_improvement_pct: int) -> bool: logging.error("Failed to find replay test generation message") return False - # Validate: replay tests were discovered + # Validate: replay tests were discovered (global count) replay_match = re.search(r"Discovered \d+ existing unit tests? and (\d+) replay tests?", stdout) if not replay_match: logging.error("Failed to find replay test discovery message") @@ -101,6 +101,17 @@ def run_test(expected_improvement_pct: int) -> bool: return False logging.info(f"Replay tests discovered: {num_replay}") + # Validate: replay test files were used per-function + replay_file_match = re.search(r"Discovered \d+ existing unit test files?, (\d+) replay test files?", stdout) + if not replay_file_match: + logging.error("Failed to find per-function replay test file discovery message") + return False + num_replay_files = int(replay_file_match.group(1)) + if num_replay_files == 0: + logging.error("No replay test files discovered per-function") + return False + logging.info(f"Replay test files per-function: {num_replay_files}") + # Validate: at least one optimization was found if "⚡️ Optimization successful! 📄 " not in stdout: logging.error("Failed to find optimization success message") From dae9b481b851433bef693663b69f7ff6aeba4349 Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 22:40:42 -0700 Subject: [PATCH 10/26] fix: restore correct argument order in process_pyproject_config The refactored Java project_root handling moved args.tests_root resolution after the project_root_from_module_root call, which passed a string instead of a Path. Restore the original order: resolve tests_root to Path first, then set test_project_root, then override both for Java multi-module projects. Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/cli_cmds/cli.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index f27817a39..c611f5cd9 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -185,16 +185,17 @@ 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 - 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.project_root = project_root_from_module_root(Path(args.module_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_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(): args.all = None return args From 74cbe2aba64e86ef936ad452573b529ecda90d1b Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 22:58:48 -0700 Subject: [PATCH 11/26] fix: Windows compatibility for Java config detection tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use Path comparisons instead of forward-slash substring matching - Avoid parse_args() in test (reads stdin on Windows) — use Namespace directly Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test_java/test_java_config_detection.py | 4 ++-- .../test_java/test_jfr_parser.py | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_languages/test_java/test_java_config_detection.py b/tests/test_languages/test_java/test_java_config_detection.py index fc5565ffb..ebb8653af 100644 --- a/tests/test_languages/test_java/test_java_config_detection.py +++ b/tests/test_languages/test_java/test_java_config_detection.py @@ -136,7 +136,7 @@ def test_defaults_when_dirs_missing(self, tmp_path: Path) -> None: 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 str(tmp_path / "src" / "main" / "java") == config["module_root"] assert config["language"] == "java" @@ -185,7 +185,7 @@ def test_properties_override_auto_detection(self, tmp_path: Path) -> None: 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"] + 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( diff --git a/tests/test_languages/test_java/test_jfr_parser.py b/tests/test_languages/test_java/test_jfr_parser.py index 8c883c0f2..8b5cf8a6e 100644 --- a/tests/test_languages/test_java/test_jfr_parser.py +++ b/tests/test_languages/test_java/test_jfr_parser.py @@ -277,6 +277,8 @@ def test_java_project_root_is_build_root_not_module(self, tmp_path: Path, monkey def test_project_root_is_path_not_string(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """project_root from process_pyproject_config should be a Path for Java projects.""" + from argparse import Namespace + (tmp_path / "pom.xml").write_text("", encoding="utf-8") src = tmp_path / "src" / "main" / "java" src.mkdir(parents=True) @@ -284,16 +286,15 @@ def test_project_root_is_path_not_string(self, tmp_path: Path, monkeypatch: pyte test.mkdir(parents=True) monkeypatch.chdir(tmp_path) - import sys - from argparse import Namespace - - sys.argv = ["codeflash", "optimize", "java", "-jar", "app.jar"] - from codeflash.cli_cmds.cli import parse_args, process_pyproject_config - - from codeflash.cli_cmds.cli import _build_parser - _build_parser.cache_clear() + from codeflash.cli_cmds.cli import process_pyproject_config - args = parse_args() + # Create a minimal args namespace matching what parse_args produces + args = Namespace( + config_file=None, module_root=None, tests_root=None, benchmarks_root=None, + ignore_paths=None, pytest_cmd=None, formatter_cmds=None, disable_telemetry=None, + disable_imports_sorting=None, git_remote=None, override_fixtures=None, + benchmark=False, verbose=False, version=False, show_config=False, reset_config=False, + ) args = process_pyproject_config(args) assert hasattr(args, "project_root") From be616d1d1f2219c3b30d929f14c20b8cd7c90c7f Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 23:05:16 -0700 Subject: [PATCH 12/26] fix: flush e2e test output to CI logs in real-time Use print(flush=True) instead of logging.info for subprocess output so CI logs show progress in real-time instead of buffering until completion. Also set PYTHONUNBUFFERED=1 for the subprocess. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/scripts/end_to_end_test_java_tracer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/scripts/end_to_end_test_java_tracer.py b/tests/scripts/end_to_end_test_java_tracer.py index 3f68d02d4..5555b041c 100644 --- a/tests/scripts/end_to_end_test_java_tracer.py +++ b/tests/scripts/end_to_end_test_java_tracer.py @@ -59,6 +59,7 @@ def run_test(expected_improvement_pct: int) -> bool: env = os.environ.copy() env["PYTHONIOENCODING"] = "utf-8" + env["PYTHONUNBUFFERED"] = "1" logging.info(f"Running command: {' '.join(command)}") logging.info(f"Working directory: {fixture_dir}") process = subprocess.Popen( @@ -73,13 +74,11 @@ def run_test(expected_improvement_pct: int) -> bool: output = [] for line in process.stdout: - logging.info(line.strip()) + print(line, end="", flush=True) output.append(line) return_code = process.wait() stdout = "".join(output) - if return_code != 0: - logging.error(f"Full output:\n{stdout}") if return_code != 0: logging.error(f"Command returned exit code {return_code}") From 803fb64f055bf330b7c64708dfe8ff655fc8e128 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 06:14:52 +0000 Subject: [PATCH 13/26] fix: add missing type params for dict in _write_maven_properties and _write_gradle_properties Co-authored-by: Saurabh Misra --- codeflash/setup/config_writer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codeflash/setup/config_writer.py b/codeflash/setup/config_writer.py index e872cfeba..4616ccf5f 100644 --- a/codeflash/setup/config_writer.py +++ b/codeflash/setup/config_writer.py @@ -8,7 +8,7 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import tomlkit @@ -124,7 +124,7 @@ def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tup return _write_gradle_properties(gradle_props_path, non_default) -def _write_maven_properties(pom_path: Path, config: dict) -> tuple[bool, str]: +def _write_maven_properties(pom_path: Path, config: dict[str, Any]) -> tuple[bool, str]: """Add codeflash.* properties to pom.xml section.""" import xml.etree.ElementTree as ET @@ -171,7 +171,7 @@ def _write_maven_properties(pom_path: Path, config: dict) -> tuple[bool, str]: return False, f"Failed to write Maven properties: {e}" -def _write_gradle_properties(props_path: Path, config: dict) -> tuple[bool, str]: +def _write_gradle_properties(props_path: Path, config: dict[str, Any]) -> tuple[bool, str]: """Add codeflash.* entries to gradle.properties.""" key_map = { "module-root": "moduleRoot", From 13dae81bd2e4424941213897fd40864fc336bc3c Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 23:48:36 -0700 Subject: [PATCH 14/26] fix: increase JFR sampling frequency and make Workload exercise functions harder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set jdk.ExecutionSample#period=1ms (default was 10ms) so JFR captures samples from shorter-running programs - Workload.main now runs 1000 rounds with larger inputs so JFR can capture method-level CPU samples (repeatString with O(n²) concat dominates ~75% of samples) Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/tracer.py | 7 ++++- .../src/main/java/com/example/Workload.java | 26 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py index 5ad449088..5cc098be5 100644 --- a/codeflash/languages/java/tracer.py +++ b/codeflash/languages/java/tracer.py @@ -122,7 +122,12 @@ def create_tracer_config( def build_jfr_env(self, jfr_file: Path) -> dict[str, str]: env = os.environ.copy() - jfr_opts = f"-XX:StartFlightRecording=filename={jfr_file.resolve()},settings=profile,dumponexit=true" + # Use profile settings with increased sampling frequency (1ms instead of default 10ms) + # This captures more samples for short-running programs + jfr_opts = ( + f"-XX:StartFlightRecording=filename={jfr_file.resolve()},settings=profile,dumponexit=true" + ",jdk.ExecutionSample#period=1ms" + ) existing = env.get("JAVA_TOOL_OPTIONS", "") env["JAVA_TOOL_OPTIONS"] = f"{existing} {jfr_opts}".strip() return env diff --git a/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java b/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java index 9b6078000..7beb2a4ea 100644 --- a/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java +++ b/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java @@ -36,20 +36,30 @@ public int instanceMethod(int x, int y) { } public static void main(String[] args) { - // Exercise the methods so the tracer can capture invocations - System.out.println("computeSum(100) = " + computeSum(100)); - System.out.println("computeSum(50) = " + computeSum(50)); + // Run methods with large inputs so JFR can capture CPU samples. + // Small inputs finish too fast (<1ms) for JFR's 10ms sampling interval. + for (int round = 0; round < 1000; round++) { + computeSum(100_000); + repeatString("hello world ", 1000); + + List nums = new ArrayList<>(); + for (int i = 1; i <= 10_000; i++) nums.add(i); + filterEvens(nums); + Workload w = new Workload(); + w.instanceMethod(100_000, 42); + } + + // Also call with small inputs for variety in traced args + System.out.println("computeSum(100) = " + computeSum(100)); System.out.println("repeatString(\"ab\", 3) = " + repeatString("ab", 3)); - System.out.println("repeatString(\"x\", 5) = " + repeatString("x", 5)); - List nums = new ArrayList<>(); - for (int i = 1; i <= 10; i++) nums.add(i); - System.out.println("filterEvens(1..10) = " + filterEvens(nums)); + List small = new ArrayList<>(); + for (int i = 1; i <= 10; i++) small.add(i); + System.out.println("filterEvens(1..10) = " + filterEvens(small)); Workload w = new Workload(); System.out.println("instanceMethod(5, 3) = " + w.instanceMethod(5, 3)); - System.out.println("instanceMethod(10, 2) = " + w.instanceMethod(10, 2)); System.out.println("Workload complete."); } From 3c63b60ae49bf9e14a99d858893d01e955ae0d37 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 16:30:22 +0000 Subject: [PATCH 15/26] fix: preserve pom.xml formatting in config writer and align write/remove priority Replace xml.etree.ElementTree with text-based regex manipulation in _write_maven_properties() and _remove_java_build_config(). ElementTree destroys XML comments, mangles namespace declarations (ns0: prefixes), and reformats whitespace. The new approach reads/writes pom.xml as plain text, only touching codeflash.* property lines. Also extracts duplicated key_map to shared _MAVEN_KEY_MAP constant and aligns remove priority to check pom.xml first (matching write order). Co-Authored-By: Claude Opus 4.6 --- codeflash/setup/config_writer.py | 154 +++++++++++++++---------- codeflash/setup/detector.py | 3 +- tests/test_setup/test_config_writer.py | 148 ++++++++++++++++++++++++ tests/test_setup/test_detector.py | 16 +++ 4 files changed, 257 insertions(+), 64 deletions(-) create mode 100644 tests/test_setup/test_config_writer.py diff --git a/codeflash/setup/config_writer.py b/codeflash/setup/config_writer.py index 4616ccf5f..43ce03eb3 100644 --- a/codeflash/setup/config_writer.py +++ b/codeflash/setup/config_writer.py @@ -124,47 +124,87 @@ def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tup 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 section.""" - import xml.etree.ElementTree as ET + """Add codeflash.* properties to pom.xml section. + + Uses text-based manipulation to preserve comments, formatting, and namespace declarations. + """ + import re try: - tree = ET.parse(str(pom_path)) - root = tree.getroot() - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - - # Find or create - properties = root.find("m:properties", ns) or root.find("properties") - if properties is None: - properties = ET.SubElement(root, "properties") - - # 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", - } + content = pom_path.read_text(encoding="utf-8") + + # Remove existing codeflash.* property lines (with surrounding whitespace) + content = re.sub(r"\n[ \t]*]*>[^<]*]*>", "", content) + + # Detect child indentation from existing properties or fall back to indent + 4 spaces + props_close = re.search(r"([ \t]*)", 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("") : props_close.start()] if "" in content else "", + ) + child_indent = child_match.group(1) if child_match else parent_indent + " " + else: + parent_indent = "" + child_indent = " " + # Build new property lines with detected indentation + new_lines = [] for key, value in config.items(): - maven_key = f"codeflash.{key_map.get(key, key)}" + 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) - - existing = properties.find(maven_key) - if existing is None: - elem = ET.SubElement(properties, maven_key) - elem.text = value - else: - existing.text = value - - tree.write(str(pom_path), xml_declaration=True, encoding="UTF-8") + new_lines.append(f"{child_indent}<{maven_key}>{value}") + + properties_block = "\n".join(new_lines) + + # Insert before + if props_close: + content = ( + content[: props_close.start()] + + properties_block + + "\n" + + parent_indent + + "" + + content[props_close.end() :] + ) + else: + # No section — create one before + project_close = re.search(r"([ \t]*)", content) + if project_close: + indent = project_close.group(1) + inner = " " + indent + props_section = ( + f"{inner}\n" + + "\n".join(f" {line}" for line in new_lines) + + f"\n{inner}\n" + ) + content = ( + content[: project_close.start()] + + props_section + + indent + + "" + + content[project_close.end() :] + ) + + pom_path.write_text(content, encoding="utf-8") return True, f"Config saved to {pom_path} " except Exception as e: @@ -173,15 +213,6 @@ def _write_maven_properties(pom_path: Path, config: dict[str, Any]) -> tuple[boo def _write_gradle_properties(props_path: Path, config: dict[str, Any]) -> 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(): @@ -195,7 +226,7 @@ def _write_gradle_properties(props_path: Path, config: dict[str, Any]) -> tuple[ 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)}" + 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): @@ -306,8 +337,25 @@ def _remove_from_pyproject(project_root: Path) -> tuple[bool, str]: 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) + """Remove codeflash.* properties from pom.xml or gradle.properties. + + Priority matches _write_java_build_config: pom.xml first, then gradle.properties. + """ + # 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") + updated = re.sub(r"\n[ \t]*]*>[^<]*]*>", "", content) + if updated != content: + 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 gradle_props = project_root / "gradle.properties" if gradle_props.exists(): try: @@ -316,33 +364,13 @@ def _remove_java_build_config(project_root: Path) -> tuple[bool, str]: line for line in lines if not line.strip().startswith("codeflash.") - and line.strip() != "# Codeflash configuration — https://docs.codeflash.ai" + 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}" - # Try pom.xml - pom_path = project_root / "pom.xml" - if pom_path.exists(): - try: - import xml.etree.ElementTree as ET - - 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}" - return True, "No Java build config found" diff --git a/codeflash/setup/detector.py b/codeflash/setup/detector.py index 06d690190..81e900436 100644 --- a/codeflash/setup/detector.py +++ b/codeflash/setup/detector.py @@ -900,7 +900,8 @@ def has_existing_config(project_root: Path) -> tuple[bool, str | None]: except Exception: pass - # Check Java build files — Java projects store config in pom.xml properties or gradle.properties + # 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 diff --git a/tests/test_setup/test_config_writer.py b/tests/test_setup/test_config_writer.py new file mode 100644 index 000000000..89426bdfd --- /dev/null +++ b/tests/test_setup/test_config_writer.py @@ -0,0 +1,148 @@ +"""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( + '\n' + "\n" + " \n" + " \n" + " 17\n" + " \n" + "\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 "" in result + assert "src/main/java" in result + + def test_preserves_namespace(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + '\n' + '\n' + " \n" + " 17\n" + " \n" + "\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( + "\n" + " \n" + " old/path\n" + " \n" + "\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 "new/path" in result + + def test_creates_properties_section(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" " 4.0.0\n" "\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 "" in result + assert "src/main/java" in result + + def test_converts_kebab_to_camelcase(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n \n \n\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 "target,build" 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( + "\n" + " \n" + " \n" + " 17\n" + " src/main/java\n" + " \n" + "\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 "" in result + assert "17" 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 diff --git a/tests/test_setup/test_detector.py b/tests/test_setup/test_detector.py index 781d393e6..3b0e165c8 100644 --- a/tests/test_setup/test_detector.py +++ b/tests/test_setup/test_detector.py @@ -558,6 +558,22 @@ def test_returns_false_when_no_config(self, tmp_path): assert has_config is False 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("4.0.0") + + 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): """Should return False for empty directory.""" has_config, config_type = has_existing_config(tmp_path) From 5942ae9e168026cec97dacb453a8f5a13c21857d Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 16:50:18 +0000 Subject: [PATCH 16/26] fix: prefer closer config file over parent Java build file in monorepos (TODO-37) Java detection in parse_config_file() short-circuited before the existing depth-comparison logic, so a parent pom.xml would override a closer package.json or pyproject.toml. Now all config sources are detected first and the closest one to CWD wins. Co-Authored-By: Claude Opus 4.6 --- codeflash/code_utils/config_parser.py | 19 +++--- tests/code_utils/test_config_parser.py | 87 ++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 tests/code_utils/test_config_parser.py diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index 1d0f13df5..832b34bcc 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -106,16 +106,21 @@ def find_conftest_files(test_paths: list[Path]) -> list[Path]: 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 - + # 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) pyproject_toml_path = find_closest_config_file("pyproject.toml") if config_file_path is None else None + # Use Java config only if no closer JS/Python config exists (monorepo support). + # In a monorepo with a parent pom.xml and a child package.json, the closer config wins. + if java_result is not None: + java_depth = len(java_result[1].parts) + has_closer = (package_json_path is not None and len(package_json_path.parent.parts) >= java_depth) or ( + pyproject_toml_path is not None and len(pyproject_toml_path.parent.parts) >= java_depth + ) + if not has_closer: + return java_result + # 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. diff --git a/tests/code_utils/test_config_parser.py b/tests/code_utils/test_config_parser.py new file mode 100644 index 000000000..dc47a4f1d --- /dev/null +++ b/tests/code_utils/test_config_parser.py @@ -0,0 +1,87 @@ +"""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("", 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("", 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("", 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" + ) From 12921447b985ef73e58d007278353c8c1da0cac1 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 16:50:31 +0000 Subject: [PATCH 17/26] fix: capture real line numbers in tracer and track dropped captures (TODO-34, TODO-38) TODO-34: TracingClassVisitor hardcoded line number to 0 because ASM's visitMethod() doesn't provide line info. Added a pre-scan pass in TracingTransformer.instrumentClass() that collects first line numbers via visitLineNumber() before the instrumentation pass. TODO-38: Serialization timeouts/failures silently dropped captures with no visibility. Added AtomicInteger droppedCaptures counter and included it in flush() metadata output. Co-Authored-By: Claude Opus 4.6 --- .../com/codeflash/tracer/TraceRecorder.java | 9 ++++- .../codeflash/tracer/TracingClassVisitor.java | 11 ++++-- .../codeflash/tracer/TracingTransformer.java | 32 +++++++++++++++++- .../resources/codeflash-runtime-1.0.0.jar | Bin 15976923 -> 15979672 bytes 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java index 2a22b74f4..28c2d2998 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java @@ -22,6 +22,7 @@ public final class TraceRecorder { private final TracerConfig config; private final TraceWriter writer; private final ConcurrentHashMap functionCounts = new ConcurrentHashMap<>(); + private final AtomicInteger droppedCaptures = new AtomicInteger(0); private final int maxFunctionCount; private final ExecutorService serializerExecutor; @@ -82,11 +83,13 @@ private void onEntryImpl(String className, String methodName, String descriptor, argsBlob = future.get(SERIALIZATION_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); + droppedCaptures.incrementAndGet(); System.err.println("[codeflash-tracer] Serialization timed out for " + className + "." + methodName); return; } catch (Exception e) { Throwable cause = e.getCause() != null ? e.getCause() : e; + droppedCaptures.incrementAndGet(); System.err.println("[codeflash-tracer] Serialization failed for " + className + "." + methodName + ": " + cause.getClass().getSimpleName() + ": " + cause.getMessage()); return; @@ -113,11 +116,15 @@ public void flush() { } metadata.put("totalCaptures", String.valueOf(totalCaptures)); + int dropped = droppedCaptures.get(); + metadata.put("droppedCaptures", String.valueOf(dropped)); + writer.writeMetadata(metadata); writer.flush(); writer.close(); System.err.println("[codeflash-tracer] Captured " + totalCaptures - + " invocations across " + functionCounts.size() + " methods"); + + " invocations across " + functionCounts.size() + " methods" + + (dropped > 0 ? " (" + dropped + " dropped due to serialization timeout/failure)" : "")); } } diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java index c760ea636..90d4cd7a0 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java @@ -4,14 +4,20 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; +import java.util.Collections; +import java.util.Map; + public class TracingClassVisitor extends ClassVisitor { private final String internalClassName; + private final Map methodLineNumbers; private String sourceFile; - public TracingClassVisitor(ClassVisitor classVisitor, String internalClassName) { + public TracingClassVisitor(ClassVisitor classVisitor, String internalClassName, + Map methodLineNumbers) { super(Opcodes.ASM9, classVisitor); this.internalClassName = internalClassName; + this.methodLineNumbers = methodLineNumbers != null ? methodLineNumbers : Collections.emptyMap(); } @Override @@ -37,7 +43,8 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, return mv; } + int lineNumber = methodLineNumbers.getOrDefault(name + descriptor, 0); return new TracingMethodAdapter(mv, access, name, descriptor, - internalClassName, 0, sourceFile != null ? sourceFile : ""); + internalClassName, lineNumber, sourceFile != null ? sourceFile : ""); } } diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java index 75c61de3a..53ac775af 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java @@ -1,10 +1,16 @@ package com.codeflash.tracer; import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; +import java.util.HashMap; +import java.util.Map; public class TracingTransformer implements ClassFileTransformer { @@ -46,6 +52,30 @@ public byte[] transform(ClassLoader loader, String className, private byte[] instrumentClass(String internalClassName, byte[] bytecode) { ClassReader cr = new ClassReader(bytecode); + + // Pre-scan: collect the first source line number for each method. + // ASM's visitMethod() doesn't provide line info — it arrives later via visitLineNumber(). + // We do a lightweight read pass first so the instrumentation pass has accurate line numbers. + Map methodLineNumbers = new HashMap<>(); + cr.accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + String key = name + descriptor; + return new MethodVisitor(Opcodes.ASM9) { + private boolean captured = false; + + @Override + public void visitLineNumber(int line, Label start) { + if (!captured) { + methodLineNumbers.put(key, line); + captured = true; + } + } + }; + } + }, ClassReader.SKIP_FRAMES); + // Use COMPUTE_MAXS only (not COMPUTE_FRAMES) to preserve original stack map frames. // COMPUTE_FRAMES recomputes all frames and calls getCommonSuperClass() which either // triggers classloader deadlocks or produces incorrect frames when returning "java/lang/Object". @@ -53,7 +83,7 @@ private byte[] instrumentClass(String internalClassName, byte[] bytecode) { // adjusts offsets for injected code. Our AdviceAdapter only injects at method entry // (before any branch points), so existing frames remain valid. ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); - TracingClassVisitor cv = new TracingClassVisitor(cw, internalClassName); + TracingClassVisitor cv = new TracingClassVisitor(cw, internalClassName, methodLineNumbers); cr.accept(cv, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); } diff --git a/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar b/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar index 48ebc0a96623f477df4640b0eeb733f5ab82b477..546a8b89dd404a7f882875bac94dbce31359c7aa 100644 GIT binary patch delta 59596 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

-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 F{U7uj=(Yd= delta 57088 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 From 970c9f86da83afcd876826fc97147994c6cb27cd Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 16:50:40 +0000 Subject: [PATCH 18/26] fix: use 3 package components for tracer instrumentation scope (TODO-39) Changed detect_packages_from_source() from min(2, len) to min(3, len) so com.aerospike.client.util produces prefix com.aerospike.client instead of com.aerospike. This reduces instrumentation to the actual source package instead of the entire organization namespace. Co-Authored-By: Claude Opus 4.6 --- codeflash/languages/java/tracer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py index 5cc098be5..ab8f19514 100644 --- a/codeflash/languages/java/tracer.py +++ b/codeflash/languages/java/tracer.py @@ -165,7 +165,7 @@ def detect_packages_from_source(module_root: Path) -> list[str]: if stripped.startswith("package "): pkg = stripped[8:].rstrip(";").strip() parts = pkg.split(".") - prefix = ".".join(parts[: min(2, len(parts))]) + prefix = ".".join(parts[: min(3, len(parts))]) packages.add(prefix) break if stripped and not stripped.startswith("//"): From c5b3687d5272bcf104eb124fff942cabf07baa8a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 26 Mar 2026 07:39:29 +0000 Subject: [PATCH 19/26] 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) --- code_to_optimize/java-gradle/codeflash.toml | 4 + code_to_optimize/java/codeflash.toml | 6 + codeflash/cli_cmds/cli.py | 8 +- codeflash/code_utils/config_parser.py | 62 +-- codeflash/languages/java/build_tools.py | 215 +-------- codeflash/setup/config_writer.py | 220 +++------ codeflash/setup/detector.py | 29 +- codeflash/tracer.py | 16 +- docs/configuration/java.mdx | 207 +++----- docs/getting-started/java-installation.mdx | 64 ++- tests/code_utils/test_config_parser.py | 87 ---- tests/scripts/end_to_end_test_utilities.py | 4 +- .../fixtures/java_maven/codeflash.toml | 5 + .../fixtures/java_tracer_e2e/codeflash.toml | 6 + .../test_java/test_java_config_detection.py | 444 ------------------ tests/test_setup/test_config_writer.py | 148 ------ tests/test_setup/test_detector.py | 16 - 17 files changed, 235 insertions(+), 1306 deletions(-) create mode 100644 code_to_optimize/java-gradle/codeflash.toml create mode 100644 code_to_optimize/java/codeflash.toml delete mode 100644 tests/code_utils/test_config_parser.py create mode 100644 tests/test_languages/fixtures/java_maven/codeflash.toml create mode 100644 tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml delete mode 100644 tests/test_languages/test_java/test_java_config_detection.py delete mode 100644 tests/test_setup/test_config_writer.py diff --git a/code_to_optimize/java-gradle/codeflash.toml b/code_to_optimize/java-gradle/codeflash.toml new file mode 100644 index 000000000..bf6e45279 --- /dev/null +++ b/code_to_optimize/java-gradle/codeflash.toml @@ -0,0 +1,4 @@ +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +formatter-cmds = [] diff --git a/code_to_optimize/java/codeflash.toml b/code_to_optimize/java/codeflash.toml new file mode 100644 index 000000000..4016df28a --- /dev/null +++ b/code_to_optimize/java/codeflash.toml @@ -0,0 +1,6 @@ +# Codeflash configuration for Java project + +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +formatter-cmds = [] diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index c611f5cd9..6e5bd44e3 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -190,12 +190,6 @@ def process_pyproject_config(args: Namespace) -> Namespace: 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_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(): args.all = None return args @@ -214,6 +208,8 @@ 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() diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index 832b34bcc..ef21ce051 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -12,29 +12,8 @@ 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 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: 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 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 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 @@ -103,34 +90,33 @@ 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]: - # 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) 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). - # In a monorepo with a parent pom.xml and a child package.json, the closer config wins. - if java_result is not None: - java_depth = len(java_result[1].parts) - has_closer = (package_json_path is not None and len(package_json_path.parent.parts) >= java_depth) or ( - pyproject_toml_path is not None and len(pyproject_toml_path.parent.parts) >= java_depth - ) - if not has_closer: - return java_result + # 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. + # from overriding a closer pyproject.toml or codeflash.toml. use_package_json = False if package_json_path: - if pyproject_toml_path is None: + if closest_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(pyproject_toml_path.parent.parts) + toml_depth = len(closest_toml_path.parent.parts) use_package_json = package_json_depth >= toml_depth if use_package_json: @@ -174,7 +160,7 @@ def parse_config_file( if config == {} and lsp_mode: 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: path_keys = ["module-root", "tests-root", "benchmarks-root"] path_list_keys = ["ignore-paths"] diff --git a/codeflash/languages/java/build_tools.py b/codeflash/languages/java/build_tools.py index f8a19c693..28db2c9aa 100644 --- a/codeflash/languages/java/build_tools.py +++ b/codeflash/languages/java/build_tools.py @@ -10,8 +10,7 @@ import xml.etree.ElementTree as ET from dataclasses import dataclass from enum import Enum -from pathlib import Path -from typing import Any +from pathlib import Path # noqa: TC003 — used at runtime 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 -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 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 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 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. diff --git a/codeflash/setup/config_writer.py b/codeflash/setup/config_writer.py index 43ce03eb3..0889690d5 100644 --- a/codeflash/setup/config_writer.py +++ b/codeflash/setup/config_writer.py @@ -8,7 +8,7 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import tomlkit @@ -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_java_build_config(detected.project_root, config) + return _write_codeflash_toml(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_java_build_config(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: - """Write codeflash config to pom.xml properties or gradle.properties. +def _write_codeflash_toml(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: + """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: project_root: Project root directory. @@ -105,141 +105,40 @@ def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tup Tuple of (success, message). """ - 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) - - -_MAVEN_KEY_MAP: dict[str, str] = { - "module-root": "moduleRoot", - "tests-root": "testsRoot", - "git-remote": "gitRemote", - "disable-telemetry": "disableTelemetry", - "ignore-paths": "ignorePaths", - "formatter-cmds": "formatterCmds", -} + codeflash_toml_path = project_root / "codeflash.toml" + 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() -def _write_maven_properties(pom_path: Path, config: dict[str, Any]) -> tuple[bool, str]: - """Add codeflash.* properties to pom.xml section. + # Ensure [tool] section exists + if "tool" not in doc: + doc["tool"] = tomlkit.table() - Uses text-based manipulation to preserve comments, formatting, and namespace declarations. - """ - import re + # Create codeflash section + codeflash_table = tomlkit.table() + codeflash_table.add(tomlkit.comment("Codeflash configuration for Java - https://docs.codeflash.ai")) - try: - content = pom_path.read_text(encoding="utf-8") - - # Remove existing codeflash.* property lines (with surrounding whitespace) - content = re.sub(r"\n[ \t]*]*>[^<]*]*>", "", content) - - # Detect child indentation from existing properties or fall back to indent + 4 spaces - props_close = re.search(r"([ \t]*)", 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("") : props_close.start()] if "" in content else "", - ) - child_indent = child_match.group(1) if child_match else parent_indent + " " - else: - parent_indent = "" - child_indent = " " - - # Build new property lines with detected indentation - new_lines = [] - for key, value in config.items(): - 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}") - - properties_block = "\n".join(new_lines) - - # Insert before - if props_close: - content = ( - content[: props_close.start()] - + properties_block - + "\n" - + parent_indent - + "" - + content[props_close.end() :] - ) - else: - # No section — create one before - project_close = re.search(r"([ \t]*)", content) - if project_close: - indent = project_close.group(1) - inner = " " + indent - props_section = ( - f"{inner}\n" - + "\n".join(f" {line}" for line in new_lines) - + f"\n{inner}\n" - ) - content = ( - content[: project_close.start()] - + props_section - + indent - + "" - + content[project_close.end() :] - ) - - pom_path.write_text(content, encoding="utf-8") - return True, f"Config saved to {pom_path} " + # Add config values + config_dict = config.to_pyproject_dict() + for key, value in config_dict.items(): + codeflash_table[key] = value - except Exception as e: - return False, f"Failed to write Maven properties: {e}" + # Update the document + doc["tool"]["codeflash"] = codeflash_table + # Write back + with codeflash_toml_path.open("w", encoding="utf8") as f: + f.write(tomlkit.dumps(doc)) -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}" + return True, f"Config saved to {codeflash_toml_path}" except Exception as e: - return False, f"Failed to write gradle.properties: {e}" + return False, f"Failed to write codeflash.toml: {e}" 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": return _remove_from_pyproject(project_root) if language == "java": - return _remove_java_build_config(project_root) + return _remove_from_codeflash_toml(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}" -def _remove_java_build_config(project_root: Path) -> tuple[bool, str]: - """Remove codeflash.* properties from pom.xml or gradle.properties. +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" - Priority matches _write_java_build_config: pom.xml first, then gradle.properties. - """ - # 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") - updated = re.sub(r"\n[ \t]*]*>[^<]*]*>", "", content) - if updated != content: - 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 - 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 \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" + if not codeflash_toml_path.exists(): + return True, "No codeflash.toml found" + + try: + with codeflash_toml_path.open("rb") as f: + doc = tomlkit.parse(f.read()) + + 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}" def _remove_from_package_json(project_root: Path) -> tuple[bool, str]: diff --git a/codeflash/setup/detector.py b/codeflash/setup/detector.py index 81e900436..defe1a22d 100644 --- a/codeflash/setup/detector.py +++ b/codeflash/setup/detector.py @@ -886,25 +886,20 @@ 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", "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) - 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 — 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 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 package.json package_json_path = project_root / "package.json" diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 5f8a1a4ab..108a3a38b 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -38,7 +38,7 @@ 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. """ @@ -66,23 +66,15 @@ def _detect_non_python_language(args: Namespace | None) -> Language | None: except Exception: pass - # 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) + # Method 2: Check project config for language field 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: diff --git a/docs/configuration/java.mdx b/docs/configuration/java.mdx index 720e5e091..9d110fc55 100644 --- a/docs/configuration/java.mdx +++ b/docs/configuration/java.mdx @@ -1,112 +1,101 @@ --- 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" -sidebarTitle: "Java (pom.xml / Gradle)" +sidebarTitle: "Java (codeflash.toml)" keywords: [ "configuration", + "codeflash.toml", "java", "maven", "gradle", "junit", - "pom.xml", - "gradle.properties", - "zero-config", ] --- # 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 -## Auto-Detection - -Codeflash inspects your build files and auto-detects: - -| Setting | Detection logic | -|---------|----------------| -| **Language** | Presence of `pom.xml` or `build.gradle` / `build.gradle.kts` | -| **Source root** | `src/main/java` (standard), or `` in `pom.xml`, or Gradle `sourceSets` | -| **Test root** | `src/test/java` (standard), or `` in `pom.xml` | -| **Test framework** | Checks build file dependencies for JUnit 5, JUnit 4, or TestNG | -| **Java version** | ``, `` in `pom.xml` | - -### Multi-module Maven projects +```toml +[tool.codeflash] +# Required +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" -For multi-module projects, Codeflash scans each module's `pom.xml` for `` and `` declarations. It picks the module with the most Java source files as the main source root, and identifies test modules by name. +# Optional +test-framework = "junit5" # "junit5", "junit4", or "testng" +disable-telemetry = false +git-remote = "origin" +ignore-paths = ["src/main/java/generated/"] +``` -For example, with this layout: +All file paths are relative to the directory containing `codeflash.toml`. -```text -my-project/ -|- client/ ← main library (most .java files) -| |- src/com/example/ -| |- pom.xml ← ${project.basedir}/src -|- test/ ← test module -| |- src/com/example/ -| |- pom.xml ← ${project.basedir}/src -|- benchmarks/ ← skipped (benchmark module) -|- pom.xml ← client, test, benchmarks -``` + +Codeflash auto-detects most settings from your project structure. Running `codeflash init` will set up the correct config — manual configuration is usually not needed. + -Codeflash auto-detects `client/src` as the source root and `test/src` as the test root — no manual configuration needed. +## Auto-Detection -## Custom Configuration +When you run `codeflash init`, Codeflash inspects your project and auto-detects: -If auto-detection doesn't match your project layout, add `codeflash.*` properties to your build files. +| 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 | - - +## Required Options -Add properties to your `pom.xml` `` section: +- **`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. -```xml - - - client/src - test/src - true - upstream - src/main/java/generated/,src/main/java/proto/ - -``` +## Optional Options -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. +- **`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 -Add properties to `gradle.properties`: +For multi-module Maven/Gradle projects, place `codeflash.toml` at the project root and set `module-root` to the module you want to optimize: -```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/ +```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 ``` - - - -## Available Properties +```toml +[tool.codeflash] +module-root = "client/src/main/java" +tests-root = "client/src/test/java" +language = "java" +``` -All properties are optional — only set values that differ from auto-detected defaults. +For non-standard layouts (like the Aerospike client where source is under `client/src/`), adjust paths accordingly: -| Property | Description | Default | -|----------|------------|---------| -| `codeflash.moduleRoot` | Source directory to optimize | Auto-detected from `` or `src/main/java` | -| `codeflash.testsRoot` | Test directory | Auto-detected from `` 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 | +```toml +[tool.codeflash] +module-root = "client/src" +tests-root = "test/src" +language = "java" +``` -## Tracer CLI Options +## Tracer Options 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 ``` -## Examples +## Example -### Standard Maven project (zero config) +### Standard Maven project ```text my-app/ @@ -135,14 +124,17 @@ my-app/ | |- test/java/com/example/ | |- AppTest.java |- pom.xml +|- codeflash.toml ``` -Just run: -```bash -codeflash optimize java -jar target/my-app.jar +```toml +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" ``` -### Standard Gradle project (zero config) +### Gradle project ```text my-lib/ @@ -150,55 +142,12 @@ my-lib/ | |- main/java/com/example/ | |- test/java/com/example/ |- build.gradle +|- codeflash.toml ``` -Just run: -```bash -codeflash optimize java -cp build/classes/java/main com.example.Main +```toml +[tool.codeflash] +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 - - client/src - test/src - -``` - - -In most cases, even non-standard multi-module layouts are auto-detected correctly from `` and `` in each module's `pom.xml`. Only add manual config if auto-detection gets it wrong. - - -## FAQ - - - - No. Codeflash auto-detects Java projects from `pom.xml` or `build.gradle`. No initialization step or config file is needed for standard layouts. - - - - Codeflash reads config from your existing build files — `pom.xml` `` for Maven, `gradle.properties` for Gradle. No separate config file is created. - - - - Add `` and `` properties to your `pom.xml` or `gradle.properties`. These override auto-detection. - - - - Codeflash scans each module's `pom.xml` for `` and ``. 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. - - diff --git a/docs/getting-started/java-installation.mdx b/docs/getting-started/java-installation.mdx index fb2a88ef2..a75e1f0b7 100644 --- a/docs/getting-started/java-installation.mdx +++ b/docs/getting-started/java-installation.mdx @@ -12,11 +12,10 @@ keywords: "junit", "junit5", "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 @@ -24,7 +23,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 +3. **A Java project** with source code under a standard directory layout Good to have (optional): @@ -46,48 +45,61 @@ uv pip install codeflash ``` - + Navigate to your Java project root (where `pom.xml` or `build.gradle` is) and run: ```bash -codeflash optimize java -jar target/my-app.jar +codeflash init ``` -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) -2. Capture method arguments using a bytecode instrumentation agent -3. Generate JUnit replay tests from the captured data -4. Rank functions by performance impact -5. Optimize the most impactful functions +This will: +- Detect your build tool (Maven/Gradle) +- Find your source and test directories +- Create a `codeflash.toml` configuration file - - - -**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). - + -## Usage examples +Check that the configuration looks correct: -**Trace and optimize a JAR application:** ```bash -codeflash optimize java -jar target/my-app.jar --app-args +cat codeflash.toml +``` + +You should see something like: + +```toml +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" ``` -**Optimize a specific file and function:** + + + +Trace and optimize a running Java program: + ```bash -codeflash --file src/main/java/com/example/Utils.java --function computeHash +codeflash optimize java -jar target/my-app.jar ``` -**Trace a long-running program with a timeout:** +Or with Maven: + ```bash -codeflash optimize --timeout 30 java -jar target/my-server.jar +codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main" ``` -Each tracing stage runs for at most 30 seconds, then the captured data is processed. +Codeflash will: +1. Profile your program using JFR (Java Flight Recorder) +2. Capture method arguments using a bytecode instrumentation agent +3. Generate JUnit replay tests from the captured data +4. Rank functions by performance impact +5. Optimize the most impactful functions + + + ## How it works diff --git a/tests/code_utils/test_config_parser.py b/tests/code_utils/test_config_parser.py deleted file mode 100644 index dc47a4f1d..000000000 --- a/tests/code_utils/test_config_parser.py +++ /dev/null @@ -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("", 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("", 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("", 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" - ) diff --git a/tests/scripts/end_to_end_test_utilities.py b/tests/scripts/end_to_end_test_utilities.py index 33825db4d..12259b339 100644 --- a/tests/scripts/end_to_end_test_utilities.py +++ b/tests/scripts/end_to_end_test_utilities.py @@ -149,8 +149,8 @@ def build_command( if 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 - has_codeflash_config = (cwd / "pom.xml").exists() or (cwd / "build.gradle").exists() or (cwd / "build.gradle.kts").exists() + # Check if config exists (pyproject.toml or codeflash.toml) - if so, don't override it + has_codeflash_config = (cwd / "codeflash.toml").exists() if not has_codeflash_config: pyproject_path = cwd / "pyproject.toml" if pyproject_path.exists(): diff --git a/tests/test_languages/fixtures/java_maven/codeflash.toml b/tests/test_languages/fixtures/java_maven/codeflash.toml new file mode 100644 index 000000000..ecd20a562 --- /dev/null +++ b/tests/test_languages/fixtures/java_maven/codeflash.toml @@ -0,0 +1,5 @@ +# Codeflash configuration for Java project + +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" diff --git a/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml b/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml new file mode 100644 index 000000000..a501ef8cb --- /dev/null +++ b/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml @@ -0,0 +1,6 @@ +# Codeflash configuration for Java project + +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" diff --git a/tests/test_languages/test_java/test_java_config_detection.py b/tests/test_languages/test_java/test_java_config_detection.py deleted file mode 100644 index ebb8653af..000000000 --- a/tests/test_languages/test_java/test_java_config_detection.py +++ /dev/null @@ -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("", 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("", 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("", 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("", 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("", 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("", 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("", 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 = """\ - - 4.0.0 - com.example - test - 1.0 - - custom/src - custom/test - true - upstream - gen/,build/ - - -""" - - -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( - '4.0.0', - 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 = """\ - - 4.0.0 - com.example - parent - 1.0 - pom - - client - test - examples - - -""" - -CLIENT_POM = """\ - - 4.0.0 - - com.example - parent - 1.0 - - client - - ${project.basedir}/src - - -""" - -TEST_POM = """\ - - 4.0.0 - - com.example - parent - 1.0 - - test - - ${project.basedir}/src - - -""" - -EXAMPLES_POM = """\ - - 4.0.0 - - com.example - parent - 1.0 - - examples - - ${project.basedir}/src - - -""" - - -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("", 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("", 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 diff --git a/tests/test_setup/test_config_writer.py b/tests/test_setup/test_config_writer.py deleted file mode 100644 index 89426bdfd..000000000 --- a/tests/test_setup/test_config_writer.py +++ /dev/null @@ -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( - '\n' - "\n" - " \n" - " \n" - " 17\n" - " \n" - "\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 "" in result - assert "src/main/java" in result - - def test_preserves_namespace(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - '\n' - '\n' - " \n" - " 17\n" - " \n" - "\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( - "\n" - " \n" - " old/path\n" - " \n" - "\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 "new/path" in result - - def test_creates_properties_section(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - "\n" " 4.0.0\n" "\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 "" in result - assert "src/main/java" in result - - def test_converts_kebab_to_camelcase(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - "\n \n \n\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 "target,build" 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( - "\n" - " \n" - " \n" - " 17\n" - " src/main/java\n" - " \n" - "\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 "" in result - assert "17" 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 diff --git a/tests/test_setup/test_detector.py b/tests/test_setup/test_detector.py index 3b0e165c8..781d393e6 100644 --- a/tests/test_setup/test_detector.py +++ b/tests/test_setup/test_detector.py @@ -558,22 +558,6 @@ def test_returns_false_when_no_config(self, tmp_path): assert has_config is False 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("4.0.0") - - 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): """Should return False for empty directory.""" has_config, config_type = has_existing_config(tmp_path) From e3701d01b048a31151a856de358071e5aa2929db Mon Sep 17 00:00:00 2001 From: HeshamHM28 Date: Fri, 27 Mar 2026 10:38:07 +0200 Subject: [PATCH 20/26] =?UTF-8?q?Fix=20=201.=20iteration=5Fid=20ordering?= =?UTF-8?q?=20=E2=80=94=20Comparator=20couldn't=20match=20baseline=20vs=20?= =?UTF-8?q?candidate=20keys=20=20=202.=20JFR=20tool=20not=20found=20?= =?UTF-8?q?=E2=80=94=20missing=20JAVA=5FHOME=20fallback=20=20=203.=20JaCoC?= =?UTF-8?q?o=20coverage=20broken=20=E2=80=94=20-DargLine=20was=20overwriti?= =?UTF-8?q?ng=20JaCoCo's=20agent=20flag=20=20=204.=20runtime=3D0=20dropped?= =?UTF-8?q?=20=E2=80=94=20if=20result.runtime:=20was=20falsy=20for=20zero-?= =?UTF-8?q?nanosecond=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/codeflash/ReplayHelper.java | 4 +++- codeflash/languages/java/jfr_parser.py | 8 ++++++++ codeflash/languages/java/maven_strategy.py | 10 +++++++++- codeflash/models/models.py | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java b/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java index 3a73038c1..c8b05a4f8 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java @@ -84,7 +84,9 @@ public void replay(String className, String methodName, String descriptor, int i private void replayBehavior(Method method, Object instance, Object[] args, String className, String methodName, String testClassName, String testMethodName) throws Exception { - String invId = testIteration + "_" + testMethodName; + // testIteration goes at the END so the Comparator's lastUnderscore stripping + // removes it, making baseline (iteration=0) and candidate (iteration=N) keys match. + String invId = testMethodName + "_" + testIteration; // Print start marker (same format as behavior instrumentation) System.out.println("!$######" + testClassName + ":" + testClassName + "." + testMethodName diff --git a/codeflash/languages/java/jfr_parser.py b/codeflash/languages/java/jfr_parser.py index 7f3816856..c7f55d507 100644 --- a/codeflash/languages/java/jfr_parser.py +++ b/codeflash/languages/java/jfr_parser.py @@ -2,6 +2,7 @@ import json import logging +import os import shutil import subprocess from datetime import datetime @@ -42,6 +43,13 @@ def _find_jfr_tool(self) -> str | None: candidate = Path(home) / "bin" / "jfr" if candidate.exists(): return str(candidate) + + java_home_env = os.environ.get("JAVA_HOME") + if java_home_env: + candidate = Path(java_home_env) / "bin" / "jfr" + if candidate.exists(): + return str(candidate) + return None def _parse(self) -> None: diff --git a/codeflash/languages/java/maven_strategy.py b/codeflash/languages/java/maven_strategy.py index fe30e4c25..963a17682 100644 --- a/codeflash/languages/java/maven_strategy.py +++ b/codeflash/languages/java/maven_strategy.py @@ -647,7 +647,15 @@ def run_tests_via_build_tool( " --add-opens java.base/java.net=ALL-UNNAMED" " --add-opens java.base/java.util.zip=ALL-UNNAMED" ) - if javaagent_arg: + if enable_coverage: + # When coverage is enabled, JaCoCo's prepare-agent goal sets argLine via + # @{argLine}. Overriding -DargLine would clobber the JaCoCo agent flag. + # Pass add-opens and javaagent via JDK_JAVA_OPTIONS instead. + jdk_opts_parts = [add_opens_flags] + if javaagent_arg: + jdk_opts_parts.insert(0, javaagent_arg) + env["JDK_JAVA_OPTIONS"] = " ".join(jdk_opts_parts) + elif javaagent_arg: cmd.append(f"-DargLine={javaagent_arg} {add_opens_flags}") else: cmd.append(f"-DargLine={add_opens_flags}") diff --git a/codeflash/models/models.py b/codeflash/models/models.py index d1cecb554..459e3b6fc 100644 --- a/codeflash/models/models.py +++ b/codeflash/models/models.py @@ -949,7 +949,7 @@ def usable_runtime_data_by_test_case(self) -> dict[InvocationId, list[int]]: by_id: dict[InvocationId, list[int]] = {} for result in self.test_results: if result.did_pass: - if result.runtime: + if result.runtime is not None: by_id.setdefault(result.id, []).append(result.runtime) else: msg = ( From f3eecac5144bdbc0fb819ca67de5de834bb41f41 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:17:33 +0000 Subject: [PATCH 21/26] style: remove extra blank line in cli.py --- codeflash/cli_cmds/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index 2c294a624..cb584a23f 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -382,7 +382,6 @@ def _build_parser() -> ArgumentParser: auth_subparsers.add_parser("login", help="Log in to Codeflash via OAuth") auth_subparsers.add_parser("status", help="Check authentication status") - trace_optimize.add_argument( "--max-function-count", type=int, From aeeca5c24153ebbdd2f40c16b1a5134f1c2fc954 Mon Sep 17 00:00:00 2001 From: HeshamHM28 <206515457+HeshamHM28@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:46:44 +0000 Subject: [PATCH 22/26] fix(java): find mvnw in parent dirs and respect --no-pr in tracer path - Walk up parent directories when looking for mvnw wrapper, fixing multi-module projects where mvnw is in the root but optimizer runs from a submodule - Respect user's --no-pr flag in Java tracer path instead of hardcoding no_pr=True, allowing PR creation from tracer-based optimizations - Add --no-pr to e2e tracer test script Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/maven_strategy.py | 16 ++++++++++------ codeflash/tracer.py | 2 +- tests/scripts/end_to_end_test_java_tracer.py | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/codeflash/languages/java/maven_strategy.py b/codeflash/languages/java/maven_strategy.py index 2f9c7f687..8f28833ad 100644 --- a/codeflash/languages/java/maven_strategy.py +++ b/codeflash/languages/java/maven_strategy.py @@ -647,12 +647,16 @@ def get_text(xpath: str, default: str | None = None) -> str | None: return None def find_executable(self, build_root: Path) -> str | None: - mvnw_path = build_root / "mvnw" - if mvnw_path.exists(): - return str(mvnw_path) - mvnw_cmd_path = build_root / "mvnw.cmd" - if mvnw_cmd_path.exists(): - return str(mvnw_cmd_path) + # Walk up parent directories to find mvnw (multi-module projects keep it in the root) + search = build_root.resolve() + while search != search.parent: + mvnw_path = search / "mvnw" + if mvnw_path.exists(): + return str(mvnw_path) + mvnw_cmd_path = search / "mvnw.cmd" + if mvnw_cmd_path.exists(): + return str(mvnw_cmd_path) + search = search.parent if Path("mvnw").exists(): return "./mvnw" if Path("mvnw.cmd").exists(): diff --git a/codeflash/tracer.py b/codeflash/tracer.py index f0ea843ec..48920be8c 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -419,7 +419,7 @@ def _run_java_tracer(existing_args: Namespace | None = None) -> ArgumentParser: config.replay_test = replay_test_paths config.previous_checkpoint_functions = None config.effort = EffortLevel.HIGH.value - config.no_pr = True + config.no_pr = getattr(config, "no_pr", False) config.file = None config.function = None config.test_project_root = project_root diff --git a/tests/scripts/end_to_end_test_java_tracer.py b/tests/scripts/end_to_end_test_java_tracer.py index 5555b041c..3612b0bbd 100644 --- a/tests/scripts/end_to_end_test_java_tracer.py +++ b/tests/scripts/end_to_end_test_java_tracer.py @@ -52,6 +52,7 @@ def run_test(expected_improvement_pct: int) -> bool: "codeflash.main", "optimize", "java", + "--no-pr", "-cp", str(classes_dir), "com.example.Workload", From 85e8a51f0dbca2eaf73ce8dce66248d98d65f71f Mon Sep 17 00:00:00 2001 From: HeshamHM28 <206515457+HeshamHM28@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:55:35 +0000 Subject: [PATCH 23/26] docs: update Java tracer documentation to match verified behavior Add mvn/gradle test suite examples, fix replay test description, document current limitations (void methods, mvnw search, --add-opens). Remove unverified claims. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../trace-and-optimize.mdx | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/optimizing-with-codeflash/trace-and-optimize.mdx b/docs/optimizing-with-codeflash/trace-and-optimize.mdx index 4c332a929..d7f423931 100644 --- a/docs/optimizing-with-codeflash/trace-and-optimize.mdx +++ b/docs/optimizing-with-codeflash/trace-and-optimize.mdx @@ -57,7 +57,7 @@ codeflash optimize --language javascript script.js ``` -To trace and optimize a running Java program, replace your `java` command with `codeflash optimize java`: +Pass your Java command after `codeflash optimize`: ```bash # JAR application @@ -66,11 +66,14 @@ codeflash optimize java -jar target/my-app.jar --app-args # Class with classpath codeflash optimize java -cp target/classes com.example.Main -# Maven exec -codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main" +# Maven test suite +codeflash optimize mvn test + +# Gradle test suite +codeflash optimize ./gradlew test ``` -For long-running programs (servers, benchmarks), use `--timeout` to limit each tracing stage: +For long-running programs, use `--timeout` to limit each tracing stage: ```bash codeflash optimize --timeout 30 java -jar target/my-app.jar @@ -221,11 +224,11 @@ The JavaScript tracer uses Babel instrumentation to capture function calls durin -The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for accurate profiling, then a bytecode instrumentation agent for argument capture. +The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for profiling, then a bytecode instrumentation agent for argument capture. 1. **Trace and optimize a Java program** - Replace your `java` command with `codeflash optimize java`: + Pass your Java command after `codeflash optimize`: ```bash # JAR application @@ -233,9 +236,15 @@ The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for ac # Class with classpath codeflash optimize java -cp target/classes com.example.Main + + # Maven test suite + codeflash optimize mvn test + + # Gradle test suite + codeflash optimize ./gradlew test ``` - Codeflash will run your program twice (once for profiling, once for argument capture), generate JUnit replay tests, then optimize the most impactful functions. + Codeflash runs your command twice (once for profiling, once for argument capture), generates JUnit replay tests from captured inputs, then optimizes the highest-impact functions. 2. **Long-running programs** @@ -245,7 +254,7 @@ The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for ac codeflash optimize --timeout 30 java -jar target/my-benchmark.jar ``` - Each stage runs for at most 30 seconds, then the program is terminated and captured data is processed. + Each stage runs for at most the specified seconds, then the program receives SIGTERM (allowing JFR dump and shutdown hooks to run), followed by SIGKILL if it doesn't exit within 5 seconds. 3. **Trace only (no optimization)** @@ -253,9 +262,9 @@ The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for ac codeflash optimize --trace-only java -jar target/my-app.jar ``` - This generates replay tests in `src/test/java/codeflash/replay/` without running the optimizer. + This generates replay tests in `src/test/java/codeflash/replay/` without running the optimizer. You can inspect the generated tests and run the optimizer later with `codeflash --replay-test path/to/ReplayTest.java`. - More Options: + Options: - `--timeout`: Maximum time (seconds) for each tracing stage. - `--max-function-count`: Maximum captures per method (default: 100). @@ -263,12 +272,22 @@ The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for ac **How the Java tracer works:** -- **Stage 1 (JFR)**: Runs your program with Java Flight Recorder enabled. JFR is built into the JVM (Java 11+), has ~1% overhead, and doesn't interfere with JIT compilation. This produces accurate method-level CPU profiling data. +- **Stage 1 (JFR profiling)**: Runs your command with Java Flight Recorder enabled via `JAVA_TOOL_OPTIONS`. JFR is built into the JVM (Java 11+) and produces method-level CPU sampling data used to rank functions by impact. -- **Stage 2 (Agent)**: Runs your program with a bytecode instrumentation agent injected via `JAVA_TOOL_OPTIONS`. The agent intercepts method entry points, serializes arguments using Kryo, and writes them to an SQLite database. A 500ms timeout per serialization prevents hangs on complex object graphs. +- **Stage 2 (argument capture)**: Runs your command again with a bytecode instrumentation agent (`codeflash-runtime`) injected via `JAVA_TOOL_OPTIONS`. The agent uses ASM to intercept method entries, serializes arguments with Kryo, and writes them to an SQLite database. A per-serialization timeout prevents hangs on complex object graphs. Constructors and synthetic methods are skipped. -- **Replay Tests**: Generated JUnit 5 test classes that deserialize captured arguments and invoke the original methods via reflection. These tests exercise your code with real-world inputs. +- **Replay tests**: Generated JUnit test classes (JUnit 5 by default, JUnit 4 if detected in the project) that use `ReplayHelper` to deserialize captured arguments from the trace database and invoke the original methods. Each captured invocation becomes a separate test method. These tests exercise your code with the exact inputs observed during tracing. + +- **Optimization**: Functions are ranked by JFR addressable time. For each function, Codeflash generates optimization candidates, verifies correctness against both replay tests and AI-generated regression tests, then benchmarks performance to select the best candidate. + +**Current limitations:** + +- The tracer captures arguments for methods that return values. Void methods are traced for profiling (Stage 1) but argument capture may not produce usable replay tests for them. +- The `--add-opens` JVM flags are automatically injected for Java 16+ compatibility with Kryo serialization. +- For Maven projects, the Maven wrapper (`mvnw`) is preferred over system Maven. Codeflash searches parent directories to find it in multi-module projects. + + From 20f76a30dd76662880662da543b4fc503beffa62 Mon Sep 17 00:00:00 2001 From: HeshamHM28 <206515457+HeshamHM28@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:56:27 +0000 Subject: [PATCH 24/26] fix(test): move --no-pr before optimize subcommand in e2e tracer test --no-pr is a top-level codeflash flag, not an optimize subcommand flag. Placing it after optimize caused it to be passed to the JVM as an unrecognized option. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/scripts/end_to_end_test_java_tracer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/scripts/end_to_end_test_java_tracer.py b/tests/scripts/end_to_end_test_java_tracer.py index 3612b0bbd..0f9f8a2ff 100644 --- a/tests/scripts/end_to_end_test_java_tracer.py +++ b/tests/scripts/end_to_end_test_java_tracer.py @@ -50,9 +50,9 @@ def run_test(expected_improvement_pct: int) -> bool: "--no-project", "-m", "codeflash.main", + "--no-pr", "optimize", "java", - "--no-pr", "-cp", str(classes_dir), "com.example.Workload", From 27535c1da707742a5ac7bda9b5d211742906702a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 1 Apr 2026 06:58:13 +0000 Subject: [PATCH 25/26] Revert "docs: update Java tracer documentation to match verified behavior" This reverts commit 85e8a51f0dbca2eaf73ce8dce66248d98d65f71f. --- .../trace-and-optimize.mdx | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/docs/optimizing-with-codeflash/trace-and-optimize.mdx b/docs/optimizing-with-codeflash/trace-and-optimize.mdx index d7f423931..4c332a929 100644 --- a/docs/optimizing-with-codeflash/trace-and-optimize.mdx +++ b/docs/optimizing-with-codeflash/trace-and-optimize.mdx @@ -57,7 +57,7 @@ codeflash optimize --language javascript script.js ``` -Pass your Java command after `codeflash optimize`: +To trace and optimize a running Java program, replace your `java` command with `codeflash optimize java`: ```bash # JAR application @@ -66,14 +66,11 @@ codeflash optimize java -jar target/my-app.jar --app-args # Class with classpath codeflash optimize java -cp target/classes com.example.Main -# Maven test suite -codeflash optimize mvn test - -# Gradle test suite -codeflash optimize ./gradlew test +# Maven exec +codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main" ``` -For long-running programs, use `--timeout` to limit each tracing stage: +For long-running programs (servers, benchmarks), use `--timeout` to limit each tracing stage: ```bash codeflash optimize --timeout 30 java -jar target/my-app.jar @@ -224,11 +221,11 @@ The JavaScript tracer uses Babel instrumentation to capture function calls durin -The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for profiling, then a bytecode instrumentation agent for argument capture. +The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for accurate profiling, then a bytecode instrumentation agent for argument capture. 1. **Trace and optimize a Java program** - Pass your Java command after `codeflash optimize`: + Replace your `java` command with `codeflash optimize java`: ```bash # JAR application @@ -236,15 +233,9 @@ The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for pr # Class with classpath codeflash optimize java -cp target/classes com.example.Main - - # Maven test suite - codeflash optimize mvn test - - # Gradle test suite - codeflash optimize ./gradlew test ``` - Codeflash runs your command twice (once for profiling, once for argument capture), generates JUnit replay tests from captured inputs, then optimizes the highest-impact functions. + Codeflash will run your program twice (once for profiling, once for argument capture), generate JUnit replay tests, then optimize the most impactful functions. 2. **Long-running programs** @@ -254,7 +245,7 @@ The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for pr codeflash optimize --timeout 30 java -jar target/my-benchmark.jar ``` - Each stage runs for at most the specified seconds, then the program receives SIGTERM (allowing JFR dump and shutdown hooks to run), followed by SIGKILL if it doesn't exit within 5 seconds. + Each stage runs for at most 30 seconds, then the program is terminated and captured data is processed. 3. **Trace only (no optimization)** @@ -262,9 +253,9 @@ The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for pr codeflash optimize --trace-only java -jar target/my-app.jar ``` - This generates replay tests in `src/test/java/codeflash/replay/` without running the optimizer. You can inspect the generated tests and run the optimizer later with `codeflash --replay-test path/to/ReplayTest.java`. + This generates replay tests in `src/test/java/codeflash/replay/` without running the optimizer. - Options: + More Options: - `--timeout`: Maximum time (seconds) for each tracing stage. - `--max-function-count`: Maximum captures per method (default: 100). @@ -272,22 +263,12 @@ The Java tracer uses a **two-stage approach**: JFR (Java Flight Recorder) for pr **How the Java tracer works:** -- **Stage 1 (JFR profiling)**: Runs your command with Java Flight Recorder enabled via `JAVA_TOOL_OPTIONS`. JFR is built into the JVM (Java 11+) and produces method-level CPU sampling data used to rank functions by impact. +- **Stage 1 (JFR)**: Runs your program with Java Flight Recorder enabled. JFR is built into the JVM (Java 11+), has ~1% overhead, and doesn't interfere with JIT compilation. This produces accurate method-level CPU profiling data. -- **Stage 2 (argument capture)**: Runs your command again with a bytecode instrumentation agent (`codeflash-runtime`) injected via `JAVA_TOOL_OPTIONS`. The agent uses ASM to intercept method entries, serializes arguments with Kryo, and writes them to an SQLite database. A per-serialization timeout prevents hangs on complex object graphs. Constructors and synthetic methods are skipped. +- **Stage 2 (Agent)**: Runs your program with a bytecode instrumentation agent injected via `JAVA_TOOL_OPTIONS`. The agent intercepts method entry points, serializes arguments using Kryo, and writes them to an SQLite database. A 500ms timeout per serialization prevents hangs on complex object graphs. -- **Replay tests**: Generated JUnit test classes (JUnit 5 by default, JUnit 4 if detected in the project) that use `ReplayHelper` to deserialize captured arguments from the trace database and invoke the original methods. Each captured invocation becomes a separate test method. These tests exercise your code with the exact inputs observed during tracing. - -- **Optimization**: Functions are ranked by JFR addressable time. For each function, Codeflash generates optimization candidates, verifies correctness against both replay tests and AI-generated regression tests, then benchmarks performance to select the best candidate. +- **Replay Tests**: Generated JUnit 5 test classes that deserialize captured arguments and invoke the original methods via reflection. These tests exercise your code with real-world inputs. - -**Current limitations:** - -- The tracer captures arguments for methods that return values. Void methods are traced for profiling (Stage 1) but argument capture may not produce usable replay tests for them. -- The `--add-opens` JVM flags are automatically injected for Java 16+ compatibility with Kryo serialization. -- For Maven projects, the Maven wrapper (`mvnw`) is preferred over system Maven. Codeflash searches parent directories to find it in multi-module projects. - - From fcaa5131ae2bf8a1ccbc8d47c95da1e70696490a Mon Sep 17 00:00:00 2001 From: HeshamHM28 <206515457+HeshamHM28@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:04:30 +0000 Subject: [PATCH 26/26] fix(java): use qualified names in replay test metadata for correct function matching Replay test metadata now stores qualified names (e.g. Matrix4f.invertLocal) instead of short names (invertLocal). This prevents mismatches when multiple classes have methods with the same name, ensuring replay tests are correctly mapped to their source functions during optimization. Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/discovery/functions_to_optimize.py | 2 +- codeflash/languages/java/replay_test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/codeflash/discovery/functions_to_optimize.py b/codeflash/discovery/functions_to_optimize.py index 8cba319b3..0280b2d85 100644 --- a/codeflash/discovery/functions_to_optimize.py +++ b/codeflash/discovery/functions_to_optimize.py @@ -604,7 +604,7 @@ def _get_java_replay_test_functions( all_functions = lang_support.discover_functions(source_code, source_file) for func in all_functions: - if func.function_name in function_names: + if func.function_name in function_names or func.qualified_name in function_names: functions[source_file].append(func) if trace_file_path is None: diff --git a/codeflash/languages/java/replay_test.py b/codeflash/languages/java/replay_test.py index 415b7a34e..fcc452a80 100644 --- a/codeflash/languages/java/replay_test.py +++ b/codeflash/languages/java/replay_test.py @@ -57,7 +57,8 @@ def generate_replay_tests( ).fetchone() invocation_count = min(count_result[0], max_run_count) - class_function_names.append(method_name) + simple_class = classname.rsplit(".", 1)[-1] + class_function_names.append(f"{simple_class}.{method_name}") safe_method = _sanitize_identifier(method_name) for i in range(invocation_count):