Skip to content

Commit 380e55b

Browse files
Merge pull request #1654 from codeflash-ai/feat/direct-jvm-behavioral-tests
feat: direct JVM execution for behavioral and line-profile tests
2 parents a3dd3be + 2e078f7 commit 380e55b

1 file changed

Lines changed: 111 additions & 16 deletions

File tree

codeflash/languages/java/test_runner.py

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636

3737
logger = logging.getLogger(__name__)
3838

39+
# Cache for classpath strings — keyed on (maven_root, test_module).
40+
# Dependencies don't change between candidates (only source code under test changes),
41+
# so we avoid calling `mvn dependency:build-classpath` (~2-3s) repeatedly.
42+
_classpath_cache: dict[tuple[Path, str | None], str] = {}
43+
3944
# Regex pattern for valid Java class names (package.ClassName format)
4045
# Allows: letters, digits, underscores, dots, and dollar signs (inner classes)
4146
_VALID_JAVA_CLASS_NAME = re.compile(r"^[a-zA-Z_$][a-zA-Z0-9_$.]*$")
@@ -411,25 +416,30 @@ def run_behavioral_tests(
411416
add_jacoco_plugin_to_pom(pom_path)
412417
coverage_xml_path = get_jacoco_xml_path(project_root)
413418

414-
# Run Maven tests from the appropriate root
415419
# Use a minimum timeout of 60s for Java builds (120s when coverage is enabled due to verify phase)
416420
min_timeout = 120 if enable_coverage else 60
417421
effective_timeout = max(timeout or 300, min_timeout)
418-
result = _run_maven_tests(
419-
maven_root,
420-
test_paths,
421-
run_env,
422-
timeout=effective_timeout,
423-
mode="behavior",
424-
enable_coverage=enable_coverage,
425-
test_module=test_module,
426-
)
427422

428-
# Find or create the JUnit XML results file
429-
# For multi-module projects, look in the test module's target directory
430-
target_dir = _get_test_module_target_dir(maven_root, test_module)
431-
surefire_dir = target_dir / "surefire-reports"
432-
result_xml_path = _get_combined_junit_xml(surefire_dir, candidate_index)
423+
if enable_coverage:
424+
# Coverage MUST use Maven — JaCoCo runs as a Maven plugin during the verify phase
425+
result = _run_maven_tests(
426+
maven_root,
427+
test_paths,
428+
run_env,
429+
timeout=effective_timeout,
430+
mode="behavior",
431+
enable_coverage=True,
432+
test_module=test_module,
433+
)
434+
target_dir = _get_test_module_target_dir(maven_root, test_module)
435+
surefire_dir = target_dir / "surefire-reports"
436+
result_xml_path = _get_combined_junit_xml(surefire_dir, candidate_index)
437+
else:
438+
# Direct JVM execution (fast path — bypasses Maven overhead)
439+
result, result_xml_path = _run_direct_or_fallback_maven(
440+
maven_root, test_module, test_paths, run_env,
441+
effective_timeout, mode="behavior", candidate_index=candidate_index,
442+
)
433443

434444
# Debug: Log Maven result and coverage file status
435445
if enable_coverage:
@@ -597,6 +607,20 @@ def _get_test_classpath(
597607
cp_file.unlink()
598608

599609

610+
def _get_test_classpath_cached(
611+
project_root: Path, env: dict[str, str], test_module: str | None = None, timeout: int = 60
612+
) -> str | None:
613+
key = (project_root, test_module)
614+
cached = _classpath_cache.get(key)
615+
if cached is not None:
616+
logger.debug("Using cached classpath for (%s, %s)", project_root, test_module)
617+
return cached
618+
result = _get_test_classpath(project_root, env, test_module, timeout)
619+
if result is not None:
620+
_classpath_cache[key] = result
621+
return result
622+
623+
600624
def _find_junit_console_standalone() -> Path | None:
601625
"""Find the JUnit Platform Console Standalone JAR in the local Maven repository.
602626
@@ -845,6 +869,77 @@ def _get_empty_result(maven_root: Path, test_module: str | None) -> tuple[Path,
845869
return result_xml_path, empty_result
846870

847871

872+
def _run_direct_or_fallback_maven(
873+
maven_root: Path,
874+
test_module: str | None,
875+
test_paths: Any,
876+
run_env: dict[str, str],
877+
timeout: int,
878+
mode: str,
879+
candidate_index: int = -1,
880+
) -> tuple[subprocess.CompletedProcess, Path]:
881+
"""Compile once, then run tests directly via JVM. Falls back to Maven on failure.
882+
883+
This mirrors the compile-once-run-many pattern from run_benchmarking_tests but
884+
for single-run modes (behavioral without coverage, line-profile).
885+
"""
886+
test_classes = _get_test_class_names(test_paths, mode=mode)
887+
if not test_classes:
888+
logger.warning("No test classes found for mode=%s, returning empty result", mode)
889+
result_xml_path, empty_result = _get_empty_result(maven_root, test_module)
890+
return empty_result, result_xml_path
891+
892+
# Step 1: Compile tests (still Maven — needed for dependency resolution)
893+
logger.debug("Step 1: Compiling tests for %s mode", mode)
894+
compile_result = _compile_tests(maven_root, run_env, test_module, timeout=120)
895+
if compile_result.returncode != 0:
896+
logger.warning("Compilation failed (rc=%d), falling back to Maven-based execution", compile_result.returncode)
897+
result = _run_maven_tests(maven_root, test_paths, run_env, timeout=timeout, mode=mode, test_module=test_module)
898+
target_dir = _get_test_module_target_dir(maven_root, test_module)
899+
surefire_dir = target_dir / "surefire-reports"
900+
result_xml_path = _get_combined_junit_xml(surefire_dir, candidate_index)
901+
return result, result_xml_path
902+
903+
# Step 2: Get classpath (cached after first call)
904+
logger.debug("Step 2: Getting classpath")
905+
classpath = _get_test_classpath_cached(maven_root, run_env, test_module, timeout=60)
906+
if not classpath:
907+
logger.warning("Failed to get classpath, falling back to Maven-based execution")
908+
result = _run_maven_tests(maven_root, test_paths, run_env, timeout=timeout, mode=mode, test_module=test_module)
909+
target_dir = _get_test_module_target_dir(maven_root, test_module)
910+
surefire_dir = target_dir / "surefire-reports"
911+
result_xml_path = _get_combined_junit_xml(surefire_dir, candidate_index)
912+
return result, result_xml_path
913+
914+
# Step 3: Run tests directly via JVM
915+
working_dir = maven_root / test_module if test_module else maven_root
916+
target_dir = _get_test_module_target_dir(maven_root, test_module)
917+
reports_dir = target_dir / "surefire-reports"
918+
reports_dir.mkdir(parents=True, exist_ok=True)
919+
920+
logger.debug("Step 3: Running %s tests directly (bypassing Maven)", mode)
921+
result = _run_tests_direct(classpath, test_classes, run_env, working_dir, timeout=timeout, reports_dir=reports_dir)
922+
923+
# Check for fallback indicators on failure (same checks as benchmarking)
924+
if result.returncode != 0:
925+
combined_output = (result.stderr or "") + (result.stdout or "")
926+
fallback_indicators = [
927+
"ConsoleLauncher",
928+
"ClassNotFoundException",
929+
"No tests were executed",
930+
"Unable to locate a Java Runtime",
931+
"No tests found",
932+
]
933+
if any(indicator in combined_output for indicator in fallback_indicators):
934+
logger.debug("Direct JVM execution failed, falling back to Maven-based execution")
935+
result = _run_maven_tests(
936+
maven_root, test_paths, run_env, timeout=timeout, mode=mode, test_module=test_module
937+
)
938+
939+
result_xml_path = _get_combined_junit_xml(reports_dir, candidate_index)
940+
return result, result_xml_path
941+
942+
848943
def _run_benchmarking_tests_maven(
849944
test_paths: Any,
850945
test_env: dict[str, str],
@@ -1041,7 +1136,7 @@ def run_benchmarking_tests(
10411136

10421137
# Step 2: Get classpath from Maven
10431138
logger.debug("Step 2: Getting classpath")
1044-
classpath = _get_test_classpath(maven_root, compile_env, test_module, timeout=60)
1139+
classpath = _get_test_classpath_cached(maven_root, compile_env, test_module, timeout=60)
10451140

10461141
if not classpath:
10471142
logger.warning("Failed to get classpath, falling back to Maven-based execution")

0 commit comments

Comments
 (0)