diff --git a/code_to_optimize/java-gradle/codeflash.toml b/code_to_optimize/java-gradle/codeflash.toml new file mode 100644 index 000000000..bf6e45279 --- /dev/null +++ b/code_to_optimize/java-gradle/codeflash.toml @@ -0,0 +1,4 @@ +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +formatter-cmds = [] diff --git a/code_to_optimize/java/codeflash.toml b/code_to_optimize/java/codeflash.toml new file mode 100644 index 000000000..4016df28a --- /dev/null +++ b/code_to_optimize/java/codeflash.toml @@ -0,0 +1,6 @@ +# Codeflash configuration for Java project + +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +formatter-cmds = [] diff --git a/codeflash/cli_cmds/cli.py b/codeflash/cli_cmds/cli.py index c611f5cd9..6e5bd44e3 100644 --- a/codeflash/cli_cmds/cli.py +++ b/codeflash/cli_cmds/cli.py @@ -190,12 +190,6 @@ def process_pyproject_config(args: Namespace) -> Namespace: if args.benchmarks_root: args.benchmarks_root = Path(args.benchmarks_root).resolve() args.test_project_root = project_root_from_module_root(args.tests_root, pyproject_file_path) - - if is_java_project and pyproject_file_path.is_dir(): - # For Java projects, pyproject_file_path IS the project root directory (not a file). - # Override project_root which may have resolved to a sub-module. - args.project_root = pyproject_file_path.resolve() - args.test_project_root = pyproject_file_path.resolve() if is_LSP_enabled(): args.all = None return args @@ -214,6 +208,8 @@ def project_root_from_module_root(module_root: Path, pyproject_file_path: Path) return current.resolve() if (current / "build.gradle").exists() or (current / "build.gradle.kts").exists(): return current.resolve() + if (current / "codeflash.toml").exists(): + return current.resolve() current = current.parent return module_root.parent.resolve() diff --git a/codeflash/code_utils/config_parser.py b/codeflash/code_utils/config_parser.py index 832b34bcc..ef21ce051 100644 --- a/codeflash/code_utils/config_parser.py +++ b/codeflash/code_utils/config_parser.py @@ -12,29 +12,8 @@ PYPROJECT_TOML_CACHE: dict[Path, Path] = {} ALL_CONFIG_FILES: dict[Path, dict[str, Path]] = {} -def _try_parse_java_build_config() -> tuple[dict[str, Any], Path] | None: - """Detect Java project from build files and parse config from pom.xml/gradle.properties. - - Returns (config_dict, project_root) if a Java project is found, None otherwise. - """ - dir_path = Path.cwd() - while dir_path != dir_path.parent: - if ( - (dir_path / "pom.xml").exists() - or (dir_path / "build.gradle").exists() - or (dir_path / "build.gradle.kts").exists() - ): - from codeflash.languages.java.build_tools import parse_java_project_config - - config = parse_java_project_config(dir_path) - if config is not None: - return config, dir_path - dir_path = dir_path.parent - return None - - def find_pyproject_toml(config_file: Path | None = None) -> Path: - # Find the pyproject.toml file on the root of the project + # Find the pyproject.toml or codeflash.toml file on the root of the project if config_file is not None: config_file = Path(config_file) @@ -50,13 +29,21 @@ def find_pyproject_toml(config_file: Path | None = None) -> Path: # see if it was encountered before in search if cur_path in PYPROJECT_TOML_CACHE: return PYPROJECT_TOML_CACHE[cur_path] + # map current path to closest file - check both pyproject.toml and codeflash.toml while dir_path != dir_path.parent: + # First check pyproject.toml (Python projects) config_file = dir_path / "pyproject.toml" if config_file.exists(): PYPROJECT_TOML_CACHE[cur_path] = config_file return config_file + # Then check codeflash.toml (Java/other projects) + config_file = dir_path / "codeflash.toml" + if config_file.exists(): + PYPROJECT_TOML_CACHE[cur_path] = config_file + return config_file + # Search in parent directories dir_path = dir_path.parent - msg = f"Could not find pyproject.toml in the current directory {Path.cwd()} or any of the parent directories. Please create it by running `codeflash init`, or pass the path to the config file with the --config-file argument." + msg = f"Could not find pyproject.toml or codeflash.toml in the current directory {Path.cwd()} or any of the parent directories. Please create it by running `codeflash init`, or pass the path to the config file with the --config-file argument." raise ValueError(msg) from None @@ -103,34 +90,33 @@ def find_conftest_files(test_paths: list[Path]) -> list[Path]: return list(list_of_conftest_files) +# TODO for claude: There should be different functions to parse it per language, which should be chosen during runtime def parse_config_file( config_file_path: Path | None = None, override_formatter_check: bool = False ) -> tuple[dict[str, Any], Path]: - # Detect all config sources — Java, package.json, pyproject.toml - java_result = _try_parse_java_build_config() if config_file_path is None else None package_json_path = find_package_json(config_file_path) pyproject_toml_path = find_closest_config_file("pyproject.toml") if config_file_path is None else None + codeflash_toml_path = find_closest_config_file("codeflash.toml") if config_file_path is None else None - # Use Java config only if no closer JS/Python config exists (monorepo support). - # In a monorepo with a parent pom.xml and a child package.json, the closer config wins. - if java_result is not None: - java_depth = len(java_result[1].parts) - has_closer = (package_json_path is not None and len(package_json_path.parent.parts) >= java_depth) or ( - pyproject_toml_path is not None and len(pyproject_toml_path.parent.parts) >= java_depth - ) - if not has_closer: - return java_result + # Pick the closest toml config (pyproject.toml or codeflash.toml). + # Java projects use codeflash.toml; Python projects use pyproject.toml. + closest_toml_path = None + if pyproject_toml_path and codeflash_toml_path: + closest_toml_path = max(pyproject_toml_path, codeflash_toml_path, key=lambda p: len(p.parent.parts)) + else: + closest_toml_path = pyproject_toml_path or codeflash_toml_path # When both config files exist, prefer the one closer to CWD. # This prevents a parent-directory package.json (e.g., monorepo root) - # from overriding a closer pyproject.toml. + # from overriding a closer pyproject.toml or codeflash.toml. use_package_json = False if package_json_path: - if pyproject_toml_path is None: + if closest_toml_path is None: use_package_json = True else: + # Compare depth: more path parts = closer to CWD = more specific package_json_depth = len(package_json_path.parent.parts) - toml_depth = len(pyproject_toml_path.parent.parts) + toml_depth = len(closest_toml_path.parent.parts) use_package_json = package_json_depth >= toml_depth if use_package_json: @@ -174,7 +160,7 @@ def parse_config_file( if config == {} and lsp_mode: return {}, config_file_path - # Preserve language field if present (important for JS/TS projects) + # Preserve language field if present (important for Java/JS projects using codeflash.toml) # default values: path_keys = ["module-root", "tests-root", "benchmarks-root"] path_list_keys = ["ignore-paths"] diff --git a/codeflash/languages/java/build_tools.py b/codeflash/languages/java/build_tools.py index f8a19c693..28db2c9aa 100644 --- a/codeflash/languages/java/build_tools.py +++ b/codeflash/languages/java/build_tools.py @@ -10,8 +10,7 @@ import logging import xml.etree.ElementTree as ET from dataclasses import dataclass from enum import Enum -from pathlib import Path -from typing import Any +from pathlib import Path # noqa: TC003 — used at runtime logger = logging.getLogger(__name__) @@ -344,218 +343,6 @@ def _parse_surefire_reports(surefire_dir: Path) -> tuple[int, int, int, int]: return tests_run, failures, errors, skipped -def parse_java_project_config(project_root: Path) -> dict[str, Any] | None: - """Parse codeflash config from Maven/Gradle build files. - - Reads codeflash.* properties from pom.xml or gradle.properties, - then fills in defaults from auto-detected build tool conventions. - - Returns None if no Java build tool is detected. - """ - build_tool = detect_build_tool(project_root) - if build_tool == BuildTool.UNKNOWN: - return None - - # Read explicit codeflash properties from build files - user_config: dict[str, str] = {} - if build_tool == BuildTool.MAVEN: - user_config = _read_maven_codeflash_properties(project_root) - elif build_tool == BuildTool.GRADLE: - user_config = _read_gradle_codeflash_properties(project_root) - - # Auto-detect defaults — for multi-module Maven projects, scan module pom.xml files - source_root = find_source_root(project_root) - test_root = find_test_root(project_root) - - if build_tool == BuildTool.MAVEN: - source_from_modules, test_from_modules = _detect_roots_from_maven_modules(project_root) - # Module-level pom.xml declarations are more precise than directory-name heuristics - if source_from_modules is not None: - source_root = source_from_modules - if test_from_modules is not None: - test_root = test_from_modules - - # Build the config dict matching the format expected by the rest of codeflash - config: dict[str, Any] = { - "language": "java", - "module_root": str( - (project_root / user_config["moduleRoot"]).resolve() - if "moduleRoot" in user_config - else (source_root or project_root / "src" / "main" / "java") - ), - "tests_root": str( - (project_root / user_config["testsRoot"]).resolve() - if "testsRoot" in user_config - else (test_root or project_root / "src" / "test" / "java") - ), - "pytest_cmd": "pytest", - "git_remote": user_config.get("gitRemote", "origin"), - "disable_telemetry": user_config.get("disableTelemetry", "false").lower() == "true", - "disable_imports_sorting": False, - "override_fixtures": False, - "benchmark": False, - "formatter_cmds": [], - "ignore_paths": [], - } - - if "ignorePaths" in user_config: - config["ignore_paths"] = [ - str((project_root / p.strip()).resolve()) for p in user_config["ignorePaths"].split(",") if p.strip() - ] - - if "formatterCmds" in user_config: - config["formatter_cmds"] = [cmd.strip() for cmd in user_config["formatterCmds"].split(",") if cmd.strip()] - - return config - - -def _read_maven_codeflash_properties(project_root: Path) -> dict[str, str]: - """Read codeflash.* properties from pom.xml section.""" - pom_path = project_root / "pom.xml" - if not pom_path.exists(): - return {} - - try: - tree = _safe_parse_xml(pom_path) - root = tree.getroot() - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - - result: dict[str, str] = {} - for props in [root.find("m:properties", ns), root.find("properties")]: - if props is None: - continue - for child in props: - tag = child.tag - # Strip Maven namespace prefix - if "}" in tag: - tag = tag.split("}", 1)[1] - if tag.startswith("codeflash.") and child.text: - key = tag[len("codeflash.") :] - result[key] = child.text.strip() - return result - except Exception: - logger.debug("Failed to read codeflash properties from pom.xml", exc_info=True) - return {} - - -def _read_gradle_codeflash_properties(project_root: Path) -> dict[str, str]: - """Read codeflash.* properties from gradle.properties.""" - props_path = project_root / "gradle.properties" - if not props_path.exists(): - return {} - - result: dict[str, str] = {} - try: - with props_path.open("r", encoding="utf-8") as f: - for line in f: - stripped = line.strip() - if stripped.startswith("#") or "=" not in stripped: - continue - key, value = stripped.split("=", 1) - key = key.strip() - if key.startswith("codeflash."): - result[key[len("codeflash.") :]] = value.strip() - return result - except Exception: - logger.debug("Failed to read codeflash properties from gradle.properties", exc_info=True) - return {} - - -def _detect_roots_from_maven_modules(project_root: Path) -> tuple[Path | None, Path | None]: - """Scan Maven module pom.xml files for custom sourceDirectory/testSourceDirectory. - - For multi-module projects like aerospike (client/, test/, benchmarks/), - finds the main source module and test module by parsing each module's build config. - """ - pom_path = project_root / "pom.xml" - if not pom_path.exists(): - return None, None - - try: - tree = _safe_parse_xml(pom_path) - root = tree.getroot() - ns = {"m": "http://maven.apache.org/POM/4.0.0"} - - # Find to get module names - modules: list[str] = [] - for modules_elem in [root.find("m:modules", ns), root.find("modules")]: - if modules_elem is not None: - for mod in modules_elem: - if mod.text: - modules.append(mod.text.strip()) - - if not modules: - return None, None - - # Collect candidate source and test roots with Java file counts - source_candidates: list[tuple[Path, int]] = [] - test_root: Path | None = None - - skip_modules = {"example", "examples", "benchmark", "benchmarks", "demo", "sample", "samples"} - - for module_name in modules: - module_pom = project_root / module_name / "pom.xml" - if not module_pom.exists(): - continue - - # Modules named "test" are test modules, not source modules - is_test_module = "test" in module_name.lower() - - try: - mod_tree = _safe_parse_xml(module_pom) - mod_root = mod_tree.getroot() - - for build in [mod_root.find("m:build", ns), mod_root.find("build")]: - if build is None: - continue - - for src_elem in [build.find("m:sourceDirectory", ns), build.find("sourceDirectory")]: - if src_elem is not None and src_elem.text: - src_text = src_elem.text.replace("${project.basedir}", str(project_root / module_name)) - src_path = Path(src_text) - if not src_path.is_absolute(): - src_path = project_root / module_name / src_path - if src_path.exists(): - if is_test_module and test_root is None: - test_root = src_path - elif module_name.lower() not in skip_modules: - java_count = sum(1 for _ in src_path.rglob("*.java")) - if java_count > 0: - source_candidates.append((src_path, java_count)) - - for test_elem in [build.find("m:testSourceDirectory", ns), build.find("testSourceDirectory")]: - if test_elem is not None and test_elem.text: - test_text = test_elem.text.replace("${project.basedir}", str(project_root / module_name)) - test_path = Path(test_text) - if not test_path.is_absolute(): - test_path = project_root / module_name / test_path - if test_path.exists() and test_root is None: - test_root = test_path - - # Also check standard module layouts - if module_name.lower() not in skip_modules and not is_test_module: - std_src = project_root / module_name / "src" / "main" / "java" - if std_src.exists(): - java_count = sum(1 for _ in std_src.rglob("*.java")) - if java_count > 0: - source_candidates.append((std_src, java_count)) - - if test_root is None: - std_test = project_root / module_name / "src" / "test" / "java" - if std_test.exists() and any(std_test.rglob("*.java")): - test_root = std_test - - except Exception: - continue - - # Pick the source root with the most Java files (likely the main library) - source_root = max(source_candidates, key=lambda x: x[1])[0] if source_candidates else None - return source_root, test_root - - except Exception: - return None, None - - def find_test_root(project_root: Path) -> Path | None: """Find the test root directory for a Java project. diff --git a/codeflash/setup/config_writer.py b/codeflash/setup/config_writer.py index 43ce03eb3..0889690d5 100644 --- a/codeflash/setup/config_writer.py +++ b/codeflash/setup/config_writer.py @@ -8,7 +8,7 @@ This module writes Codeflash configuration to native config files: from __future__ import annotations import json -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import tomlkit @@ -38,7 +38,7 @@ def write_config(detected: DetectedProject, config: CodeflashConfig | None = Non if detected.language == "python": return _write_pyproject_toml(detected.project_root, config) if detected.language == "java": - return _write_java_build_config(detected.project_root, config) + return _write_codeflash_toml(detected.project_root, config) return _write_package_json(detected.project_root, config) @@ -92,10 +92,10 @@ def _write_pyproject_toml(project_root: Path, config: CodeflashConfig) -> tuple[ return False, f"Failed to write pyproject.toml: {e}" -def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: - """Write codeflash config to pom.xml properties or gradle.properties. +def _write_codeflash_toml(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: + """Write config to codeflash.toml [tool.codeflash] section for Java projects. - Only writes non-default values. Standard Maven/Gradle layouts need no config. + Creates codeflash.toml if it doesn't exist. Args: project_root: Project root directory. @@ -105,141 +105,40 @@ def _write_java_build_config(project_root: Path, config: CodeflashConfig) -> tup Tuple of (success, message). """ - config_dict = config.to_pyproject_dict() - - # Filter out default values — only write overrides - defaults = {"module-root": "src/main/java", "tests-root": "src/test/java", "language": "java"} - non_default = {k: v for k, v in config_dict.items() if k not in defaults or str(v) != defaults.get(k)} - # Remove empty lists and False booleans - non_default = {k: v for k, v in non_default.items() if v not in ([], False, "", None)} - - if not non_default: - return True, "Standard Maven/Gradle layout detected — no config needed" - - pom_path = project_root / "pom.xml" - if pom_path.exists(): - return _write_maven_properties(pom_path, non_default) - - gradle_props_path = project_root / "gradle.properties" - return _write_gradle_properties(gradle_props_path, non_default) - - -_MAVEN_KEY_MAP: dict[str, str] = { - "module-root": "moduleRoot", - "tests-root": "testsRoot", - "git-remote": "gitRemote", - "disable-telemetry": "disableTelemetry", - "ignore-paths": "ignorePaths", - "formatter-cmds": "formatterCmds", -} - - -def _write_maven_properties(pom_path: Path, config: dict[str, Any]) -> tuple[bool, str]: - """Add codeflash.* properties to pom.xml section. - - Uses text-based manipulation to preserve comments, formatting, and namespace declarations. - """ - import re + codeflash_toml_path = project_root / "codeflash.toml" try: - content = pom_path.read_text(encoding="utf-8") - - # Remove existing codeflash.* property lines (with surrounding whitespace) - content = re.sub(r"\n[ \t]*]*>[^<]*]*>", "", content) - - # Detect child indentation from existing properties or fall back to indent + 4 spaces - props_close = re.search(r"([ \t]*)", content) - if props_close: - parent_indent = props_close.group(1) - # Try to detect child indent from an existing property element - child_match = re.search( - r"\n([ \t]+)<[a-zA-Z]", - content[content.find("") : props_close.start()] if "" in content else "", - ) - child_indent = child_match.group(1) if child_match else parent_indent + " " + # Load existing or create new + if codeflash_toml_path.exists(): + with codeflash_toml_path.open("rb") as f: + doc = tomlkit.parse(f.read()) else: - parent_indent = "" - child_indent = " " + doc = tomlkit.document() - # Build new property lines with detected indentation - new_lines = [] - for key, value in config.items(): - maven_key = f"codeflash.{_MAVEN_KEY_MAP.get(key, key)}" - if isinstance(value, list): - value = ",".join(str(v) for v in value) - elif isinstance(value, bool): - value = str(value).lower() - else: - value = str(value) - new_lines.append(f"{child_indent}<{maven_key}>{value}") + # Ensure [tool] section exists + if "tool" not in doc: + doc["tool"] = tomlkit.table() - properties_block = "\n".join(new_lines) + # Create codeflash section + codeflash_table = tomlkit.table() + codeflash_table.add(tomlkit.comment("Codeflash configuration for Java - https://docs.codeflash.ai")) - # Insert before - 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() :] - ) + # Add config values + config_dict = config.to_pyproject_dict() + for key, value in config_dict.items(): + codeflash_table[key] = value - pom_path.write_text(content, encoding="utf-8") - return True, f"Config saved to {pom_path} " + # 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)) + + return True, f"Config saved to {codeflash_toml_path}" except Exception as e: - return False, f"Failed to write Maven properties: {e}" - - -def _write_gradle_properties(props_path: Path, config: dict[str, Any]) -> tuple[bool, str]: - """Add codeflash.* entries to gradle.properties.""" - try: - lines = [] - if props_path.exists(): - lines = props_path.read_text(encoding="utf-8").splitlines() - - # Remove existing codeflash.* lines - lines = [line for line in lines if not line.strip().startswith("codeflash.")] - - # Add new config - if lines and lines[-1].strip(): - lines.append("") - lines.append("# Codeflash configuration — https://docs.codeflash.ai") - for key, value in config.items(): - gradle_key = f"codeflash.{_MAVEN_KEY_MAP.get(key, key)}" - if isinstance(value, list): - value = ",".join(str(v) for v in value) - elif isinstance(value, bool): - value = str(value).lower() - else: - value = str(value) - lines.append(f"{gradle_key}={value}") - - props_path.write_text("\n".join(lines) + "\n", encoding="utf-8") - return True, f"Config saved to {props_path}" - - except Exception as e: - return False, f"Failed to write gradle.properties: {e}" + return False, f"Failed to write codeflash.toml: {e}" def _write_package_json(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]: @@ -307,7 +206,7 @@ def remove_config(project_root: Path, language: str) -> tuple[bool, str]: if language == "python": return _remove_from_pyproject(project_root) if language == "java": - return _remove_java_build_config(project_root) + return _remove_from_codeflash_toml(project_root) return _remove_from_package_json(project_root) @@ -336,42 +235,29 @@ def _remove_from_pyproject(project_root: Path) -> tuple[bool, str]: return False, f"Failed to remove config: {e}" -def _remove_java_build_config(project_root: Path) -> tuple[bool, str]: - """Remove codeflash.* properties from pom.xml or gradle.properties. +def _remove_from_codeflash_toml(project_root: Path) -> tuple[bool, str]: + """Remove [tool.codeflash] section from codeflash.toml.""" + codeflash_toml_path = project_root / "codeflash.toml" - Priority matches _write_java_build_config: pom.xml first, then gradle.properties. - """ - # Try pom.xml first (matches write priority) — text-based removal preserves formatting - pom_path = project_root / "pom.xml" - if pom_path.exists(): - try: - import re + if not codeflash_toml_path.exists(): + return True, "No codeflash.toml found" - 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: + with codeflash_toml_path.open("rb") as f: + doc = tomlkit.parse(f.read()) - # Try gradle.properties - gradle_props = project_root / "gradle.properties" - if gradle_props.exists(): - try: - lines = gradle_props.read_text(encoding="utf-8").splitlines() - filtered = [ - line - for line in lines - if not line.strip().startswith("codeflash.") - and line.strip() != "# Codeflash configuration \u2014 https://docs.codeflash.ai" - ] - gradle_props.write_text("\n".join(filtered) + "\n", encoding="utf-8") - return True, "Removed codeflash properties from gradle.properties" - except Exception as e: - return False, f"Failed to remove config from gradle.properties: {e}" + if "tool" in doc and "codeflash" in doc["tool"]: + del doc["tool"]["codeflash"] - return True, "No Java build config found" + with codeflash_toml_path.open("w", encoding="utf8") as f: + f.write(tomlkit.dumps(doc)) + + return True, "Removed [tool.codeflash] section from codeflash.toml" + + return True, "No codeflash config found in codeflash.toml" + + except Exception as e: + return False, f"Failed to remove config: {e}" def _remove_from_package_json(project_root: Path) -> tuple[bool, str]: diff --git a/codeflash/setup/detector.py b/codeflash/setup/detector.py index 81e900436..defe1a22d 100644 --- a/codeflash/setup/detector.py +++ b/codeflash/setup/detector.py @@ -886,25 +886,20 @@ def has_existing_config(project_root: Path) -> tuple[bool, str | None]: Returns: Tuple of (has_config, config_file_type). - config_file_type is "pyproject.toml", "pom.xml", "build.gradle", "package.json", or None. + config_file_type is "pyproject.toml", "codeflash.toml", "package.json", or None. """ - # Check pyproject.toml (Python projects) - pyproject_path = project_root / "pyproject.toml" - if pyproject_path.exists(): - try: - with pyproject_path.open("rb") as f: - data = tomlkit.parse(f.read()) - if "tool" in data and "codeflash" in data["tool"]: - return True, "pyproject.toml" - except Exception: - pass - - # Check Java build files — for zero-config Java, any build file means "configured" - # because Java config is auto-detected from build files without explicit codeflash.* properties - for build_file in ("pom.xml", "build.gradle", "build.gradle.kts"): - if (project_root / build_file).exists(): - return True, build_file + # Check TOML config files (pyproject.toml, codeflash.toml) + for toml_filename in ("pyproject.toml", "codeflash.toml"): + toml_path = project_root / toml_filename + if toml_path.exists(): + try: + with toml_path.open("rb") as f: + data = tomlkit.parse(f.read()) + if "tool" in data and "codeflash" in data["tool"]: + return True, toml_filename + except Exception: + pass # Check package.json package_json_path = project_root / "package.json" diff --git a/codeflash/tracer.py b/codeflash/tracer.py index 5f8a1a4ab..108a3a38b 100644 --- a/codeflash/tracer.py +++ b/codeflash/tracer.py @@ -38,7 +38,7 @@ logger = logging.getLogger(__name__) def _detect_non_python_language(args: Namespace | None) -> Language | None: - """Detect if the project uses a non-Python language from --file or build files. + """Detect if the project uses a non-Python language from --file or config. Returns a Language enum value if non-Python detected, None otherwise. """ @@ -66,23 +66,15 @@ def _detect_non_python_language(args: Namespace | None) -> Language | None: except Exception: pass - # Method 2: Detect Java from build files (pom.xml / build.gradle) - try: - from codeflash.languages.java.build_tools import BuildTool, detect_build_tool - - cwd = Path.cwd() - if detect_build_tool(cwd) != BuildTool.UNKNOWN: - return Language.JAVA - except Exception: - pass - - # Method 3: Check config file for language field (JS/TS via package.json) + # Method 2: Check project config for language field try: from codeflash.code_utils.config_parser import parse_config_file config_file = getattr(args, "config_file_path", None) if args else None config, _ = parse_config_file(config_file) lang_str = config.get("language", "") + if lang_str == "java": + return Language.JAVA if lang_str in ("javascript", "typescript"): return Language(lang_str) except Exception: diff --git a/docs/configuration/java.mdx b/docs/configuration/java.mdx index 720e5e091..9d110fc55 100644 --- a/docs/configuration/java.mdx +++ b/docs/configuration/java.mdx @@ -1,112 +1,101 @@ --- title: "Java Configuration" -description: "Configure Codeflash for Java projects — zero config for standard layouts" +description: "Configure Codeflash for Java projects using codeflash.toml" icon: "java" -sidebarTitle: "Java (pom.xml / Gradle)" +sidebarTitle: "Java (codeflash.toml)" keywords: [ "configuration", + "codeflash.toml", "java", "maven", "gradle", "junit", - "pom.xml", - "gradle.properties", - "zero-config", ] --- # Java Configuration -**Standard Maven/Gradle projects need zero configuration.** Codeflash auto-detects your project structure from `pom.xml` or `build.gradle` — no config file is required. +Codeflash stores its configuration in `codeflash.toml` under the `[tool.codeflash]` section. -For projects with non-standard layouts, you can add `codeflash.*` properties to your existing `pom.xml` or `gradle.properties`. +## Full Reference + +```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. + ## Auto-Detection -Codeflash inspects your build files and auto-detects: +When you run `codeflash init`, Codeflash inspects your project and auto-detects: | Setting | Detection logic | |---------|----------------| -| **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` | +| `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 | -### Multi-module Maven projects +## Required Options -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. +- **`module-root`**: The source directory to optimize. Only code under this directory is discovered for optimization. For standard Maven/Gradle projects, this is `src/main/java`. +- **`tests-root`**: The directory where your tests are located. Codeflash discovers existing tests and places generated replay tests here. +- **`language`**: Must be set to `"java"` for Java projects. -For example, with this layout: +## Optional Options + +- **`test-framework`**: Test framework. Auto-detected from build dependencies. Supported values: `"junit5"` (default), `"junit4"`, `"testng"`. +- **`disable-telemetry`**: Disable anonymized telemetry. Defaults to `false`. +- **`git-remote`**: Git remote for pull requests. Defaults to `"origin"`. +- **`ignore-paths`**: Paths within `module-root` to skip during optimization. + +## Multi-Module Projects + +For multi-module Maven/Gradle projects, place `codeflash.toml` at the project root and set `module-root` to the module you want to optimize: ```text my-project/ -|- client/ ← main library (most .java files) -| |- src/com/example/ -| |- pom.xml ← ${project.basedir}/src -|- test/ ← test module -| |- src/com/example/ -| |- pom.xml ← ${project.basedir}/src -|- benchmarks/ ← skipped (benchmark module) -|- pom.xml ← client, test, benchmarks +|- client/ +| |- src/main/java/com/example/client/ +| |- src/test/java/com/example/client/ +|- server/ +| |- src/main/java/com/example/server/ +|- pom.xml +|- codeflash.toml ``` -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/ - +```toml +[tool.codeflash] +module-root = "client/src/main/java" +tests-root = "client/src/test/java" +language = "java" ``` -This follows the same pattern as SonarQube (`sonar.sources`), JaCoCo, and other Java tools — config lives in the build file, not a separate tool-specific file. +For non-standard layouts (like the Aerospike client where source is under `client/src/`), adjust paths accordingly: - - - -Add properties to `gradle.properties`: - -```properties -# Only set values that differ from auto-detected defaults -codeflash.moduleRoot=lib/src/main/java -codeflash.testsRoot=lib/src/test/java -codeflash.disableTelemetry=true -codeflash.gitRemote=upstream -codeflash.ignorePaths=src/main/java/generated/ +```toml +[tool.codeflash] +module-root = "client/src" +tests-root = "test/src" +language = "java" ``` - - - -## 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 +## Tracer Options When using `codeflash optimize` to trace a Java program, these CLI options are available: @@ -122,9 +111,9 @@ Example with timeout: codeflash optimize --timeout 30 java -jar target/my-app.jar --app-args ``` -## Examples +## Example -### Standard Maven project (zero config) +### Standard Maven project ```text my-app/ @@ -135,14 +124,17 @@ my-app/ | |- test/java/com/example/ | |- AppTest.java |- pom.xml +|- codeflash.toml ``` -Just run: -```bash -codeflash optimize java -jar target/my-app.jar +```toml +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" ``` -### Standard Gradle project (zero config) +### Gradle project ```text my-lib/ @@ -150,55 +142,12 @@ my-lib/ | |- main/java/com/example/ | |- test/java/com/example/ |- build.gradle +|- codeflash.toml ``` -Just run: -```bash -codeflash optimize java -cp build/classes/java/main com.example.Main +```toml +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" ``` - -### Non-standard layout (with config) - -```text -aerospike-client-java/ -|- client/ -| |- src/com/aerospike/client/ ← source here (not src/main/java) -| |- pom.xml -|- test/ -| |- src/com/aerospike/test/ ← tests here -| |- pom.xml -|- pom.xml -``` - -If auto-detection doesn't pick up the right modules, add to the root `pom.xml`: - -```xml - - client/src - test/src - -``` - - -In most cases, even non-standard multi-module layouts are auto-detected correctly from `` and `` in each module's `pom.xml`. Only add manual config if auto-detection gets it wrong. - - -## FAQ - - - - No. Codeflash auto-detects Java projects from `pom.xml` or `build.gradle`. No initialization step or config file is needed for standard layouts. - - - - Codeflash reads config from your existing build files — `pom.xml` `` for Maven, `gradle.properties` for Gradle. No separate config file is created. - - - - Add `` and `` properties to your `pom.xml` or `gradle.properties`. These override auto-detection. - - - - Codeflash scans each module's `pom.xml` for `` and ``. It picks the module with the most Java files as the source root (skipping modules named `examples`, `benchmarks`, etc.) and identifies `test` modules for the test root. - - diff --git a/docs/getting-started/java-installation.mdx b/docs/getting-started/java-installation.mdx index fb2a88ef2..a75e1f0b7 100644 --- a/docs/getting-started/java-installation.mdx +++ b/docs/getting-started/java-installation.mdx @@ -12,11 +12,10 @@ keywords: "junit", "junit5", "tracing", - "zero-config", ] --- -Codeflash supports Java projects using Maven or Gradle build systems. **No configuration file is needed** — Codeflash auto-detects your project structure from `pom.xml` or `build.gradle`. +Codeflash supports Java projects using Maven or Gradle build systems. It uses a two-stage tracing approach to capture method arguments and profiling data from running Java programs, then optimizes the hottest functions. ### Prerequisites @@ -24,7 +23,7 @@ Before installing Codeflash, ensure you have: 1. **Java 11 or above** installed 2. **Maven or Gradle** as your build tool -3. **A Java project** with source code +3. **A Java project** with source code under a standard directory layout Good to have (optional): @@ -46,15 +45,51 @@ uv pip install codeflash ``` - + Navigate to your Java project root (where `pom.xml` or `build.gradle` is) and run: +```bash +codeflash init +``` + +This will: +- Detect your build tool (Maven/Gradle) +- Find your source and test directories +- Create a `codeflash.toml` configuration file + + + + +Check that the configuration looks correct: + +```bash +cat codeflash.toml +``` + +You should see something like: + +```toml +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" +``` + + + + +Trace and optimize a running Java program: + ```bash codeflash optimize java -jar target/my-app.jar ``` -That's it — no `init` step, no config file. Codeflash detects Maven/Gradle automatically and infers source and test directories from your build files. +Or with Maven: + +```bash +codeflash optimize mvn exec:java -Dexec.mainClass="com.example.Main" +``` Codeflash will: 1. Profile your program using JFR (Java Flight Recorder) @@ -66,29 +101,6 @@ Codeflash will: - -**Zero config for standard projects.** If your project uses the standard Maven/Gradle layout (`src/main/java`, `src/test/java`), everything is auto-detected. For non-standard layouts, see the [configuration guide](/configuration/java). - - -## Usage examples - -**Trace and optimize a JAR application:** -```bash -codeflash optimize java -jar target/my-app.jar --app-args -``` - -**Optimize a specific file and function:** -```bash -codeflash --file src/main/java/com/example/Utils.java --function computeHash -``` - -**Trace a long-running program with a timeout:** -```bash -codeflash optimize --timeout 30 java -jar target/my-server.jar -``` - -Each tracing stage runs for at most 30 seconds, then the captured data is processed. - ## How it works Codeflash uses a **two-stage tracing** approach for Java: diff --git a/tests/code_utils/test_config_parser.py b/tests/code_utils/test_config_parser.py deleted file mode 100644 index dc47a4f1d..000000000 --- a/tests/code_utils/test_config_parser.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Tests for config_parser.py — monorepo language detection priority.""" - -from __future__ import annotations - -import json -import os -from pathlib import Path -from unittest.mock import patch - -import pytest - -from codeflash.code_utils.config_parser import parse_config_file - - -class TestMonorepoConfigPriority: - """Verify that closer config files win over parent Java build files in monorepos.""" - - def test_closer_package_json_wins_over_parent_pom_xml(self, tmp_path: Path) -> None: - """In monorepo/frontend/, a local package.json should win over a parent pom.xml.""" - # Parent Java project - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - (tmp_path / "src" / "main" / "java").mkdir(parents=True) - - # Child JS project - frontend = tmp_path / "frontend" - frontend.mkdir() - (frontend / "package.json").write_text( - json.dumps({"name": "frontend", "codeflash": {"moduleRoot": "src"}}), - encoding="utf-8", - ) - (frontend / "src").mkdir() - - with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls: - mock_path_cls.cwd.return_value = frontend - # find_package_json also uses Path.cwd; mock it at the source - with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls: - mock_js_path_cls.cwd.return_value = frontend - # Also need to let normal Path operations work - mock_path_cls.side_effect = Path - mock_path_cls.cwd.return_value = frontend - mock_js_path_cls.side_effect = Path - mock_js_path_cls.cwd.return_value = frontend - - config, root = parse_config_file() - - # Should detect JS, not Java - assert config.get("language") != "java", ( - "Closer package.json should take priority over parent pom.xml" - ) - - def test_java_wins_when_no_closer_js_config(self, tmp_path: Path) -> None: - """When only a pom.xml exists (no package.json/pyproject.toml closer), Java config wins.""" - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - (tmp_path / "src" / "main" / "java").mkdir(parents=True) - - with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls: - mock_path_cls.side_effect = Path - mock_path_cls.cwd.return_value = tmp_path - with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls: - mock_js_path_cls.side_effect = Path - mock_js_path_cls.cwd.return_value = tmp_path - - config, root = parse_config_file() - - assert config.get("language") == "java" - - def test_same_level_package_json_wins_over_pom_xml(self, tmp_path: Path) -> None: - """When pom.xml and package.json are at the same level, package.json wins (more specific).""" - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - (tmp_path / "src" / "main" / "java").mkdir(parents=True) - (tmp_path / "package.json").write_text( - json.dumps({"name": "mixed-project", "codeflash": {"moduleRoot": "src"}}), - encoding="utf-8", - ) - - with patch("codeflash.code_utils.config_parser.Path") as mock_path_cls: - mock_path_cls.side_effect = Path - mock_path_cls.cwd.return_value = tmp_path - with patch("codeflash.code_utils.config_js.Path") as mock_js_path_cls: - mock_js_path_cls.side_effect = Path - mock_js_path_cls.cwd.return_value = tmp_path - - config, root = parse_config_file() - - assert config.get("language") != "java", ( - "Same-level package.json should take priority over pom.xml" - ) diff --git a/tests/scripts/end_to_end_test_utilities.py b/tests/scripts/end_to_end_test_utilities.py index 33825db4d..12259b339 100644 --- a/tests/scripts/end_to_end_test_utilities.py +++ b/tests/scripts/end_to_end_test_utilities.py @@ -149,8 +149,8 @@ def build_command( if config.function_name: base_command.extend(["--function", config.function_name]) - # Check if config exists (pyproject.toml, pom.xml, build.gradle) - if so, don't override it - has_codeflash_config = (cwd / "pom.xml").exists() or (cwd / "build.gradle").exists() or (cwd / "build.gradle.kts").exists() + # Check if config exists (pyproject.toml or codeflash.toml) - if so, don't override it + has_codeflash_config = (cwd / "codeflash.toml").exists() if not has_codeflash_config: pyproject_path = cwd / "pyproject.toml" if pyproject_path.exists(): diff --git a/tests/test_languages/fixtures/java_maven/codeflash.toml b/tests/test_languages/fixtures/java_maven/codeflash.toml new file mode 100644 index 000000000..ecd20a562 --- /dev/null +++ b/tests/test_languages/fixtures/java_maven/codeflash.toml @@ -0,0 +1,5 @@ +# Codeflash configuration for Java project + +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" diff --git a/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml b/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml new file mode 100644 index 000000000..a501ef8cb --- /dev/null +++ b/tests/test_languages/fixtures/java_tracer_e2e/codeflash.toml @@ -0,0 +1,6 @@ +# Codeflash configuration for Java project + +[tool.codeflash] +module-root = "src/main/java" +tests-root = "src/test/java" +language = "java" diff --git a/tests/test_languages/test_java/test_java_config_detection.py b/tests/test_languages/test_java/test_java_config_detection.py deleted file mode 100644 index ebb8653af..000000000 --- a/tests/test_languages/test_java/test_java_config_detection.py +++ /dev/null @@ -1,444 +0,0 @@ -"""Tests for Java project auto-detection from Maven/Gradle build files. - -Tests that codeflash can detect Java projects and infer module-root, -tests-root, and other config from pom.xml / build.gradle / gradle.properties -without requiring a standalone codeflash.toml config file. -""" - -from __future__ import annotations - -from pathlib import Path - -import pytest - -from codeflash.languages.java.build_tools import ( - BuildTool, - detect_build_tool, - find_source_root, - find_test_root, - parse_java_project_config, -) - - -# --------------------------------------------------------------------------- -# Build tool detection -# --------------------------------------------------------------------------- - - -class TestDetectBuildTool: - def test_detect_maven(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - assert detect_build_tool(tmp_path) == BuildTool.MAVEN - - def test_detect_gradle(self, tmp_path: Path) -> None: - (tmp_path / "build.gradle").write_text("", encoding="utf-8") - assert detect_build_tool(tmp_path) == BuildTool.GRADLE - - def test_detect_gradle_kts(self, tmp_path: Path) -> None: - (tmp_path / "build.gradle.kts").write_text("", encoding="utf-8") - assert detect_build_tool(tmp_path) == BuildTool.GRADLE - - def test_maven_takes_priority_over_gradle(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - (tmp_path / "build.gradle").write_text("", encoding="utf-8") - assert detect_build_tool(tmp_path) == BuildTool.MAVEN - - def test_unknown_when_no_build_file(self, tmp_path: Path) -> None: - assert detect_build_tool(tmp_path) == BuildTool.UNKNOWN - - def test_detect_maven_in_parent(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - child = tmp_path / "module" - child.mkdir() - assert detect_build_tool(child) == BuildTool.MAVEN - - -# --------------------------------------------------------------------------- -# Source / test root detection (standard layouts) -# --------------------------------------------------------------------------- - - -class TestFindSourceRoot: - def test_standard_maven_layout(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - src = tmp_path / "src" / "main" / "java" - src.mkdir(parents=True) - assert find_source_root(tmp_path) == src - - def test_fallback_to_src_with_java_files(self, tmp_path: Path) -> None: - src = tmp_path / "src" - src.mkdir() - (src / "App.java").write_text("class App {}", encoding="utf-8") - assert find_source_root(tmp_path) == src - - def test_returns_none_when_no_source(self, tmp_path: Path) -> None: - assert find_source_root(tmp_path) is None - - -class TestFindTestRoot: - def test_standard_maven_layout(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - test = tmp_path / "src" / "test" / "java" - test.mkdir(parents=True) - assert find_test_root(tmp_path) == test - - def test_fallback_to_test_dir(self, tmp_path: Path) -> None: - test = tmp_path / "test" - test.mkdir() - assert find_test_root(tmp_path) == test - - def test_fallback_to_tests_dir(self, tmp_path: Path) -> None: - tests = tmp_path / "tests" - tests.mkdir() - assert find_test_root(tmp_path) == tests - - def test_returns_none_when_no_test_dir(self, tmp_path: Path) -> None: - assert find_test_root(tmp_path) is None - - -# --------------------------------------------------------------------------- -# parse_java_project_config — standard layouts -# --------------------------------------------------------------------------- - - -class TestParseJavaProjectConfigStandard: - def test_standard_maven_project(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - src = tmp_path / "src" / "main" / "java" - src.mkdir(parents=True) - test = tmp_path / "src" / "test" / "java" - test.mkdir(parents=True) - - config = parse_java_project_config(tmp_path) - assert config is not None - assert config["language"] == "java" - assert config["module_root"] == str(src) - assert config["tests_root"] == str(test) - - def test_standard_gradle_project(self, tmp_path: Path) -> None: - (tmp_path / "build.gradle").write_text("", encoding="utf-8") - src = tmp_path / "src" / "main" / "java" - src.mkdir(parents=True) - test = tmp_path / "src" / "test" / "java" - test.mkdir(parents=True) - - config = parse_java_project_config(tmp_path) - assert config is not None - assert config["language"] == "java" - assert config["module_root"] == str(src) - assert config["tests_root"] == str(test) - - def test_returns_none_for_non_java_project(self, tmp_path: Path) -> None: - assert parse_java_project_config(tmp_path) is None - - def test_defaults_when_dirs_missing(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - config = parse_java_project_config(tmp_path) - assert config is not None - # Falls back to default paths even if they don't exist - assert str(tmp_path / "src" / "main" / "java") == config["module_root"] - assert config["language"] == "java" - - -# --------------------------------------------------------------------------- -# parse_java_project_config — Maven properties (codeflash.*) -# --------------------------------------------------------------------------- - -MAVEN_POM_WITH_PROPERTIES = """\ - - 4.0.0 - com.example - test - 1.0 - - custom/src - custom/test - true - upstream - gen/,build/ - - -""" - - -class TestMavenCodeflashProperties: - def test_reads_custom_properties(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text(MAVEN_POM_WITH_PROPERTIES, encoding="utf-8") - (tmp_path / "custom" / "src").mkdir(parents=True) - (tmp_path / "custom" / "test").mkdir(parents=True) - - config = parse_java_project_config(tmp_path) - assert config is not None - assert config["module_root"] == str((tmp_path / "custom" / "src").resolve()) - assert config["tests_root"] == str((tmp_path / "custom" / "test").resolve()) - assert config["disable_telemetry"] is True - assert config["git_remote"] == "upstream" - assert len(config["ignore_paths"]) == 2 - - def test_properties_override_auto_detection(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text(MAVEN_POM_WITH_PROPERTIES, encoding="utf-8") - # Create standard dirs AND custom dirs - (tmp_path / "src" / "main" / "java").mkdir(parents=True) - (tmp_path / "custom" / "src").mkdir(parents=True) - (tmp_path / "custom" / "test").mkdir(parents=True) - - config = parse_java_project_config(tmp_path) - assert config is not None - # Should use custom paths from properties, not auto-detected standard paths - assert config["module_root"] == str((tmp_path / "custom" / "src").resolve()) - - def test_no_properties_uses_defaults(self, tmp_path: Path) -> None: - (tmp_path / "pom.xml").write_text( - '4.0.0', - encoding="utf-8", - ) - (tmp_path / "src" / "main" / "java").mkdir(parents=True) - - config = parse_java_project_config(tmp_path) - assert config is not None - assert config["disable_telemetry"] is False - assert config["git_remote"] == "origin" - - -# --------------------------------------------------------------------------- -# parse_java_project_config — Gradle properties -# --------------------------------------------------------------------------- - - -class TestGradleCodeflashProperties: - def test_reads_gradle_properties(self, tmp_path: Path) -> None: - (tmp_path / "build.gradle").write_text("", encoding="utf-8") - (tmp_path / "gradle.properties").write_text( - "codeflash.moduleRoot=lib/src\ncodeflash.testsRoot=lib/test\ncodeflash.disableTelemetry=true\n", - encoding="utf-8", - ) - (tmp_path / "lib" / "src").mkdir(parents=True) - (tmp_path / "lib" / "test").mkdir(parents=True) - - config = parse_java_project_config(tmp_path) - assert config is not None - assert config["module_root"] == str((tmp_path / "lib" / "src").resolve()) - assert config["tests_root"] == str((tmp_path / "lib" / "test").resolve()) - assert config["disable_telemetry"] is True - - def test_ignores_non_codeflash_properties(self, tmp_path: Path) -> None: - (tmp_path / "build.gradle").write_text("", encoding="utf-8") - (tmp_path / "gradle.properties").write_text( - "org.gradle.jvmargs=-Xmx2g\ncodeflash.gitRemote=upstream\n", - encoding="utf-8", - ) - - config = parse_java_project_config(tmp_path) - assert config is not None - assert config["git_remote"] == "upstream" - - def test_no_gradle_properties_uses_defaults(self, tmp_path: Path) -> None: - (tmp_path / "build.gradle").write_text("", encoding="utf-8") - (tmp_path / "src" / "main" / "java").mkdir(parents=True) - (tmp_path / "src" / "test" / "java").mkdir(parents=True) - - config = parse_java_project_config(tmp_path) - assert config is not None - assert config["git_remote"] == "origin" - assert config["disable_telemetry"] is False - - -# --------------------------------------------------------------------------- -# Multi-module Maven projects -# --------------------------------------------------------------------------- - -PARENT_POM = """\ - - 4.0.0 - com.example - parent - 1.0 - pom - - client - test - examples - - -""" - -CLIENT_POM = """\ - - 4.0.0 - - com.example - parent - 1.0 - - client - - ${project.basedir}/src - - -""" - -TEST_POM = """\ - - 4.0.0 - - com.example - parent - 1.0 - - test - - ${project.basedir}/src - - -""" - -EXAMPLES_POM = """\ - - 4.0.0 - - com.example - parent - 1.0 - - examples - - ${project.basedir}/src - - -""" - - -class TestMultiModuleMaven: - @pytest.fixture - def multi_module_project(self, tmp_path: Path) -> Path: - """Create a multi-module Maven project mimicking aerospike's layout.""" - (tmp_path / "pom.xml").write_text(PARENT_POM, encoding="utf-8") - - # Client module — main library with the most Java files - client = tmp_path / "client" - client.mkdir() - (client / "pom.xml").write_text(CLIENT_POM, encoding="utf-8") - client_src = client / "src" / "com" / "example" / "client" - client_src.mkdir(parents=True) - for i in range(10): - (client_src / f"Class{i}.java").write_text(f"class Class{i} {{}}", encoding="utf-8") - - # Test module — test code - test = tmp_path / "test" - test.mkdir() - (test / "pom.xml").write_text(TEST_POM, encoding="utf-8") - test_src = test / "src" / "com" / "example" / "test" - test_src.mkdir(parents=True) - (test_src / "ClientTest.java").write_text("class ClientTest {}", encoding="utf-8") - - # Examples module — should be skipped - examples = tmp_path / "examples" - examples.mkdir() - (examples / "pom.xml").write_text(EXAMPLES_POM, encoding="utf-8") - examples_src = examples / "src" / "com" / "example" - examples_src.mkdir(parents=True) - (examples_src / "Example.java").write_text("class Example {}", encoding="utf-8") - - return tmp_path - - def test_detects_client_as_source_root(self, multi_module_project: Path) -> None: - config = parse_java_project_config(multi_module_project) - assert config is not None - assert config["module_root"] == str(multi_module_project / "client" / "src") - - def test_detects_test_module_as_test_root(self, multi_module_project: Path) -> None: - config = parse_java_project_config(multi_module_project) - assert config is not None - assert config["tests_root"] == str(multi_module_project / "test" / "src") - - def test_skips_examples_module(self, multi_module_project: Path) -> None: - config = parse_java_project_config(multi_module_project) - assert config is not None - # The module_root should be client/src, not examples/src - assert config["module_root"] == str(multi_module_project / "client" / "src") - - def test_picks_module_with_most_java_files(self, multi_module_project: Path) -> None: - """Client has 10 .java files, examples has 1 — client should win.""" - config = parse_java_project_config(multi_module_project) - assert config is not None - assert "client" in config["module_root"] - - -# --------------------------------------------------------------------------- -# Language detection from config_parser -# --------------------------------------------------------------------------- - - -class TestLanguageDetectionViaConfigParser: - def test_java_detected_from_pom_xml(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - (tmp_path / "src" / "main" / "java").mkdir(parents=True) - (tmp_path / "src" / "test" / "java").mkdir(parents=True) - monkeypatch.chdir(tmp_path) - - from codeflash.code_utils.config_parser import _try_parse_java_build_config - - result = _try_parse_java_build_config() - assert result is not None - config, project_root = result - assert config["language"] == "java" - assert project_root == tmp_path - - def test_java_detected_from_build_gradle(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - (tmp_path / "build.gradle").write_text("", encoding="utf-8") - (tmp_path / "src" / "main" / "java").mkdir(parents=True) - monkeypatch.chdir(tmp_path) - - from codeflash.code_utils.config_parser import _try_parse_java_build_config - - result = _try_parse_java_build_config() - assert result is not None - config, _ = result - assert config["language"] == "java" - - def test_no_java_detected_for_python_project(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - (tmp_path / "pyproject.toml").write_text("[tool.codeflash]\nmodule-root='src'\ntests-root='tests'\n", encoding="utf-8") - monkeypatch.chdir(tmp_path) - - from codeflash.code_utils.config_parser import _try_parse_java_build_config - - result = _try_parse_java_build_config() - assert result is None - - -# --------------------------------------------------------------------------- -# Language detection from tracer -# --------------------------------------------------------------------------- - - -class TestTracerLanguageDetection: - def test_detects_java_from_build_files(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - (tmp_path / "pom.xml").write_text("", encoding="utf-8") - monkeypatch.chdir(tmp_path) - - from codeflash.languages.base import Language - from codeflash.tracer import _detect_non_python_language - - result = _detect_non_python_language(None) - assert result == Language.JAVA - - def test_no_detection_without_build_files(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.chdir(tmp_path) - - from codeflash.tracer import _detect_non_python_language - - result = _detect_non_python_language(None) - assert result is None - - def test_detects_java_from_file_extension(self, tmp_path: Path) -> None: - java_file = tmp_path / "App.java" - java_file.write_text("class App {}", encoding="utf-8") - - from argparse import Namespace - - from codeflash.languages.base import Language - from codeflash.tracer import _detect_non_python_language - - args = Namespace(file=str(java_file)) - result = _detect_non_python_language(args) - assert result == Language.JAVA diff --git a/tests/test_setup/test_config_writer.py b/tests/test_setup/test_config_writer.py deleted file mode 100644 index 89426bdfd..000000000 --- a/tests/test_setup/test_config_writer.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Tests for config_writer module — Java pom.xml formatting preservation.""" - -from pathlib import Path - - -class TestWriteMavenProperties: - """Tests for _write_maven_properties — text-based pom.xml editing.""" - - def test_preserves_comments(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - '\n' - "\n" - " \n" - " \n" - " 17\n" - " \n" - "\n", - encoding="utf-8", - ) - - from codeflash.setup.config_writer import _write_maven_properties - - ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"}) - result = pom.read_text(encoding="utf-8") - - assert ok - assert "" in result - assert "src/main/java" in result - - def test_preserves_namespace(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - '\n' - '\n' - " \n" - " 17\n" - " \n" - "\n", - encoding="utf-8", - ) - - from codeflash.setup.config_writer import _write_maven_properties - - ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"}) - result = pom.read_text(encoding="utf-8") - - assert ok - assert 'xmlns="http://maven.apache.org/POM/4.0.0"' in result - # Must NOT have ns0: prefix (ElementTree bug) - assert "ns0:" not in result - - def test_updates_existing_codeflash_properties(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - "\n" - " \n" - " old/path\n" - " \n" - "\n", - encoding="utf-8", - ) - - from codeflash.setup.config_writer import _write_maven_properties - - ok, _ = _write_maven_properties(pom, {"module-root": "new/path"}) - result = pom.read_text(encoding="utf-8") - - assert ok - assert "old/path" not in result - assert "new/path" in result - - def test_creates_properties_section(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - "\n" " 4.0.0\n" "\n", - encoding="utf-8", - ) - - from codeflash.setup.config_writer import _write_maven_properties - - ok, _ = _write_maven_properties(pom, {"module-root": "src/main/java"}) - result = pom.read_text(encoding="utf-8") - - assert ok - assert "" in result - assert "src/main/java" in result - - def test_converts_kebab_to_camelcase(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - "\n \n \n\n", - encoding="utf-8", - ) - - from codeflash.setup.config_writer import _write_maven_properties - - ok, _ = _write_maven_properties(pom, {"ignore-paths": ["target", "build"]}) - result = pom.read_text(encoding="utf-8") - - assert ok - assert "target,build" in result - - -class TestRemoveJavaBuildConfig: - """Tests for _remove_java_build_config — preserves formatting during removal.""" - - def test_removes_codeflash_from_pom_preserving_others(self, tmp_path: Path) -> None: - pom = tmp_path / "pom.xml" - pom.write_text( - "\n" - " \n" - " \n" - " 17\n" - " src/main/java\n" - " \n" - "\n", - encoding="utf-8", - ) - - from codeflash.setup.config_writer import _remove_java_build_config - - ok, _ = _remove_java_build_config(tmp_path) - result = pom.read_text(encoding="utf-8") - - assert ok - assert "" in result - assert "17" in result - assert "codeflash.moduleRoot" not in result - - def test_removes_codeflash_from_gradle_properties(self, tmp_path: Path) -> None: - gradle = tmp_path / "gradle.properties" - gradle.write_text( - "org.gradle.jvmargs=-Xmx2g\n" - "# Codeflash configuration \u2014 https://docs.codeflash.ai\n" - "codeflash.moduleRoot=src/main/java\n" - "codeflash.testsRoot=src/test/java\n", - encoding="utf-8", - ) - - from codeflash.setup.config_writer import _remove_java_build_config - - ok, _ = _remove_java_build_config(tmp_path) - result = gradle.read_text(encoding="utf-8") - - assert ok - assert "org.gradle.jvmargs=-Xmx2g" in result - assert "codeflash." not in result diff --git a/tests/test_setup/test_detector.py b/tests/test_setup/test_detector.py index 3b0e165c8..781d393e6 100644 --- a/tests/test_setup/test_detector.py +++ b/tests/test_setup/test_detector.py @@ -558,22 +558,6 @@ class TestHasExistingConfig: 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)