2929logger = 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
33125class 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