Auto config

This commit is contained in:
HeshamHM28 2026-03-26 06:38:33 +02:00
parent 6f6c0398b3
commit e7d07d073f
14 changed files with 1049 additions and 165 deletions

View file

@ -1,4 +0,0 @@
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
formatter-cmds = []

View file

@ -1,6 +0,0 @@
# Codeflash configuration for Java project
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
formatter-cmds = []

View file

@ -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()

View file

@ -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

View file

@ -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"]

View file

@ -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",

View 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

View file

@ -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]:

View file

@ -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"

View file

@ -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

View file

@ -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():

View file

@ -1,5 +0,0 @@
# Codeflash configuration for Java project
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"

View file

@ -1,6 +0,0 @@
# Codeflash configuration for Java project
[tool.codeflash]
module-root = "src/main/java"
tests-root = "src/test/java"
language = "java"

View 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")