mirror of
https://github.com/codeflash-ai/codeflash.git
synced 2026-05-04 18:25:17 +00:00
Auto config
This commit is contained in:
parent
6f6c0398b3
commit
e7d07d073f
14 changed files with 1049 additions and 165 deletions
|
|
@ -1,4 +0,0 @@
|
|||
[tool.codeflash]
|
||||
module-root = "src/main/java"
|
||||
tests-root = "src/test/java"
|
||||
formatter-cmds = []
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Codeflash configuration for Java project
|
||||
|
||||
[tool.codeflash]
|
||||
module-root = "src/main/java"
|
||||
tests-root = "src/test/java"
|
||||
formatter-cmds = []
|
||||
|
|
@ -185,11 +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
|
||||
args.project_root = project_root_from_module_root(args.module_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
|
||||
|
|
@ -208,8 +214,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()
|
||||
|
|
|
|||
|
|
@ -232,12 +232,18 @@ def should_modify_java_config() -> tuple[bool, dict[str, Any] | None]:
|
|||
|
||||
project_root = Path.cwd()
|
||||
|
||||
# Check for existing codeflash config in pom.xml or a separate config file
|
||||
codeflash_config_path = project_root / "codeflash.toml"
|
||||
if codeflash_config_path.exists():
|
||||
return Confirm.ask(
|
||||
"A Codeflash config already exists. Do you want to re-configure it?", default=False, show_default=True
|
||||
), None
|
||||
# Check for existing codeflash config in pom.xml properties or gradle.properties
|
||||
from codeflash.languages.java.build_config_strategy import get_config_strategy
|
||||
|
||||
try:
|
||||
strategy = get_config_strategy(project_root)
|
||||
existing = strategy.read_codeflash_properties(project_root)
|
||||
if existing:
|
||||
return Confirm.ask(
|
||||
"A Codeflash config already exists. Do you want to re-configure it?", default=False, show_default=True
|
||||
), None
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return True, None
|
||||
|
||||
|
|
@ -436,42 +442,37 @@ def get_java_formatter_cmd(formatter: str, build_tool: JavaBuildTool) -> list[st
|
|||
if formatter == "other":
|
||||
global formatter_warning_shown
|
||||
if not formatter_warning_shown:
|
||||
click.echo("In codeflash.toml, please replace 'your-formatter' with your formatter command.")
|
||||
click.echo("In your build config, please replace 'your-formatter' with your formatter command.")
|
||||
formatter_warning_shown = True
|
||||
return ["your-formatter $file"]
|
||||
return ["disabled"]
|
||||
|
||||
|
||||
def configure_java_project(setup_info: JavaSetupInfo) -> bool:
|
||||
"""Configure codeflash.toml for Java projects."""
|
||||
import tomlkit
|
||||
"""Configure codeflash in pom.xml properties or gradle.properties."""
|
||||
from codeflash.languages.java.build_config_strategy import get_config_strategy
|
||||
|
||||
codeflash_config_path = Path.cwd() / "codeflash.toml"
|
||||
curdir = Path.cwd()
|
||||
|
||||
# Build config
|
||||
# Build config dict with only non-default overrides
|
||||
config: dict[str, Any] = {}
|
||||
|
||||
# Detect values
|
||||
curdir = Path.cwd()
|
||||
source_root = setup_info.module_root_override or detect_java_source_root(curdir)
|
||||
test_root = setup_info.test_root_override or detect_java_test_root(curdir)
|
||||
|
||||
config["language"] = "java"
|
||||
config["module-root"] = source_root
|
||||
config["tests-root"] = test_root
|
||||
# Only include non-default values
|
||||
defaults = {"module-root": "src/main/java", "tests-root": "src/test/java"}
|
||||
if source_root != defaults["module-root"]:
|
||||
config["module-root"] = source_root
|
||||
if test_root != defaults["tests-root"]:
|
||||
config["tests-root"] = test_root
|
||||
|
||||
# Formatter
|
||||
if setup_info.formatter_override is not None:
|
||||
if setup_info.formatter_override != ["disabled"]:
|
||||
config["formatter-cmds"] = setup_info.formatter_override
|
||||
else:
|
||||
config["formatter-cmds"] = []
|
||||
if setup_info.formatter_override is not None and setup_info.formatter_override != ["disabled"]:
|
||||
config["formatter-cmds"] = setup_info.formatter_override
|
||||
|
||||
# Git remote
|
||||
if setup_info.git_remote and setup_info.git_remote not in ("", "origin"):
|
||||
config["git-remote"] = setup_info.git_remote
|
||||
|
||||
# User preferences
|
||||
if setup_info.disable_telemetry:
|
||||
config["disable-telemetry"] = True
|
||||
|
||||
|
|
@ -481,27 +482,19 @@ def configure_java_project(setup_info: JavaSetupInfo) -> bool:
|
|||
if setup_info.benchmarks_root:
|
||||
config["benchmarks-root"] = setup_info.benchmarks_root
|
||||
|
||||
try:
|
||||
# Create TOML document
|
||||
doc = tomlkit.document()
|
||||
doc.add(tomlkit.comment("Codeflash configuration for Java project"))
|
||||
doc.add(tomlkit.nl())
|
||||
|
||||
codeflash_table = tomlkit.table()
|
||||
for key, value in config.items():
|
||||
codeflash_table.add(key, value)
|
||||
|
||||
doc.add("tool", tomlkit.table())
|
||||
doc["tool"]["codeflash"] = codeflash_table
|
||||
|
||||
with codeflash_config_path.open("w", encoding="utf-8") as f:
|
||||
f.write(tomlkit.dumps(doc))
|
||||
|
||||
click.echo(f"Created Codeflash configuration in {codeflash_config_path}")
|
||||
if not config:
|
||||
click.echo("Standard Maven/Gradle layout detected — no config needed")
|
||||
click.echo()
|
||||
return True
|
||||
except OSError as e:
|
||||
click.echo(f"Failed to create codeflash.toml: {e}")
|
||||
|
||||
try:
|
||||
strategy = get_config_strategy(curdir)
|
||||
ok, msg = strategy.write_codeflash_properties(curdir, config)
|
||||
click.echo(msg)
|
||||
click.echo()
|
||||
return ok
|
||||
except ValueError as e:
|
||||
click.echo(f"Failed to write config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,29 @@ PYPROJECT_TOML_CACHE: dict[Path, Path] = {}
|
|||
ALL_CONFIG_FILES: dict[Path, dict[str, Path]] = {}
|
||||
|
||||
|
||||
def _try_parse_java_build_config() -> tuple[dict[str, Any], Path] | None:
|
||||
"""Detect Java project from build files and parse config from pom.xml/gradle.properties.
|
||||
|
||||
Returns (config_dict, project_root) if a Java project is found, None otherwise.
|
||||
"""
|
||||
dir_path = Path.cwd()
|
||||
while dir_path != dir_path.parent:
|
||||
if (
|
||||
(dir_path / "pom.xml").exists()
|
||||
or (dir_path / "build.gradle").exists()
|
||||
or (dir_path / "build.gradle.kts").exists()
|
||||
):
|
||||
from codeflash.languages.java.build_config_strategy 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,34 @@ 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 build files, 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
|
||||
|
||||
# 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
|
||||
# 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 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 +174,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"]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ test execution, and optimization using tree-sitter for parsing and
|
|||
Maven/Gradle for build operations.
|
||||
"""
|
||||
|
||||
from codeflash.languages.java.build_config_strategy import (
|
||||
BuildConfigStrategy,
|
||||
GradleConfigStrategy,
|
||||
MavenConfigStrategy,
|
||||
get_config_strategy,
|
||||
)
|
||||
from codeflash.languages.java.build_tool_strategy import BuildToolStrategy, get_strategy
|
||||
from codeflash.languages.java.build_tools import (
|
||||
BuildTool,
|
||||
|
|
@ -96,9 +102,12 @@ from codeflash.languages.java.test_runner import (
|
|||
)
|
||||
|
||||
__all__ = [
|
||||
# Build config strategy
|
||||
"BuildConfigStrategy",
|
||||
# Build tools
|
||||
"BuildTool",
|
||||
"BuildToolStrategy",
|
||||
"GradleConfigStrategy",
|
||||
# Parser
|
||||
"JavaAnalyzer",
|
||||
# Assertion removal
|
||||
|
|
@ -118,6 +127,7 @@ __all__ = [
|
|||
"JavaSupport",
|
||||
# Test runner
|
||||
"JavaTestRunResult",
|
||||
"MavenConfigStrategy",
|
||||
"MavenTestResult",
|
||||
"ResolvedImport",
|
||||
"add_codeflash_dependency",
|
||||
|
|
@ -151,6 +161,7 @@ __all__ = [
|
|||
"format_java_code",
|
||||
"format_java_file",
|
||||
"get_class_methods",
|
||||
"get_config_strategy",
|
||||
"get_java_analyzer",
|
||||
"get_java_support",
|
||||
"get_method_by_name",
|
||||
|
|
|
|||
456
codeflash/languages/java/build_config_strategy.py
Normal file
456
codeflash/languages/java/build_config_strategy.py
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
"""Strategy pattern for Java build-config read/write/remove operations.
|
||||
|
||||
Defines BuildConfigStrategy ABC with MavenConfigStrategy (lxml-based pom.xml)
|
||||
and GradleConfigStrategy (line-based gradle.properties) implementations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from lxml import etree
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAVEN_NS = "http://maven.apache.org/POM/4.0.0"
|
||||
|
||||
# Maps kebab-case config keys to camelCase Maven/Gradle property names
|
||||
_KEY_MAP: dict[str, str] = {
|
||||
"module-root": "moduleRoot",
|
||||
"tests-root": "testsRoot",
|
||||
"git-remote": "gitRemote",
|
||||
"disable-telemetry": "disableTelemetry",
|
||||
"ignore-paths": "ignorePaths",
|
||||
"formatter-cmds": "formatterCmds",
|
||||
}
|
||||
|
||||
|
||||
class BuildConfigStrategy(ABC):
|
||||
"""Strategy interface for Java build-config read/write/remove operations."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str: ...
|
||||
|
||||
@abstractmethod
|
||||
def read_codeflash_properties(self, project_root: Path) -> dict[str, str]:
|
||||
"""Read codeflash.* properties from the build file.
|
||||
|
||||
Returns a dict mapping property suffix to value, e.g. {"moduleRoot": "src/main/java"}.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def write_codeflash_properties(self, project_root: Path, config: dict[str, Any]) -> tuple[bool, str]:
|
||||
"""Write codeflash.* properties to the build file. Only writes non-default overrides."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def remove_codeflash_properties(self, project_root: Path) -> tuple[bool, str]:
|
||||
"""Remove all codeflash.* properties from the build file."""
|
||||
...
|
||||
|
||||
|
||||
def _local_tag(element: etree._Element) -> str:
|
||||
"""Strip namespace prefix from an lxml element tag to get the local name."""
|
||||
tag = element.tag
|
||||
if isinstance(tag, str) and tag.startswith("{"):
|
||||
return tag.split("}", 1)[1]
|
||||
return str(tag)
|
||||
|
||||
|
||||
def _make_tag(root: etree._Element, local_name: str) -> str:
|
||||
"""Create a tag name respecting the document's default namespace."""
|
||||
ns = root.nsmap.get(None)
|
||||
if ns:
|
||||
return f"{{{ns}}}{local_name}"
|
||||
return local_name
|
||||
|
||||
|
||||
def _find_element(parent: etree._Element, local_name: str) -> etree._Element | None:
|
||||
"""Find a direct child element by local name, handling namespaces."""
|
||||
ns = parent.nsmap.get(None)
|
||||
if ns:
|
||||
return parent.find(f"{{{ns}}}{local_name}")
|
||||
return parent.find(local_name)
|
||||
|
||||
|
||||
def _detect_child_indent(properties_elem: etree._Element) -> str:
|
||||
"""Detect indentation used for children of a <properties> element."""
|
||||
for child in properties_elem:
|
||||
if isinstance(child.tag, str) and child.tail and "\n" in child.tail:
|
||||
# Indent is whitespace after the last newline in tail
|
||||
lines = child.tail.split("\n")
|
||||
if len(lines) > 1 and lines[-1].strip() == "":
|
||||
return lines[-1]
|
||||
# Try the element's own text (whitespace before first child)
|
||||
if properties_elem.text and "\n" in properties_elem.text:
|
||||
lines = properties_elem.text.split("\n")
|
||||
if len(lines) > 1:
|
||||
return lines[-1]
|
||||
return " " # 8-space default (typical Maven indent)
|
||||
|
||||
|
||||
def _format_value(value: Any) -> str:
|
||||
"""Convert a config value to a string suitable for build file properties."""
|
||||
if isinstance(value, list):
|
||||
return ",".join(str(v) for v in value)
|
||||
if isinstance(value, bool):
|
||||
return str(value).lower()
|
||||
return str(value)
|
||||
|
||||
|
||||
class MavenConfigStrategy(BuildConfigStrategy):
|
||||
"""Read/write/remove codeflash.* properties in pom.xml using lxml."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Maven"
|
||||
|
||||
def read_codeflash_properties(self, project_root: Path) -> dict[str, str]:
|
||||
pom_path = project_root / "pom.xml"
|
||||
if not pom_path.exists():
|
||||
return {}
|
||||
try:
|
||||
_tree, root = self._parse_pom(pom_path)
|
||||
props = _find_element(root, "properties")
|
||||
if props is None:
|
||||
return {}
|
||||
result: dict[str, str] = {}
|
||||
for child in props:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
local = _local_tag(child)
|
||||
if local.startswith("codeflash.") and child.text:
|
||||
key = local[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 write_codeflash_properties(self, project_root: Path, config: dict[str, Any]) -> tuple[bool, str]:
|
||||
pom_path = project_root / "pom.xml"
|
||||
if not pom_path.exists():
|
||||
return False, f"No pom.xml found at {project_root}"
|
||||
try:
|
||||
tree, root = self._parse_pom(pom_path)
|
||||
props = _find_element(root, "properties")
|
||||
|
||||
if props is None:
|
||||
# Create <properties> section
|
||||
props = etree.SubElement(root, _make_tag(root, "properties"))
|
||||
props.text = "\n "
|
||||
props.tail = "\n"
|
||||
else:
|
||||
# Remove existing codeflash.* elements
|
||||
for child in list(props):
|
||||
if isinstance(child.tag, str) and _local_tag(child).startswith("codeflash."):
|
||||
self._remove_preserving_whitespace(props, child)
|
||||
|
||||
indent = _detect_child_indent(props)
|
||||
|
||||
# Add new codeflash.* elements
|
||||
for key, value in config.items():
|
||||
prop_name = f"codeflash.{_KEY_MAP.get(key, key)}"
|
||||
tag = _make_tag(root, prop_name)
|
||||
elem = etree.SubElement(props, tag)
|
||||
elem.text = _format_value(value)
|
||||
elem.tail = "\n" + indent
|
||||
|
||||
# Fix the last element's tail to align with the closing </properties>
|
||||
last = props[-1] if len(props) > 0 else None
|
||||
if last is not None:
|
||||
# Closing tag indent = one level less than child indent
|
||||
parent_indent = indent[:-4] if len(indent) >= 4 else indent[:-2] if len(indent) >= 2 else ""
|
||||
last.tail = "\n" + parent_indent
|
||||
|
||||
# Ensure props.text has proper indent for first child
|
||||
if props.text is None or props.text.strip() == "":
|
||||
props.text = "\n" + indent
|
||||
|
||||
tree.write(str(pom_path), xml_declaration=True, encoding="UTF-8")
|
||||
return True, f"Config saved to {pom_path} <properties>"
|
||||
except Exception as e:
|
||||
return False, f"Failed to write Maven properties: {e}"
|
||||
|
||||
def remove_codeflash_properties(self, project_root: Path) -> tuple[bool, str]:
|
||||
pom_path = project_root / "pom.xml"
|
||||
if not pom_path.exists():
|
||||
return True, "No pom.xml found"
|
||||
try:
|
||||
tree, root = self._parse_pom(pom_path)
|
||||
props = _find_element(root, "properties")
|
||||
if props is None:
|
||||
return True, "No codeflash properties found in pom.xml"
|
||||
|
||||
removed = False
|
||||
for child in list(props):
|
||||
if isinstance(child.tag, str) and _local_tag(child).startswith("codeflash."):
|
||||
self._remove_preserving_whitespace(props, child)
|
||||
removed = True
|
||||
|
||||
if removed:
|
||||
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}"
|
||||
|
||||
@staticmethod
|
||||
def _parse_pom(pom_path: Path) -> tuple[etree._ElementTree, etree._Element]:
|
||||
parser = etree.XMLParser(remove_blank_text=False, strip_cdata=False)
|
||||
tree = etree.parse(str(pom_path), parser)
|
||||
return tree, tree.getroot()
|
||||
|
||||
@staticmethod
|
||||
def _remove_preserving_whitespace(parent: etree._Element, child: etree._Element) -> None:
|
||||
"""Remove a child element, merging its tail whitespace into the previous sibling or parent text."""
|
||||
prev = child.getprevious()
|
||||
if prev is not None:
|
||||
# Merge child's tail into previous sibling's tail
|
||||
prev.tail = (
|
||||
(prev.tail or "") if child.tail is None else (prev.tail or "").rstrip(" \t") + (child.tail or "")
|
||||
)
|
||||
# First child — merge tail into parent's text
|
||||
elif child.tail is not None:
|
||||
parent.text = (parent.text or "").rstrip(" \t") + child.tail
|
||||
parent.remove(child)
|
||||
|
||||
|
||||
class GradleConfigStrategy(BuildConfigStrategy):
|
||||
"""Read/write/remove codeflash.* properties in gradle.properties."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "Gradle"
|
||||
|
||||
def read_codeflash_properties(self, project_root: Path) -> dict[str, str]:
|
||||
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 write_codeflash_properties(self, project_root: Path, config: dict[str, Any]) -> tuple[bool, str]:
|
||||
props_path = project_root / "gradle.properties"
|
||||
try:
|
||||
lines: list[str] = []
|
||||
if props_path.exists():
|
||||
lines = props_path.read_text(encoding="utf-8").splitlines()
|
||||
|
||||
# Remove existing codeflash.* lines and our comment header
|
||||
lines = [
|
||||
line
|
||||
for line in lines
|
||||
if not line.strip().startswith("codeflash.")
|
||||
and line.strip() != "# Codeflash configuration \u2014 https://docs.codeflash.ai"
|
||||
]
|
||||
|
||||
# Add blank line separator if needed
|
||||
if lines and lines[-1].strip():
|
||||
lines.append("")
|
||||
lines.append("# Codeflash configuration \u2014 https://docs.codeflash.ai")
|
||||
for key, value in config.items():
|
||||
gradle_key = f"codeflash.{_KEY_MAP.get(key, key)}"
|
||||
lines.append(f"{gradle_key}={_format_value(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 remove_codeflash_properties(self, project_root: Path) -> tuple[bool, str]:
|
||||
props_path = project_root / "gradle.properties"
|
||||
if not props_path.exists():
|
||||
return True, "No gradle.properties found"
|
||||
try:
|
||||
lines = props_path.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"
|
||||
]
|
||||
props_path.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}"
|
||||
|
||||
|
||||
def get_config_strategy(project_root: Path) -> BuildConfigStrategy:
|
||||
"""Detect build tool and return the appropriate config strategy."""
|
||||
from codeflash.languages.java.build_tools import BuildTool, detect_build_tool
|
||||
|
||||
build_tool = detect_build_tool(project_root)
|
||||
if build_tool == BuildTool.MAVEN:
|
||||
return MavenConfigStrategy()
|
||||
if build_tool == BuildTool.GRADLE:
|
||||
return GradleConfigStrategy()
|
||||
msg = f"No supported Java build tool found in {project_root}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
from codeflash.languages.java.build_tools import BuildTool, detect_build_tool, find_source_root, find_test_root
|
||||
|
||||
build_tool = detect_build_tool(project_root)
|
||||
if build_tool == BuildTool.UNKNOWN:
|
||||
return None
|
||||
|
||||
try:
|
||||
strategy = get_config_strategy(project_root)
|
||||
user_config = strategy.read_codeflash_properties(project_root)
|
||||
except ValueError:
|
||||
user_config = {}
|
||||
|
||||
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)
|
||||
if source_from_modules is not None:
|
||||
source_root = source_from_modules
|
||||
if test_from_modules is not None:
|
||||
test_root = test_from_modules
|
||||
|
||||
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 _detect_roots_from_maven_modules(project_root: Path) -> tuple[Path | None, Path | None]:
|
||||
"""Scan Maven module pom.xml files for custom sourceDirectory/testSourceDirectory."""
|
||||
from codeflash.languages.java.build_tools import _safe_parse_xml
|
||||
|
||||
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": MAVEN_NS}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
|
@ -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,23 @@ def _write_codeflash_toml(project_root: Path, config: CodeflashConfig) -> tuple[
|
|||
Tuple of (success, message).
|
||||
|
||||
"""
|
||||
codeflash_toml_path = project_root / "codeflash.toml"
|
||||
from codeflash.languages.java.build_config_strategy import get_config_strategy
|
||||
|
||||
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)}
|
||||
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 \u2014 no config needed"
|
||||
|
||||
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()
|
||||
|
||||
# Ensure [tool] section exists
|
||||
if "tool" not in doc:
|
||||
doc["tool"] = tomlkit.table()
|
||||
|
||||
# Create codeflash section
|
||||
codeflash_table = tomlkit.table()
|
||||
codeflash_table.add(tomlkit.comment("Codeflash configuration for Java - https://docs.codeflash.ai"))
|
||||
|
||||
# Add config values
|
||||
config_dict = config.to_pyproject_dict()
|
||||
for key, value in config_dict.items():
|
||||
codeflash_table[key] = value
|
||||
|
||||
# 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 codeflash.toml: {e}"
|
||||
strategy = get_config_strategy(project_root)
|
||||
return strategy.write_codeflash_properties(project_root, non_default)
|
||||
except ValueError as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def _write_package_json(project_root: Path, config: CodeflashConfig) -> tuple[bool, str]:
|
||||
|
|
@ -206,7 +189,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 +218,15 @@ 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"
|
||||
def _remove_java_build_config(project_root: Path) -> tuple[bool, str]:
|
||||
"""Remove codeflash.* properties from pom.xml or gradle.properties."""
|
||||
from codeflash.languages.java.build_config_strategy import get_config_strategy
|
||||
|
||||
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}"
|
||||
strategy = get_config_strategy(project_root)
|
||||
return strategy.remove_codeflash_properties(project_root)
|
||||
except ValueError:
|
||||
return True, "No Java build config found"
|
||||
|
||||
|
||||
def _remove_from_package_json(project_root: Path) -> tuple[bool, str]:
|
||||
|
|
|
|||
|
|
@ -886,20 +886,24 @@ def has_existing_config(project_root: Path) -> tuple[bool, str | None]:
|
|||
|
||||
Returns:
|
||||
Tuple of (has_config, config_file_type).
|
||||
config_file_type is "pyproject.toml", "codeflash.toml", "package.json", or None.
|
||||
config_file_type is "pyproject.toml", "pom.xml", "build.gradle", "package.json", or None.
|
||||
|
||||
"""
|
||||
# Check TOML config files (pyproject.toml, codeflash.toml)
|
||||
for toml_filename in ("pyproject.toml", "codeflash.toml"):
|
||||
toml_path = project_root / toml_filename
|
||||
if toml_path.exists():
|
||||
try:
|
||||
with toml_path.open("rb") as f:
|
||||
data = tomlkit.parse(f.read())
|
||||
if "tool" in data and "codeflash" in data["tool"]:
|
||||
return True, toml_filename
|
||||
except Exception:
|
||||
pass
|
||||
# Check pyproject.toml (Python projects)
|
||||
pyproject_path = project_root / "pyproject.toml"
|
||||
if pyproject_path.exists():
|
||||
try:
|
||||
with pyproject_path.open("rb") as f:
|
||||
data = tomlkit.parse(f.read())
|
||||
if "tool" in data and "codeflash" in data["tool"]:
|
||||
return True, "pyproject.toml"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check Java build files — zero-config: build file presence means "configured"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,19 @@ def _detect_non_python_language(args: Namespace | None) -> Language | None:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# Method 2: Check project config for language field
|
||||
# Method 2: Check for Java build files (pom.xml, build.gradle, build.gradle.kts)
|
||||
cwd = Path.cwd()
|
||||
search = cwd
|
||||
while search != search.parent:
|
||||
if (
|
||||
(search / "pom.xml").exists()
|
||||
or (search / "build.gradle").exists()
|
||||
or (search / "build.gradle.kts").exists()
|
||||
):
|
||||
return Language.JAVA
|
||||
search = search.parent
|
||||
|
||||
# Method 3: Check project config for language field
|
||||
try:
|
||||
from codeflash.code_utils.config_parser import parse_config_file
|
||||
|
||||
|
|
@ -90,7 +102,8 @@ def main(args: Namespace | None = None) -> ArgumentParser:
|
|||
#
|
||||
# Detection methods (in priority order):
|
||||
# 1. --file pointing to a .java/.js/.ts file
|
||||
# 2. language field in project config (codeflash.toml or pyproject.toml)
|
||||
# 2. Java build files (pom.xml, build.gradle, build.gradle.kts)
|
||||
# 3. language field in project config (pyproject.toml, package.json)
|
||||
detected_language = _detect_non_python_language(args)
|
||||
if detected_language is not None:
|
||||
from codeflash.languages import Language
|
||||
|
|
|
|||
|
|
@ -149,8 +149,10 @@ def build_command(
|
|||
if config.function_name:
|
||||
base_command.extend(["--function", config.function_name])
|
||||
|
||||
# Check if config exists (pyproject.toml or codeflash.toml) - if so, don't override it
|
||||
has_codeflash_config = (cwd / "codeflash.toml").exists()
|
||||
# Check if config exists (pyproject.toml, pom.xml, build.gradle) - if so, don't override it
|
||||
has_codeflash_config = (
|
||||
(cwd / "pom.xml").exists() or (cwd / "build.gradle").exists() or (cwd / "build.gradle.kts").exists()
|
||||
)
|
||||
if not has_codeflash_config:
|
||||
pyproject_path = cwd / "pyproject.toml"
|
||||
if pyproject_path.exists():
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
# Codeflash configuration for Java project
|
||||
|
||||
[tool.codeflash]
|
||||
module-root = "src/main/java"
|
||||
tests-root = "src/test/java"
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Codeflash configuration for Java project
|
||||
|
||||
[tool.codeflash]
|
||||
module-root = "src/main/java"
|
||||
tests-root = "src/test/java"
|
||||
language = "java"
|
||||
439
tests/test_languages/test_java/test_build_config_strategy.py
Normal file
439
tests/test_languages/test_java/test_build_config_strategy.py
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
"""Tests for BuildConfigStrategy — Maven (lxml) and Gradle config read/write/remove."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from codeflash.languages.java.build_config_strategy import (
|
||||
GradleConfigStrategy,
|
||||
MavenConfigStrategy,
|
||||
get_config_strategy,
|
||||
parse_java_project_config,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MavenConfigStrategy — read
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMavenRead:
|
||||
def test_reads_codeflash_properties_with_namespace(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
|
||||
" <properties>\n"
|
||||
" <maven.compiler.source>17</maven.compiler.source>\n"
|
||||
" <codeflash.moduleRoot>custom/src</codeflash.moduleRoot>\n"
|
||||
" <codeflash.testsRoot>custom/test</codeflash.testsRoot>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = MavenConfigStrategy().read_codeflash_properties(tmp_path)
|
||||
assert result == {"moduleRoot": "custom/src", "testsRoot": "custom/test"}
|
||||
|
||||
def test_reads_codeflash_properties_without_namespace(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
"<project>\n"
|
||||
" <properties>\n"
|
||||
" <codeflash.gitRemote>upstream</codeflash.gitRemote>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = MavenConfigStrategy().read_codeflash_properties(tmp_path)
|
||||
assert result == {"gitRemote": "upstream"}
|
||||
|
||||
def test_returns_empty_when_no_properties(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text("<project></project>\n", encoding="utf-8")
|
||||
assert MavenConfigStrategy().read_codeflash_properties(tmp_path) == {}
|
||||
|
||||
def test_returns_empty_when_no_pom(self, tmp_path: Path) -> None:
|
||||
assert MavenConfigStrategy().read_codeflash_properties(tmp_path) == {}
|
||||
|
||||
def test_ignores_non_codeflash_properties(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
"<project>\n"
|
||||
" <properties>\n"
|
||||
" <maven.compiler.source>17</maven.compiler.source>\n"
|
||||
" <codeflash.moduleRoot>src</codeflash.moduleRoot>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = MavenConfigStrategy().read_codeflash_properties(tmp_path)
|
||||
assert "maven.compiler.source" not in result
|
||||
assert result == {"moduleRoot": "src"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MavenConfigStrategy — write
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMavenWrite:
|
||||
def test_preserves_comments(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
"<project>\n"
|
||||
" <!-- Important comment -->\n"
|
||||
" <properties>\n"
|
||||
" <maven.compiler.source>17</maven.compiler.source>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src/main/java"})
|
||||
result = pom.read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "<!-- Important comment -->" in result
|
||||
assert "codeflash.moduleRoot" in result
|
||||
|
||||
def test_preserves_namespace(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
|
||||
" <properties>\n"
|
||||
" <maven.compiler.source>17</maven.compiler.source>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"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 — lxml avoids this)
|
||||
assert "ns0:" not in result
|
||||
|
||||
def test_preserves_existing_properties(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
"<project>\n"
|
||||
" <properties>\n"
|
||||
" <maven.compiler.source>17</maven.compiler.source>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src"})
|
||||
result = pom.read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "<maven.compiler.source>17</maven.compiler.source>" in result
|
||||
assert "codeflash.moduleRoot" in result
|
||||
|
||||
def test_updates_existing_codeflash_properties(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
"<project>\n"
|
||||
" <properties>\n"
|
||||
" <codeflash.moduleRoot>old/path</codeflash.moduleRoot>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "new/path"})
|
||||
result = pom.read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "old/path" not in result
|
||||
assert "<codeflash.moduleRoot>new/path</codeflash.moduleRoot>" in result
|
||||
|
||||
def test_creates_properties_section(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text("<project>\n <modelVersion>4.0.0</modelVersion>\n</project>\n", encoding="utf-8")
|
||||
|
||||
ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src/main/java"})
|
||||
result = pom.read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "properties" in result
|
||||
assert "codeflash.moduleRoot" in result
|
||||
|
||||
def test_converts_kebab_to_camelcase(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text("<project>\n <properties>\n </properties>\n</project>\n", encoding="utf-8")
|
||||
|
||||
ok, _ = MavenConfigStrategy().write_codeflash_properties(
|
||||
tmp_path, {"ignore-paths": ["target", "build"]}
|
||||
)
|
||||
result = pom.read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "<codeflash.ignorePaths>target,build</codeflash.ignorePaths>" in result
|
||||
|
||||
def test_handles_boolean_values(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text("<project>\n <properties>\n </properties>\n</project>\n", encoding="utf-8")
|
||||
|
||||
ok, _ = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"disable-telemetry": True})
|
||||
result = pom.read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "<codeflash.disableTelemetry>true</codeflash.disableTelemetry>" in result
|
||||
|
||||
def test_returns_error_when_no_pom(self, tmp_path: Path) -> None:
|
||||
ok, msg = MavenConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "src"})
|
||||
assert not ok
|
||||
assert "No pom.xml" in msg
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MavenConfigStrategy — remove
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMavenRemove:
|
||||
def test_removes_only_codeflash_properties(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
"<project>\n"
|
||||
" <!-- Keep me -->\n"
|
||||
" <properties>\n"
|
||||
" <maven.compiler.source>17</maven.compiler.source>\n"
|
||||
" <codeflash.moduleRoot>src/main/java</codeflash.moduleRoot>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = MavenConfigStrategy().remove_codeflash_properties(tmp_path)
|
||||
result = pom.read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "<!-- Keep me -->" in result
|
||||
assert "<maven.compiler.source>17</maven.compiler.source>" in result
|
||||
assert "codeflash.moduleRoot" not in result
|
||||
|
||||
def test_preserves_comments_after_removal(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
"<project>\n"
|
||||
" <!-- Project comment -->\n"
|
||||
" <properties>\n"
|
||||
" <!-- Property comment -->\n"
|
||||
" <codeflash.moduleRoot>src</codeflash.moduleRoot>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = MavenConfigStrategy().remove_codeflash_properties(tmp_path)
|
||||
result = pom.read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "<!-- Project comment -->" in result
|
||||
assert "<!-- Property comment -->" in result
|
||||
assert "codeflash" not in result
|
||||
|
||||
def test_noop_when_no_codeflash_properties(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
"<project>\n <properties>\n <foo>bar</foo>\n </properties>\n</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = MavenConfigStrategy().remove_codeflash_properties(tmp_path)
|
||||
assert ok
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MavenConfigStrategy — roundtrip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMavenRoundtrip:
|
||||
def test_write_then_read_roundtrip(self, tmp_path: Path) -> None:
|
||||
pom = tmp_path / "pom.xml"
|
||||
pom.write_text(
|
||||
"<project>\n <properties>\n </properties>\n</project>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
strategy = MavenConfigStrategy()
|
||||
strategy.write_codeflash_properties(
|
||||
tmp_path,
|
||||
{"module-root": "client/src", "git-remote": "upstream", "disable-telemetry": True},
|
||||
)
|
||||
result = strategy.read_codeflash_properties(tmp_path)
|
||||
assert result["moduleRoot"] == "client/src"
|
||||
assert result["gitRemote"] == "upstream"
|
||||
assert result["disableTelemetry"] == "true"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GradleConfigStrategy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGradleRead:
|
||||
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(
|
||||
"org.gradle.jvmargs=-Xmx2g\ncodeflash.moduleRoot=lib/src\ncodeflash.disableTelemetry=true\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = GradleConfigStrategy().read_codeflash_properties(tmp_path)
|
||||
assert result == {"moduleRoot": "lib/src", "disableTelemetry": "true"}
|
||||
|
||||
def test_ignores_non_codeflash(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "gradle.properties").write_text(
|
||||
"org.gradle.jvmargs=-Xmx2g\ncodeflash.gitRemote=upstream\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = GradleConfigStrategy().read_codeflash_properties(tmp_path)
|
||||
assert "org.gradle.jvmargs" not in result
|
||||
assert result == {"gitRemote": "upstream"}
|
||||
|
||||
def test_returns_empty_when_no_file(self, tmp_path: Path) -> None:
|
||||
assert GradleConfigStrategy().read_codeflash_properties(tmp_path) == {}
|
||||
|
||||
|
||||
class TestGradleWrite:
|
||||
def test_writes_gradle_properties(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "gradle.properties").write_text("org.gradle.jvmargs=-Xmx2g\n", encoding="utf-8")
|
||||
ok, _ = GradleConfigStrategy().write_codeflash_properties(
|
||||
tmp_path, {"module-root": "lib/src", "disable-telemetry": True}
|
||||
)
|
||||
result = (tmp_path / "gradle.properties").read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "org.gradle.jvmargs=-Xmx2g" in result
|
||||
assert "codeflash.moduleRoot=lib/src" in result
|
||||
assert "codeflash.disableTelemetry=true" in result
|
||||
|
||||
def test_creates_file_if_missing(self, tmp_path: Path) -> None:
|
||||
ok, _ = GradleConfigStrategy().write_codeflash_properties(tmp_path, {"git-remote": "upstream"})
|
||||
result = (tmp_path / "gradle.properties").read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "codeflash.gitRemote=upstream" in result
|
||||
|
||||
def test_updates_existing_codeflash_properties(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "gradle.properties").write_text(
|
||||
"codeflash.moduleRoot=old\ncodeflash.gitRemote=origin\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = GradleConfigStrategy().write_codeflash_properties(tmp_path, {"module-root": "new"})
|
||||
result = (tmp_path / "gradle.properties").read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "codeflash.moduleRoot=new" in result
|
||||
assert "old" not in result
|
||||
|
||||
|
||||
class TestGradleRemove:
|
||||
def test_removes_codeflash_from_gradle_properties(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "gradle.properties").write_text(
|
||||
"org.gradle.jvmargs=-Xmx2g\n"
|
||||
"# Codeflash configuration \u2014 https://docs.codeflash.ai\n"
|
||||
"codeflash.moduleRoot=src/main/java\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ok, _ = GradleConfigStrategy().remove_codeflash_properties(tmp_path)
|
||||
result = (tmp_path / "gradle.properties").read_text(encoding="utf-8")
|
||||
|
||||
assert ok
|
||||
assert "org.gradle.jvmargs=-Xmx2g" in result
|
||||
assert "codeflash." not in result
|
||||
|
||||
def test_noop_when_no_file(self, tmp_path: Path) -> None:
|
||||
ok, _ = GradleConfigStrategy().remove_codeflash_properties(tmp_path)
|
||||
assert ok
|
||||
|
||||
|
||||
class TestGradleRoundtrip:
|
||||
def test_write_then_read_roundtrip(self, tmp_path: Path) -> None:
|
||||
strategy = GradleConfigStrategy()
|
||||
strategy.write_codeflash_properties(tmp_path, {"module-root": "lib/src", "git-remote": "upstream"})
|
||||
result = strategy.read_codeflash_properties(tmp_path)
|
||||
assert result["moduleRoot"] == "lib/src"
|
||||
assert result["gitRemote"] == "upstream"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Factory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetConfigStrategy:
|
||||
def test_returns_maven_for_pom(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
|
||||
assert isinstance(get_config_strategy(tmp_path), MavenConfigStrategy)
|
||||
|
||||
def test_returns_gradle_for_build_gradle(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "build.gradle").write_text("", encoding="utf-8")
|
||||
assert isinstance(get_config_strategy(tmp_path), GradleConfigStrategy)
|
||||
|
||||
def test_raises_for_unknown(self, tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="No supported Java build tool"):
|
||||
get_config_strategy(tmp_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# parse_java_project_config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestParseJavaProjectConfig:
|
||||
def test_standard_maven_project(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
|
||||
src = tmp_path / "src" / "main" / "java"
|
||||
src.mkdir(parents=True)
|
||||
test = tmp_path / "src" / "test" / "java"
|
||||
test.mkdir(parents=True)
|
||||
|
||||
config = parse_java_project_config(tmp_path)
|
||||
assert config is not None
|
||||
assert config["language"] == "java"
|
||||
assert config["module_root"] == str(src)
|
||||
assert config["tests_root"] == str(test)
|
||||
|
||||
def test_standard_gradle_project(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "build.gradle").write_text("", encoding="utf-8")
|
||||
src = tmp_path / "src" / "main" / "java"
|
||||
src.mkdir(parents=True)
|
||||
test = tmp_path / "src" / "test" / "java"
|
||||
test.mkdir(parents=True)
|
||||
|
||||
config = parse_java_project_config(tmp_path)
|
||||
assert config is not None
|
||||
assert config["language"] == "java"
|
||||
|
||||
def test_returns_none_for_non_java(self, tmp_path: Path) -> None:
|
||||
assert parse_java_project_config(tmp_path) is None
|
||||
|
||||
def test_maven_with_custom_properties(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "pom.xml").write_text(
|
||||
'<project xmlns="http://maven.apache.org/POM/4.0.0">\n'
|
||||
" <properties>\n"
|
||||
" <codeflash.moduleRoot>custom/src</codeflash.moduleRoot>\n"
|
||||
" <codeflash.testsRoot>custom/test</codeflash.testsRoot>\n"
|
||||
" <codeflash.disableTelemetry>true</codeflash.disableTelemetry>\n"
|
||||
" </properties>\n"
|
||||
"</project>\n",
|
||||
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
|
||||
|
||||
def test_defaults_when_dirs_missing(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
|
||||
config = parse_java_project_config(tmp_path)
|
||||
assert config is not None
|
||||
assert config["module_root"] == str(tmp_path / "src" / "main" / "java")
|
||||
Loading…
Reference in a new issue