Skip to content

Commit 3fdebd3

Browse files
misrasaurabh1claude
andcommitted
feat: add multi-module Maven project support for Java tests
- Add _find_multi_module_root() to detect when tests are in a separate module - Add _get_test_module_target_dir() to find the correct surefire reports dir - Update run_behavioral_tests() and run_benchmarking_tests() to: - Run Maven from the parent project root for multi-module projects - Use -pl <module> -am to build only the test module and dependencies - Use -DfailIfNoTests=false to allow modules without tests to pass - Use -DskipTests=false to override pom.xml skipTests settings - Look for surefire reports in the test module's target directory Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d2050b1 commit 3fdebd3

1 file changed

Lines changed: 119 additions & 5 deletions

File tree

codeflash/languages/java/test_runner.py

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,98 @@
2929
logger = logging.getLogger(__name__)
3030

3131

32+
def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path, str | None]:
33+
"""Find the multi-module Maven parent root if tests are in a different module.
34+
35+
For multi-module Maven projects, tests may be in a separate module from the source code.
36+
This function detects this situation and returns the parent project root along with
37+
the module containing the tests.
38+
39+
Args:
40+
project_root: The current project root (typically the source module).
41+
test_paths: TestFiles object or list of test file paths.
42+
43+
Returns:
44+
Tuple of (maven_root, test_module_name) where:
45+
- maven_root: The directory to run Maven from (parent if multi-module, else project_root)
46+
- test_module_name: The name of the test module if different from project_root, else None
47+
48+
"""
49+
# Get test file paths
50+
test_file_paths: list[Path] = []
51+
if hasattr(test_paths, "test_files"):
52+
for test_file in test_paths.test_files:
53+
if hasattr(test_file, "instrumented_behavior_file_path") and test_file.instrumented_behavior_file_path:
54+
test_file_paths.append(test_file.instrumented_behavior_file_path)
55+
elif isinstance(test_paths, (list, tuple)):
56+
test_file_paths = [Path(p) if isinstance(p, str) else p for p in test_paths]
57+
58+
if not test_file_paths:
59+
return project_root, None
60+
61+
# Check if any test file is outside the project_root
62+
test_outside_project = False
63+
test_dir: Path | None = None
64+
for test_path in test_file_paths:
65+
try:
66+
test_path.relative_to(project_root)
67+
except ValueError:
68+
# Test is outside project_root
69+
test_outside_project = True
70+
test_dir = test_path.parent
71+
break
72+
73+
if not test_outside_project:
74+
return project_root, None
75+
76+
# Find common parent that contains both project_root and test files
77+
# and has a pom.xml with <modules> section
78+
current = project_root.parent
79+
while current != current.parent:
80+
pom_path = current / "pom.xml"
81+
if pom_path.exists():
82+
# Check if this is a multi-module pom
83+
try:
84+
content = pom_path.read_text(encoding="utf-8")
85+
if "<modules>" in content:
86+
# Found multi-module parent
87+
# Get the relative module name for the test directory
88+
if test_dir:
89+
try:
90+
test_module = test_dir.relative_to(current)
91+
# Get the top-level module name (first component)
92+
test_module_name = test_module.parts[0] if test_module.parts else None
93+
logger.debug(
94+
"Detected multi-module Maven project. Root: %s, Test module: %s",
95+
current,
96+
test_module_name,
97+
)
98+
return current, test_module_name
99+
except ValueError:
100+
pass
101+
except Exception:
102+
pass
103+
current = current.parent
104+
105+
return project_root, None
106+
107+
108+
def _get_test_module_target_dir(maven_root: Path, test_module: str | None) -> Path:
109+
"""Get the target directory for the test module.
110+
111+
Args:
112+
maven_root: The Maven project root.
113+
test_module: The test module name, or None if not a multi-module project.
114+
115+
Returns:
116+
Path to the target directory where surefire reports will be.
117+
118+
"""
119+
if test_module:
120+
return maven_root / test_module / "target"
121+
return maven_root / "target"
122+
123+
32124
@dataclass
33125
class JavaTestRunResult:
34126
"""Result of running Java tests."""
@@ -76,6 +168,9 @@ def run_behavioral_tests(
76168
"""
77169
project_root = project_root or cwd
78170

171+
# Detect multi-module Maven projects where tests are in a different module
172+
maven_root, test_module = _find_multi_module_root(project_root, test_paths)
173+
79174
# Create SQLite database path for behavior capture - use standard path that parse_test_results expects
80175
sqlite_db_path = get_run_tmp_file(Path(f"test_return_values_{candidate_index}.sqlite"))
81176

@@ -88,6 +183,7 @@ def run_behavioral_tests(
88183
run_env["CODEFLASH_OUTPUT_FILE"] = str(sqlite_db_path) # SQLite output path
89184

90185
# If coverage is enabled, ensure JaCoCo is configured
186+
# For multi-module projects, add JaCoCo to the source module (project_root), not the test module
91187
coverage_xml_path: Path | None = None
92188
if enable_coverage:
93189
pom_path = project_root / "pom.xml"
@@ -97,18 +193,21 @@ def run_behavioral_tests(
97193
add_jacoco_plugin_to_pom(pom_path)
98194
coverage_xml_path = get_jacoco_xml_path(project_root)
99195

100-
# Run Maven tests
196+
# Run Maven tests from the appropriate root
101197
result = _run_maven_tests(
102-
project_root,
198+
maven_root,
103199
test_paths,
104200
run_env,
105201
timeout=timeout or 300,
106202
mode="behavior",
107203
enable_coverage=enable_coverage,
204+
test_module=test_module,
108205
)
109206

110207
# Find or create the JUnit XML results file
111-
surefire_dir = project_root / "target" / "surefire-reports"
208+
# For multi-module projects, look in the test module's target directory
209+
target_dir = _get_test_module_target_dir(maven_root, test_module)
210+
surefire_dir = target_dir / "surefire-reports"
112211
result_xml_path = _get_combined_junit_xml(surefire_dir, candidate_index)
113212

114213
# Return coverage_xml_path as the fourth element when coverage is enabled
@@ -150,6 +249,9 @@ def run_benchmarking_tests(
150249

151250
project_root = project_root or cwd
152251

252+
# Detect multi-module Maven projects where tests are in a different module
253+
maven_root, test_module = _find_multi_module_root(project_root, test_paths)
254+
153255
# Collect stdout from all loops
154256
all_stdout = []
155257
all_stderr = []
@@ -168,11 +270,12 @@ def run_benchmarking_tests(
168270

169271
# Run Maven tests for this loop
170272
result = _run_maven_tests(
171-
project_root,
273+
maven_root,
172274
test_paths,
173275
run_env,
174276
timeout=timeout or 120, # Per-loop timeout
175277
mode="performance",
278+
test_module=test_module,
176279
)
177280

178281
last_result = result
@@ -219,7 +322,9 @@ def run_benchmarking_tests(
219322
)
220323

221324
# Find or create the JUnit XML results file (from last run)
222-
surefire_dir = project_root / "target" / "surefire-reports"
325+
# For multi-module projects, look in the test module's target directory
326+
target_dir = _get_test_module_target_dir(maven_root, test_module)
327+
surefire_dir = target_dir / "surefire-reports"
223328
result_xml_path = _get_combined_junit_xml(surefire_dir, -1) # Use -1 for benchmark
224329

225330
return result_xml_path, combined_result
@@ -328,6 +433,7 @@ def _run_maven_tests(
328433
timeout: int = 300,
329434
mode: str = "behavior",
330435
enable_coverage: bool = False,
436+
test_module: str | None = None,
331437
) -> subprocess.CompletedProcess:
332438
"""Run Maven tests with Surefire.
333439
@@ -338,6 +444,7 @@ def _run_maven_tests(
338444
timeout: Maximum execution time in seconds.
339445
mode: Testing mode - "behavior" or "performance".
340446
enable_coverage: Whether to enable JaCoCo coverage collection.
447+
test_module: For multi-module projects, the module containing tests.
341448
342449
Returns:
343450
CompletedProcess with test results.
@@ -361,6 +468,13 @@ def _run_maven_tests(
361468
# We don't need to call jacoco:report explicitly since the plugin config binds it to test phase
362469
cmd = [mvn, "test", "-fae"] # Fail at end to run all tests
363470

471+
# For multi-module projects, specify which module to test
472+
if test_module:
473+
# -am = also make dependencies
474+
# -DfailIfNoTests=false allows dependency modules without tests to pass
475+
# -DskipTests=false overrides any skipTests=true in pom.xml
476+
cmd.extend(["-pl", test_module, "-am", "-DfailIfNoTests=false", "-DskipTests=false"])
477+
364478
if test_filter:
365479
cmd.append(f"-Dtest={test_filter}")
366480

0 commit comments

Comments
 (0)