From 3f95ff604abbec93b9cddc0e0665be45e5415310 Mon Sep 17 00:00:00 2001 From: misrasaurabh1 Date: Thu, 19 Mar 2026 19:11:06 -0700 Subject: [PATCH 01/41] =?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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] feat: smart ReplayHelper with behavior capture and performance timing ReplayHelper now reads CODEFLASH_MODE env var and produces the same output as the existing test instrumentation: - Behavior mode: captures return value via Kryo serialization, writes to SQLite (test_results table) for correctness comparison, prints start/end timing markers - Performance mode: runs inner loop for JIT warmup, prints timing markers for each iteration matching the expected format - No mode: just invokes the method (trace-only or manual testing) This achieves feature parity with the existing test instrumentation for replay tests, which call functions via reflection and can't be wrapped by text-level instrumentation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/com/codeflash/ReplayHelper.java | 236 +++++++++++++++--- .../resources/codeflash-runtime-1.0.0.jar | Bin 15973968 -> 15976923 bytes 2 files changed, 198 insertions(+), 38 deletions(-) diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java b/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java index f4b9ec453..3a73038c1 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/ReplayHelper.java @@ -12,20 +12,179 @@ public class ReplayHelper { - private final Connection db; + private final Connection traceDb; + + // Codeflash instrumentation state — read from environment variables once + private final String mode; // "behavior", "performance", or null + private final int loopIndex; + private final String testIteration; + private final String outputFile; // SQLite path for behavior capture + private final int innerIterations; // for performance looping + + // Behavior mode: lazily opened SQLite connection for writing results + private Connection behaviorDb; + private boolean behaviorDbInitialized; public ReplayHelper(String traceDbPath) { try { - this.db = DriverManager.getConnection("jdbc:sqlite:" + traceDbPath); + this.traceDb = DriverManager.getConnection("jdbc:sqlite:" + traceDbPath); } catch (SQLException e) { throw new RuntimeException("Failed to open trace database: " + traceDbPath, e); } + + // Read codeflash instrumentation env vars (set by the test runner) + this.mode = System.getenv("CODEFLASH_MODE"); + this.loopIndex = parseIntEnv("CODEFLASH_LOOP_INDEX", 1); + this.testIteration = getEnvOrDefault("CODEFLASH_TEST_ITERATION", "0"); + this.outputFile = System.getenv("CODEFLASH_OUTPUT_FILE"); + this.innerIterations = parseIntEnv("CODEFLASH_INNER_ITERATIONS", 10); } public void replay(String className, String methodName, String descriptor, int invocationIndex) throws Exception { - // Query the function_calls table for this method at the given index + // Deserialize args and resolve method (done once, outside timing) + Object[] allArgs = loadArgs(className, methodName, descriptor, invocationIndex); + Class targetClass = Class.forName(className); + + Type[] paramTypes = Type.getArgumentTypes(descriptor); + Class[] paramClasses = new Class[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + paramClasses[i] = typeToClass(paramTypes[i]); + } + + Method method = targetClass.getDeclaredMethod(methodName, paramClasses); + method.setAccessible(true); + boolean isStatic = Modifier.isStatic(method.getModifiers()); + + Object instance = null; + if (!isStatic) { + try { + java.lang.reflect.Constructor ctor = targetClass.getDeclaredConstructor(); + ctor.setAccessible(true); + instance = ctor.newInstance(); + } catch (NoSuchMethodException e) { + instance = new org.objenesis.ObjenesisStd().newInstance(targetClass); + } + } + + // Get the calling test method name from the stack trace + String testMethodName = getCallingTestMethodName(); + // Module name = the test class that called us + String testClassName = getCallingTestClassName(); + + if ("behavior".equals(mode)) { + replayBehavior(method, instance, allArgs, className, methodName, testClassName, testMethodName); + } else if ("performance".equals(mode)) { + replayPerformance(method, instance, allArgs, className, methodName, testClassName, testMethodName); + } else { + // No codeflash mode — just invoke (trace-only or manual testing) + method.invoke(instance, allArgs); + } + } + + private void replayBehavior(Method method, Object instance, Object[] args, + String className, String methodName, + String testClassName, String testMethodName) throws Exception { + String invId = testIteration + "_" + testMethodName; + + // Print start marker (same format as behavior instrumentation) + System.out.println("!$######" + testClassName + ":" + testClassName + "." + testMethodName + + ":" + methodName + ":" + loopIndex + ":" + invId + "######$!"); + + long startNs = System.nanoTime(); + Object result; + try { + result = method.invoke(instance, args); + } catch (java.lang.reflect.InvocationTargetException e) { + throw (Exception) e.getCause(); + } + long durationNs = System.nanoTime() - startNs; + + // Print end marker + System.out.println("!######" + testClassName + ":" + testClassName + "." + testMethodName + + ":" + methodName + ":" + loopIndex + ":" + invId + ":" + durationNs + "######!"); + + // Write return value to SQLite for correctness comparison + if (outputFile != null && !outputFile.isEmpty()) { + writeBehaviorResult(testClassName, testMethodName, methodName, invId, durationNs, result); + } + } + + private void replayPerformance(Method method, Object instance, Object[] args, + String className, String methodName, + String testClassName, String testMethodName) throws Exception { + // Performance mode: run inner loop for JIT warmup, print timing for each iteration + int maxInner = innerIterations; + for (int inner = 0; inner < maxInner; inner++) { + int loopId = (loopIndex - 1) * maxInner + inner; + String invId = testMethodName; + + // Print start marker + System.out.println("!$######" + testClassName + ":" + testClassName + "." + testMethodName + + ":" + methodName + ":" + loopId + ":" + invId + "######$!"); + + long startNs = System.nanoTime(); + try { + method.invoke(instance, args); + } catch (java.lang.reflect.InvocationTargetException e) { + // Swallow — performance mode doesn't check correctness + } + long durationNs = System.nanoTime() - startNs; + + // Print end marker + System.out.println("!######" + testClassName + ":" + testClassName + "." + testMethodName + + ":" + methodName + ":" + loopId + ":" + invId + ":" + durationNs + "######!"); + } + } + + private void writeBehaviorResult(String testClassName, String testMethodName, + String functionName, String invId, + long durationNs, Object result) { + try { + ensureBehaviorDb(); + String sql = "INSERT INTO test_results VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + try (PreparedStatement ps = behaviorDb.prepareStatement(sql)) { + ps.setString(1, testClassName); // test_module_path + ps.setString(2, testClassName); // test_class_name + ps.setString(3, testMethodName); // test_function_name + ps.setString(4, functionName); // function_getting_tested + ps.setInt(5, loopIndex); // loop_index + ps.setString(6, invId); // iteration_id + ps.setLong(7, durationNs); // runtime + ps.setBytes(8, serializeResult(result)); // return_value + ps.setString(9, "function_call"); // verification_type + ps.executeUpdate(); + } + } catch (Exception e) { + System.err.println("ReplayHelper: SQLite behavior write error: " + e.getMessage()); + } + } + + private void ensureBehaviorDb() throws SQLException { + if (behaviorDbInitialized) return; + behaviorDbInitialized = true; + behaviorDb = DriverManager.getConnection("jdbc:sqlite:" + outputFile); + try (java.sql.Statement stmt = behaviorDb.createStatement()) { + stmt.execute("CREATE TABLE IF NOT EXISTS test_results (" + + "test_module_path TEXT, test_class_name TEXT, test_function_name TEXT, " + + "function_getting_tested TEXT, loop_index INTEGER, iteration_id TEXT, " + + "runtime INTEGER, return_value BLOB, verification_type TEXT)"); + } + } + + private byte[] serializeResult(Object result) { + if (result == null) return null; + try { + return Serializer.serialize(result); + } catch (Exception e) { + // Fall back to String.valueOf if Kryo fails + return String.valueOf(result).getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + } + + private Object[] loadArgs(String className, String methodName, String descriptor, int invocationIndex) + throws SQLException { byte[] argsBlob; - try (PreparedStatement stmt = db.prepareStatement( + try (PreparedStatement stmt = traceDb.prepareStatement( "SELECT args FROM function_calls " + "WHERE classname = ? AND function = ? AND descriptor = ? " + "ORDER BY time_ns LIMIT 1 OFFSET ?")) { @@ -43,46 +202,35 @@ public void replay(String className, String methodName, String descriptor, int i } } - // Deserialize args Object deserialized = Serializer.deserialize(argsBlob); if (!(deserialized instanceof Object[])) { throw new RuntimeException("Deserialized args is not Object[], got: " + (deserialized == null ? "null" : deserialized.getClass().getName())); } - Object[] allArgs = (Object[]) deserialized; - - // Load the target class - Class targetClass = Class.forName(className); + return (Object[]) deserialized; + } - // Parse descriptor to find parameter types - Type[] paramTypes = Type.getArgumentTypes(descriptor); - Class[] paramClasses = new Class[paramTypes.length]; - for (int i = 0; i < paramTypes.length; i++) { - paramClasses[i] = typeToClass(paramTypes[i]); + private static String getCallingTestMethodName() { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + // Walk up: [0]=getStackTrace, [1]=this method, [2]=replay(), [3]=calling test method + for (int i = 3; i < stack.length; i++) { + String method = stack[i].getMethodName(); + if (method.startsWith("replay_")) { + return method; + } } + return stack.length > 3 ? stack[3].getMethodName() : "unknown"; + } - // Find the method - Method method = targetClass.getDeclaredMethod(methodName, paramClasses); - method.setAccessible(true); - - boolean isStatic = Modifier.isStatic(method.getModifiers()); - - if (isStatic) { - method.invoke(null, allArgs); - } else { - // Args contain only explicit parameters (no 'this'). - // Create a default instance via no-arg constructor or Kryo. - Object instance; - try { - java.lang.reflect.Constructor ctor = targetClass.getDeclaredConstructor(); - ctor.setAccessible(true); - instance = ctor.newInstance(); - } catch (NoSuchMethodException e) { - // Fall back to Objenesis instantiation (no constructor needed) - instance = new org.objenesis.ObjenesisStd().newInstance(targetClass); + private static String getCallingTestClassName() { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + for (int i = 3; i < stack.length; i++) { + String cls = stack[i].getClassName(); + if (cls.contains("ReplayTest") || cls.contains("replay")) { + return cls; } - method.invoke(instance, allArgs); } + return stack.length > 3 ? stack[3].getClassName() : "unknown"; } private static Class typeToClass(Type type) throws ClassNotFoundException { @@ -106,11 +254,23 @@ private static Class typeToClass(Type type) throws ClassNotFoundException { } } + private static int parseIntEnv(String name, int defaultValue) { + String val = System.getenv(name); + if (val == null || val.isEmpty()) return defaultValue; + try { return Integer.parseInt(val); } catch (NumberFormatException e) { return defaultValue; } + } + + private static String getEnvOrDefault(String name, String defaultValue) { + String val = System.getenv(name); + return (val != null && !val.isEmpty()) ? val : defaultValue; + } + public void close() { - try { - if (db != null) db.close(); - } catch (SQLException e) { - System.err.println("Error closing ReplayHelper: " + e.getMessage()); + try { if (traceDb != null) traceDb.close(); } catch (SQLException e) { + System.err.println("Error closing ReplayHelper trace db: " + e.getMessage()); + } + try { if (behaviorDb != null) behaviorDb.close(); } catch (SQLException e) { + System.err.println("Error closing ReplayHelper behavior db: " + e.getMessage()); } } } diff --git a/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar b/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar index 10a03b3cc7b53fd7a6a87708b3cc0b412332f38a..48ebc0a96623f477df4640b0eeb733f5ab82b477 100644 GIT binary patch delta 45011 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 R`>EI|t%^gV1KF56|35)Usg?i$ delta 42432 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

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

oJL#~t&}D>U><)7=Otu#s@@*MNIre;N)F98{oA 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 Date: Thu, 19 Mar 2026 22:34:56 -0700 Subject: [PATCH 09/41] 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/41] 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/41] 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/41] 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/41] 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/41] fix: increase JFR sampling frequency and make Workload exercise functions harder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set jdk.ExecutionSample#period=1ms (default was 10ms) so JFR captures samples from shorter-running programs - Workload.main now runs 1000 rounds with larger inputs so JFR can capture method-level CPU samples (repeatString with O(n²) concat dominates ~75% of samples) Co-Authored-By: Claude Opus 4.6 (1M context) --- codeflash/languages/java/tracer.py | 7 ++++- .../src/main/java/com/example/Workload.java | 26 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py index 5ad449088..5cc098be5 100644 --- a/codeflash/languages/java/tracer.py +++ b/codeflash/languages/java/tracer.py @@ -122,7 +122,12 @@ def create_tracer_config( def build_jfr_env(self, jfr_file: Path) -> dict[str, str]: env = os.environ.copy() - jfr_opts = f"-XX:StartFlightRecording=filename={jfr_file.resolve()},settings=profile,dumponexit=true" + # Use profile settings with increased sampling frequency (1ms instead of default 10ms) + # This captures more samples for short-running programs + jfr_opts = ( + f"-XX:StartFlightRecording=filename={jfr_file.resolve()},settings=profile,dumponexit=true" + ",jdk.ExecutionSample#period=1ms" + ) existing = env.get("JAVA_TOOL_OPTIONS", "") env["JAVA_TOOL_OPTIONS"] = f"{existing} {jfr_opts}".strip() return env diff --git a/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java b/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java index 9b6078000..7beb2a4ea 100644 --- a/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java +++ b/tests/test_languages/fixtures/java_tracer_e2e/src/main/java/com/example/Workload.java @@ -36,20 +36,30 @@ public int instanceMethod(int x, int y) { } public static void main(String[] args) { - // Exercise the methods so the tracer can capture invocations - System.out.println("computeSum(100) = " + computeSum(100)); - System.out.println("computeSum(50) = " + computeSum(50)); + // Run methods with large inputs so JFR can capture CPU samples. + // Small inputs finish too fast (<1ms) for JFR's 10ms sampling interval. + for (int round = 0; round < 1000; round++) { + computeSum(100_000); + repeatString("hello world ", 1000); + + List nums = new ArrayList<>(); + for (int i = 1; i <= 10_000; i++) nums.add(i); + filterEvens(nums); + Workload w = new Workload(); + w.instanceMethod(100_000, 42); + } + + // Also call with small inputs for variety in traced args + System.out.println("computeSum(100) = " + computeSum(100)); System.out.println("repeatString(\"ab\", 3) = " + repeatString("ab", 3)); - System.out.println("repeatString(\"x\", 5) = " + repeatString("x", 5)); - List nums = new ArrayList<>(); - for (int i = 1; i <= 10; i++) nums.add(i); - System.out.println("filterEvens(1..10) = " + filterEvens(nums)); + List small = new ArrayList<>(); + for (int i = 1; i <= 10; i++) small.add(i); + System.out.println("filterEvens(1..10) = " + filterEvens(small)); Workload w = new Workload(); System.out.println("instanceMethod(5, 3) = " + w.instanceMethod(5, 3)); - System.out.println("instanceMethod(10, 2) = " + w.instanceMethod(10, 2)); System.out.println("Workload complete."); } From 3c63b60ae49bf9e14a99d858893d01e955ae0d37 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 16:30:22 +0000 Subject: [PATCH 15/41] fix: preserve pom.xml formatting in config writer and align write/remove priority Replace xml.etree.ElementTree with text-based regex manipulation in _write_maven_properties() and _remove_java_build_config(). ElementTree destroys XML comments, mangles namespace declarations (ns0: prefixes), and reformats whitespace. The new approach reads/writes pom.xml as plain text, only touching codeflash.* property lines. Also extracts duplicated key_map to shared _MAVEN_KEY_MAP constant and aligns remove priority to check pom.xml first (matching write order). Co-Authored-By: Claude Opus 4.6 --- codeflash/setup/config_writer.py | 154 +++++++++++++++---------- codeflash/setup/detector.py | 3 +- tests/test_setup/test_config_writer.py | 148 ++++++++++++++++++++++++ tests/test_setup/test_detector.py | 16 +++ 4 files changed, 257 insertions(+), 64 deletions(-) create mode 100644 tests/test_setup/test_config_writer.py diff --git a/codeflash/setup/config_writer.py b/codeflash/setup/config_writer.py index 4616ccf5f..43ce03eb3 100644 --- a/codeflash/setup/config_writer.py +++ b/codeflash/setup/config_writer.py @@ -124,47 +124,87 @@ def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tup return _write_gradle_properties(gradle_props_path, non_default) +_MAVEN_KEY_MAP: dict[str, str] = { + "module-root": "moduleRoot", + "tests-root": "testsRoot", + "git-remote": "gitRemote", + "disable-telemetry": "disableTelemetry", + "ignore-paths": "ignorePaths", + "formatter-cmds": "formatterCmds", +} + + def _write_maven_properties(pom_path: Path, config: dict[str, Any]) -> tuple[bool, str]: - """Add codeflash.* properties to pom.xml section.""" - import xml.etree.ElementTree as ET + """Add codeflash.* properties to pom.xml section. + + Uses text-based manipulation to preserve comments, formatting, and namespace declarations. + """ + import re try: - tree = ET.parse(str(pom_path)) - root = tree.getroot() - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - - # Find or create - properties = root.find("m:properties", ns) or root.find("properties") - if properties is None: - properties = ET.SubElement(root, "properties") - - # Convert kebab-case keys to camelCase for Maven convention - key_map = { - "module-root": "moduleRoot", - "tests-root": "testsRoot", - "git-remote": "gitRemote", - "disable-telemetry": "disableTelemetry", - "ignore-paths": "ignorePaths", - "formatter-cmds": "formatterCmds", - } + content = pom_path.read_text(encoding="utf-8") + + # Remove existing codeflash.* property lines (with surrounding whitespace) + content = re.sub(r"\n[ \t]*]*>[^<]*]*>", "", content) + + # Detect child indentation from existing properties or fall back to indent + 4 spaces + props_close = re.search(r"([ \t]*)", content) + if props_close: + parent_indent = props_close.group(1) + # Try to detect child indent from an existing property element + child_match = re.search( + r"\n([ \t]+)<[a-zA-Z]", + content[content.find("") : props_close.start()] if "" in content else "", + ) + child_indent = child_match.group(1) if child_match else parent_indent + " " + else: + parent_indent = "" + child_indent = " " + # Build new property lines with detected indentation + new_lines = [] for key, value in config.items(): - maven_key = f"codeflash.{key_map.get(key, key)}" + maven_key = f"codeflash.{_MAVEN_KEY_MAP.get(key, key)}" if isinstance(value, list): value = ",".join(str(v) for v in value) elif isinstance(value, bool): value = str(value).lower() else: value = str(value) - - existing = properties.find(maven_key) - if existing is None: - elem = ET.SubElement(properties, maven_key) - elem.text = value - else: - existing.text = value - - tree.write(str(pom_path), xml_declaration=True, encoding="UTF-8") + new_lines.append(f"{child_indent}<{maven_key}>{value}") + + properties_block = "\n".join(new_lines) + + # Insert before + if props_close: + content = ( + content[: props_close.start()] + + properties_block + + "\n" + + parent_indent + + "" + + content[props_close.end() :] + ) + else: + # No section — create one before + project_close = re.search(r"([ \t]*)", content) + if project_close: + indent = project_close.group(1) + inner = " " + indent + props_section = ( + f"{inner}\n" + + "\n".join(f" {line}" for line in new_lines) + + f"\n{inner}\n" + ) + content = ( + content[: project_close.start()] + + props_section + + indent + + "" + + content[project_close.end() :] + ) + + pom_path.write_text(content, encoding="utf-8") return True, f"Config saved to {pom_path} " except Exception as e: @@ -173,15 +213,6 @@ def _write_maven_properties(pom_path: Path, config: dict[str, Any]) -> tuple[boo def _write_gradle_properties(props_path: Path, config: dict[str, Any]) -> tuple[bool, str]: """Add codeflash.* entries to gradle.properties.""" - key_map = { - "module-root": "moduleRoot", - "tests-root": "testsRoot", - "git-remote": "gitRemote", - "disable-telemetry": "disableTelemetry", - "ignore-paths": "ignorePaths", - "formatter-cmds": "formatterCmds", - } - try: lines = [] if props_path.exists(): @@ -195,7 +226,7 @@ def _write_gradle_properties(props_path: Path, config: dict[str, Any]) -> tuple[ lines.append("") lines.append("# Codeflash configuration — https://docs.codeflash.ai") for key, value in config.items(): - gradle_key = f"codeflash.{key_map.get(key, key)}" + gradle_key = f"codeflash.{_MAVEN_KEY_MAP.get(key, key)}" if isinstance(value, list): value = ",".join(str(v) for v in value) elif isinstance(value, bool): @@ -306,8 +337,25 @@ def _remove_from_pyproject(project_root: Path) -> tuple[bool, str]: def _remove_java_build_config(project_root: Path) -> tuple[bool, str]: - """Remove codeflash.* properties from pom.xml or gradle.properties.""" - # Try gradle.properties first (simpler) + """Remove codeflash.* properties from pom.xml or gradle.properties. + + Priority matches _write_java_build_config: pom.xml first, then gradle.properties. + """ + # Try pom.xml first (matches write priority) — text-based removal preserves formatting + pom_path = project_root / "pom.xml" + if pom_path.exists(): + try: + import re + + content = pom_path.read_text(encoding="utf-8") + updated = re.sub(r"\n[ \t]*]*>[^<]*]*>", "", content) + if updated != content: + pom_path.write_text(updated, encoding="utf-8") + return True, "Removed codeflash properties from pom.xml" + except Exception as e: + return False, f"Failed to remove config from pom.xml: {e}" + + # Try gradle.properties gradle_props = project_root / "gradle.properties" if gradle_props.exists(): try: @@ -316,33 +364,13 @@ def _remove_java_build_config(project_root: Path) -> tuple[bool, str]: line for line in lines if not line.strip().startswith("codeflash.") - and line.strip() != "# Codeflash configuration — https://docs.codeflash.ai" + and line.strip() != "# Codeflash configuration \u2014 https://docs.codeflash.ai" ] gradle_props.write_text("\n".join(filtered) + "\n", encoding="utf-8") return True, "Removed codeflash properties from gradle.properties" except Exception as e: return False, f"Failed to remove config from gradle.properties: {e}" - # Try pom.xml - pom_path = project_root / "pom.xml" - if pom_path.exists(): - try: - import xml.etree.ElementTree as ET - - tree = ET.parse(str(pom_path)) - root = tree.getroot() - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - for properties in [root.find("m:properties", ns), root.find("properties")]: - if properties is None: - continue - to_remove = [child for child in properties if child.tag.split("}")[-1].startswith("codeflash.")] - for elem in to_remove: - properties.remove(elem) - tree.write(str(pom_path), xml_declaration=True, encoding="UTF-8") - return True, "Removed codeflash properties from pom.xml" - except Exception as e: - return False, f"Failed to remove config from pom.xml: {e}" - return True, "No Java build config found" diff --git a/codeflash/setup/detector.py b/codeflash/setup/detector.py index 06d690190..81e900436 100644 --- a/codeflash/setup/detector.py +++ b/codeflash/setup/detector.py @@ -900,7 +900,8 @@ def has_existing_config(project_root: Path) -> tuple[bool, str | None]: except Exception: pass - # Check Java build files — Java projects store config in pom.xml properties or gradle.properties + # Check Java build files — for zero-config Java, any build file means "configured" + # because Java config is auto-detected from build files without explicit codeflash.* properties for build_file in ("pom.xml", "build.gradle", "build.gradle.kts"): if (project_root / build_file).exists(): return True, build_file diff --git a/tests/test_setup/test_config_writer.py b/tests/test_setup/test_config_writer.py new file mode 100644 index 000000000..89426bdfd --- /dev/null +++ b/tests/test_setup/test_config_writer.py @@ -0,0 +1,148 @@ +"""Tests for config_writer module — Java pom.xml formatting preservation.""" + +from pathlib import Path + + +class TestWriteMavenProperties: + """Tests for _write_maven_properties — text-based pom.xml editing.""" + + def test_preserves_comments(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + '\n' + "\n" + " \n" + " \n" + " 17\n" + " \n" + "\n", + encoding="utf-8", + ) + + from codeflash.setup.config_writer import _write_maven_properties + + ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "" in result + assert "src/main/java" in result + + def test_preserves_namespace(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + '\n' + '\n' + " \n" + " 17\n" + " \n" + "\n", + encoding="utf-8", + ) + + from codeflash.setup.config_writer import _write_maven_properties + + ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert 'xmlns="http://maven.apache.org/POM/4.0.0"' in result + # Must NOT have ns0: prefix (ElementTree bug) + assert "ns0:" not in result + + def test_updates_existing_codeflash_properties(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" + " \n" + " old/path\n" + " \n" + "\n", + encoding="utf-8", + ) + + from codeflash.setup.config_writer import _write_maven_properties + + ok, _ = _write_maven_properties(pom, {"module-root": "new/path"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "old/path" not in result + assert "new/path" in result + + def test_creates_properties_section(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" " 4.0.0\n" "\n", + encoding="utf-8", + ) + + from codeflash.setup.config_writer import _write_maven_properties + + ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "" in result + assert "src/main/java" in result + + def test_converts_kebab_to_camelcase(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n \n \n\n", + encoding="utf-8", + ) + + from codeflash.setup.config_writer import _write_maven_properties + + ok, _ = _write_maven_properties(pom, {"ignore-paths": ["target", "build"]}) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "target,build" in result + + +class TestRemoveJavaBuildConfig: + """Tests for _remove_java_build_config — preserves formatting during removal.""" + + def test_removes_codeflash_from_pom_preserving_others(self, tmp_path: Path) -> None: + pom = tmp_path / "pom.xml" + pom.write_text( + "\n" + " \n" + " \n" + " 17\n" + " src/main/java\n" + " \n" + "\n", + encoding="utf-8", + ) + + from codeflash.setup.config_writer import _remove_java_build_config + + ok, _ = _remove_java_build_config(tmp_path) + result = pom.read_text(encoding="utf-8") + + assert ok + assert "" in result + assert "17" in result + assert "codeflash.moduleRoot" not in result + + def test_removes_codeflash_from_gradle_properties(self, tmp_path: Path) -> None: + gradle = tmp_path / "gradle.properties" + gradle.write_text( + "org.gradle.jvmargs=-Xmx2g\n" + "# Codeflash configuration \u2014 https://docs.codeflash.ai\n" + "codeflash.moduleRoot=src/main/java\n" + "codeflash.testsRoot=src/test/java\n", + encoding="utf-8", + ) + + from codeflash.setup.config_writer import _remove_java_build_config + + ok, _ = _remove_java_build_config(tmp_path) + result = gradle.read_text(encoding="utf-8") + + assert ok + assert "org.gradle.jvmargs=-Xmx2g" in result + assert "codeflash." not in result diff --git a/tests/test_setup/test_detector.py b/tests/test_setup/test_detector.py index 781d393e6..3b0e165c8 100644 --- a/tests/test_setup/test_detector.py +++ b/tests/test_setup/test_detector.py @@ -558,6 +558,22 @@ def test_returns_false_when_no_config(self, tmp_path): assert has_config is False assert config_type is None + def test_java_pom_xml_is_zero_config(self, tmp_path): + """Java projects with pom.xml are zero-config — build file presence means configured.""" + (tmp_path / "pom.xml").write_text("4.0.0") + + has_config, config_type = has_existing_config(tmp_path) + assert has_config is True + assert config_type == "pom.xml" + + def test_java_build_gradle_is_zero_config(self, tmp_path): + """Java projects with build.gradle are zero-config — build file presence means configured.""" + (tmp_path / "build.gradle").write_text('plugins { id "java" }') + + has_config, config_type = has_existing_config(tmp_path) + assert has_config is True + assert config_type == "build.gradle" + def test_returns_false_for_empty_directory(self, tmp_path): """Should return False for empty directory.""" has_config, config_type = has_existing_config(tmp_path) From 5942ae9e168026cec97dacb453a8f5a13c21857d Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 16:50:18 +0000 Subject: [PATCH 16/41] fix: prefer closer config file over parent Java build file in monorepos (TODO-37) Java detection in parse_config_file() short-circuited before the existing depth-comparison logic, so a parent pom.xml would override a closer package.json or pyproject.toml. Now all config sources are detected first and the closest one to CWD wins. Co-Authored-By: Claude Opus 4.6 --- codeflash/code_utils/config_parser.py | 19 +++--- tests/code_utils/test_config_parser.py | 87 ++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 tests/code_utils/test_config_parser.py diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index 1d0f13df5..832b34bcc 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -106,16 +106,21 @@ def find_conftest_files(test_paths: list[Path]) -> list[Path]: def parse_config_file( config_file_path: Path | None = None, override_formatter_check: bool = False ) -> tuple[dict[str, Any], Path]: - # Java projects: read config from pom.xml/gradle.properties (no standalone config file needed) - if config_file_path is None: - java_config = _try_parse_java_build_config() - if java_config is not None: - config, project_root = java_config - return config, project_root - + # Detect all config sources — Java, package.json, pyproject.toml + java_result = _try_parse_java_build_config() if config_file_path is None else None package_json_path = find_package_json(config_file_path) pyproject_toml_path = find_closest_config_file("pyproject.toml") if config_file_path is None else None + # Use Java config only if no closer JS/Python config exists (monorepo support). + # In a monorepo with a parent pom.xml and a child package.json, the closer config wins. + if java_result is not None: + java_depth = len(java_result[1].parts) + has_closer = (package_json_path is not None and len(package_json_path.parent.parts) >= java_depth) or ( + pyproject_toml_path is not None and len(pyproject_toml_path.parent.parts) >= java_depth + ) + if not has_closer: + return java_result + # When both config files exist, prefer the one closer to CWD. # This prevents a parent-directory package.json (e.g., monorepo root) # from overriding a closer pyproject.toml. diff --git a/tests/code_utils/test_config_parser.py b/tests/code_utils/test_config_parser.py new file mode 100644 index 000000000..dc47a4f1d --- /dev/null +++ b/tests/code_utils/test_config_parser.py @@ -0,0 +1,87 @@ +"""Tests for config_parser.py — monorepo language detection priority.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from unittest.mock import patch + +import pytest + +from codeflash.code_utils.config_parser import parse_config_file + + +class TestMonorepoConfigPriority: + """Verify that closer config files win over parent Java build files in monorepos.""" + + def test_closer_package_json_wins_over_parent_pom_xml(self, tmp_path: Path) -> None: + """In monorepo/frontend/, a local package.json should win over a parent pom.xml.""" + # Parent Java project + (tmp_path / "pom.xml").write_text("", encoding="utf-8") + (tmp_path / "src" / "main" / "java").mkdir(parents=True) + + # Child JS project + frontend = tmp_path / "frontend" + frontend.mkdir() + (frontend / "package.json").write_text( + json.dumps({"name": "frontend", "codeflash": {"moduleRoot": "src"}}), + encoding="utf-8", + ) + (frontend / "src").mkdir() + + with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls: + mock_path_cls.cwd.return_value = frontend + # find_package_json also uses Path.cwd; mock it at the source + with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls: + mock_js_path_cls.cwd.return_value = frontend + # Also need to let normal Path operations work + mock_path_cls.side_effect = Path + mock_path_cls.cwd.return_value = frontend + mock_js_path_cls.side_effect = Path + mock_js_path_cls.cwd.return_value = frontend + + config, root = parse_config_file() + + # Should detect JS, not Java + assert config.get("language") != "java", ( + "Closer package.json should take priority over parent pom.xml" + ) + + def test_java_wins_when_no_closer_js_config(self, tmp_path: Path) -> None: + """When only a pom.xml exists (no package.json/pyproject.toml closer), Java config wins.""" + (tmp_path / "pom.xml").write_text("", encoding="utf-8") + (tmp_path / "src" / "main" / "java").mkdir(parents=True) + + with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls: + mock_path_cls.side_effect = Path + mock_path_cls.cwd.return_value = tmp_path + with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls: + mock_js_path_cls.side_effect = Path + mock_js_path_cls.cwd.return_value = tmp_path + + config, root = parse_config_file() + + assert config.get("language") == "java" + + def test_same_level_package_json_wins_over_pom_xml(self, tmp_path: Path) -> None: + """When pom.xml and package.json are at the same level, package.json wins (more specific).""" + (tmp_path / "pom.xml").write_text("", encoding="utf-8") + (tmp_path / "src" / "main" / "java").mkdir(parents=True) + (tmp_path / "package.json").write_text( + json.dumps({"name": "mixed-project", "codeflash": {"moduleRoot": "src"}}), + encoding="utf-8", + ) + + with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls: + mock_path_cls.side_effect = Path + mock_path_cls.cwd.return_value = tmp_path + with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls: + mock_js_path_cls.side_effect = Path + mock_js_path_cls.cwd.return_value = tmp_path + + config, root = parse_config_file() + + assert config.get("language") != "java", ( + "Same-level package.json should take priority over pom.xml" + ) From 12921447b985ef73e58d007278353c8c1da0cac1 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 16:50:31 +0000 Subject: [PATCH 17/41] fix: capture real line numbers in tracer and track dropped captures (TODO-34, TODO-38) TODO-34: TracingClassVisitor hardcoded line number to 0 because ASM's visitMethod() doesn't provide line info. Added a pre-scan pass in TracingTransformer.instrumentClass() that collects first line numbers via visitLineNumber() before the instrumentation pass. TODO-38: Serialization timeouts/failures silently dropped captures with no visibility. Added AtomicInteger droppedCaptures counter and included it in flush() metadata output. Co-Authored-By: Claude Opus 4.6 --- .../com/codeflash/tracer/TraceRecorder.java | 9 ++++- .../codeflash/tracer/TracingClassVisitor.java | 11 ++++-- .../codeflash/tracer/TracingTransformer.java | 32 +++++++++++++++++- .../resources/codeflash-runtime-1.0.0.jar | Bin 15976923 -> 15979672 bytes 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java index 2a22b74f4..28c2d2998 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java @@ -22,6 +22,7 @@ public final class TraceRecorder { private final TracerConfig config; private final TraceWriter writer; private final ConcurrentHashMap functionCounts = new ConcurrentHashMap<>(); + private final AtomicInteger droppedCaptures = new AtomicInteger(0); private final int maxFunctionCount; private final ExecutorService serializerExecutor; @@ -82,11 +83,13 @@ private void onEntryImpl(String className, String methodName, String descriptor, argsBlob = future.get(SERIALIZATION_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); + droppedCaptures.incrementAndGet(); System.err.println("[codeflash-tracer] Serialization timed out for " + className + "." + methodName); return; } catch (Exception e) { Throwable cause = e.getCause() != null ? e.getCause() : e; + droppedCaptures.incrementAndGet(); System.err.println("[codeflash-tracer] Serialization failed for " + className + "." + methodName + ": " + cause.getClass().getSimpleName() + ": " + cause.getMessage()); return; @@ -113,11 +116,15 @@ public void flush() { } metadata.put("totalCaptures", String.valueOf(totalCaptures)); + int dropped = droppedCaptures.get(); + metadata.put("droppedCaptures", String.valueOf(dropped)); + writer.writeMetadata(metadata); writer.flush(); writer.close(); System.err.println("[codeflash-tracer] Captured " + totalCaptures - + " invocations across " + functionCounts.size() + " methods"); + + " invocations across " + functionCounts.size() + " methods" + + (dropped > 0 ? " (" + dropped + " dropped due to serialization timeout/failure)" : "")); } } diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java index c760ea636..90d4cd7a0 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingClassVisitor.java @@ -4,14 +4,20 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; +import java.util.Collections; +import java.util.Map; + public class TracingClassVisitor extends ClassVisitor { private final String internalClassName; + private final Map methodLineNumbers; private String sourceFile; - public TracingClassVisitor(ClassVisitor classVisitor, String internalClassName) { + public TracingClassVisitor(ClassVisitor classVisitor, String internalClassName, + Map methodLineNumbers) { super(Opcodes.ASM9, classVisitor); this.internalClassName = internalClassName; + this.methodLineNumbers = methodLineNumbers != null ? methodLineNumbers : Collections.emptyMap(); } @Override @@ -37,7 +43,8 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, return mv; } + int lineNumber = methodLineNumbers.getOrDefault(name + descriptor, 0); return new TracingMethodAdapter(mv, access, name, descriptor, - internalClassName, 0, sourceFile != null ? sourceFile : ""); + internalClassName, lineNumber, sourceFile != null ? sourceFile : ""); } } diff --git a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java index 75c61de3a..53ac775af 100644 --- a/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java +++ b/codeflash-java-runtime/src/main/java/com/codeflash/tracer/TracingTransformer.java @@ -1,10 +1,16 @@ package com.codeflash.tracer; import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; +import java.util.HashMap; +import java.util.Map; public class TracingTransformer implements ClassFileTransformer { @@ -46,6 +52,30 @@ public byte[] transform(ClassLoader loader, String className, private byte[] instrumentClass(String internalClassName, byte[] bytecode) { ClassReader cr = new ClassReader(bytecode); + + // Pre-scan: collect the first source line number for each method. + // ASM's visitMethod() doesn't provide line info — it arrives later via visitLineNumber(). + // We do a lightweight read pass first so the instrumentation pass has accurate line numbers. + Map methodLineNumbers = new HashMap<>(); + cr.accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + String key = name + descriptor; + return new MethodVisitor(Opcodes.ASM9) { + private boolean captured = false; + + @Override + public void visitLineNumber(int line, Label start) { + if (!captured) { + methodLineNumbers.put(key, line); + captured = true; + } + } + }; + } + }, ClassReader.SKIP_FRAMES); + // Use COMPUTE_MAXS only (not COMPUTE_FRAMES) to preserve original stack map frames. // COMPUTE_FRAMES recomputes all frames and calls getCommonSuperClass() which either // triggers classloader deadlocks or produces incorrect frames when returning "java/lang/Object". @@ -53,7 +83,7 @@ private byte[] instrumentClass(String internalClassName, byte[] bytecode) { // adjusts offsets for injected code. Our AdviceAdapter only injects at method entry // (before any branch points), so existing frames remain valid. ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); - TracingClassVisitor cv = new TracingClassVisitor(cw, internalClassName); + TracingClassVisitor cv = new TracingClassVisitor(cw, internalClassName, methodLineNumbers); cr.accept(cv, ClassReader.EXPAND_FRAMES); return cw.toByteArray(); } diff --git a/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar b/codeflash/languages/java/resources/codeflash-runtime-1.0.0.jar index 48ebc0a96623f477df4640b0eeb733f5ab82b477..546a8b89dd404a7f882875bac94dbce31359c7aa 100644 GIT binary patch delta 59596 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

-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 F{U7uj=(Yd= delta 57088 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 From 970c9f86da83afcd876826fc97147994c6cb27cd Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 16:50:40 +0000 Subject: [PATCH 18/41] fix: use 3 package components for tracer instrumentation scope (TODO-39) Changed detect_packages_from_source() from min(2, len) to min(3, len) so com.aerospike.client.util produces prefix com.aerospike.client instead of com.aerospike. This reduces instrumentation to the actual source package instead of the entire organization namespace. Co-Authored-By: Claude Opus 4.6 --- codeflash/languages/java/tracer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codeflash/languages/java/tracer.py b/codeflash/languages/java/tracer.py index 5cc098be5..ab8f19514 100644 --- a/codeflash/languages/java/tracer.py +++ b/codeflash/languages/java/tracer.py @@ -165,7 +165,7 @@ def detect_packages_from_source(module_root: Path) -> list[str]: if stripped.startswith("package "): pkg = stripped[8:].rstrip(";").strip() parts = pkg.split(".") - prefix = ".".join(parts[: min(2, len(parts))]) + prefix = ".".join(parts[: min(3, len(parts))]) packages.add(prefix) break if stripped and not stripped.startswith("//"): From af924cbad70da11eb917c6347f2490aac3db2b67 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 00:21:22 +0000 Subject: [PATCH 19/41] fix: set language singleton early so git diff auto-detection works for all languages The language singleton was only set after function discovery, but get_git_diff() needs it during discovery to filter by file extension. Now set it in process_pyproject_config() based on the config file type. Co-Authored-By: Claude Opus 4.6 --- codeflash/cli_cmds/cli.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index c611f5cd9..e337bb3b6 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -10,6 +10,7 @@ from codeflash.code_utils import env_utils from codeflash.code_utils.code_utils import exit_with_message, normalize_ignore_paths from codeflash.code_utils.config_parser import parse_config_file +from codeflash.languages import set_current_language from codeflash.languages.test_framework import set_current_test_framework from codeflash.lsp.helpers import is_LSP_enabled from codeflash.version import __version__ as version @@ -108,10 +109,18 @@ def process_pyproject_config(args: Namespace) -> Namespace: assert args.module_root is not None, "--module-root must be specified" assert Path(args.module_root).is_dir(), f"--module-root {args.module_root} must be a valid directory" - # For JS/TS projects, tests_root is optional (Jest auto-discovers tests) - # Default to module_root if not specified - is_js_ts_project = pyproject_config.get("language") in ("javascript", "typescript") - is_java_project = pyproject_config.get("language") == "java" + # Determine project language from explicit config or config file name. + # Java projects use codeflash.toml but don't always have an explicit language field. + config_language = pyproject_config.get("language") + if config_language is None and pyproject_file_path.name == "codeflash.toml": + config_language = "java" + is_js_ts_project = config_language in ("javascript", "typescript") + is_java_project = config_language == "java" + + # Set the language singleton early so downstream code (e.g. get_git_diff) + # can use current_language_support() before function discovery. + if config_language: + set_current_language(config_language) # Set the test framework singleton for JS/TS projects if is_js_ts_project and pyproject_config.get("test_framework"): From ef49ad7d0489acc140eb0c1df03473398b8412cd Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 00:23:02 +0000 Subject: [PATCH 20/41] fix: set language singleton early so git diff auto-detection works for all languages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The language singleton was only set after function discovery, but get_git_diff() needs it during discovery to filter by file extension. - config_parser.py: set config["language"] based on config file type (codeflash.toml → java, pyproject.toml → python) so all project types return a language - cli.py: call set_current_language() in process_pyproject_config() using the config value, before the optimizer runs Co-Authored-By: Claude Opus 4.6 --- codeflash/cli_cmds/cli.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index e337bb3b6..d994c6869 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -109,18 +109,13 @@ def process_pyproject_config(args: Namespace) -> Namespace: assert args.module_root is not None, "--module-root must be specified" assert Path(args.module_root).is_dir(), f"--module-root {args.module_root} must be a valid directory" - # Determine project language from explicit config or config file name. - # Java projects use codeflash.toml but don't always have an explicit language field. - config_language = pyproject_config.get("language") - if config_language is None and pyproject_file_path.name == "codeflash.toml": - config_language = "java" - is_js_ts_project = config_language in ("javascript", "typescript") - is_java_project = config_language == "java" + is_js_ts_project = pyproject_config.get("language") in ("javascript", "typescript") + is_java_project = pyproject_config.get("language") == "java" # Set the language singleton early so downstream code (e.g. get_git_diff) # can use current_language_support() before function discovery. - if config_language: - set_current_language(config_language) + if pyproject_config.get("language"): + set_current_language(pyproject_config["language"]) # Set the test framework singleton for JS/TS projects if is_js_ts_project and pyproject_config.get("test_framework"): From ecb8d48e9b214093f878ad8eebf7187991dd9927 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:03:34 +0000 Subject: [PATCH 21/41] feat(07-01): make get_git_diff() language-agnostic using registry extensions - Replace current_language_support().file_extensions with get_supported_extensions() from registry - Update tests: remove singleton dependency, add unsupported extension filtering test - Mixed Python+Java diffs now return both file types regardless of singleton state Co-Authored-By: Claude Opus 4.6 --- tests/test_git_utils.py | 65 +++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/tests/test_git_utils.py b/tests/test_git_utils.py index f3f23c1d9..cb0837468 100644 --- a/tests/test_git_utils.py +++ b/tests/test_git_utils.py @@ -282,64 +282,53 @@ def helper(): """ +UNSUPPORTED_LANG_DIFF = """\ +--- a/src/main.rs ++++ b/src/main.rs +@@ -1,3 +1,4 @@ + fn main() { ++ let x = 1; + println!("Hello"); + +""" + + class TestGetGitDiffMultiLanguage(unittest.TestCase): @patch("codeflash.code_utils.git_utils.git.Repo") - def test_java_diff_found_when_language_is_java(self, mock_repo_cls): - from codeflash.languages.current import reset_current_language, set_current_language - + def test_java_diff_found_without_singleton(self, mock_repo_cls): repo = mock_repo_cls.return_value repo.head.commit.hexsha = "abc123" repo.working_dir = "/repo" repo.git.diff.return_value = JAVA_ADDITION_DIFF - set_current_language("java") - try: - result = get_git_diff(repo_directory=None, uncommitted_changes=True) - assert len(result) == 1 - key = list(result.keys())[0] - assert str(key).endswith("Fibonacci.java") - assert result[key] == [7, 8] - finally: - reset_current_language() + result = get_git_diff(repo_directory=None, uncommitted_changes=True) + assert len(result) == 1 + key = list(result.keys())[0] + assert str(key).endswith("Fibonacci.java") + assert result[key] == [7, 8] @patch("codeflash.code_utils.git_utils.git.Repo") - def test_java_diff_found_regardless_of_current_language(self, mock_repo_cls): - from codeflash.languages.current import reset_current_language, set_current_language - + def test_unsupported_extension_still_filtered(self, mock_repo_cls): repo = mock_repo_cls.return_value repo.head.commit.hexsha = "abc123" repo.working_dir = "/repo" - repo.git.diff.return_value = JAVA_ADDITION_DIFF + repo.git.diff.return_value = UNSUPPORTED_LANG_DIFF - # get_git_diff uses all registered extensions, not just the current language's - set_current_language("python") - try: - result = get_git_diff(repo_directory=None, uncommitted_changes=True) - assert len(result) == 1 - key = list(result.keys())[0] - assert str(key).endswith("Fibonacci.java") - finally: - reset_current_language() + result = get_git_diff(repo_directory=None, uncommitted_changes=True) + assert len(result) == 0 @patch("codeflash.code_utils.git_utils.git.Repo") - def test_mixed_lang_diff_returns_all_supported_extensions(self, mock_repo_cls): - from codeflash.languages.current import reset_current_language, set_current_language - + def test_mixed_lang_diff_returns_all_languages(self, mock_repo_cls): repo = mock_repo_cls.return_value repo.head.commit.hexsha = "abc123" repo.working_dir = "/repo" repo.git.diff.return_value = MIXED_LANG_DIFF - # All supported extensions are returned regardless of current language - set_current_language("python") - try: - result = get_git_diff(repo_directory=None, uncommitted_changes=True) - assert len(result) == 2 - paths = [str(k) for k in result.keys()] - assert any(p.endswith("utils.py") for p in paths) - assert any(p.endswith("App.java") for p in paths) - finally: - reset_current_language() + result = get_git_diff(repo_directory=None, uncommitted_changes=True) + assert len(result) == 2 + keys = [str(k) for k in result.keys()] + assert any(k.endswith("utils.py") for k in keys) + assert any(k.endswith("App.java") for k in keys) if __name__ == "__main__": From 2394fdd2cf4976ba86310ef4db32e6069367e342 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:08:32 +0000 Subject: [PATCH 22/41] feat(07-02): add LanguageConfig dataclass and find_all_config_files() - Add LanguageConfig dataclass with config, config_path, language fields - Add find_all_config_files() that discovers all codeflash configs in project hierarchy - Supports pyproject.toml (Python), codeflash.toml (Java), package.json (JS/TS) - Skips configs without [tool.codeflash] section, closest config wins per language - Add 6 tests covering discovery, filtering, parent directory search, deduplication Co-Authored-By: Claude Opus 4.6 --- codeflash/code_utils/config_parser.py | 58 +++++++++++++++++++++++ tests/test_multi_config_discovery.py | 67 +++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 tests/test_multi_config_discovery.py diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index 832b34bcc..6dd4da66a 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -1,17 +1,26 @@ from __future__ import annotations +from dataclasses import dataclass from pathlib import Path from typing import Any import tomlkit from codeflash.code_utils.config_js import find_package_json, parse_package_json_config +from codeflash.languages.language_enum import Language from codeflash.lsp.helpers import is_LSP_enabled PYPROJECT_TOML_CACHE: dict[Path, Path] = {} ALL_CONFIG_FILES: dict[Path, dict[str, Path]] = {} +@dataclass +class LanguageConfig: + config: dict[str, Any] + config_path: Path + language: Language + + 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. @@ -103,6 +112,55 @@ def find_conftest_files(test_paths: list[Path]) -> list[Path]: return list(list_of_conftest_files) +def find_all_config_files(start_dir: Path | None = None) -> list[LanguageConfig]: + if start_dir is None: + start_dir = Path.cwd() + + configs: list[LanguageConfig] = [] + seen_languages: set[Language] = set() + + toml_configs = {"pyproject.toml": Language.PYTHON, "codeflash.toml": Language.JAVA} + + dir_path = start_dir.resolve() + while True: + for config_name, language in toml_configs.items(): + if language in seen_languages: + continue + config_file = dir_path / config_name + if config_file.exists(): + try: + with config_file.open("rb") as f: + data = tomlkit.parse(f.read()) + tool = data.get("tool", {}) + if isinstance(tool, dict) and "codeflash" in tool: + seen_languages.add(language) + configs.append( + LanguageConfig(config=dict(tool["codeflash"]), config_path=config_file, language=language) + ) + except Exception: + continue + + if Language.JAVASCRIPT not in seen_languages: + package_json = dir_path / "package.json" + if package_json.exists(): + try: + result = parse_package_json_config(package_json) + if result is not None: + config, path = result + seen_languages.add(Language.JAVASCRIPT) + configs.append(LanguageConfig(config=config, config_path=path, language=Language.JAVASCRIPT)) + except Exception: + pass + + parent = dir_path.parent + if parent == dir_path: + break + dir_path = parent + + return configs + + + def parse_config_file( config_file_path: Path | None = None, override_formatter_check: bool = False ) -> tuple[dict[str, Any], Path]: diff --git a/tests/test_multi_config_discovery.py b/tests/test_multi_config_discovery.py new file mode 100644 index 000000000..a913693fe --- /dev/null +++ b/tests/test_multi_config_discovery.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from pathlib import Path + +import tomlkit + +from codeflash.code_utils.config_parser import LanguageConfig, find_all_config_files +from codeflash.languages.language_enum import Language + + +def write_toml(path: Path, data: dict) -> None: + path.write_text(tomlkit.dumps(data), encoding="utf-8") + + +class TestFindAllConfigFiles: + def test_finds_pyproject_toml_with_codeflash_section(self, tmp_path: Path, monkeypatch) -> None: + write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 1 + assert result[0].language == Language.PYTHON + assert result[0].config_path == tmp_path / "pyproject.toml" + + def test_finds_codeflash_toml(self, tmp_path: Path, monkeypatch) -> None: + write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}}) + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 1 + assert result[0].language == Language.JAVA + assert result[0].config_path == tmp_path / "codeflash.toml" + + def test_finds_multiple_configs(self, tmp_path: Path, monkeypatch) -> None: + write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) + write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}}) + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 2 + languages = {r.language for r in result} + assert languages == {Language.PYTHON, Language.JAVA} + + def test_skips_pyproject_without_codeflash_section(self, tmp_path: Path, monkeypatch) -> None: + write_toml(tmp_path / "pyproject.toml", {"tool": {"black": {"line-length": 120}}}) + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 0 + + def test_finds_config_in_parent_directory(self, tmp_path: Path, monkeypatch) -> None: + write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) + subdir = tmp_path / "subproject" + subdir.mkdir() + write_toml(subdir / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}}) + monkeypatch.chdir(subdir) + result = find_all_config_files() + assert len(result) == 2 + languages = {r.language for r in result} + assert languages == {Language.PYTHON, Language.JAVA} + + def test_closest_config_wins_per_language(self, tmp_path: Path, monkeypatch) -> None: + write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "."}}}) + subdir = tmp_path / "sub" + subdir.mkdir() + write_toml(subdir / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) + monkeypatch.chdir(subdir) + result = find_all_config_files() + assert len(result) == 1 + assert result[0].language == Language.PYTHON + assert result[0].config_path == subdir / "pyproject.toml" From 0bf63e5c59c0aa6086f961cefa845f2e9439f842 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:09:19 +0000 Subject: [PATCH 23/41] test(07-02): verify find_all_functions_in_file uses registry lookup (DISC-04) - Add smoke test confirming get_language_support usage, not singleton - No code changes needed, function already uses per-file registry Co-Authored-By: Claude Opus 4.6 --- tests/test_multi_config_discovery.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_multi_config_discovery.py b/tests/test_multi_config_discovery.py index a913693fe..a0bf8f45f 100644 --- a/tests/test_multi_config_discovery.py +++ b/tests/test_multi_config_discovery.py @@ -65,3 +65,14 @@ def test_closest_config_wins_per_language(self, tmp_path: Path, monkeypatch) -> assert len(result) == 1 assert result[0].language == Language.PYTHON assert result[0].config_path == subdir / "pyproject.toml" + + +def test_find_all_functions_uses_registry_not_singleton() -> None: + """DISC-04: Verify find_all_functions_in_file uses per-file registry lookup.""" + import inspect + + from codeflash.discovery.functions_to_optimize import find_all_functions_in_file + + source = inspect.getsource(find_all_functions_in_file) + assert "get_language_support" in source + assert "current_language_support" not in source From fbefa872e7ab7bd5391c293d4e892eaf0276dc4b Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:29:18 +0000 Subject: [PATCH 24/41] feat(08-01): extract apply_language_config and add tests - Add apply_language_config() to cli.py for multi-language mode config application - Import LanguageConfig and Language enum in cli.py - Create test_multi_language_orchestration.py with 9 tests covering: module_root/tests_root setting, path resolution, project_root, CLI override preservation, formatter_cmds, language singleton, Python/Java config handling, Java default tests_root Co-Authored-By: Claude Opus 4.6 --- codeflash/cli_cmds/cli.py | 75 ++++++++- tests/test_multi_language_orchestration.py | 169 +++++++++++++++++++++ 2 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 tests/test_multi_language_orchestration.py diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index d994c6869..a1d9aa78d 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -9,8 +9,9 @@ from codeflash.cli_cmds.console import apologize_and_exit, logger from codeflash.code_utils import env_utils from codeflash.code_utils.code_utils import exit_with_message, normalize_ignore_paths -from codeflash.code_utils.config_parser import parse_config_file +from codeflash.code_utils.config_parser import LanguageConfig, parse_config_file from codeflash.languages import set_current_language +from codeflash.languages.language_enum import Language from codeflash.languages.test_framework import set_current_test_framework from codeflash.lsp.helpers import is_LSP_enabled from codeflash.version import __version__ as version @@ -258,6 +259,78 @@ def handle_optimize_all_arg_parsing(args: Namespace) -> Namespace: return args +def apply_language_config(args: Namespace, lang_config: LanguageConfig) -> Namespace: + config = lang_config.config + config_path = lang_config.config_path + + supported_keys = [ + "module_root", + "tests_root", + "benchmarks_root", + "ignore_paths", + "pytest_cmd", + "formatter_cmds", + "disable_telemetry", + "disable_imports_sorting", + "git_remote", + "override_fixtures", + ] + for key in supported_keys: + if key in config and ((hasattr(args, key) and getattr(args, key) is None) or not hasattr(args, key)): + setattr(args, key, config[key]) + + assert args.module_root is not None, "--module-root must be specified" + assert Path(args.module_root).is_dir(), f"--module-root {args.module_root} must be a valid directory" + + set_current_language(lang_config.language) + + is_js_ts = lang_config.language in (Language.JAVASCRIPT, Language.TYPESCRIPT) + if is_js_ts and config.get("test_framework"): + set_current_test_framework(config["test_framework"]) + + is_java = lang_config.language == Language.JAVA + if args.tests_root is None: + if is_java: + for test_dir in ["src/test/java", "test", "tests"]: + test_path = Path(args.module_root).parent / test_dir if "/" in test_dir else Path(test_dir) + if not test_path.is_absolute(): + test_path = Path.cwd() / test_path + if test_path.is_dir(): + args.tests_root = str(test_path) + break + if args.tests_root is None: + args.tests_root = str(Path.cwd() / "src" / "test" / "java") + elif is_js_ts: + for test_dir in ["test", "tests", "__tests__"]: + if Path(test_dir).is_dir(): + args.tests_root = test_dir + break + if args.tests_root is None and args.module_root: + module_root_path = Path(args.module_root) + for test_dir in ["test", "tests", "__tests__"]: + test_path = module_root_path / test_dir + if test_path.is_dir(): + args.tests_root = str(test_path) + break + if args.tests_root is None: + args.tests_root = args.module_root + else: + raise AssertionError("--tests-root must be specified") + + assert Path(args.tests_root).is_dir(), f"--tests-root {args.tests_root} must be a valid directory" + + args.module_root = Path(args.module_root).resolve() + if hasattr(args, "ignore_paths") and args.ignore_paths is not None: + args.ignore_paths = normalize_ignore_paths(args.ignore_paths, base_path=args.module_root) + args.project_root = project_root_from_module_root(args.module_root, config_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, config_path) + + return args + + def _handle_show_config() -> None: """Show current or auto-detected Codeflash configuration.""" from rich.table import Table diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py new file mode 100644 index 000000000..8e68c2e94 --- /dev/null +++ b/tests/test_multi_language_orchestration.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +from argparse import Namespace +from pathlib import Path +from unittest.mock import patch + +import tomlkit + +from codeflash.code_utils.config_parser import LanguageConfig +from codeflash.languages.language_enum import Language + + +def write_toml(path: Path, data: dict) -> None: + path.write_text(tomlkit.dumps(data), encoding="utf-8") + + +def make_base_args(**overrides) -> Namespace: + defaults = { + "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, + "config_file": None, + "file": None, + "function": None, + "no_pr": False, + "verbose": False, + } + defaults.update(overrides) + return Namespace(**defaults) + + +class TestApplyLanguageConfig: + def test_sets_module_root(self, tmp_path: Path) -> None: + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + config = {"module_root": str(src)} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + args = make_base_args() + + from codeflash.cli_cmds.cli import apply_language_config + + result = apply_language_config(args, lang_config) + assert result.module_root == src.resolve() + + def test_sets_tests_root(self, tmp_path: Path) -> None: + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + tests = tmp_path / "src" / "test" / "java" + tests.mkdir(parents=True) + config = {"module_root": str(src), "tests_root": str(tests)} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + args = make_base_args() + + from codeflash.cli_cmds.cli import apply_language_config + + result = apply_language_config(args, lang_config) + assert result.tests_root == tests.resolve() + + def test_resolves_paths_relative_to_config_parent(self, tmp_path: Path) -> None: + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + tests = tmp_path / "src" / "test" / "java" + tests.mkdir(parents=True) + config = {"module_root": str(src), "tests_root": str(tests)} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + args = make_base_args() + + from codeflash.cli_cmds.cli import apply_language_config + + result = apply_language_config(args, lang_config) + assert result.module_root.is_absolute() + assert result.tests_root.is_absolute() + + def test_sets_project_root(self, tmp_path: Path) -> None: + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + tests = tmp_path / "src" / "test" / "java" + tests.mkdir(parents=True) + (tmp_path / "pom.xml").touch() + config = {"module_root": str(src), "tests_root": str(tests)} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + args = make_base_args() + + from codeflash.cli_cmds.cli import apply_language_config + + result = apply_language_config(args, lang_config) + assert result.project_root == tmp_path.resolve() + + def test_preserves_cli_overrides(self, tmp_path: Path) -> None: + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + override_module = tmp_path / "custom" + override_module.mkdir() + tests = tmp_path / "src" / "test" / "java" + tests.mkdir(parents=True) + config = {"module_root": str(src), "tests_root": str(tests)} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + args = make_base_args(module_root=str(override_module)) + + from codeflash.cli_cmds.cli import apply_language_config + + result = apply_language_config(args, lang_config) + assert result.module_root == override_module.resolve() + + def test_copies_formatter_cmds(self, tmp_path: Path) -> None: + src = tmp_path / "src" + src.mkdir() + tests = tmp_path / "tests" + tests.mkdir() + config = {"module_root": str(src), "tests_root": str(tests), "formatter_cmds": ["black $file"]} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "pyproject.toml", language=Language.PYTHON) + args = make_base_args() + + from codeflash.cli_cmds.cli import apply_language_config + + result = apply_language_config(args, lang_config) + assert result.formatter_cmds == ["black $file"] + + def test_sets_language_singleton(self, tmp_path: Path) -> None: + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + tests = tmp_path / "src" / "test" / "java" + tests.mkdir(parents=True) + config = {"module_root": str(src), "tests_root": str(tests)} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + args = make_base_args() + + with patch("codeflash.cli_cmds.cli.set_current_language") as mock_set: + from codeflash.cli_cmds.cli import apply_language_config + + apply_language_config(args, lang_config) + mock_set.assert_called_once_with(Language.JAVA) + + def test_handles_python_config(self, tmp_path: Path) -> None: + src = tmp_path / "src" + src.mkdir() + tests = tmp_path / "tests" + tests.mkdir() + config = {"module_root": str(src), "tests_root": str(tests)} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "pyproject.toml", language=Language.PYTHON) + args = make_base_args() + + from codeflash.cli_cmds.cli import apply_language_config + + result = apply_language_config(args, lang_config) + assert result.module_root == src.resolve() + assert result.tests_root == tests.resolve() + + def test_java_default_tests_root(self, tmp_path: Path, monkeypatch) -> None: + src = tmp_path / "src" / "main" / "java" + src.mkdir(parents=True) + default_tests = tmp_path / "src" / "test" / "java" + default_tests.mkdir(parents=True) + monkeypatch.chdir(tmp_path) + config = {"module_root": str(src)} + lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + args = make_base_args() + + from codeflash.cli_cmds.cli import apply_language_config + + result = apply_language_config(args, lang_config) + assert result.tests_root == default_tests.resolve() From e4131c189f0144be3cdcd99f894f0421877c9078 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:32:15 +0000 Subject: [PATCH 25/41] feat(08-01): wire multi-language orchestration loop into main.py - Add orchestration loop that iterates over all discovered LanguageConfigs - Deep-copy args per language pass to prevent mutation leakage - Run git/GitHub checks once before loop via handle_optimize_all_arg_parsing - Preserve fallback to single-config path when find_all_config_files returns empty - Add 4 orchestration tests: sequential passes, singleton per pass, fallback to single config, args isolation between passes Co-Authored-By: Claude Opus 4.6 --- codeflash/main.py | 62 +++++++-- tests/test_multi_language_orchestration.py | 144 +++++++++++++++++++++ 2 files changed, 193 insertions(+), 13 deletions(-) diff --git a/codeflash/main.py b/codeflash/main.py index 80d6d156a..3547b846d 100644 --- a/codeflash/main.py +++ b/codeflash/main.py @@ -6,6 +6,8 @@ from __future__ import annotations +import copy +import logging import os import sys from pathlib import Path @@ -17,11 +19,16 @@ warnings.filterwarnings("ignore") -from codeflash.cli_cmds.cli import parse_args, process_pyproject_config +from codeflash.cli_cmds.cli import ( + apply_language_config, + handle_optimize_all_arg_parsing, + parse_args, + process_pyproject_config, +) from codeflash.cli_cmds.console import paneled_text from codeflash.code_utils import env_utils from codeflash.code_utils.checkpoint import ask_should_use_checkpoint_get_functions -from codeflash.code_utils.config_parser import parse_config_file +from codeflash.code_utils.config_parser import find_all_config_files, parse_config_file from codeflash.code_utils.version_check import check_for_newer_minor_version if TYPE_CHECKING: @@ -72,21 +79,50 @@ def main() -> None: ask_run_end_to_end_test(args) else: - # Check for first-run experience (no config exists) - loaded_args = _handle_config_loading(args) - if loaded_args is None: - sys.exit(0) - args = loaded_args + language_configs = find_all_config_files() - if not env_utils.check_formatter_installed(args.formatter_cmds): + if not language_configs: + # Fallback: no multi-config found, use existing single-config path + loaded_args = _handle_config_loading(args) + if loaded_args is None: + sys.exit(0) + args = loaded_args + + if not env_utils.check_formatter_installed(args.formatter_cmds): + return + args.previous_checkpoint_functions = ask_should_use_checkpoint_get_functions(args) + init_sentry(enabled=not args.disable_telemetry, exclude_errors=True) + posthog_cf.initialize_posthog(enabled=not args.disable_telemetry) + + from codeflash.optimization import optimizer + + optimizer.run_with_args(args) return - args.previous_checkpoint_functions = ask_should_use_checkpoint_get_functions(args) - init_sentry(enabled=not args.disable_telemetry, exclude_errors=True) - posthog_cf.initialize_posthog(enabled=not args.disable_telemetry) - from codeflash.optimization import optimizer + # Multi-language path: run git/GitHub checks ONCE before the loop + args = handle_optimize_all_arg_parsing(args) + + logger = logging.getLogger("codeflash") + for lang_config in language_configs: + pass_args = copy.deepcopy(args) + pass_args = apply_language_config(pass_args, lang_config) + + if hasattr(pass_args, "all") and pass_args.all is not None: + pass_args.all = pass_args.module_root + + if not env_utils.check_formatter_installed(pass_args.formatter_cmds): + logger.info("Skipping %s: formatter not installed", lang_config.language.value) + continue + + pass_args.previous_checkpoint_functions = ask_should_use_checkpoint_get_functions(pass_args) + init_sentry(enabled=not pass_args.disable_telemetry, exclude_errors=True) + posthog_cf.initialize_posthog(enabled=not pass_args.disable_telemetry) + + logger.info("Processing %s (config: %s)", lang_config.language.value, lang_config.config_path) + + from codeflash.optimization import optimizer - optimizer.run_with_args(args) + optimizer.run_with_args(pass_args) def _handle_config_loading(args: Namespace) -> Namespace | None: diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index 8e68c2e94..14d304178 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -31,6 +31,12 @@ def make_base_args(**overrides) -> Namespace: "function": None, "no_pr": False, "verbose": False, + "command": None, + "verify_setup": False, + "version": False, + "show_config": False, + "reset_config": False, + "previous_checkpoint_functions": [], } defaults.update(overrides) return Namespace(**defaults) @@ -167,3 +173,141 @@ def test_java_default_tests_root(self, tmp_path: Path, monkeypatch) -> None: result = apply_language_config(args, lang_config) assert result.tests_root == default_tests.resolve() + + +def make_lang_config(tmp_path: Path, language: Language, subdir: str = "") -> LanguageConfig: + if language == Language.PYTHON: + src = tmp_path / subdir / "src" if subdir else tmp_path / "src" + tests = tmp_path / subdir / "tests" if subdir else tmp_path / "tests" + src.mkdir(parents=True, exist_ok=True) + tests.mkdir(parents=True, exist_ok=True) + config_path = tmp_path / subdir / "pyproject.toml" if subdir else tmp_path / "pyproject.toml" + return LanguageConfig( + config={"module_root": str(src), "tests_root": str(tests)}, + config_path=config_path, + language=Language.PYTHON, + ) + src = tmp_path / subdir / "src" / "main" / "java" if subdir else tmp_path / "src" / "main" / "java" + tests = tmp_path / subdir / "src" / "test" / "java" if subdir else tmp_path / "src" / "test" / "java" + src.mkdir(parents=True, exist_ok=True) + tests.mkdir(parents=True, exist_ok=True) + config_path = tmp_path / subdir / "codeflash.toml" if subdir else tmp_path / "codeflash.toml" + return LanguageConfig( + config={"module_root": str(src), "tests_root": str(tests)}, + config_path=config_path, + language=Language.JAVA, + ) + + +class TestMultiLanguageOrchestration: + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_sequential_passes_calls_optimizer_per_language( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 2 + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.cli_cmds.cli.set_current_language") + def test_singleton_set_per_pass( + self, + mock_set_lang, + _ver, + _banner, + mock_parse_args, + mock_find_configs, + mock_run, + _handle_all, + _fmt, + _ckpt, + tmp_path: Path, + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + + from codeflash.main import main + + main() + + # set_current_language is called once per language pass via apply_language_config + lang_calls = [c for c in mock_set_lang.call_args_list if c[0][0] in (Language.PYTHON, Language.JAVA)] + assert len(lang_calls) >= 2 + called_langs = {c[0][0] for c in lang_calls} + assert Language.PYTHON in called_langs + assert Language.JAVA in called_langs + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files", return_value=[]) + @patch("codeflash.main._handle_config_loading") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_fallback_to_single_config_when_no_multi_configs( + self, _ver, _banner, mock_parse_args, mock_handle_config, mock_run, _fmt, _ckpt, tmp_path: Path + ) -> None: + base = make_base_args( + disable_telemetry=False, formatter_cmds=[], module_root=str(tmp_path), tests_root=str(tmp_path) + ) + mock_parse_args.return_value = base + mock_handle_config.return_value = base + + from codeflash.main import main + + main() + + mock_handle_config.assert_called_once() + mock_run.assert_called_once() + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_args_deep_copied_between_passes( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 2 + call1_args = mock_run.call_args_list[0][0][0] + call2_args = mock_run.call_args_list[1][0][0] + # Args should be different objects (deep copied) + assert call1_args is not call2_args + # Module roots should differ between Python and Java configs + assert call1_args.module_root != call2_args.module_root From e3639277119d3f5536342fd5661940534f3c361d Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:38:08 +0000 Subject: [PATCH 26/41] feat(08-02): extract normalize_toml_config and apply in find_all_config_files - Extract shared normalization logic (path resolution, defaults, key conversion) into normalize_toml_config() - Use it in both find_all_config_files and parse_config_file to eliminate duplication - Add 6 tests verifying normalization behavior Co-Authored-By: Claude Opus 4.6 --- codeflash/code_utils/config_parser.py | 97 ++++++++++++---------- tests/test_multi_language_orchestration.py | 49 ++++++++++- 2 files changed, 99 insertions(+), 47 deletions(-) diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index 6dd4da66a..a6581a531 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -112,6 +112,51 @@ def find_conftest_files(test_paths: list[Path]) -> list[Path]: return list(list_of_conftest_files) +def normalize_toml_config(config: dict[str, Any], config_file_path: Path) -> dict[str, Any]: + path_keys = ["module-root", "tests-root", "benchmarks-root"] + path_list_keys = ["ignore-paths"] + str_keys = {"pytest-cmd": "pytest", "git-remote": "origin"} + bool_keys = { + "override-fixtures": False, + "disable-telemetry": False, + "disable-imports-sorting": False, + "benchmark": False, + } + list_str_keys = {"formatter-cmds": []} + + for key, default_value in str_keys.items(): + if key in config: + config[key] = str(config[key]) + else: + config[key] = default_value + for key, default_value in bool_keys.items(): + if key in config: + config[key] = bool(config[key]) + else: + config[key] = default_value + for key in path_keys: + if key in config: + config[key] = str((config_file_path.parent / Path(config[key])).resolve()) + for key, default_value in list_str_keys.items(): + if key in config: + config[key] = [str(cmd) for cmd in config[key]] + else: + config[key] = default_value + for key in path_list_keys: + if key in config: + config[key] = [str((config_file_path.parent / path).resolve()) for path in config[key]] + else: + config[key] = [] + + # Convert hyphenated keys to underscored keys + for key in list(config.keys()): + if "-" in key: + config[key.replace("-", "_")] = config[key] + del config[key] + + return config + + def find_all_config_files(start_dir: Path | None = None) -> list[LanguageConfig]: if start_dir is None: start_dir = Path.cwd() @@ -133,9 +178,11 @@ def find_all_config_files(start_dir: Path | None = None) -> list[LanguageConfig] data = tomlkit.parse(f.read()) tool = data.get("tool", {}) if isinstance(tool, dict) and "codeflash" in tool: + raw_config = dict(tool["codeflash"]) + normalized = normalize_toml_config(raw_config, config_file) seen_languages.add(language) configs.append( - LanguageConfig(config=dict(tool["codeflash"]), config_path=config_file, language=language) + LanguageConfig(config=normalized, config_path=config_file, language=language) ) except Exception: continue @@ -232,55 +279,13 @@ def parse_config_file( if config == {} and lsp_mode: return {}, config_file_path - # 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"] - str_keys = {"pytest-cmd": "pytest", "git-remote": "origin"} - bool_keys = { - "override-fixtures": False, - "disable-telemetry": False, - "disable-imports-sorting": False, - "benchmark": False, - } - # Note: formatter-cmds defaults to empty list. For Python projects, black is typically - # detected by the project detector. For Java projects, no formatter is supported yet. - list_str_keys = {"formatter-cmds": []} - - for key, default_value in str_keys.items(): - if key in config: - config[key] = str(config[key]) - else: - config[key] = default_value - for key, default_value in bool_keys.items(): - if key in config: - config[key] = bool(config[key]) - else: - config[key] = default_value - for key in path_keys: - if key in config: - config[key] = str((Path(config_file_path).parent / Path(config[key])).resolve()) - for key, default_value in list_str_keys.items(): - if key in config: - config[key] = [str(cmd) for cmd in config[key]] - else: - config[key] = default_value - - for key in path_list_keys: - if key in config: - config[key] = [str((Path(config_file_path).parent / path).resolve()) for path in config[key]] - else: - config[key] = [] + config = normalize_toml_config(config, config_file_path) # see if this is happening during GitHub actions setup - if config.get("formatter-cmds") and len(config.get("formatter-cmds")) > 0 and not override_formatter_check: - assert config.get("formatter-cmds")[0] != "your-formatter $file", ( + if config.get("formatter_cmds") and len(config.get("formatter_cmds")) > 0 and not override_formatter_check: + assert config.get("formatter_cmds")[0] != "your-formatter $file", ( "The formatter command is not set correctly in pyproject.toml. Please set the " "formatter command in the 'formatter-cmds' key. More info - https://docs.codeflash.ai/configuration" ) - for key in list(config.keys()): - if "-" in key: - config[key.replace("-", "_")] = config[key] - del config[key] return config, config_file_path diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index 14d304178..b7e6aa9dc 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -6,7 +6,7 @@ import tomlkit -from codeflash.code_utils.config_parser import LanguageConfig +from codeflash.code_utils.config_parser import LanguageConfig, normalize_toml_config from codeflash.languages.language_enum import Language @@ -311,3 +311,50 @@ def test_args_deep_copied_between_passes( assert call1_args is not call2_args # Module roots should differ between Python and Java configs assert call1_args.module_root != call2_args.module_root + + +class TestNormalizeTomlConfig: + def test_converts_hyphenated_keys_to_underscored(self, tmp_path: Path) -> None: + config = {"module-root": "src", "tests-root": "tests"} + (tmp_path / "src").mkdir() + (tmp_path / "tests").mkdir() + result = normalize_toml_config(config, tmp_path / "codeflash.toml") + assert "module_root" in result + assert "tests_root" in result + assert "module-root" not in result + assert "tests-root" not in result + + def test_resolves_paths_relative_to_config_parent(self, tmp_path: Path) -> None: + src = tmp_path / "src" + src.mkdir() + config = {"module-root": "src"} + result = normalize_toml_config(config, tmp_path / "codeflash.toml") + assert result["module_root"] == str(src.resolve()) + + def test_applies_default_values(self, tmp_path: Path) -> None: + config: dict = {} + result = normalize_toml_config(config, tmp_path / "codeflash.toml") + assert result["formatter_cmds"] == [] + assert result["disable_telemetry"] is False + assert result["override_fixtures"] is False + assert result["git_remote"] == "origin" + assert result["pytest_cmd"] == "pytest" + + def test_preserves_explicit_values(self, tmp_path: Path) -> None: + config = {"disable-telemetry": True, "formatter-cmds": ["prettier $file"]} + result = normalize_toml_config(config, tmp_path / "codeflash.toml") + assert result["disable_telemetry"] is True + assert result["formatter_cmds"] == ["prettier $file"] + + def test_resolves_ignore_paths(self, tmp_path: Path) -> None: + config = {"ignore-paths": ["build", "dist"]} + result = normalize_toml_config(config, tmp_path / "codeflash.toml") + assert result["ignore_paths"] == [ + str((tmp_path / "build").resolve()), + str((tmp_path / "dist").resolve()), + ] + + def test_empty_ignore_paths_default(self, tmp_path: Path) -> None: + config: dict = {} + result = normalize_toml_config(config, tmp_path / "codeflash.toml") + assert result["ignore_paths"] == [] From 82c278cd9010c81b13ca790ec84c2ec9c64d1986 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:39:11 +0000 Subject: [PATCH 27/41] feat(08-02): add per-language error isolation in orchestration loop - Wrap each language pass in try/except so one failure doesn't block others - Track per-language status (success/failed/skipped) in results dict - Add 3 tests verifying error isolation and failure tracking Co-Authored-By: Claude Opus 4.6 --- codeflash/main.py | 43 ++++++++---- tests/test_multi_language_orchestration.py | 77 ++++++++++++++++++++++ 2 files changed, 107 insertions(+), 13 deletions(-) diff --git a/codeflash/main.py b/codeflash/main.py index 3547b846d..1946dee53 100644 --- a/codeflash/main.py +++ b/codeflash/main.py @@ -103,26 +103,43 @@ def main() -> None: args = handle_optimize_all_arg_parsing(args) logger = logging.getLogger("codeflash") + results: dict[str, str] = {} for lang_config in language_configs: - pass_args = copy.deepcopy(args) - pass_args = apply_language_config(pass_args, lang_config) + lang_name = lang_config.language.value + try: + pass_args = copy.deepcopy(args) + pass_args = apply_language_config(pass_args, lang_config) - if hasattr(pass_args, "all") and pass_args.all is not None: - pass_args.all = pass_args.module_root + if hasattr(pass_args, "all") and pass_args.all is not None: + pass_args.all = pass_args.module_root - if not env_utils.check_formatter_installed(pass_args.formatter_cmds): - logger.info("Skipping %s: formatter not installed", lang_config.language.value) - continue + if not env_utils.check_formatter_installed(pass_args.formatter_cmds): + logger.info("Skipping %s: formatter not installed", lang_name) + results[lang_name] = "skipped" + continue - pass_args.previous_checkpoint_functions = ask_should_use_checkpoint_get_functions(pass_args) - init_sentry(enabled=not pass_args.disable_telemetry, exclude_errors=True) - posthog_cf.initialize_posthog(enabled=not pass_args.disable_telemetry) + pass_args.previous_checkpoint_functions = ask_should_use_checkpoint_get_functions(pass_args) + init_sentry(enabled=not pass_args.disable_telemetry, exclude_errors=True) + posthog_cf.initialize_posthog(enabled=not pass_args.disable_telemetry) - logger.info("Processing %s (config: %s)", lang_config.language.value, lang_config.config_path) + logger.info("Processing %s (config: %s)", lang_name, lang_config.config_path) + + from codeflash.optimization import optimizer + + optimizer.run_with_args(pass_args) + results[lang_name] = "success" + except Exception: + logger.exception("Error processing %s, continuing with remaining languages", lang_name) + results[lang_name] = "failed" + + _log_orchestration_summary(logger, results) - from codeflash.optimization import optimizer - optimizer.run_with_args(pass_args) +def _log_orchestration_summary(logger: logging.Logger, results: dict[str, str]) -> None: + if not results: + return + parts = [f"{lang}: {status}" for lang, status in results.items()] + logger.info("Multi-language orchestration complete: %s", ", ".join(parts)) def _handle_config_loading(args: Namespace) -> Namespace | None: diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index b7e6aa9dc..756842916 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -313,6 +313,83 @@ def test_args_deep_copied_between_passes( assert call1_args.module_root != call2_args.module_root + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_error_in_one_language_does_not_block_others( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + # First call (Python) raises, second call (Java) succeeds + mock_run.side_effect = [RuntimeError("Python optimizer crashed"), None] + + from codeflash.main import main + + main() + + assert mock_run.call_count == 2 + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_orchestration_summary_logged( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + + with patch("codeflash.main._log_orchestration_summary") as mock_summary: + from codeflash.main import main + + main() + + mock_summary.assert_called_once() + results = mock_summary.call_args[0][1] + assert results["python"] == "success" + assert results["java"] == "success" + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_summary_reports_failure_status( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + mock_run.side_effect = [RuntimeError("boom"), None] + + with patch("codeflash.main._log_orchestration_summary") as mock_summary: + from codeflash.main import main + + main() + + results = mock_summary.call_args[0][1] + assert results["python"] == "failed" + assert results["java"] == "success" + + class TestNormalizeTomlConfig: def test_converts_hyphenated_keys_to_underscored(self, tmp_path: Path) -> None: config = {"module-root": "src", "tests-root": "tests"} From 22839eb8f7e8f0c5cd7d4930039a40809b05050e Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:39:53 +0000 Subject: [PATCH 28/41] test(08-02): add summary logging tests for orchestration results - Test summary format with all success, mixed statuses, and empty results - Test skipped status when formatter check fails - 4 new tests covering _log_orchestration_summary behavior Co-Authored-By: Claude Opus 4.6 --- tests/test_multi_language_orchestration.py | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index 756842916..030afe818 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -390,6 +390,73 @@ def test_summary_reports_failure_status( assert results["java"] == "success" +class TestOrchestrationSummaryLogging: + def test_summary_format_all_success(self) -> None: + import logging + + from codeflash.main import _log_orchestration_summary + + with patch.object(logging.Logger, "info") as mock_info: + logger = logging.getLogger("codeflash.test") + _log_orchestration_summary(logger, {"python": "success", "java": "success"}) + mock_info.assert_called_once() + msg = mock_info.call_args[0][0] % mock_info.call_args[0][1:] + assert "python: success" in msg + assert "java: success" in msg + + def test_summary_format_mixed_statuses(self) -> None: + import logging + + from codeflash.main import _log_orchestration_summary + + with patch.object(logging.Logger, "info") as mock_info: + logger = logging.getLogger("codeflash.test") + _log_orchestration_summary(logger, {"python": "failed", "java": "success", "javascript": "skipped"}) + mock_info.assert_called_once() + msg = mock_info.call_args[0][0] % mock_info.call_args[0][1:] + assert "python: failed" in msg + assert "java: success" in msg + assert "javascript: skipped" in msg + + def test_summary_no_results_no_log(self) -> None: + import logging + + from codeflash.main import _log_orchestration_summary + + with patch.object(logging.Logger, "info") as mock_info: + logger = logging.getLogger("codeflash.test") + _log_orchestration_summary(logger, {}) + mock_info.assert_not_called() + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed") + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_summary_reports_skipped_status( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, mock_fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + # Python formatter check fails (skipped), Java succeeds + mock_fmt.side_effect = [False, True] + + with patch("codeflash.main._log_orchestration_summary") as mock_summary: + from codeflash.main import main + + main() + + results = mock_summary.call_args[0][1] + assert results["python"] == "skipped" + assert results["java"] == "success" + assert mock_run.call_count == 1 + + class TestNormalizeTomlConfig: def test_converts_hyphenated_keys_to_underscored(self, tmp_path: Path) -> None: config = {"module-root": "src", "tests-root": "tests"} From a92c3207b740e1c45e3e470a275ce70e92425212 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:40:49 +0000 Subject: [PATCH 29/41] docs(08-02): complete error isolation and config normalization plan Co-Authored-By: Claude Opus 4.6 --- .planning/STATE.md | 22 +++++ .planning/config.json | 7 ++ .../08-02-PLAN.md | 59 ++++++++++++ .../08-02-SUMMARY.md | 95 +++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 .planning/STATE.md create mode 100644 .planning/config.json create mode 100644 .planning/phases/08-sequential-multi-language-orchestration/08-02-PLAN.md create mode 100644 .planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 000000000..cc9e7d157 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,22 @@ +# Project State + +## Current Position +- **Phase:** 08 - Sequential Multi-Language Orchestration +- **Plan:** 02 (Complete) +- **Status:** Complete + +## Progress +- Plan 08-01: Complete (apply_language_config + orchestration loop) +- Plan 08-02: Complete (error isolation + config normalization + summary logging) + +## Decisions +- Multi-language orchestration uses sequential passes with deep-copied args +- find_all_config_files walks up from CWD collecting per-language configs +- apply_language_config mirrors process_pyproject_config for the multi-config path +- normalize_toml_config is the shared helper for config normalization (path resolution, defaults, key conversion) +- Per-language error isolation: try/except in loop with status tracking dict +- Summary logging: comma-separated "lang: status" pairs via logger.info + +## Last Session +- **Stopped at:** Completed 08-02-PLAN.md +- **Timestamp:** 2026-03-18T04:40:02Z diff --git a/.planning/config.json b/.planning/config.json new file mode 100644 index 000000000..e24dad826 --- /dev/null +++ b/.planning/config.json @@ -0,0 +1,7 @@ +{ + "executor_model": "opus", + "commit_docs": true, + "parallelization": false, + "branching_strategy": "none", + "verifier_enabled": false +} diff --git a/.planning/phases/08-sequential-multi-language-orchestration/08-02-PLAN.md b/.planning/phases/08-sequential-multi-language-orchestration/08-02-PLAN.md new file mode 100644 index 000000000..1c415136b --- /dev/null +++ b/.planning/phases/08-sequential-multi-language-orchestration/08-02-PLAN.md @@ -0,0 +1,59 @@ +--- +phase: 08-sequential-multi-language-orchestration +plan: 02 +type: implementation +autonomous: true +wave: 1 +depends_on: [08-01] +--- + +# Plan 08-02: Error Isolation and Config Normalization for Multi-Language Orchestration + +## Objective + +Harden the multi-language orchestration loop with per-language error isolation and ensure `find_all_config_files` normalizes config values consistently with `parse_config_file`. + +## Context + +- @codeflash/main.py — multi-language orchestration loop from 08-01 +- @codeflash/cli_cmds/cli.py — apply_language_config from 08-01 +- @codeflash/code_utils/config_parser.py — find_all_config_files and parse_config_file +- @tests/test_multi_language_orchestration.py — existing tests from 08-01 + +## Tasks + +### Task 1: Normalize config values in find_all_config_files +type="auto" + +`find_all_config_files` reads raw toml data but doesn't normalize it the way `parse_config_file` does (path resolution, hyphen-to-underscore key conversion, defaults for missing keys). Add a helper that normalizes the raw config dict so `apply_language_config` receives consistent data. + +**Done criteria:** +- Config values from `find_all_config_files` have paths resolved relative to config file parent +- Hyphenated keys are converted to underscored keys +- Default values are applied for missing keys (formatter_cmds=[], disable_telemetry=False, etc.) +- Tests verify normalization + +### Task 2: Add per-language error isolation in main.py orchestration loop +type="auto" + +Wrap each language pass in a try/except so one language failure doesn't prevent other languages from being optimized. Log the error and continue. + +**Done criteria:** +- Exception in one language pass logs the error and continues to next language +- Test verifies that if optimizer.run_with_args raises for one language, the other language still runs +- Summary logging at end of loop reports which languages succeeded/failed + +### Task 3: Add summary logging for multi-language orchestration results +type="auto" + +After the orchestration loop completes, log a summary showing which languages were processed and their status (success/failure/skipped). + +**Done criteria:** +- Summary log message after loop shows per-language status +- Test verifies summary includes correct language names and statuses + +## Verification + +- All existing tests in test_multi_language_orchestration.py still pass +- New tests cover normalization, error isolation, and summary logging +- `uv run prek` passes diff --git a/.planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md b/.planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md new file mode 100644 index 000000000..b2a5d7ce7 --- /dev/null +++ b/.planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md @@ -0,0 +1,95 @@ +--- +phase: 08-sequential-multi-language-orchestration +plan: 02 +subsystem: cli +tags: [multi-language, config-normalization, error-isolation, orchestration] + +requires: + - phase: 08-01 + provides: apply_language_config, multi-language orchestration loop, find_all_config_files +provides: + - normalize_toml_config helper for consistent config normalization + - Per-language error isolation in orchestration loop + - Orchestration summary logging with per-language status +affects: [config-parser, main-entry-point, multi-language-support] + +tech-stack: + added: [] + patterns: [shared-normalization-helper, error-isolation-loop, status-tracking-dict] + +key-files: + created: [] + modified: + - codeflash/code_utils/config_parser.py + - codeflash/main.py + - tests/test_multi_language_orchestration.py + +key-decisions: + - "Extract normalize_toml_config as shared helper used by both find_all_config_files and parse_config_file" + - "Track per-language status as dict[str, str] with values success/failed/skipped" + - "Log orchestration summary after loop completes with all statuses" + +patterns-established: + - "Config normalization: always use normalize_toml_config for toml-based configs" + - "Error isolation: wrap per-language passes in try/except, track status, continue on failure" + +requirements-completed: [] + +duration: 3min +completed: 2026-03-18 +--- + +# Phase 08 Plan 02: Error Isolation and Config Normalization Summary + +**Shared config normalization via normalize_toml_config, per-language error isolation with status tracking, and orchestration summary logging** + +## Performance + +- **Duration:** 3 min +- **Started:** 2026-03-18T04:36:44Z +- **Completed:** 2026-03-18T04:40:02Z +- **Tasks:** 3 +- **Files modified:** 3 + +## Accomplishments +- Extracted `normalize_toml_config` helper that resolves paths, applies defaults, and converts hyphenated keys -- used by both `find_all_config_files` and `parse_config_file` to eliminate duplication +- Added per-language error isolation so one language failure does not prevent other languages from being optimized +- Added orchestration summary logging showing per-language status (success/failed/skipped) after the loop completes +- 13 new tests covering normalization, error isolation, skipped status, and summary logging format + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Normalize config values in find_all_config_files** - `97e21aab` (feat) +2. **Task 2: Per-language error isolation in orchestration loop** - `a3014ec0` (feat) +3. **Task 3: Summary logging tests for orchestration results** - `dcf366e2` (test) + +## Files Created/Modified +- `codeflash/code_utils/config_parser.py` - Added normalize_toml_config helper, used in find_all_config_files and parse_config_file +- `codeflash/main.py` - Added error isolation try/except, status tracking dict, _log_orchestration_summary helper +- `tests/test_multi_language_orchestration.py` - 13 new tests (6 normalization, 3 error isolation, 4 summary logging) + +## Decisions Made +- Extracted normalization into a standalone function rather than keeping it duplicated between parse_config_file and find_all_config_files +- Used a simple dict[str, str] for status tracking rather than a more complex result type +- Summary logging uses logger.info with comma-separated "lang: status" pairs + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered +None + +## User Setup Required +None - no external service configuration required. + +## Next Phase Readiness +- Multi-language orchestration is now robust against per-language failures +- Config normalization is consistent across single-config and multi-config paths +- Ready for further multi-language pipeline enhancements + +--- +*Phase: 08-sequential-multi-language-orchestration* +*Completed: 2026-03-18* From 0219de9f1579e5f77798583fa839a3939a3125d6 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:48:31 +0000 Subject: [PATCH 30/41] docs(08): create gap closure plan for CLI path routing --- .../08-03-PLAN.md | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 .planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md diff --git a/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md b/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md new file mode 100644 index 000000000..1bdc3ebbf --- /dev/null +++ b/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md @@ -0,0 +1,219 @@ +--- +phase: 08-sequential-multi-language-orchestration +plan: 03 +type: execute +wave: 1 +depends_on: [08-02] +files_modified: + - codeflash/main.py + - tests/test_multi_language_orchestration.py +autonomous: true +gap_closure: true +requirements: [PROC-04, PROC-05, PROC-06] + +must_haves: + truths: + - "codeflash --file path/to/File.java filters language_configs to only the matching language" + - "codeflash --all sets pass_args.all = pass_args.module_root per language pass" + - "codeflash with no flags runs all language passes without filtering" + artifacts: + - path: "codeflash/main.py" + provides: "--file filtering logic before orchestration loop" + contains: "get_language_support" + - path: "tests/test_multi_language_orchestration.py" + provides: "Tests for --file, --all, and no-flags paths" + contains: "test_file_flag_filters" + key_links: + - from: "codeflash/main.py" + to: "codeflash/languages/registry.py" + via: "get_language_support(Path(args.file)) for --file language detection" + pattern: "get_language_support.*args\\.file" +--- + + +Close three verification gaps from Phase 8: --file flag routing (PROC-05), --all flag verification (PROC-04), and no-flags path verification (PROC-06). + +Purpose: The core orchestration loop works but CLI flag routing is incomplete. --file should filter to a single language, --all should walk each language's module_root (already partially implemented on line 114), and no-flags should run all passes (already works but needs tests). + +Output: Updated main.py with --file filtering, plus tests covering all three CLI paths. + + + +@/home/ubuntu/.claude/get-shit-done/workflows/execute-plan.md +@/home/ubuntu/.claude/get-shit-done/templates/summary.md + + + +@.planning/STATE.md +@.planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md +@.planning/phases/08-sequential-multi-language-orchestration/08-VERIFICATION.md + + + +```python +def get_language_support(identifier: Path | Language | str) -> LanguageSupport: + """Accepts Path (uses suffix), Language enum, or str. Raises UnsupportedLanguageError.""" + +def is_language_supported(identifier: Path | Language | str) -> bool: + """Returns True if supported, False otherwise.""" +``` + + +```python +@dataclass +class LanguageConfig: + config: dict[str, Any] + config_path: Path + language: Language +``` + + +```python +class Language(str, Enum): + PYTHON = "python" + JAVASCRIPT = "javascript" + TYPESCRIPT = "typescript" + JAVA = "java" +``` + + +```python +language_configs = find_all_config_files() + +if not language_configs: + # Fallback: single-config path + ... + return + +args = handle_optimize_all_arg_parsing(args) +logger = logging.getLogger("codeflash") +results: dict[str, str] = {} +for lang_config in language_configs: + lang_name = lang_config.language.value + try: + pass_args = copy.deepcopy(args) + pass_args = apply_language_config(pass_args, lang_config) + if hasattr(pass_args, "all") and pass_args.all is not None: + pass_args.all = pass_args.module_root + # ... formatter check, checkpoint, sentry, posthog ... + optimizer.run_with_args(pass_args) + results[lang_name] = "success" + except Exception: + logger.exception("Error processing %s, continuing with remaining languages", lang_name) + results[lang_name] = "failed" +_log_orchestration_summary(logger, results) +``` + + + + + + + Task 1: Add --file language filtering to multi-language orchestration + codeflash/main.py + + - codeflash/main.py (full file, especially lines 82-135) + - codeflash/languages/registry.py (get_language_support function) + - codeflash/code_utils/config_parser.py (LanguageConfig dataclass) + + +Add --file language detection and filtering AFTER the `find_all_config_files()` call and BEFORE the `handle_optimize_all_arg_parsing(args)` call (between current lines 82 and 103). + +Insert this logic after `language_configs = find_all_config_files()` and after the `if not language_configs:` fallback block: + +```python +# Filter to single language when --file is specified +if hasattr(args, "file") and args.file: + from codeflash.languages.registry import get_language_support, UnsupportedLanguageError + try: + file_lang_support = get_language_support(Path(args.file)) + file_language = file_lang_support.language + matching_configs = [lc for lc in language_configs if lc.language == file_language] + if matching_configs: + language_configs = matching_configs + # If no matching config found, let all configs run (existing behavior handles it) + except UnsupportedLanguageError: + pass # Unknown extension, let all configs run +``` + +Add `from codeflash.languages.registry import get_language_support, UnsupportedLanguageError` to the top-level imports at the top of the file (near the other imports from codeflash modules, around line 22-32). Do NOT use a local import inside the function since the registry module has no circular import issues with main.py. + +The key behavior: when `--file src/main/java/Foo.java` is passed, detect it's Java via `get_language_support(Path(args.file))`, then filter `language_configs` to only Java configs. This means the loop runs only one pass for the file's language instead of iterating over all languages. + + + cd /home/ubuntu/code/codeflash && uv run python -c "from codeflash.main import main; print('import ok')" + + + - `codeflash/main.py` contains `get_language_support` import from `codeflash.languages.registry` + - `codeflash/main.py` contains `if hasattr(args, "file") and args.file:` before the orchestration loop + - `codeflash/main.py` contains `matching_configs = [lc for lc in language_configs if lc.language == file_language]` + - The filtering block is placed AFTER the `if not language_configs:` fallback block and BEFORE `handle_optimize_all_arg_parsing(args)` + + --file flag detects file language via extension and filters language_configs to only the matching language before the orchestration loop runs + + + + Task 2: Add tests for --file, --all, and no-flags CLI paths + tests/test_multi_language_orchestration.py + + - tests/test_multi_language_orchestration.py (full file) + - codeflash/main.py (updated from Task 1) + + + - test_file_flag_filters_to_matching_language: When args.file="Foo.java", only Java config's optimizer.run_with_args is called (call_count == 1), not Python + - test_file_flag_python_file_filters_to_python: When args.file="module.py", only Python config's optimizer.run_with_args is called (call_count == 1) + - test_file_flag_unknown_extension_runs_all: When args.file="Foo.rs" (unsupported), all language configs run (call_count == 2 for py+java) + - test_file_flag_no_matching_config_runs_all: When args.file="Foo.java" but only Python config exists, all configs run (call_count == 1 for py only) + - test_all_flag_sets_module_root_per_language: When args.all is set, each pass gets pass_args.all == pass_args.module_root (verify via mock_run.call_args_list) + - test_no_flags_runs_all_language_passes: When neither args.file nor args.all is set, all language configs run (call_count == 2 for py+java) + + +Add a new test class `TestCLIPathRouting` to `tests/test_multi_language_orchestration.py`. Use the same patch decorator pattern as the existing `TestMultiLanguageOrchestration` class. + +For each test: +1. Create Python and/or Java LanguageConfigs using the existing `make_lang_config` helper +2. Mock `find_all_config_files` to return the configs +3. Mock `parse_args` to return `make_base_args(...)` with the relevant flag set +4. Call `main()` and verify `mock_run.call_count` and the args passed to each call + +For the `--file` tests, set `file="path/to/Foo.java"` in `make_base_args(file="path/to/Foo.java")`. + +For the `--all` test, set `all=""` in `make_base_args(all="")` (empty string means "use module_root" per handle_optimize_all_arg_parsing). Verify that each call to `mock_run` received `pass_args.all == pass_args.module_root` by inspecting `mock_run.call_args_list[i][0][0].all`. + +For the no-flags test, set neither `file` nor `all` in `make_base_args()`. Verify `mock_run.call_count == 2`. + +Important: The `--file` tests need `get_language_support` to work correctly. Since it uses the real registry, ensure the test file's extension is one that the registry knows (`.java`, `.py`). The registry is auto-populated via `_ensure_languages_registered()` which imports the support modules. + + + cd /home/ubuntu/code/codeflash && uv run pytest tests/test_multi_language_orchestration.py -x -v 2>&1 | tail -40 + + + - `tests/test_multi_language_orchestration.py` contains class `TestCLIPathRouting` + - Test `test_file_flag_filters_to_matching_language` passes and verifies call_count == 1 + - Test `test_all_flag_sets_module_root_per_language` passes and verifies pass_args.all == pass_args.module_root for each call + - Test `test_no_flags_runs_all_language_passes` passes and verifies call_count == 2 + - All existing 26 tests still pass + - `uv run prek` passes + + At least 6 new tests covering --file filtering, --all per-language module_root, and no-flags path all pass alongside existing tests + + + + + +- `uv run pytest tests/test_multi_language_orchestration.py -x -v` -- all tests pass (existing 26 + new 6+) +- `uv run prek` -- linting and type checking pass +- `grep -n "get_language_support" codeflash/main.py` -- import and usage present +- `grep -n "TestCLIPathRouting" tests/test_multi_language_orchestration.py` -- new test class exists + + + +- --file flag routes to single-language pass when file extension matches a configured language +- --all flag correctly sets pass_args.all = pass_args.module_root per language (existing behavior verified by tests) +- No-flags path runs all language passes (existing behavior verified by tests) +- All tests pass, no regressions + + + +After completion, create `.planning/phases/08-sequential-multi-language-orchestration/08-03-SUMMARY.md` + From 64f092fea29595897a59ef8baf8f27f3f48ddb07 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:51:47 +0000 Subject: [PATCH 31/41] docs(08): revise 08-03 plan based on checker feedback --- .../08-03-PLAN.md | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md b/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md index 1bdc3ebbf..d1e679d7a 100644 --- a/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md +++ b/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md @@ -33,7 +33,7 @@ must_haves: Close three verification gaps from Phase 8: --file flag routing (PROC-05), --all flag verification (PROC-04), and no-flags path verification (PROC-06). -Purpose: The core orchestration loop works but CLI flag routing is incomplete. --file should filter to a single language, --all should walk each language's module_root (already partially implemented on line 114), and no-flags should run all passes (already works but needs tests). +Purpose: The core orchestration loop works for all three CLI paths. --all and no-flags already route correctly through the multi-language loop (confirmed in main.py lines 82-135: `find_all_config_files()` runs unconditionally, the loop iterates all configs, and line 113-114 adjusts `pass_args.all` per language). The only CODE change needed is --file filtering (PROC-05) — currently --file runs all language passes instead of filtering to the file's language. PROC-04 (--all) and PROC-06 (no-flags) only need TEST COVERAGE to prove the existing routing works. Output: Updated main.py with --file filtering, plus tests covering all three CLI paths. @@ -77,6 +77,10 @@ class Language(str, Enum): ``` + ```python language_configs = find_all_config_files() @@ -119,12 +123,19 @@ _log_orchestration_summary(logger, results) Add --file language detection and filtering AFTER the `find_all_config_files()` call and BEFORE the `handle_optimize_all_arg_parsing(args)` call (between current lines 82 and 103). -Insert this logic after `language_configs = find_all_config_files()` and after the `if not language_configs:` fallback block: +First, add the top-level import near the other codeflash imports (around line 22-32): + +```python +from codeflash.languages.registry import get_language_support, UnsupportedLanguageError +``` + +Do NOT use a local import inside the function — the registry module has no circular import issues with main.py. All imports from codeflash.languages must be at top-level. + +Then insert this filtering logic after `language_configs = find_all_config_files()` and after the `if not language_configs:` fallback block, BEFORE `args = handle_optimize_all_arg_parsing(args)`: ```python # Filter to single language when --file is specified if hasattr(args, "file") and args.file: - from codeflash.languages.registry import get_language_support, UnsupportedLanguageError try: file_lang_support = get_language_support(Path(args.file)) file_language = file_lang_support.language @@ -136,7 +147,7 @@ if hasattr(args, "file") and args.file: pass # Unknown extension, let all configs run ``` -Add `from codeflash.languages.registry import get_language_support, UnsupportedLanguageError` to the top-level imports at the top of the file (near the other imports from codeflash modules, around line 22-32). Do NOT use a local import inside the function since the registry module has no circular import issues with main.py. +Note: the import is already at the top of the file, so this block does NOT contain any import statement. The key behavior: when `--file src/main/java/Foo.java` is passed, detect it's Java via `get_language_support(Path(args.file))`, then filter `language_configs` to only Java configs. This means the loop runs only one pass for the file's language instead of iterating over all languages. @@ -144,7 +155,7 @@ The key behavior: when `--file src/main/java/Foo.java` is passed, detect it's Ja cd /home/ubuntu/code/codeflash && uv run python -c "from codeflash.main import main; print('import ok')" - - `codeflash/main.py` contains `get_language_support` import from `codeflash.languages.registry` + - `codeflash/main.py` has `from codeflash.languages.registry import get_language_support, UnsupportedLanguageError` as a top-level import (NOT inside a function) - `codeflash/main.py` contains `if hasattr(args, "file") and args.file:` before the orchestration loop - `codeflash/main.py` contains `matching_configs = [lc for lc in language_configs if lc.language == file_language]` - The filtering block is placed AFTER the `if not language_configs:` fallback block and BEFORE `handle_optimize_all_arg_parsing(args)` @@ -182,6 +193,8 @@ For the `--all` test, set `all=""` in `make_base_args(all="")` (empty string mea For the no-flags test, set neither `file` nor `all` in `make_base_args()`. Verify `mock_run.call_count == 2`. +PROC-04 note: The --all routing ALREADY works in main.py (lines 82-114). The `find_all_config_files()` call runs unconditionally, the for-loop iterates all configs, and lines 113-114 set `pass_args.all = pass_args.module_root` per language. The `test_all_flag_sets_module_root_per_language` test proves this existing behavior — no code changes are needed for --all, only this test. + Important: The `--file` tests need `get_language_support` to work correctly. Since it uses the real registry, ensure the test file's extension is one that the registry knows (`.java`, `.py`). The registry is auto-populated via `_ensure_languages_registered()` which imports the support modules. From 369aee35b2ba290c71b3284af85282b71d873137 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 04:54:28 +0000 Subject: [PATCH 32/41] fix(08): revise plan 03 based on checker feedback --- .../08-03-PLAN.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md b/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md index d1e679d7a..4b6e776cc 100644 --- a/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md +++ b/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md @@ -121,9 +121,9 @@ _log_orchestration_summary(logger, results) - codeflash/code_utils/config_parser.py (LanguageConfig dataclass) -Add --file language detection and filtering AFTER the `find_all_config_files()` call and BEFORE the `handle_optimize_all_arg_parsing(args)` call (between current lines 82 and 103). +Add --file language detection and filtering AFTER the `find_all_config_files()` call and BEFORE the `handle_optimize_all_arg_parsing(args)` call (between current lines 82 and 103). Two sequential steps: -First, add the top-level import near the other codeflash imports (around line 22-32): +Step 1: Add this import at the top of main.py, near the other codeflash imports (around line 22-32): ```python from codeflash.languages.registry import get_language_support, UnsupportedLanguageError @@ -131,7 +131,7 @@ from codeflash.languages.registry import get_language_support, UnsupportedLangua Do NOT use a local import inside the function — the registry module has no circular import issues with main.py. All imports from codeflash.languages must be at top-level. -Then insert this filtering logic after `language_configs = find_all_config_files()` and after the `if not language_configs:` fallback block, BEFORE `args = handle_optimize_all_arg_parsing(args)`: +Step 2: Insert this filtering block after `language_configs = find_all_config_files()` and after the `if not language_configs:` fallback block, BEFORE `args = handle_optimize_all_arg_parsing(args)`. This block uses the import added in Step 1: ```python # Filter to single language when --file is specified @@ -147,8 +147,6 @@ if hasattr(args, "file") and args.file: pass # Unknown extension, let all configs run ``` -Note: the import is already at the top of the file, so this block does NOT contain any import statement. - The key behavior: when `--file src/main/java/Foo.java` is passed, detect it's Java via `get_language_support(Path(args.file))`, then filter `language_configs` to only Java configs. This means the loop runs only one pass for the file's language instead of iterating over all languages. @@ -176,7 +174,7 @@ The key behavior: when `--file src/main/java/Foo.java` is passed, detect it's Ja - test_file_flag_unknown_extension_runs_all: When args.file="Foo.rs" (unsupported), all language configs run (call_count == 2 for py+java) - test_file_flag_no_matching_config_runs_all: When args.file="Foo.java" but only Python config exists, all configs run (call_count == 1 for py only) - test_all_flag_sets_module_root_per_language: When args.all is set, each pass gets pass_args.all == pass_args.module_root (verify via mock_run.call_args_list) - - test_no_flags_runs_all_language_passes: When neither args.file nor args.all is set, all language configs run (call_count == 2 for py+java) + - test_no_flags_runs_all_language_passes: When args.file is None AND args.all is None (no CLI flags specified), all language configs run (call_count == 2 for py+java). Note: git diff integration for changed-file detection is tested separately in Phase 7 (tests/test_git_utils.py) and is out of scope for this unit test. This test verifies only that the orchestration loop iterates all configs when no filtering flags are present. Add a new test class `TestCLIPathRouting` to `tests/test_multi_language_orchestration.py`. Use the same patch decorator pattern as the existing `TestMultiLanguageOrchestration` class. @@ -191,7 +189,7 @@ For the `--file` tests, set `file="path/to/Foo.java"` in `make_base_args(file="p For the `--all` test, set `all=""` in `make_base_args(all="")` (empty string means "use module_root" per handle_optimize_all_arg_parsing). Verify that each call to `mock_run` received `pass_args.all == pass_args.module_root` by inspecting `mock_run.call_args_list[i][0][0].all`. -For the no-flags test, set neither `file` nor `all` in `make_base_args()`. Verify `mock_run.call_count == 2`. +For the no-flags test, use `make_base_args()` with defaults so that `args.file` is None and `args.all` is None. Verify `mock_run.call_count == 2`. This confirms the orchestration loop runs all language passes when no filtering flags are present. (Git diff integration for the no-flags path is already covered by Phase 7's tests/test_git_utils.py and is not in scope here.) PROC-04 note: The --all routing ALREADY works in main.py (lines 82-114). The `find_all_config_files()` call runs unconditionally, the for-loop iterates all configs, and lines 113-114 set `pass_args.all = pass_args.module_root` per language. The `test_all_flag_sets_module_root_per_language` test proves this existing behavior — no code changes are needed for --all, only this test. From 51649ff61cb7fd974943dc7f547cf382be23c1f4 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 05:03:59 +0000 Subject: [PATCH 33/41] feat(08-03): add --file language filtering to multi-language orchestration - Detect file language via get_language_support(Path(args.file)) - Filter language_configs to only the matching language before loop - Gracefully handle unsupported extensions and missing configs Co-Authored-By: Claude Opus 4.6 --- codeflash/main.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/codeflash/main.py b/codeflash/main.py index 1946dee53..6e5ce13b6 100644 --- a/codeflash/main.py +++ b/codeflash/main.py @@ -30,6 +30,7 @@ from codeflash.code_utils.checkpoint import ask_should_use_checkpoint_get_functions from codeflash.code_utils.config_parser import find_all_config_files, parse_config_file from codeflash.code_utils.version_check import check_for_newer_minor_version +from codeflash.languages.registry import get_language_support, UnsupportedLanguageError if TYPE_CHECKING: from argparse import Namespace @@ -99,6 +100,18 @@ def main() -> None: optimizer.run_with_args(args) return + # Filter to single language when --file is specified + if hasattr(args, "file") and args.file: + try: + file_lang_support = get_language_support(Path(args.file)) + file_language = file_lang_support.language + matching_configs = [lc for lc in language_configs if lc.language == file_language] + if matching_configs: + language_configs = matching_configs + # If no matching config found, let all configs run (existing behavior handles it) + except UnsupportedLanguageError: + pass # Unknown extension, let all configs run + # Multi-language path: run git/GitHub checks ONCE before the loop args = handle_optimize_all_arg_parsing(args) From ecd08ab03ab2aa594e0299debb651900976129a4 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 05:05:05 +0000 Subject: [PATCH 34/41] test(08-03): add CLI path routing tests for --file, --all, and no-flags - test_file_flag_filters_to_matching_language: Java file runs only Java pass - test_file_flag_python_file_filters_to_python: Python file runs only Python pass - test_file_flag_unknown_extension_runs_all: .rs file runs all language passes - test_file_flag_no_matching_config_runs_all: Java file with only Python config runs all - test_all_flag_sets_module_root_per_language: --all sets pass_args.all per language - test_no_flags_runs_all_language_passes: no flags runs all language passes Co-Authored-By: Claude Opus 4.6 --- tests/test_multi_language_orchestration.py | 136 +++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index 030afe818..b545868f2 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -457,6 +457,142 @@ def test_summary_reports_skipped_status( assert mock_run.call_count == 1 +class TestCLIPathRouting: + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_file_flag_filters_to_matching_language( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(file="path/to/Foo.java", disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 1 + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_file_flag_python_file_filters_to_python( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(file="module.py", disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 1 + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_file_flag_unknown_extension_runs_all( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(file="Foo.rs", disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 2 + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_file_flag_no_matching_config_runs_all( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + mock_find_configs.return_value = [py_config] + mock_parse_args.return_value = make_base_args(file="Foo.java", disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 1 + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_all_flag_sets_module_root_per_language( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(all="", disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 2 + for call in mock_run.call_args_list: + passed_args = call[0][0] + assert passed_args.all == passed_args.module_root + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_no_flags_runs_all_language_passes( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + java_config = make_lang_config(tmp_path, Language.JAVA) + mock_find_configs.return_value = [py_config, java_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 2 + + class TestNormalizeTomlConfig: def test_converts_hyphenated_keys_to_underscored(self, tmp_path: Path) -> None: config = {"module-root": "src", "tests-root": "tests"} From f350013b68c33910ce4aa490f70aba2461305a3e Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 22:26:13 +0000 Subject: [PATCH 35/41] test(09-02): add failing tests for unconfigured language detection - Tests for detect_unconfigured_languages() function - Tests for auto_configure_language() success and failure paths - Test for per-language logging output Co-Authored-By: Claude Opus 4.6 --- tests/test_multi_language_orchestration.py | 87 +++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index b545868f2..495d10bcd 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -1,8 +1,9 @@ from __future__ import annotations +import logging from argparse import Namespace from pathlib import Path -from unittest.mock import patch +from unittest.mock import MagicMock, patch import tomlkit @@ -638,3 +639,87 @@ def test_empty_ignore_paths_default(self, tmp_path: Path) -> None: config: dict = {} result = normalize_toml_config(config, tmp_path / "codeflash.toml") assert result["ignore_paths"] == [] + + +class TestUnconfiguredLanguageDetection: + def test_detects_unconfigured_java_from_changed_files(self) -> None: + from codeflash.main import detect_unconfigured_languages + + configs = [LanguageConfig(config={}, config_path=Path("pyproject.toml"), language=Language.PYTHON)] + changed = [Path("src/main/java/Foo.java"), Path("src/Bar.py")] + result = detect_unconfigured_languages(configs, changed) + assert Language.JAVA in result + assert Language.PYTHON not in result + + def test_no_unconfigured_when_all_configured(self) -> None: + from codeflash.main import detect_unconfigured_languages + + configs = [ + LanguageConfig(config={}, config_path=Path("pyproject.toml"), language=Language.PYTHON), + LanguageConfig(config={}, config_path=Path("codeflash.toml"), language=Language.JAVA), + ] + changed = [Path("Foo.java"), Path("bar.py")] + result = detect_unconfigured_languages(configs, changed) + assert result == set() + + def test_ignores_unsupported_extensions(self) -> None: + from codeflash.main import detect_unconfigured_languages + + changed = [Path("main.rs"), Path("lib.go")] + result = detect_unconfigured_languages([], changed) + assert result == set() + + @patch("codeflash.main.find_all_config_files") + def test_auto_config_adds_language_config_on_success(self, mock_find_configs, tmp_path: Path) -> None: + from codeflash.main import auto_configure_language + + new_lc = LanguageConfig(config={}, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + mock_find_configs.return_value = [new_lc] + + logger = logging.getLogger("codeflash.test") + with ( + patch("codeflash.main.write_config", return_value=(True, "Created codeflash.toml")) as mock_write, + patch("codeflash.main.detect_project_for_language") as mock_detect, + ): + mock_detect.return_value = MagicMock() + result = auto_configure_language(Language.JAVA, tmp_path, logger) + + assert result is not None + assert result.language == Language.JAVA + mock_write.assert_called_once() + + def test_auto_config_failure_logs_warning(self, tmp_path: Path, caplog: object) -> None: + from codeflash.main import auto_configure_language + + logger = logging.getLogger("codeflash.test") + with ( + patch("codeflash.main.detect_project_for_language", side_effect=RuntimeError("detection failed")), + caplog.at_level(logging.WARNING), # type: ignore[union-attr] + ): + result = auto_configure_language(Language.JAVA, tmp_path, logger) + + assert result is None + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_per_language_logging_shows_config_path( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + py_config = make_lang_config(tmp_path, Language.PYTHON) + mock_find_configs.return_value = [py_config] + mock_parse_args.return_value = make_base_args(disable_telemetry=False) + + with patch("codeflash.main._log_orchestration_summary"): + from codeflash.main import main + + with patch("logging.Logger.info") as mock_log_info: + main() + logged_messages = [str(call) for call in mock_log_info.call_args_list] + processing_logs = [m for m in logged_messages if "Processing" in m and "config:" in m] + assert len(processing_logs) >= 1 From 7a80478ba0f0ae60d09cf086e22fc39ffd9ecbc6 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 18 Mar 2026 22:30:18 +0000 Subject: [PATCH 36/41] feat(09-02): add unconfigured language auto-detection and config creation - Add detect_unconfigured_languages() to identify languages in changed files lacking configs - Add detect_project_for_language() using per-language detection helpers (avoids wrong-language pitfall) - Add auto_configure_language() that writes config and re-discovers it in one step - Add get_changed_file_paths() helper using git diff - Wire auto-config into orchestration loop (only for subagent/no-flags path) - Failed auto-config logs warning with manual setup instructions, continues gracefully - Per-language "Processing {lang} (config: {path})" logging confirmed working Co-Authored-By: Claude Opus 4.6 --- codeflash/main.py | 113 ++++++++++++++++++++- tests/test_multi_language_orchestration.py | 3 +- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/codeflash/main.py b/codeflash/main.py index 6e5ce13b6..26a565548 100644 --- a/codeflash/main.py +++ b/codeflash/main.py @@ -30,11 +30,16 @@ from codeflash.code_utils.checkpoint import ask_should_use_checkpoint_get_functions from codeflash.code_utils.config_parser import find_all_config_files, parse_config_file from codeflash.code_utils.version_check import check_for_newer_minor_version -from codeflash.languages.registry import get_language_support, UnsupportedLanguageError +from codeflash.languages.registry import UnsupportedLanguageError, get_language_support +from codeflash.setup.config_writer import write_config if TYPE_CHECKING: from argparse import Namespace + from codeflash.code_utils.config_parser import LanguageConfig + from codeflash.languages.language_enum import Language + from codeflash.setup.detector import DetectedProject + def main() -> None: """Entry point for the codeflash command-line interface.""" @@ -82,6 +87,20 @@ def main() -> None: else: language_configs = find_all_config_files() + # Auto-configure unconfigured languages detected from changed files + # Only for subagent/no-flags path (not --file which targets a specific file) + logger = logging.getLogger("codeflash") + if not (hasattr(args, "file") and args.file): + changed_files = get_changed_file_paths() + if changed_files: + unconfigured = detect_unconfigured_languages(language_configs, changed_files) + if unconfigured: + project_root = Path.cwd() + for lang in unconfigured: + new_config = auto_configure_language(lang, project_root, logger) + if new_config is not None: + language_configs.append(new_config) + if not language_configs: # Fallback: no multi-config found, use existing single-config path loaded_args = _handle_config_loading(args) @@ -115,7 +134,6 @@ def main() -> None: # Multi-language path: run git/GitHub checks ONCE before the loop args = handle_optimize_all_arg_parsing(args) - logger = logging.getLogger("codeflash") results: dict[str, str] = {} for lang_config in language_configs: lang_name = lang_config.language.value @@ -155,6 +173,97 @@ def _log_orchestration_summary(logger: logging.Logger, results: dict[str, str]) logger.info("Multi-language orchestration complete: %s", ", ".join(parts)) +def detect_unconfigured_languages(language_configs: list[LanguageConfig], changed_files: list[Path]) -> set[Language]: + configured = {lc.language for lc in language_configs} + changed_languages: set[Language] = set() + for f in changed_files: + try: + lang_support = get_language_support(f) + changed_languages.add(lang_support.language) + except UnsupportedLanguageError: + pass + return changed_languages - configured + + +def get_changed_file_paths() -> list[Path]: + import subprocess + + try: + result = subprocess.run( + ["git", "diff", "--name-only", "HEAD~1"], capture_output=True, text=True, timeout=10, check=False + ) + if result.returncode == 0: + return [Path(line) for line in result.stdout.strip().splitlines() if line] + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + pass + return [] + + +def detect_project_for_language(language: Language, project_root: Path) -> DetectedProject: + from codeflash.setup.detector import ( + DetectedProject, + _detect_formatter, + _detect_ignore_paths, + _detect_java_module_root, + _detect_js_module_root, + _detect_python_module_root, + _detect_test_runner, + _detect_tests_root, + ) + + lang_str = language.value + + module_root_detectors = { + "python": _detect_python_module_root, + "java": _detect_java_module_root, + "javascript": _detect_js_module_root, + } + + detector = module_root_detectors.get(lang_str) + if detector is None: + msg = f"No auto-detection available for {lang_str}" + raise ValueError(msg) + + module_root, _ = detector(project_root) + tests_root, _ = _detect_tests_root(project_root, lang_str) + test_runner, _ = _detect_test_runner(project_root, lang_str) + formatter_cmds, _ = _detect_formatter(project_root, lang_str) + ignore_paths, _ = _detect_ignore_paths(project_root, lang_str) + + return DetectedProject( + language=lang_str, + project_root=project_root, + module_root=module_root, + tests_root=tests_root, + test_runner=test_runner, + formatter_cmds=formatter_cmds, + ignore_paths=ignore_paths, + ) + + +def auto_configure_language(language: Language, project_root: Path, logger: logging.Logger) -> LanguageConfig | None: + lang_str = language.value + try: + detected = detect_project_for_language(language, project_root) + success, msg = write_config(detected) + if success: + logger.info("Auto-created config for %s: %s", lang_str, msg) + logger.info("Review the generated config file to verify paths are correct.") + new_configs = find_all_config_files() + for nc in new_configs: + if nc.language == language: + return nc + logger.warning("Config was created for %s but could not be re-discovered.", lang_str) + return None + logger.warning("Could not auto-configure %s: %s. Skipping.", lang_str, msg) + logger.info("Run 'codeflash init' to set up %s manually.", lang_str) + return None + except Exception: + logger.exception("Auto-detection failed for %s. Skipping.", lang_str) + logger.info("Run 'codeflash init' to set up %s manually.", lang_str) + return None + + def _handle_config_loading(args: Namespace) -> Namespace | None: """Handle config loading with first-run experience support. diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index 495d10bcd..fa8c20bc3 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -269,8 +269,9 @@ def test_singleton_set_per_pass( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.main.get_changed_file_paths", return_value=[]) def test_fallback_to_single_config_when_no_multi_configs( - self, _ver, _banner, mock_parse_args, mock_handle_config, mock_run, _fmt, _ckpt, tmp_path: Path + self, _changed, _ver, _banner, mock_parse_args, mock_handle_config, mock_run, _fmt, _ckpt, tmp_path: Path ) -> None: base = make_base_args( disable_telemetry=False, formatter_cmds=[], module_root=str(tmp_path), tests_root=str(tmp_path) From dff13a6a325bde32cdc8a56a2256464b727ecdb4 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 19 Mar 2026 01:58:50 +0000 Subject: [PATCH 37/41] test(10-01): fill Python unit test gaps for multi-language modules - Add JS/TS config discovery tests (package.json, all three config types) - Add malformed TOML and missing codeflash section tests - Add JS/TS extension git diff tests (.js, .ts, .jsx, .tsx) - Add mixed three-language git diff test - Add TypeScript/JSX file flag routing tests - Add direct function coverage for get_changed_file_paths, detect_project_for_language - Add empty config normalize test - 13 new tests across 3 files (60 -> 73 total) Co-Authored-By: Claude Opus 4.6 --- tests/test_git_utils.py | 92 ++++++++++++++++ tests/test_multi_config_discovery.py | 33 ++++++ tests/test_multi_language_orchestration.py | 118 +++++++++++++++++++++ 3 files changed, 243 insertions(+) diff --git a/tests/test_git_utils.py b/tests/test_git_utils.py index cb0837468..0666a6136 100644 --- a/tests/test_git_utils.py +++ b/tests/test_git_utils.py @@ -292,6 +292,68 @@ def helper(): """ +JS_TS_DIFF = """\ +--- a/src/app.js ++++ b/src/app.js +@@ -1,3 +1,4 @@ + function start() { ++ const x = 1; + return true; + +--- a/src/utils.ts ++++ b/src/utils.ts +@@ -1,3 +1,4 @@ + function helper() { ++ const y = 2; + return false; + +--- a/src/Component.jsx ++++ b/src/Component.jsx +@@ -1,3 +1,4 @@ + function Component() { ++ const a = null; + return null; + +--- a/src/Page.tsx ++++ b/src/Page.tsx +@@ -1,3 +1,4 @@ + function Page() { ++ const b = null; + return null; + +""" + +ALL_THREE_LANGS_DIFF = """\ +--- a/src/main.py ++++ b/src/main.py +@@ -1,3 +1,4 @@ + def main(): ++ x = 1 + return True + +--- a/src/Main.java ++++ b/src/Main.java +@@ -1,3 +1,4 @@ + public class Main { ++ int x = 1; + public static void main(String[] args) {} + +--- a/src/app.js ++++ b/src/app.js +@@ -1,3 +1,4 @@ + function app() { ++ const x = 1; + return true; + +--- a/src/utils.ts ++++ b/src/utils.ts +@@ -1,3 +1,4 @@ + function util() { ++ const y = 2; + return false; + +""" + class TestGetGitDiffMultiLanguage(unittest.TestCase): @patch("codeflash.code_utils.git_utils.git.Repo") @@ -330,6 +392,36 @@ def test_mixed_lang_diff_returns_all_languages(self, mock_repo_cls): assert any(k.endswith("utils.py") for k in keys) assert any(k.endswith("App.java") for k in keys) + @patch("codeflash.code_utils.git_utils.git.Repo") + def test_js_ts_extensions_found(self, mock_repo_cls): + repo = mock_repo_cls.return_value + repo.head.commit.hexsha = "abc123" + repo.working_dir = "/repo" + repo.git.diff.return_value = JS_TS_DIFF + + result = get_git_diff(repo_directory=None, uncommitted_changes=True) + assert len(result) == 4 + keys = [str(k) for k in result.keys()] + assert any(k.endswith("app.js") for k in keys) + assert any(k.endswith("utils.ts") for k in keys) + assert any(k.endswith("Component.jsx") for k in keys) + assert any(k.endswith("Page.tsx") for k in keys) + + @patch("codeflash.code_utils.git_utils.git.Repo") + def test_mixed_all_three_languages(self, mock_repo_cls): + repo = mock_repo_cls.return_value + repo.head.commit.hexsha = "abc123" + repo.working_dir = "/repo" + repo.git.diff.return_value = ALL_THREE_LANGS_DIFF + + result = get_git_diff(repo_directory=None, uncommitted_changes=True) + assert len(result) == 4 + keys = [str(k) for k in result.keys()] + assert any(k.endswith("main.py") for k in keys) + assert any(k.endswith("Main.java") for k in keys) + assert any(k.endswith("app.js") for k in keys) + assert any(k.endswith("utils.ts") for k in keys) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_multi_config_discovery.py b/tests/test_multi_config_discovery.py index a0bf8f45f..162f93693 100644 --- a/tests/test_multi_config_discovery.py +++ b/tests/test_multi_config_discovery.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from pathlib import Path import tomlkit @@ -66,6 +67,38 @@ def test_closest_config_wins_per_language(self, tmp_path: Path, monkeypatch) -> assert result[0].language == Language.PYTHON assert result[0].config_path == subdir / "pyproject.toml" + def test_finds_package_json_with_codeflash_section(self, tmp_path: Path, monkeypatch) -> None: + pkg = {"codeflash": {"moduleRoot": "src"}} + (tmp_path / "package.json").write_text(json.dumps(pkg), encoding="utf-8") + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 1 + assert result[0].language == Language.JAVASCRIPT + assert result[0].config_path == tmp_path / "package.json" + + def test_finds_all_three_config_types(self, tmp_path: Path, monkeypatch) -> None: + write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) + write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}}) + pkg = {"codeflash": {"moduleRoot": "src"}} + (tmp_path / "package.json").write_text(json.dumps(pkg), encoding="utf-8") + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 3 + languages = {r.language for r in result} + assert languages == {Language.PYTHON, Language.JAVA, Language.JAVASCRIPT} + + def test_malformed_toml_skipped(self, tmp_path: Path, monkeypatch) -> None: + (tmp_path / "codeflash.toml").write_text("not valid [toml", encoding="utf-8") + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 0 + + def test_missing_codeflash_section_skipped(self, tmp_path: Path, monkeypatch) -> None: + write_toml(tmp_path / "codeflash.toml", {"tool": {"other": {"key": "value"}}}) + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 0 + def test_find_all_functions_uses_registry_not_singleton() -> None: """DISC-04: Verify find_all_functions_in_file uses per-file registry lookup.""" diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index fa8c20bc3..63a279826 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import logging from argparse import Namespace from pathlib import Path @@ -188,6 +189,17 @@ def make_lang_config(tmp_path: Path, language: Language, subdir: str = "") -> La config_path=config_path, language=Language.PYTHON, ) + if language == Language.JAVASCRIPT: + src = tmp_path / subdir / "src" if subdir else tmp_path / "src" + tests = tmp_path / subdir / "tests" if subdir else tmp_path / "tests" + src.mkdir(parents=True, exist_ok=True) + tests.mkdir(parents=True, exist_ok=True) + config_path = tmp_path / subdir / "package.json" if subdir else tmp_path / "package.json" + return LanguageConfig( + config={"module_root": str(src), "tests_root": str(tests)}, + config_path=config_path, + language=Language.JAVASCRIPT, + ) src = tmp_path / subdir / "src" / "main" / "java" if subdir else tmp_path / "src" / "main" / "java" tests = tmp_path / subdir / "src" / "test" / "java" if subdir else tmp_path / "src" / "test" / "java" src.mkdir(parents=True, exist_ok=True) @@ -594,6 +606,112 @@ def test_no_flags_runs_all_language_passes( assert mock_run.call_count == 2 + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_file_flag_typescript_extension( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + # .tsx maps to Language.TYPESCRIPT, which is distinct from Language.JAVASCRIPT. + # When no TYPESCRIPT config exists, all configs run (fallback behavior). + py_config = make_lang_config(tmp_path, Language.PYTHON) + js_config = make_lang_config(tmp_path, Language.JAVASCRIPT, subdir="js-proj") + mock_find_configs.return_value = [py_config, js_config] + mock_parse_args.return_value = make_base_args(file="path/to/Component.tsx", disable_telemetry=False) + + from codeflash.main import main + + main() + + # No TYPESCRIPT config exists, so all configs run (same as unknown extension) + assert mock_run.call_count == 2 + + @patch("codeflash.main.ask_should_use_checkpoint_get_functions", return_value=[]) + @patch("codeflash.main.env_utils.check_formatter_installed", return_value=True) + @patch("codeflash.main.handle_optimize_all_arg_parsing", side_effect=lambda args: args) + @patch("codeflash.optimization.optimizer.run_with_args") + @patch("codeflash.main.find_all_config_files") + @patch("codeflash.main.parse_args") + @patch("codeflash.main.print_codeflash_banner") + @patch("codeflash.main.check_for_newer_minor_version") + def test_file_flag_jsx_extension( + self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + ) -> None: + # .jsx maps to Language.JAVASCRIPT, so it correctly filters to the JS config. + py_config = make_lang_config(tmp_path, Language.PYTHON) + js_config = make_lang_config(tmp_path, Language.JAVASCRIPT, subdir="js-proj") + mock_find_configs.return_value = [py_config, js_config] + mock_parse_args.return_value = make_base_args(file="path/to/Widget.jsx", disable_telemetry=False) + + from codeflash.main import main + + main() + + assert mock_run.call_count == 1 + + +class TestDirectFunctionCoverage: + @patch("subprocess.run") + def test_get_changed_file_paths_returns_diff_files(self, mock_subprocess) -> None: + from codeflash.main import get_changed_file_paths + + mock_subprocess.return_value = MagicMock(returncode=0, stdout="src/main.py\nsrc/App.java\n") + result = get_changed_file_paths() + assert len(result) == 2 + assert Path("src/main.py") in result + assert Path("src/App.java") in result + + @patch("subprocess.run") + def test_get_changed_file_paths_returns_empty_on_failure(self, mock_subprocess) -> None: + from codeflash.main import get_changed_file_paths + + mock_subprocess.return_value = MagicMock(returncode=1, stdout="") + result = get_changed_file_paths() + assert result == [] + + def test_detect_project_for_language_java(self, tmp_path: Path) -> None: + from codeflash.main import detect_project_for_language + + with ( + patch( + "codeflash.setup.detector._detect_java_module_root", + return_value=(tmp_path / "src/main/java", "pom.xml"), + ), + patch( + "codeflash.setup.detector._detect_tests_root", + return_value=(tmp_path / "src/test/java", "maven"), + ), + patch("codeflash.setup.detector._detect_test_runner", return_value=("maven", "pom.xml")), + patch("codeflash.setup.detector._detect_formatter", return_value=([], None)), + patch("codeflash.setup.detector._detect_ignore_paths", return_value=([], None)), + ): + result = detect_project_for_language(Language.JAVA, tmp_path) + assert result is not None + assert result.language == "java" + + def test_detect_project_for_language_unsupported(self) -> None: + from codeflash.main import detect_project_for_language + + mock_lang = MagicMock() + mock_lang.value = "rust" + try: + detect_project_for_language(mock_lang, Path("/tmp")) + assert False, "Should have raised ValueError" + except ValueError as e: + assert "No auto-detection available" in str(e) + + def test_empty_config_no_module_root(self, tmp_path: Path) -> None: + config: dict = {} + result = normalize_toml_config(config, tmp_path / "codeflash.toml") + assert result["formatter_cmds"] == [] + assert result["disable_telemetry"] is False + assert "module_root" not in result + class TestNormalizeTomlConfig: def test_converts_hyphenated_keys_to_underscored(self, tmp_path: Path) -> None: From 9c50bc111b0fdb4cca1168351dad4da0392b36f3 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Thu, 19 Mar 2026 02:47:28 +0000 Subject: [PATCH 38/41] fix(10-03): resolve test pollution in multi-language orchestration tests - Mock posthog and sentry initialization in all tests calling main() to prevent SystemExit when prior tests overwrite CODEFLASH_API_KEY - Re-register JavaSupport in clear_registry test to prevent Java language lookup failures in subsequent tests Co-Authored-By: Claude Opus 4.6 --- tests/test_languages/test_registry.py | 2 + tests/test_multi_language_orchestration.py | 68 +++++++++++++++++----- 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/tests/test_languages/test_registry.py b/tests/test_languages/test_registry.py index cdb44e1af..417a4a62e 100644 --- a/tests/test_languages/test_registry.py +++ b/tests/test_languages/test_registry.py @@ -272,6 +272,7 @@ def test_clear_registry_removes_everything(self): assert not is_language_supported(Language.PYTHON) # Re-register all languages by importing + from codeflash.languages.java.support import JavaSupport from codeflash.languages.javascript.support import JavaScriptSupport, TypeScriptSupport from codeflash.languages.python.support import PythonSupport @@ -279,6 +280,7 @@ def test_clear_registry_removes_everything(self): register_language(PythonSupport) register_language(JavaScriptSupport) register_language(TypeScriptSupport) + register_language(JavaSupport) # Should be supported again assert is_language_supported(Language.PYTHON) diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index 63a279826..18200e331 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -221,8 +221,10 @@ class TestMultiLanguageOrchestration: @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_sequential_passes_calls_optimizer_per_language( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -243,10 +245,14 @@ def test_sequential_passes_calls_optimizer_per_language( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") @patch("codeflash.cli_cmds.cli.set_current_language") def test_singleton_set_per_pass( self, mock_set_lang, + _sentry, + _posthog, _ver, _banner, mock_parse_args, @@ -281,9 +287,11 @@ def test_singleton_set_per_pass( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") @patch("codeflash.main.get_changed_file_paths", return_value=[]) def test_fallback_to_single_config_when_no_multi_configs( - self, _changed, _ver, _banner, mock_parse_args, mock_handle_config, mock_run, _fmt, _ckpt, tmp_path: Path + self, _changed, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_handle_config, mock_run, _fmt, _ckpt, tmp_path: Path ) -> None: base = make_base_args( disable_telemetry=False, formatter_cmds=[], module_root=str(tmp_path), tests_root=str(tmp_path) @@ -306,8 +314,10 @@ def test_fallback_to_single_config_when_no_multi_configs( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_args_deep_copied_between_passes( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -335,8 +345,10 @@ def test_args_deep_copied_between_passes( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_error_in_one_language_does_not_block_others( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -359,8 +371,10 @@ def test_error_in_one_language_does_not_block_others( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_orchestration_summary_logged( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -385,8 +399,10 @@ def test_orchestration_summary_logged( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_summary_reports_failure_status( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -450,8 +466,10 @@ def test_summary_no_results_no_log(self) -> None: @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_summary_reports_skipped_status( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, mock_fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, mock_fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -480,8 +498,10 @@ class TestCLIPathRouting: @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_file_flag_filters_to_matching_language( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -502,8 +522,10 @@ def test_file_flag_filters_to_matching_language( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_file_flag_python_file_filters_to_python( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -524,8 +546,10 @@ def test_file_flag_python_file_filters_to_python( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_file_flag_unknown_extension_runs_all( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -546,8 +570,10 @@ def test_file_flag_unknown_extension_runs_all( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_file_flag_no_matching_config_runs_all( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) mock_find_configs.return_value = [py_config] @@ -567,8 +593,10 @@ def test_file_flag_no_matching_config_runs_all( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_all_flag_sets_module_root_per_language( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -592,8 +620,10 @@ def test_all_flag_sets_module_root_per_language( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_no_flags_runs_all_language_passes( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) java_config = make_lang_config(tmp_path, Language.JAVA) @@ -614,8 +644,10 @@ def test_no_flags_runs_all_language_passes( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_file_flag_typescript_extension( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: # .tsx maps to Language.TYPESCRIPT, which is distinct from Language.JAVASCRIPT. # When no TYPESCRIPT config exists, all configs run (fallback behavior). @@ -639,8 +671,10 @@ def test_file_flag_typescript_extension( @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_file_flag_jsx_extension( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: # .jsx maps to Language.JAVASCRIPT, so it correctly filters to the JS config. py_config = make_lang_config(tmp_path, Language.PYTHON) @@ -827,8 +861,10 @@ def test_auto_config_failure_logs_warning(self, tmp_path: Path, caplog: object) @patch("codeflash.main.parse_args") @patch("codeflash.main.print_codeflash_banner") @patch("codeflash.main.check_for_newer_minor_version") + @patch("codeflash.telemetry.posthog_cf.initialize_posthog") + @patch("codeflash.telemetry.sentry.init_sentry") def test_per_language_logging_shows_config_path( - self, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path + self, _sentry, _posthog, _ver, _banner, mock_parse_args, mock_find_configs, mock_run, _handle_all, _fmt, _ckpt, tmp_path: Path ) -> None: py_config = make_lang_config(tmp_path, Language.PYTHON) mock_find_configs.return_value = [py_config] From 68e40aa4576eb06d563eec52d9d039ab9eff152b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 03:42:09 +0000 Subject: [PATCH 39/41] style: auto-fix ruff formatting in config_parser.py --- codeflash/code_utils/config_parser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index a6581a531..ad832f46c 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -181,9 +181,7 @@ def find_all_config_files(start_dir: Path | None = None) -> list[LanguageConfig] raw_config = dict(tool["codeflash"]) normalized = normalize_toml_config(raw_config, config_file) seen_languages.add(language) - configs.append( - LanguageConfig(config=normalized, config_path=config_file, language=language) - ) + configs.append(LanguageConfig(config=normalized, config_path=config_file, language=language)) except Exception: continue From e41517d43912aedc38db9a3da75b2323ec96a657 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 24 Mar 2026 22:37:32 +0000 Subject: [PATCH 40/41] chore: remove planning files from PR Co-Authored-By: Claude Opus 4.6 --- .planning/STATE.md | 22 -- .planning/config.json | 7 - .../08-02-PLAN.md | 59 ----- .../08-02-SUMMARY.md | 95 -------- .../08-03-PLAN.md | 230 ------------------ 5 files changed, 413 deletions(-) delete mode 100644 .planning/STATE.md delete mode 100644 .planning/config.json delete mode 100644 .planning/phases/08-sequential-multi-language-orchestration/08-02-PLAN.md delete mode 100644 .planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md delete mode 100644 .planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md diff --git a/.planning/STATE.md b/.planning/STATE.md deleted file mode 100644 index cc9e7d157..000000000 --- a/.planning/STATE.md +++ /dev/null @@ -1,22 +0,0 @@ -# Project State - -## Current Position -- **Phase:** 08 - Sequential Multi-Language Orchestration -- **Plan:** 02 (Complete) -- **Status:** Complete - -## Progress -- Plan 08-01: Complete (apply_language_config + orchestration loop) -- Plan 08-02: Complete (error isolation + config normalization + summary logging) - -## Decisions -- Multi-language orchestration uses sequential passes with deep-copied args -- find_all_config_files walks up from CWD collecting per-language configs -- apply_language_config mirrors process_pyproject_config for the multi-config path -- normalize_toml_config is the shared helper for config normalization (path resolution, defaults, key conversion) -- Per-language error isolation: try/except in loop with status tracking dict -- Summary logging: comma-separated "lang: status" pairs via logger.info - -## Last Session -- **Stopped at:** Completed 08-02-PLAN.md -- **Timestamp:** 2026-03-18T04:40:02Z diff --git a/.planning/config.json b/.planning/config.json deleted file mode 100644 index e24dad826..000000000 --- a/.planning/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "executor_model": "opus", - "commit_docs": true, - "parallelization": false, - "branching_strategy": "none", - "verifier_enabled": false -} diff --git a/.planning/phases/08-sequential-multi-language-orchestration/08-02-PLAN.md b/.planning/phases/08-sequential-multi-language-orchestration/08-02-PLAN.md deleted file mode 100644 index 1c415136b..000000000 --- a/.planning/phases/08-sequential-multi-language-orchestration/08-02-PLAN.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -phase: 08-sequential-multi-language-orchestration -plan: 02 -type: implementation -autonomous: true -wave: 1 -depends_on: [08-01] ---- - -# Plan 08-02: Error Isolation and Config Normalization for Multi-Language Orchestration - -## Objective - -Harden the multi-language orchestration loop with per-language error isolation and ensure `find_all_config_files` normalizes config values consistently with `parse_config_file`. - -## Context - -- @codeflash/main.py — multi-language orchestration loop from 08-01 -- @codeflash/cli_cmds/cli.py — apply_language_config from 08-01 -- @codeflash/code_utils/config_parser.py — find_all_config_files and parse_config_file -- @tests/test_multi_language_orchestration.py — existing tests from 08-01 - -## Tasks - -### Task 1: Normalize config values in find_all_config_files -type="auto" - -`find_all_config_files` reads raw toml data but doesn't normalize it the way `parse_config_file` does (path resolution, hyphen-to-underscore key conversion, defaults for missing keys). Add a helper that normalizes the raw config dict so `apply_language_config` receives consistent data. - -**Done criteria:** -- Config values from `find_all_config_files` have paths resolved relative to config file parent -- Hyphenated keys are converted to underscored keys -- Default values are applied for missing keys (formatter_cmds=[], disable_telemetry=False, etc.) -- Tests verify normalization - -### Task 2: Add per-language error isolation in main.py orchestration loop -type="auto" - -Wrap each language pass in a try/except so one language failure doesn't prevent other languages from being optimized. Log the error and continue. - -**Done criteria:** -- Exception in one language pass logs the error and continues to next language -- Test verifies that if optimizer.run_with_args raises for one language, the other language still runs -- Summary logging at end of loop reports which languages succeeded/failed - -### Task 3: Add summary logging for multi-language orchestration results -type="auto" - -After the orchestration loop completes, log a summary showing which languages were processed and their status (success/failure/skipped). - -**Done criteria:** -- Summary log message after loop shows per-language status -- Test verifies summary includes correct language names and statuses - -## Verification - -- All existing tests in test_multi_language_orchestration.py still pass -- New tests cover normalization, error isolation, and summary logging -- `uv run prek` passes diff --git a/.planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md b/.planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md deleted file mode 100644 index b2a5d7ce7..000000000 --- a/.planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -phase: 08-sequential-multi-language-orchestration -plan: 02 -subsystem: cli -tags: [multi-language, config-normalization, error-isolation, orchestration] - -requires: - - phase: 08-01 - provides: apply_language_config, multi-language orchestration loop, find_all_config_files -provides: - - normalize_toml_config helper for consistent config normalization - - Per-language error isolation in orchestration loop - - Orchestration summary logging with per-language status -affects: [config-parser, main-entry-point, multi-language-support] - -tech-stack: - added: [] - patterns: [shared-normalization-helper, error-isolation-loop, status-tracking-dict] - -key-files: - created: [] - modified: - - codeflash/code_utils/config_parser.py - - codeflash/main.py - - tests/test_multi_language_orchestration.py - -key-decisions: - - "Extract normalize_toml_config as shared helper used by both find_all_config_files and parse_config_file" - - "Track per-language status as dict[str, str] with values success/failed/skipped" - - "Log orchestration summary after loop completes with all statuses" - -patterns-established: - - "Config normalization: always use normalize_toml_config for toml-based configs" - - "Error isolation: wrap per-language passes in try/except, track status, continue on failure" - -requirements-completed: [] - -duration: 3min -completed: 2026-03-18 ---- - -# Phase 08 Plan 02: Error Isolation and Config Normalization Summary - -**Shared config normalization via normalize_toml_config, per-language error isolation with status tracking, and orchestration summary logging** - -## Performance - -- **Duration:** 3 min -- **Started:** 2026-03-18T04:36:44Z -- **Completed:** 2026-03-18T04:40:02Z -- **Tasks:** 3 -- **Files modified:** 3 - -## Accomplishments -- Extracted `normalize_toml_config` helper that resolves paths, applies defaults, and converts hyphenated keys -- used by both `find_all_config_files` and `parse_config_file` to eliminate duplication -- Added per-language error isolation so one language failure does not prevent other languages from being optimized -- Added orchestration summary logging showing per-language status (success/failed/skipped) after the loop completes -- 13 new tests covering normalization, error isolation, skipped status, and summary logging format - -## Task Commits - -Each task was committed atomically: - -1. **Task 1: Normalize config values in find_all_config_files** - `97e21aab` (feat) -2. **Task 2: Per-language error isolation in orchestration loop** - `a3014ec0` (feat) -3. **Task 3: Summary logging tests for orchestration results** - `dcf366e2` (test) - -## Files Created/Modified -- `codeflash/code_utils/config_parser.py` - Added normalize_toml_config helper, used in find_all_config_files and parse_config_file -- `codeflash/main.py` - Added error isolation try/except, status tracking dict, _log_orchestration_summary helper -- `tests/test_multi_language_orchestration.py` - 13 new tests (6 normalization, 3 error isolation, 4 summary logging) - -## Decisions Made -- Extracted normalization into a standalone function rather than keeping it duplicated between parse_config_file and find_all_config_files -- Used a simple dict[str, str] for status tracking rather than a more complex result type -- Summary logging uses logger.info with comma-separated "lang: status" pairs - -## Deviations from Plan - -None - plan executed exactly as written. - -## Issues Encountered -None - -## User Setup Required -None - no external service configuration required. - -## Next Phase Readiness -- Multi-language orchestration is now robust against per-language failures -- Config normalization is consistent across single-config and multi-config paths -- Ready for further multi-language pipeline enhancements - ---- -*Phase: 08-sequential-multi-language-orchestration* -*Completed: 2026-03-18* diff --git a/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md b/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md deleted file mode 100644 index 4b6e776cc..000000000 --- a/.planning/phases/08-sequential-multi-language-orchestration/08-03-PLAN.md +++ /dev/null @@ -1,230 +0,0 @@ ---- -phase: 08-sequential-multi-language-orchestration -plan: 03 -type: execute -wave: 1 -depends_on: [08-02] -files_modified: - - codeflash/main.py - - tests/test_multi_language_orchestration.py -autonomous: true -gap_closure: true -requirements: [PROC-04, PROC-05, PROC-06] - -must_haves: - truths: - - "codeflash --file path/to/File.java filters language_configs to only the matching language" - - "codeflash --all sets pass_args.all = pass_args.module_root per language pass" - - "codeflash with no flags runs all language passes without filtering" - artifacts: - - path: "codeflash/main.py" - provides: "--file filtering logic before orchestration loop" - contains: "get_language_support" - - path: "tests/test_multi_language_orchestration.py" - provides: "Tests for --file, --all, and no-flags paths" - contains: "test_file_flag_filters" - key_links: - - from: "codeflash/main.py" - to: "codeflash/languages/registry.py" - via: "get_language_support(Path(args.file)) for --file language detection" - pattern: "get_language_support.*args\\.file" ---- - - -Close three verification gaps from Phase 8: --file flag routing (PROC-05), --all flag verification (PROC-04), and no-flags path verification (PROC-06). - -Purpose: The core orchestration loop works for all three CLI paths. --all and no-flags already route correctly through the multi-language loop (confirmed in main.py lines 82-135: `find_all_config_files()` runs unconditionally, the loop iterates all configs, and line 113-114 adjusts `pass_args.all` per language). The only CODE change needed is --file filtering (PROC-05) — currently --file runs all language passes instead of filtering to the file's language. PROC-04 (--all) and PROC-06 (no-flags) only need TEST COVERAGE to prove the existing routing works. - -Output: Updated main.py with --file filtering, plus tests covering all three CLI paths. - - - -@/home/ubuntu/.claude/get-shit-done/workflows/execute-plan.md -@/home/ubuntu/.claude/get-shit-done/templates/summary.md - - - -@.planning/STATE.md -@.planning/phases/08-sequential-multi-language-orchestration/08-02-SUMMARY.md -@.planning/phases/08-sequential-multi-language-orchestration/08-VERIFICATION.md - - - -```python -def get_language_support(identifier: Path | Language | str) -> LanguageSupport: - """Accepts Path (uses suffix), Language enum, or str. Raises UnsupportedLanguageError.""" - -def is_language_supported(identifier: Path | Language | str) -> bool: - """Returns True if supported, False otherwise.""" -``` - - -```python -@dataclass -class LanguageConfig: - config: dict[str, Any] - config_path: Path - language: Language -``` - - -```python -class Language(str, Enum): - PYTHON = "python" - JAVASCRIPT = "javascript" - TYPESCRIPT = "typescript" - JAVA = "java" -``` - - - -```python -language_configs = find_all_config_files() - -if not language_configs: - # Fallback: single-config path - ... - return - -args = handle_optimize_all_arg_parsing(args) -logger = logging.getLogger("codeflash") -results: dict[str, str] = {} -for lang_config in language_configs: - lang_name = lang_config.language.value - try: - pass_args = copy.deepcopy(args) - pass_args = apply_language_config(pass_args, lang_config) - if hasattr(pass_args, "all") and pass_args.all is not None: - pass_args.all = pass_args.module_root - # ... formatter check, checkpoint, sentry, posthog ... - optimizer.run_with_args(pass_args) - results[lang_name] = "success" - except Exception: - logger.exception("Error processing %s, continuing with remaining languages", lang_name) - results[lang_name] = "failed" -_log_orchestration_summary(logger, results) -``` - - - - - - - Task 1: Add --file language filtering to multi-language orchestration - codeflash/main.py - - - codeflash/main.py (full file, especially lines 82-135) - - codeflash/languages/registry.py (get_language_support function) - - codeflash/code_utils/config_parser.py (LanguageConfig dataclass) - - -Add --file language detection and filtering AFTER the `find_all_config_files()` call and BEFORE the `handle_optimize_all_arg_parsing(args)` call (between current lines 82 and 103). Two sequential steps: - -Step 1: Add this import at the top of main.py, near the other codeflash imports (around line 22-32): - -```python -from codeflash.languages.registry import get_language_support, UnsupportedLanguageError -``` - -Do NOT use a local import inside the function — the registry module has no circular import issues with main.py. All imports from codeflash.languages must be at top-level. - -Step 2: Insert this filtering block after `language_configs = find_all_config_files()` and after the `if not language_configs:` fallback block, BEFORE `args = handle_optimize_all_arg_parsing(args)`. This block uses the import added in Step 1: - -```python -# Filter to single language when --file is specified -if hasattr(args, "file") and args.file: - try: - file_lang_support = get_language_support(Path(args.file)) - file_language = file_lang_support.language - matching_configs = [lc for lc in language_configs if lc.language == file_language] - if matching_configs: - language_configs = matching_configs - # If no matching config found, let all configs run (existing behavior handles it) - except UnsupportedLanguageError: - pass # Unknown extension, let all configs run -``` - -The key behavior: when `--file src/main/java/Foo.java` is passed, detect it's Java via `get_language_support(Path(args.file))`, then filter `language_configs` to only Java configs. This means the loop runs only one pass for the file's language instead of iterating over all languages. - - - cd /home/ubuntu/code/codeflash && uv run python -c "from codeflash.main import main; print('import ok')" - - - - `codeflash/main.py` has `from codeflash.languages.registry import get_language_support, UnsupportedLanguageError` as a top-level import (NOT inside a function) - - `codeflash/main.py` contains `if hasattr(args, "file") and args.file:` before the orchestration loop - - `codeflash/main.py` contains `matching_configs = [lc for lc in language_configs if lc.language == file_language]` - - The filtering block is placed AFTER the `if not language_configs:` fallback block and BEFORE `handle_optimize_all_arg_parsing(args)` - - --file flag detects file language via extension and filters language_configs to only the matching language before the orchestration loop runs - - - - Task 2: Add tests for --file, --all, and no-flags CLI paths - tests/test_multi_language_orchestration.py - - - tests/test_multi_language_orchestration.py (full file) - - codeflash/main.py (updated from Task 1) - - - - test_file_flag_filters_to_matching_language: When args.file="Foo.java", only Java config's optimizer.run_with_args is called (call_count == 1), not Python - - test_file_flag_python_file_filters_to_python: When args.file="module.py", only Python config's optimizer.run_with_args is called (call_count == 1) - - test_file_flag_unknown_extension_runs_all: When args.file="Foo.rs" (unsupported), all language configs run (call_count == 2 for py+java) - - test_file_flag_no_matching_config_runs_all: When args.file="Foo.java" but only Python config exists, all configs run (call_count == 1 for py only) - - test_all_flag_sets_module_root_per_language: When args.all is set, each pass gets pass_args.all == pass_args.module_root (verify via mock_run.call_args_list) - - test_no_flags_runs_all_language_passes: When args.file is None AND args.all is None (no CLI flags specified), all language configs run (call_count == 2 for py+java). Note: git diff integration for changed-file detection is tested separately in Phase 7 (tests/test_git_utils.py) and is out of scope for this unit test. This test verifies only that the orchestration loop iterates all configs when no filtering flags are present. - - -Add a new test class `TestCLIPathRouting` to `tests/test_multi_language_orchestration.py`. Use the same patch decorator pattern as the existing `TestMultiLanguageOrchestration` class. - -For each test: -1. Create Python and/or Java LanguageConfigs using the existing `make_lang_config` helper -2. Mock `find_all_config_files` to return the configs -3. Mock `parse_args` to return `make_base_args(...)` with the relevant flag set -4. Call `main()` and verify `mock_run.call_count` and the args passed to each call - -For the `--file` tests, set `file="path/to/Foo.java"` in `make_base_args(file="path/to/Foo.java")`. - -For the `--all` test, set `all=""` in `make_base_args(all="")` (empty string means "use module_root" per handle_optimize_all_arg_parsing). Verify that each call to `mock_run` received `pass_args.all == pass_args.module_root` by inspecting `mock_run.call_args_list[i][0][0].all`. - -For the no-flags test, use `make_base_args()` with defaults so that `args.file` is None and `args.all` is None. Verify `mock_run.call_count == 2`. This confirms the orchestration loop runs all language passes when no filtering flags are present. (Git diff integration for the no-flags path is already covered by Phase 7's tests/test_git_utils.py and is not in scope here.) - -PROC-04 note: The --all routing ALREADY works in main.py (lines 82-114). The `find_all_config_files()` call runs unconditionally, the for-loop iterates all configs, and lines 113-114 set `pass_args.all = pass_args.module_root` per language. The `test_all_flag_sets_module_root_per_language` test proves this existing behavior — no code changes are needed for --all, only this test. - -Important: The `--file` tests need `get_language_support` to work correctly. Since it uses the real registry, ensure the test file's extension is one that the registry knows (`.java`, `.py`). The registry is auto-populated via `_ensure_languages_registered()` which imports the support modules. - - - cd /home/ubuntu/code/codeflash && uv run pytest tests/test_multi_language_orchestration.py -x -v 2>&1 | tail -40 - - - - `tests/test_multi_language_orchestration.py` contains class `TestCLIPathRouting` - - Test `test_file_flag_filters_to_matching_language` passes and verifies call_count == 1 - - Test `test_all_flag_sets_module_root_per_language` passes and verifies pass_args.all == pass_args.module_root for each call - - Test `test_no_flags_runs_all_language_passes` passes and verifies call_count == 2 - - All existing 26 tests still pass - - `uv run prek` passes - - At least 6 new tests covering --file filtering, --all per-language module_root, and no-flags path all pass alongside existing tests - - - - - -- `uv run pytest tests/test_multi_language_orchestration.py -x -v` -- all tests pass (existing 26 + new 6+) -- `uv run prek` -- linting and type checking pass -- `grep -n "get_language_support" codeflash/main.py` -- import and usage present -- `grep -n "TestCLIPathRouting" tests/test_multi_language_orchestration.py` -- new test class exists - - - -- --file flag routes to single-language pass when file extension matches a configured language -- --all flag correctly sets pass_args.all = pass_args.module_root per language (existing behavior verified by tests) -- No-flags path runs all language passes (existing behavior verified by tests) -- All tests pass, no regressions - - - -After completion, create `.planning/phases/08-sequential-multi-language-orchestration/08-03-SUMMARY.md` - From cdaa52608c3861e648130f76f12948d9f724f208 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Wed, 25 Mar 2026 21:17:16 +0000 Subject: [PATCH 41/41] fix: align multi-language discovery with zero-config Java and add monorepo subdirectory scanning Adapt find_all_config_files() after rebasing on java-config-redesign (PR #1880): - Java detected via pom.xml/build.gradle instead of codeflash.toml - Add subdirectory scan for monorepo language subprojects (java/, js/ etc.) - Extract _check_dir_for_configs() to eliminate duplicated detection logic - Fix --all flag in multi-language mode (module_root wasn't available during resolution) - Add Java project_root directory override in apply_language_config() - Update all tests to use build-tool detection mocks and directory-based Java paths - Add 5 new monorepo discovery tests (subdir Java, subdir JS, all-three, skip-hidden, root-wins) Co-Authored-By: Claude Opus 4.6 --- codeflash/cli_cmds/cli.py | 5 + codeflash/code_utils/config_parser.py | 101 +++++++++++----- codeflash/main.py | 7 +- tests/test_multi_config_discovery.py | 130 ++++++++++++++++++--- tests/test_multi_language_orchestration.py | 37 +++--- 5 files changed, 215 insertions(+), 65 deletions(-) diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index a1d9aa78d..7230eb3bc 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -328,6 +328,11 @@ def apply_language_config(args: Namespace, lang_config: LanguageConfig) -> Names args.benchmarks_root = Path(args.benchmarks_root).resolve() args.test_project_root = project_root_from_module_root(args.tests_root, config_path) + if is_java and config_path.is_dir(): + # For Java projects, config_path IS the project root directory (from build-tool detection). + args.project_root = config_path.resolve() + args.test_project_root = config_path.resolve() + return args diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index ad832f46c..6d165e9a4 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -157,6 +157,66 @@ def normalize_toml_config(config: dict[str, Any], config_file_path: Path) -> dic return config +def _parse_java_config_for_dir(dir_path: Path) -> dict[str, Any] | None: + from codeflash.languages.java.build_tools import parse_java_project_config + + return parse_java_project_config(dir_path) + + +_SUBDIR_SKIP = frozenset({ + ".git", ".hg", ".svn", "node_modules", ".venv", "venv", "__pycache__", + "target", "build", "dist", ".tox", ".mypy_cache", ".ruff_cache", ".pytest_cache", +}) + + +def _check_dir_for_configs( + dir_path: Path, + configs: list[LanguageConfig], + seen_languages: set[Language], +) -> None: + """Check a single directory for language config files and append any found to *configs*.""" + if Language.PYTHON not in seen_languages: + pyproject = dir_path / "pyproject.toml" + if pyproject.exists(): + try: + with pyproject.open("rb") as f: + data = tomlkit.parse(f.read()) + tool = data.get("tool", {}) + if isinstance(tool, dict) and "codeflash" in tool: + raw_config = dict(tool["codeflash"]) + normalized = normalize_toml_config(raw_config, pyproject) + seen_languages.add(Language.PYTHON) + configs.append(LanguageConfig(config=normalized, config_path=pyproject, language=Language.PYTHON)) + except Exception: + pass + + if Language.JAVASCRIPT not in seen_languages: + package_json = dir_path / "package.json" + if package_json.exists(): + try: + result = parse_package_json_config(package_json) + if result is not None: + config, path = result + seen_languages.add(Language.JAVASCRIPT) + configs.append(LanguageConfig(config=config, config_path=path, language=Language.JAVASCRIPT)) + except Exception: + pass + + if Language.JAVA not in seen_languages: + if ( + (dir_path / "pom.xml").exists() + or (dir_path / "build.gradle").exists() + or (dir_path / "build.gradle.kts").exists() + ): + try: + java_config = _parse_java_config_for_dir(dir_path) + if java_config is not None: + seen_languages.add(Language.JAVA) + configs.append(LanguageConfig(config=java_config, config_path=dir_path, language=Language.JAVA)) + except Exception: + pass + + def find_all_config_files(start_dir: Path | None = None) -> list[LanguageConfig]: if start_dir is None: start_dir = Path.cwd() @@ -164,44 +224,25 @@ def find_all_config_files(start_dir: Path | None = None) -> list[LanguageConfig] configs: list[LanguageConfig] = [] seen_languages: set[Language] = set() - toml_configs = {"pyproject.toml": Language.PYTHON, "codeflash.toml": Language.JAVA} - + # Walk upward from start_dir to filesystem root (closest config wins per language) dir_path = start_dir.resolve() while True: - for config_name, language in toml_configs.items(): - if language in seen_languages: - continue - config_file = dir_path / config_name - if config_file.exists(): - try: - with config_file.open("rb") as f: - data = tomlkit.parse(f.read()) - tool = data.get("tool", {}) - if isinstance(tool, dict) and "codeflash" in tool: - raw_config = dict(tool["codeflash"]) - normalized = normalize_toml_config(raw_config, config_file) - seen_languages.add(language) - configs.append(LanguageConfig(config=normalized, config_path=config_file, language=language)) - except Exception: - continue - - if Language.JAVASCRIPT not in seen_languages: - package_json = dir_path / "package.json" - if package_json.exists(): - try: - result = parse_package_json_config(package_json) - if result is not None: - config, path = result - seen_languages.add(Language.JAVASCRIPT) - configs.append(LanguageConfig(config=config, config_path=path, language=Language.JAVASCRIPT)) - except Exception: - pass + _check_dir_for_configs(dir_path, configs, seen_languages) parent = dir_path.parent if parent == dir_path: break dir_path = parent + # Scan immediate subdirectories for monorepo language subprojects + resolved_start = start_dir.resolve() + try: + subdirs = sorted(p for p in resolved_start.iterdir() if p.is_dir() and p.name not in _SUBDIR_SKIP) + except OSError: + subdirs = [] + for subdir in subdirs: + _check_dir_for_configs(subdir, configs, seen_languages) + return configs diff --git a/codeflash/main.py b/codeflash/main.py index 26a565548..0beda6d61 100644 --- a/codeflash/main.py +++ b/codeflash/main.py @@ -131,6 +131,11 @@ def main() -> None: except UnsupportedLanguageError: pass # Unknown extension, let all configs run + # Track whether --all was originally requested (before handle_optimize_all_arg_parsing + # resolves it — in multi-language mode, module_root isn't available yet so the resolution + # produces None; we re-resolve per language inside the loop) + optimize_all_requested = hasattr(args, "all") and args.all is not None + # Multi-language path: run git/GitHub checks ONCE before the loop args = handle_optimize_all_arg_parsing(args) @@ -141,7 +146,7 @@ def main() -> None: pass_args = copy.deepcopy(args) pass_args = apply_language_config(pass_args, lang_config) - if hasattr(pass_args, "all") and pass_args.all is not None: + if optimize_all_requested: pass_args.all = pass_args.module_root if not env_utils.check_formatter_installed(pass_args.formatter_cmds): diff --git a/tests/test_multi_config_discovery.py b/tests/test_multi_config_discovery.py index 162f93693..90cc7eca3 100644 --- a/tests/test_multi_config_discovery.py +++ b/tests/test_multi_config_discovery.py @@ -2,10 +2,11 @@ import json from pathlib import Path +from unittest.mock import patch import tomlkit -from codeflash.code_utils.config_parser import LanguageConfig, find_all_config_files +from codeflash.code_utils.config_parser import find_all_config_files from codeflash.languages.language_enum import Language @@ -22,19 +23,29 @@ def test_finds_pyproject_toml_with_codeflash_section(self, tmp_path: Path, monke assert result[0].language == Language.PYTHON assert result[0].config_path == tmp_path / "pyproject.toml" - def test_finds_codeflash_toml(self, tmp_path: Path, monkeypatch) -> None: - write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}}) + def test_finds_java_via_build_tool_detection(self, tmp_path: Path, monkeypatch) -> None: + java_config = {"language": "java", "module_root": str(tmp_path / "src/main/java")} + (tmp_path / "pom.xml").write_text("", encoding="utf-8") monkeypatch.chdir(tmp_path) - result = find_all_config_files() + with patch( + "codeflash.code_utils.config_parser._parse_java_config_for_dir", + return_value=java_config, + ): + result = find_all_config_files() assert len(result) == 1 assert result[0].language == Language.JAVA - assert result[0].config_path == tmp_path / "codeflash.toml" + assert result[0].config_path == tmp_path - def test_finds_multiple_configs(self, tmp_path: Path, monkeypatch) -> None: + def test_finds_multiple_configs_python_and_java(self, tmp_path: Path, monkeypatch) -> None: write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) - write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}}) + java_config = {"language": "java", "module_root": str(tmp_path / "src/main/java")} + (tmp_path / "pom.xml").write_text("", encoding="utf-8") monkeypatch.chdir(tmp_path) - result = find_all_config_files() + with patch( + "codeflash.code_utils.config_parser._parse_java_config_for_dir", + return_value=java_config, + ): + result = find_all_config_files() assert len(result) == 2 languages = {r.language for r in result} assert languages == {Language.PYTHON, Language.JAVA} @@ -49,9 +60,14 @@ def test_finds_config_in_parent_directory(self, tmp_path: Path, monkeypatch) -> write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) subdir = tmp_path / "subproject" subdir.mkdir() - write_toml(subdir / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}}) + java_config = {"language": "java", "module_root": str(subdir / "src/main/java")} + (subdir / "pom.xml").write_text("", encoding="utf-8") monkeypatch.chdir(subdir) - result = find_all_config_files() + with patch( + "codeflash.code_utils.config_parser._parse_java_config_for_dir", + return_value=java_config, + ): + result = find_all_config_files() assert len(result) == 2 languages = {r.language for r in result} assert languages == {Language.PYTHON, Language.JAVA} @@ -78,27 +94,111 @@ def test_finds_package_json_with_codeflash_section(self, tmp_path: Path, monkeyp def test_finds_all_three_config_types(self, tmp_path: Path, monkeypatch) -> None: write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) - write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}}) pkg = {"codeflash": {"moduleRoot": "src"}} (tmp_path / "package.json").write_text(json.dumps(pkg), encoding="utf-8") + java_config = {"language": "java", "module_root": str(tmp_path / "src/main/java")} + (tmp_path / "pom.xml").write_text("", encoding="utf-8") monkeypatch.chdir(tmp_path) - result = find_all_config_files() + with patch( + "codeflash.code_utils.config_parser._parse_java_config_for_dir", + return_value=java_config, + ): + result = find_all_config_files() assert len(result) == 3 languages = {r.language for r in result} assert languages == {Language.PYTHON, Language.JAVA, Language.JAVASCRIPT} - def test_malformed_toml_skipped(self, tmp_path: Path, monkeypatch) -> None: - (tmp_path / "codeflash.toml").write_text("not valid [toml", encoding="utf-8") + def test_no_java_when_no_build_file_exists(self, tmp_path: Path, monkeypatch) -> None: monkeypatch.chdir(tmp_path) result = find_all_config_files() assert len(result) == 0 def test_missing_codeflash_section_skipped(self, tmp_path: Path, monkeypatch) -> None: - write_toml(tmp_path / "codeflash.toml", {"tool": {"other": {"key": "value"}}}) + write_toml(tmp_path / "pyproject.toml", {"tool": {"other": {"key": "value"}}}) + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 0 + + def test_finds_java_in_subdirectory(self, tmp_path: Path, monkeypatch) -> None: + """Monorepo: Java project in a subdirectory is discovered from the repo root.""" + write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) + java_dir = tmp_path / "java" + java_dir.mkdir() + (java_dir / "pom.xml").write_text("", encoding="utf-8") + java_config = {"language": "java", "module_root": str(java_dir / "src/main/java")} + monkeypatch.chdir(tmp_path) + with patch( + "codeflash.code_utils.config_parser._parse_java_config_for_dir", + return_value=java_config, + ): + result = find_all_config_files() + assert len(result) == 2 + languages = {r.language for r in result} + assert languages == {Language.PYTHON, Language.JAVA} + java_result = next(r for r in result if r.language == Language.JAVA) + assert java_result.config_path == java_dir + + def test_finds_js_in_subdirectory(self, tmp_path: Path, monkeypatch) -> None: + """Monorepo: JS project in a subdirectory is discovered from the repo root.""" + write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) + js_dir = tmp_path / "js" + js_dir.mkdir() + pkg = {"codeflash": {"moduleRoot": "src"}} + (js_dir / "package.json").write_text(json.dumps(pkg), encoding="utf-8") + monkeypatch.chdir(tmp_path) + result = find_all_config_files() + assert len(result) == 2 + languages = {r.language for r in result} + assert languages == {Language.PYTHON, Language.JAVASCRIPT} + + def test_finds_all_three_in_monorepo_subdirs(self, tmp_path: Path, monkeypatch) -> None: + """Monorepo: Python at root, Java and JS in subdirectories.""" + write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}}) + java_dir = tmp_path / "java" + java_dir.mkdir() + (java_dir / "pom.xml").write_text("", encoding="utf-8") + java_config = {"language": "java", "module_root": str(java_dir / "src/main/java")} + js_dir = tmp_path / "js" + js_dir.mkdir() + pkg = {"codeflash": {"moduleRoot": "src"}} + (js_dir / "package.json").write_text(json.dumps(pkg), encoding="utf-8") + monkeypatch.chdir(tmp_path) + with patch( + "codeflash.code_utils.config_parser._parse_java_config_for_dir", + return_value=java_config, + ): + result = find_all_config_files() + assert len(result) == 3 + languages = {r.language for r in result} + assert languages == {Language.PYTHON, Language.JAVA, Language.JAVASCRIPT} + + def test_skips_hidden_and_build_subdirs(self, tmp_path: Path, monkeypatch) -> None: + """Subdirectory scan skips .git, node_modules, target, etc.""" + for name in [".git", "node_modules", "target", "build", "__pycache__"]: + d = tmp_path / name + d.mkdir() + write_toml(d / "pyproject.toml", {"tool": {"codeflash": {"module-root": "."}}}) monkeypatch.chdir(tmp_path) result = find_all_config_files() assert len(result) == 0 + def test_root_config_wins_over_subdir(self, tmp_path: Path, monkeypatch) -> None: + """Config at CWD (found during upward walk) takes precedence over subdirectory.""" + (tmp_path / "pom.xml").write_text("", encoding="utf-8") + java_dir = tmp_path / "java" + java_dir.mkdir() + (java_dir / "pom.xml").write_text("", encoding="utf-8") + java_config = {"language": "java", "module_root": str(tmp_path / "src/main/java")} + monkeypatch.chdir(tmp_path) + with patch( + "codeflash.code_utils.config_parser._parse_java_config_for_dir", + return_value=java_config, + ): + result = find_all_config_files() + java_results = [r for r in result if r.language == Language.JAVA] + assert len(java_results) == 1 + assert java_results[0].config_path == tmp_path + def test_find_all_functions_uses_registry_not_singleton() -> None: """DISC-04: Verify find_all_functions_in_file uses per-file registry lookup.""" diff --git a/tests/test_multi_language_orchestration.py b/tests/test_multi_language_orchestration.py index 18200e331..41e4ed9d7 100644 --- a/tests/test_multi_language_orchestration.py +++ b/tests/test_multi_language_orchestration.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import logging from argparse import Namespace from pathlib import Path @@ -49,7 +48,7 @@ def test_sets_module_root(self, tmp_path: Path) -> None: src = tmp_path / "src" / "main" / "java" src.mkdir(parents=True) config = {"module_root": str(src)} - lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + lang_config = LanguageConfig(config=config, config_path=tmp_path, language=Language.JAVA) args = make_base_args() from codeflash.cli_cmds.cli import apply_language_config @@ -63,7 +62,7 @@ def test_sets_tests_root(self, tmp_path: Path) -> None: tests = tmp_path / "src" / "test" / "java" tests.mkdir(parents=True) config = {"module_root": str(src), "tests_root": str(tests)} - lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + lang_config = LanguageConfig(config=config, config_path=tmp_path, language=Language.JAVA) args = make_base_args() from codeflash.cli_cmds.cli import apply_language_config @@ -77,7 +76,7 @@ def test_resolves_paths_relative_to_config_parent(self, tmp_path: Path) -> None: tests = tmp_path / "src" / "test" / "java" tests.mkdir(parents=True) config = {"module_root": str(src), "tests_root": str(tests)} - lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + lang_config = LanguageConfig(config=config, config_path=tmp_path, language=Language.JAVA) args = make_base_args() from codeflash.cli_cmds.cli import apply_language_config @@ -93,7 +92,7 @@ def test_sets_project_root(self, tmp_path: Path) -> None: tests.mkdir(parents=True) (tmp_path / "pom.xml").touch() config = {"module_root": str(src), "tests_root": str(tests)} - lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + lang_config = LanguageConfig(config=config, config_path=tmp_path, language=Language.JAVA) args = make_base_args() from codeflash.cli_cmds.cli import apply_language_config @@ -109,7 +108,7 @@ def test_preserves_cli_overrides(self, tmp_path: Path) -> None: tests = tmp_path / "src" / "test" / "java" tests.mkdir(parents=True) config = {"module_root": str(src), "tests_root": str(tests)} - lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + lang_config = LanguageConfig(config=config, config_path=tmp_path, language=Language.JAVA) args = make_base_args(module_root=str(override_module)) from codeflash.cli_cmds.cli import apply_language_config @@ -137,7 +136,7 @@ def test_sets_language_singleton(self, tmp_path: Path) -> None: tests = tmp_path / "src" / "test" / "java" tests.mkdir(parents=True) config = {"module_root": str(src), "tests_root": str(tests)} - lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + lang_config = LanguageConfig(config=config, config_path=tmp_path, language=Language.JAVA) args = make_base_args() with patch("codeflash.cli_cmds.cli.set_current_language") as mock_set: @@ -168,7 +167,7 @@ def test_java_default_tests_root(self, tmp_path: Path, monkeypatch) -> None: default_tests.mkdir(parents=True) monkeypatch.chdir(tmp_path) config = {"module_root": str(src)} - lang_config = LanguageConfig(config=config, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + lang_config = LanguageConfig(config=config, config_path=tmp_path, language=Language.JAVA) args = make_base_args() from codeflash.cli_cmds.cli import apply_language_config @@ -204,7 +203,7 @@ def make_lang_config(tmp_path: Path, language: Language, subdir: str = "") -> La tests = tmp_path / subdir / "src" / "test" / "java" if subdir else tmp_path / "src" / "test" / "java" src.mkdir(parents=True, exist_ok=True) tests.mkdir(parents=True, exist_ok=True) - config_path = tmp_path / subdir / "codeflash.toml" if subdir else tmp_path / "codeflash.toml" + config_path = tmp_path / subdir if subdir else tmp_path return LanguageConfig( config={"module_root": str(src), "tests_root": str(tests)}, config_path=config_path, @@ -741,7 +740,7 @@ def test_detect_project_for_language_unsupported(self) -> None: def test_empty_config_no_module_root(self, tmp_path: Path) -> None: config: dict = {} - result = normalize_toml_config(config, tmp_path / "codeflash.toml") + result = normalize_toml_config(config, tmp_path / "pyproject.toml") assert result["formatter_cmds"] == [] assert result["disable_telemetry"] is False assert "module_root" not in result @@ -752,7 +751,7 @@ def test_converts_hyphenated_keys_to_underscored(self, tmp_path: Path) -> None: config = {"module-root": "src", "tests-root": "tests"} (tmp_path / "src").mkdir() (tmp_path / "tests").mkdir() - result = normalize_toml_config(config, tmp_path / "codeflash.toml") + result = normalize_toml_config(config, tmp_path / "pyproject.toml") assert "module_root" in result assert "tests_root" in result assert "module-root" not in result @@ -762,12 +761,12 @@ def test_resolves_paths_relative_to_config_parent(self, tmp_path: Path) -> None: src = tmp_path / "src" src.mkdir() config = {"module-root": "src"} - result = normalize_toml_config(config, tmp_path / "codeflash.toml") + result = normalize_toml_config(config, tmp_path / "pyproject.toml") assert result["module_root"] == str(src.resolve()) def test_applies_default_values(self, tmp_path: Path) -> None: config: dict = {} - result = normalize_toml_config(config, tmp_path / "codeflash.toml") + result = normalize_toml_config(config, tmp_path / "pyproject.toml") assert result["formatter_cmds"] == [] assert result["disable_telemetry"] is False assert result["override_fixtures"] is False @@ -776,13 +775,13 @@ def test_applies_default_values(self, tmp_path: Path) -> None: def test_preserves_explicit_values(self, tmp_path: Path) -> None: config = {"disable-telemetry": True, "formatter-cmds": ["prettier $file"]} - result = normalize_toml_config(config, tmp_path / "codeflash.toml") + result = normalize_toml_config(config, tmp_path / "pyproject.toml") assert result["disable_telemetry"] is True assert result["formatter_cmds"] == ["prettier $file"] def test_resolves_ignore_paths(self, tmp_path: Path) -> None: config = {"ignore-paths": ["build", "dist"]} - result = normalize_toml_config(config, tmp_path / "codeflash.toml") + result = normalize_toml_config(config, tmp_path / "pyproject.toml") assert result["ignore_paths"] == [ str((tmp_path / "build").resolve()), str((tmp_path / "dist").resolve()), @@ -790,7 +789,7 @@ def test_resolves_ignore_paths(self, tmp_path: Path) -> None: def test_empty_ignore_paths_default(self, tmp_path: Path) -> None: config: dict = {} - result = normalize_toml_config(config, tmp_path / "codeflash.toml") + result = normalize_toml_config(config, tmp_path / "pyproject.toml") assert result["ignore_paths"] == [] @@ -809,7 +808,7 @@ def test_no_unconfigured_when_all_configured(self) -> None: configs = [ LanguageConfig(config={}, config_path=Path("pyproject.toml"), language=Language.PYTHON), - LanguageConfig(config={}, config_path=Path("codeflash.toml"), language=Language.JAVA), + LanguageConfig(config={}, config_path=Path(), language=Language.JAVA), ] changed = [Path("Foo.java"), Path("bar.py")] result = detect_unconfigured_languages(configs, changed) @@ -826,12 +825,12 @@ def test_ignores_unsupported_extensions(self) -> None: def test_auto_config_adds_language_config_on_success(self, mock_find_configs, tmp_path: Path) -> None: from codeflash.main import auto_configure_language - new_lc = LanguageConfig(config={}, config_path=tmp_path / "codeflash.toml", language=Language.JAVA) + new_lc = LanguageConfig(config={}, config_path=tmp_path, language=Language.JAVA) mock_find_configs.return_value = [new_lc] logger = logging.getLogger("codeflash.test") with ( - patch("codeflash.main.write_config", return_value=(True, "Created codeflash.toml")) as mock_write, + patch("codeflash.main.write_config", return_value=(True, "Created config")) as mock_write, patch("codeflash.main.detect_project_for_language") as mock_detect, ): mock_detect.return_value = MagicMock()