From 3f95ff604abbec93b9cddc0e0665be45e5415310 Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 19:11:06 -0700 Subject: [PATCH 01/14] =?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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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 ztHz4l+XIeE`qxgAR<*~(o;zO&J5ghHUb?r4BM4rPHrzor>|$`cr3}VT#xCPxxVNC0 zchLuHJ3`!dEr$bBUgDOiBN`!e1Is)&>eUJ3*<~YVE_K4%d|@-Gg=2qKc@NetI>Xv# z3qyjb<9!?~4m0HP4zBZ~GZxEYeBp)}oXjXud>vArTL-#8F=r1qJ-8paZ4`!r=O5-s zdQy!!C5J)B8DGMIeQH*Te5Nv@KMb|R%l*u2SXXRrgHLj1^V}K-df64?V|d9wTsE1# z1&n%lP^Wpg`8X2}Ns$8i_PpAN46nS@EB|G8xu+mqQ3OL@7a)2}aPL;~vEpuhPBX@DJ z5M_Dh!|G8Ktgbv(!kYM|fXrLihwD;SRzhE#C(T}SrnD~(p!44;8O3X5^3tb9!_T)b zDzbnBg?Mox8a+1Bmo5*2vzD#OO!(bF=YG&mZOP%0?aNf}S2CIGPHQgP9A1X+0RBb? zTJ&cHgmXBizpTCRpofx?U$Zn9mPB(TcwCt#bsB(;&xRN!f?u8bgGb@_2f)wRK}r}; zHe(QD&ml^LU#ZaE7%0wI!r{n)*fT_~gIl~9>?-7@Dd85v*Ii0RowARX8Iq2Hq8HgI z*~t5!y-&hHh4$$(ZGK7O7mI>QRAKq$$N>hY+)~l5y|Hi={Bl)b+*q^{9+WX8TJS7q zpLmEo7{}3@Z#i>mFf#9)lrx?~uram$#F?!_aF*`;gEKP?WscNjD2i?Mn_OYemNVu>X1(jQnt4!i)^;%dJ(;X)(hg zyJEp*I1@Y4J%*TDbELXaIsRt|?Ck0{0&p8#u`nlAuH}k@Q~C(3>2?krmW)8%92#;a zcBHI}poYsAW&lQCnaRw3!md&|*j2sL_Cf zaWTW(LR**Y&9JM##$b!?95xz*lnm&_nZ;w!#u0ruWA#qvLiS^!o8ONk86(Oqcnwn+ zd_I=LC1Y^}3m(FmXiHfGd=*q?DfAuAkU4_RNcQPXjjC|)955b+J`=d^7%MDZH^!q? zT2G``<1zG;CQ|%=*fs@CKv&&XFro1>Et)XDT#F3SB%1uypdAxnwPzaFu2nGPzv=LY z$@a}uk%h)#0_-@OBRWgU#k4gJ1vny=orIcJY**^4(}hVG^Ok!# zk~3MRL)Mdl+&svUxXCy^PB^Xy#W~J@I`Sa6 zjKk2;rG)9IWb!MHWdA6apX!WA#uQ&E;ERnbbSYi>PMbnzu$2lEJS(sk#uw3M$O6>7 zNIDHmpd8Ke#_7g?CVb4TQc+Hri9~PkUIl?yG5z_55rrYF*x_lS)vRx#(s;cMkt* zQDIEgEpX<|u7|X9#B+mrvTnju`DbIo*}1h4>&w8#^gqYur1@~atT~q*xQc7FbPtxk zfYokO96NP;Ex_6NcS}lIAe$we4_2z`Q`3bQ+tXWdB&1!XVvl-!A$<03&0+a5x#1!p z;q5qrW3r?Cx`MnmAli<-$a@Kj!HE}dv?`c!gDUE>UtYsctS!bFAUg{;6ExzKieidsi@wfZ z3Y`%N4ECnbQxzuUzYOEFa6U&$&*He^wHvn*8Ou<5HExEOV%8btnr8-Jc|qRG;iTz$ zrEgODdaDTDq4pEoo|p8mtK` zAs%^vTjzGD@t|i6d2@&(2*HMz-fb1&Mj706&nj6{Vc0RQgXpoTepso?6@8PT_zO2E z$+9s*z0*{-LDqr_JE5gCS3{B@M}jGKHOB6_GfW&z>DMdl*zdx)3YzCQVqaKkO@>jh zTDt~Ty)SY<{fjH>lV^9p7Ab(oU*)i27=~X8Lweuj$ehO&W+ZW`!q9wOiHDA>(NZ>R zp|I^1SBP6Hixs}zWlS&`twTSzxX&fa*I|B2dBP=W<8eEYTvnk?87FHLQ=SFU0h-e+_<*ZauX7z2n-UpHVlD4QQ>$Ra_gjwV|A^7|?Rx*Ec|W_Xj4M zE!@JJqO6W68V@`+VoW#r%B_-nW5Er#s8WR9VG{;q0hMk*qD<(4tGKfOgg(Eya-?k) zUOi>M3L-yVdT)m865bnSy_sZAU#0*LE9zEpU*zo*IvZa_h>by?J!Z0sSS~|rSW)_N zyw3YK>7(~T!F4c;x+jGV#H3crkOjjT(vI9?YxF66ADZIJNG0M#@Ag5*Z!~usydO^{ zpWs(*S^F9-tFoi<#}QWLe&{@oQ^4zF#&l=a3q{D24nP((mD?jJvGyLo)FYeDwL=eL z1D!U9i;;yrg)d1;rU4}k!O&TG2x*NU!#IevR$u)B59Qc66%NC3y(QdDau!DAzLOXY z@`PK$WU)AO?}SNc>I+j!IK)hj0KSyWOyu=7pw7dXe+JLTZ}G5JJv^+&jSMn?Sg+>@ z3p#b@3v8k@FkpRg1pj|+U3p+l$M$a5=lf7ga}!M+Jjb6lqgzjg<6V~ zr1qks$oZn$*lQ0_MUf~f_MoM;g;vNSi(EV9_c?d&yYD6a{UgrjEHiiRHus#*%ws#8 zN=JIW8~>;+zG9Eb>!c2Mnl-)p1ff6Q16Wxv;jD2O;oMy~De67`kaZp#^#f=q&+)Xq zfWx*}xLn_h%l5M!db>`PGZL{AGhkRHL;BKI&l zU+f1=*aeiE#B0Gwi*s=POZTJQACP?D25=>-F9;(g{|xKg9BG~Y5D%r2pJDj;sx(wD z-GrT7kvsrHBM3I^kHKq5RtJHk-IA`j@fy&Vj9k4f5!~^CRP_*$kN;+5vB)f7@8*Oz zTxSucNa6K(#gFFv0<*_YC6W}1FOtWM{P02|*$K$Z4?PSd?KLC5oV3<*+E*F=;*Eq| zk068hiJ;!6q)jlSWCo|-8NE{qwwm29e~-;l;RE%M{RY)GQ0gJTc_93b(Y!f zvX&U(w;q`C^l*>{N#S|coaeTub(!$nYKCn&;~gq4)RuwIM< z`7OgE+$EgVF%NGHzdTfa^tAW5V=%y6Ry~%aHpL(eAHykJ=p&Ik;kk`C<87pVzrwIe zfP~W`k^9eKWKXc>8lG2=!x|~#IACl^p+-!LLINV4k>GX`@ro{QM&2iY_(n?v-Lj@P z-SQgCF0W%@8vnj47lA%=t{9y6YJCwP}AUFG93861+3=6A2a+#Umdol zJL7V-3IW&8_6ds{*@vNR81`OxF(UksWu{G~b3Hsi7 z6k^@7@rD0jgbXojhSG!+IXamogRcfl=~6Wt?D!=~l{Ct7&Ks&U3T9#kREe0wVs7Ep9I2W;Nxyja26%499L_c&K=~O$IAS z#(jUCHRmzwHj_K(6oLphK)$QSe1f^rPZ%#??)0@BPHb;hlP!GkTZMSSaSV zCg9(j#WgK5-N!E>QqW109{YEq*k`hhn1crc^D|T~jLM`MkWG&+<9T)RGU9Glg(0HL zc?NxH=~E=NQ?DSR_f1ATDe!k>9Za-8BPpyqi#)idYa<--(BgR49_OhGnvv!G9X3nw z;mq)z-^<$>j(5jL{hz2#c@4i`m7@PBk1_1*X$c`2?M}!4D0ejs>~F?Z#-G1o?_>?? zf3-Zy;5{b~J7_5RCrc|+LfS}l2dz~ zK7K0TE^dnp7bxd0Zq=oCF@ub~%EC~C>ut>hbgzaq?QcXIcvpiC{9PVq82Lbz8Zw{D zo7(&XgXlsPLYe6mhS94Aa;mRwOs8WrTa=fYEo!PHF2g;*EnaHyo2vQvF1d%}I{jLO zT5|H*->~nkW{_l?U(X_O?VpD$wt<1XALC;@J`dHjeqb$Wf(@O?Lo)qS8w zF8A5LriH;nm;Blwa~Wv|tE$>ts4lwZd#4sb<@Nxo6ZjDTy&|I&sRtJY)s{p#@;TF0c z1(?9Mjxr;u-SV9%%>{>WgN=P+%qlADj*(S+7+VP4Hytf>{d?zY>G7?sd#|g7E~$Th z74qtfQ7gi=w7+{rm7~RhO(=rz-kuf)=|$ze#JjyM5aUOn$$_GUh0^^k;F%ARdFeEe z5pTN5>QqfgIa6Rw!Nuj_SPNkF5d6Dd3-YaLX)!X69}kiC@g;bz2aU{w|P^sKlp?Jh+h zd?!j|%iM%aG zqPZ15ySOdmN}RK%i{O>2z~&}Ctr(uU2y>FExNya|m5h8QCatAq{P!H{-cQOWihi5* z0M?ZL1SvPSof6sJs>Gg>mLO!|Ozx z>`4x9(F{6PjRw8K{IHFe1>zA

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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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."); }