2424from codeflash .code_utils .code_utils import get_run_tmp_file
2525from codeflash .languages .base import TestResult
2626
27+ _INCLUDE_PATTERN = re .compile (r"""(?:^|(?<=\s))include\s*\(?[^)]*\)?""" , re .MULTILINE | re .DOTALL )
28+
29+ _LISTOF_PATTERN = re .compile (r"""listOf\s*\(([^)]*)\)""" , re .DOTALL )
30+
31+ _QUOTED_PATTERN = re .compile (r"""['"]([^'"]+)['"]""" )
32+
2733_result_counter = itertools .count (1 )
2834
2935
@@ -205,12 +211,28 @@ def _extract_modules_from_settings_gradle(content: str) -> list[str]:
205211 Looks for include directives like:
206212 include("module-a", "module-b") // Kotlin DSL
207213 include 'module-a', 'module-b' // Groovy DSL
214+ Also handles dynamic Kotlin DSL patterns like:
215+ val allProjects = listOf("module-a", "module-b")
216+ include(*(allProjects + ...).toTypedArray())
208217 Module names may be prefixed with ':' which is stripped.
209218 """
210219 modules : list [str ] = []
211- for match in re .findall (r"""include\s*\(?[^)\n]*\)?""" , content ):
212- for name in re .findall (r"""['"]([^'"]+)['"]""" , match ):
220+ # Standard include(...) directives — word boundary avoids matching variable names
221+ # like 'includedProjects'. Pattern allows multi-line includes (e.g., eureka's
222+ # include 'module-a',\n 'module-b',\n 'module-c')
223+ for match in _INCLUDE_PATTERN .findall (content ):
224+ for name in _QUOTED_PATTERN .findall (match ):
213225 modules .append (name .lstrip (":" ))
226+ # Kotlin DSL: val ... = listOf("module-a", "module-b", ...) spanning multiple lines.
227+ # Used when settings.gradle.kts builds the include list dynamically.
228+ if not modules or not any ("/" not in m and "." not in m for m in modules ):
229+ seen = set (modules )
230+ for match in _LISTOF_PATTERN .findall (content ):
231+ for name in _QUOTED_PATTERN .findall (match ):
232+ stripped = name .lstrip (":" )
233+ if stripped not in seen :
234+ modules .append (stripped )
235+ seen .add (stripped )
214236 return modules
215237
216238
@@ -269,6 +291,50 @@ def _match_module_from_rel_path(rel_path: Path, modules: list[str]) -> str | Non
269291 return None
270292
271293
294+ def _read_config_module_root (project_root : Path ) -> str | None :
295+ """Read module-root from codeflash.toml or pyproject.toml."""
296+ for cfg_name in ("codeflash.toml" , "pyproject.toml" ):
297+ cfg_path = project_root / cfg_name
298+ if cfg_path .exists ():
299+ try :
300+ cfg_text = cfg_path .read_text (encoding = "utf-8" )
301+ m = re .search (r'module-root\s*=\s*["\']([^"\']+)["\']' , cfg_text )
302+ if m :
303+ return m .group (1 ).strip ().strip ("/" )
304+ except Exception :
305+ pass
306+ return None
307+
308+
309+ def _infer_module_from_config (project_root : Path ) -> str | None :
310+ """Infer the target Gradle module from codeflash config in gradle.properties.
311+
312+ Reads codeflash.moduleRoot or codeflash.testsRoot and extracts the first
313+ path component as the module name. Verifies the module directory has a
314+ build.gradle(.kts) file.
315+ """
316+ props_file = project_root / "gradle.properties"
317+ if not props_file .exists ():
318+ return None
319+ try :
320+ content = props_file .read_text (encoding = "utf-8" )
321+ except Exception :
322+ return None
323+
324+ for key in ("codeflash.moduleRoot" , "codeflash.testsRoot" ):
325+ for line in content .splitlines ():
326+ line = line .strip ()
327+ if line .startswith (key + "=" ):
328+ value = line .split ("=" , 1 )[1 ].strip ()
329+ # Extract first path component (e.g. "rewrite-core/src/main/java" → "rewrite-core")
330+ candidate = Path (value ).parts [0 ] if Path (value ).parts else None
331+ if candidate :
332+ module_dir = project_root / candidate
333+ if (module_dir / "build.gradle.kts" ).exists () or (module_dir / "build.gradle" ).exists ():
334+ return candidate
335+ return None
336+
337+
272338def _find_multi_module_root (project_root : Path , test_paths : Any ) -> tuple [Path , str | None ]:
273339 """Find the multi-module parent root if tests are in a different module.
274340
@@ -287,10 +353,18 @@ def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path,
287353 test_file_paths .append (test_file .benchmarking_file_path )
288354 elif hasattr (test_file , "instrumented_behavior_file_path" ) and test_file .instrumented_behavior_file_path :
289355 test_file_paths .append (test_file .instrumented_behavior_file_path )
356+ elif hasattr (test_file , "original_file_path" ) and test_file .original_file_path :
357+ test_file_paths .append (test_file .original_file_path )
290358 elif isinstance (test_paths , (list , tuple )):
291359 test_file_paths = [Path (p ) if isinstance (p , str ) else p for p in test_paths ]
292360
293361 if not test_file_paths :
362+ # No test file paths available — try to infer the module from codeflash config
363+ # in gradle.properties (e.g. codeflash.moduleRoot=rewrite-core/src/main/java).
364+ module = _infer_module_from_config (project_root )
365+ if module :
366+ logger .info ("Inferred module '%s' from codeflash config (no test file paths)" , module )
367+ return project_root , module
294368 return project_root , None
295369
296370 test_outside_project = False
@@ -320,14 +394,46 @@ def _find_multi_module_root(project_root: Path, test_paths: Any) -> tuple[Path,
320394 module_counts [matched ] = module_counts .get (matched , 0 ) + 1
321395
322396 if module_counts :
323- best_module = max (module_counts , key = lambda m : module_counts [m ])
397+ # On ties, prefer the module matching codeflash.toml module-root
398+ config_module = _read_config_module_root (project_root )
399+ max_count = max (module_counts .values ())
400+ tied = [m for m , c in module_counts .items () if c == max_count ]
401+ if config_module and config_module in tied :
402+ best_module = config_module
403+ else :
404+ best_module = max (module_counts , key = lambda m : module_counts [m ])
324405 logger .debug (
325406 "Detected multi-module project. Root: %s, Module votes: %s, Selected: %s" ,
326407 project_root ,
327408 module_counts ,
328409 best_module ,
329410 )
330411 return project_root , best_module
412+
413+ # project_root has no sub-modules — check if it is itself a sub-module
414+ # of a parent multi-module project (e.g. rewrite-core/ inside rewrite/).
415+ parent = project_root .parent
416+ while parent != parent .parent :
417+ if _is_build_root (parent ):
418+ parent_modules = _detect_modules (parent )
419+ if parent_modules :
420+ try :
421+ rel_path = project_root .relative_to (parent )
422+ matched = _match_module_from_rel_path (rel_path , parent_modules )
423+ if matched :
424+ logger .debug ("Detected project_root as sub-module. Root: %s, Module: %s" , parent , matched )
425+ return parent , matched
426+ except ValueError :
427+ pass
428+ parent = parent .parent
429+
430+ # Last resort: settings.gradle may use dynamic includes that _detect_modules
431+ # can't parse. Fall back to codeflash config in gradle.properties.
432+ module = _infer_module_from_config (project_root )
433+ if module :
434+ logger .info ("Inferred module '%s' from codeflash config (dynamic settings.gradle)" , module )
435+ return project_root , module
436+
331437 return project_root , None
332438
333439 current = project_root .parent
@@ -420,7 +526,7 @@ def run_behavioral_tests(
420526 if enable_coverage :
421527 coverage_xml_path = strategy .setup_coverage (build_root , test_module , project_root )
422528
423- min_timeout = 300 if enable_coverage else 60
529+ min_timeout = 1200 if enable_coverage else 60
424530 effective_timeout = max (timeout or 300 , min_timeout )
425531
426532 if enable_coverage :
0 commit comments