|
36 | 36 |
|
37 | 37 | logger = logging.getLogger(__name__) |
38 | 38 |
|
| 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 | + |
39 | 44 | # Regex pattern for valid Java class names (package.ClassName format) |
40 | 45 | # Allows: letters, digits, underscores, dots, and dollar signs (inner classes) |
41 | 46 | _VALID_JAVA_CLASS_NAME = re.compile(r"^[a-zA-Z_$][a-zA-Z0-9_$.]*$") |
@@ -411,25 +416,30 @@ def run_behavioral_tests( |
411 | 416 | add_jacoco_plugin_to_pom(pom_path) |
412 | 417 | coverage_xml_path = get_jacoco_xml_path(project_root) |
413 | 418 |
|
414 | | - # Run Maven tests from the appropriate root |
415 | 419 | # Use a minimum timeout of 60s for Java builds (120s when coverage is enabled due to verify phase) |
416 | 420 | min_timeout = 120 if enable_coverage else 60 |
417 | 421 | 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 | | - ) |
427 | 422 |
|
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 | + ) |
433 | 443 |
|
434 | 444 | # Debug: Log Maven result and coverage file status |
435 | 445 | if enable_coverage: |
@@ -597,6 +607,20 @@ def _get_test_classpath( |
597 | 607 | cp_file.unlink() |
598 | 608 |
|
599 | 609 |
|
| 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 | + |
600 | 624 | def _find_junit_console_standalone() -> Path | None: |
601 | 625 | """Find the JUnit Platform Console Standalone JAR in the local Maven repository. |
602 | 626 |
|
@@ -845,6 +869,77 @@ def _get_empty_result(maven_root: Path, test_module: str | None) -> tuple[Path, |
845 | 869 | return result_xml_path, empty_result |
846 | 870 |
|
847 | 871 |
|
| 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 | + |
848 | 943 | def _run_benchmarking_tests_maven( |
849 | 944 | test_paths: Any, |
850 | 945 | test_env: dict[str, str], |
@@ -1041,7 +1136,7 @@ def run_benchmarking_tests( |
1041 | 1136 |
|
1042 | 1137 | # Step 2: Get classpath from Maven |
1043 | 1138 | 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) |
1045 | 1140 |
|
1046 | 1141 | if not classpath: |
1047 | 1142 | logger.warning("Failed to get classpath, falling back to Maven-based execution") |
|
0 commit comments