|
| 1 | +"""Tests for GradleStrategy._resolve_project_classpath and _compile_dependency_modules.""" |
| 2 | + |
| 3 | +import os |
| 4 | +import subprocess |
| 5 | +from pathlib import Path |
| 6 | +from unittest.mock import patch |
| 7 | + |
| 8 | +import pytest |
| 9 | + |
| 10 | +from codeflash.languages.java.gradle_strategy import GradleStrategy |
| 11 | + |
| 12 | + |
| 13 | +@pytest.fixture() |
| 14 | +def strategy(): |
| 15 | + return GradleStrategy() |
| 16 | + |
| 17 | + |
| 18 | +@pytest.fixture() |
| 19 | +def build_root(tmp_path): |
| 20 | + """Create a multi-module Gradle project layout with missing JARs.""" |
| 21 | + root = (tmp_path / "project").resolve() |
| 22 | + root.mkdir() |
| 23 | + |
| 24 | + # Module A: compiled (has classes but no JAR) |
| 25 | + mod_a = root / "module-a" |
| 26 | + (mod_a / "build" / "classes" / "java" / "main" / "com" / "example").mkdir(parents=True) |
| 27 | + (mod_a / "build" / "classes" / "java" / "main" / "com" / "example" / "A.class").write_bytes(b"") |
| 28 | + (mod_a / "build" / "resources" / "main" / "META-INF").mkdir(parents=True) |
| 29 | + (mod_a / "build" / "resources" / "main" / "META-INF" / "services.txt").write_bytes(b"") |
| 30 | + |
| 31 | + # Module B: compiled with Kotlin (has kotlin classes but no JAR) |
| 32 | + mod_b = root / "module-b" |
| 33 | + (mod_b / "build" / "classes" / "kotlin" / "main" / "com" / "example").mkdir(parents=True) |
| 34 | + (mod_b / "build" / "classes" / "kotlin" / "main" / "com" / "example" / "B.class").write_bytes(b"") |
| 35 | + (mod_b / "build" / "classes" / "java" / "main" / "com" / "example").mkdir(parents=True) |
| 36 | + (mod_b / "build" / "classes" / "java" / "main" / "com" / "example" / "BHelper.class").write_bytes(b"") |
| 37 | + |
| 38 | + # Module C: uncompiled (no build/classes at all — testRuntimeOnly dep) |
| 39 | + mod_c = root / "module-c" |
| 40 | + mod_c.mkdir() |
| 41 | + |
| 42 | + # External dependency JAR (exists) |
| 43 | + ext_dir = tmp_path / "gradle-cache" |
| 44 | + ext_dir.mkdir() |
| 45 | + ext_jar = ext_dir / "some-lib-1.0.jar" |
| 46 | + ext_jar.write_bytes(b"") |
| 47 | + |
| 48 | + return root |
| 49 | + |
| 50 | + |
| 51 | +def _make_classpath(build_root: Path, tmp_path: Path) -> str: |
| 52 | + """Build a classpath string mimicking Gradle's testRuntimeClasspath output.""" |
| 53 | + sep = os.pathsep |
| 54 | + ext_jar = str(tmp_path / "gradle-cache" / "some-lib-1.0.jar") |
| 55 | + return sep.join([ |
| 56 | + str(build_root / "module-a" / "build" / "libs" / "module-a-1.0.jar"), |
| 57 | + ext_jar, |
| 58 | + str(build_root / "module-b" / "build" / "libs" / "module-b-1.0.jar"), |
| 59 | + str(build_root / "module-c" / "build" / "libs" / "module-c-1.0.jar"), |
| 60 | + ]) |
| 61 | + |
| 62 | + |
| 63 | +def test_replaces_missing_jars_with_class_dirs(strategy, build_root, tmp_path): |
| 64 | + """Missing project JARs should be replaced with class/resource directories.""" |
| 65 | + classpath = _make_classpath(build_root, tmp_path) |
| 66 | + |
| 67 | + with ( |
| 68 | + patch.object(GradleStrategy, "find_executable", return_value="gradle"), |
| 69 | + patch("codeflash.languages.java.test_runner._run_cmd_kill_pg_on_timeout") as mock_run, |
| 70 | + ): |
| 71 | + # Mock the compilation of module-c |
| 72 | + mock_run.return_value = subprocess.CompletedProcess(args=["gradle"], returncode=0, stdout="", stderr="") |
| 73 | + # Simulate that compilation creates the class directory |
| 74 | + (build_root / "module-c" / "build" / "classes" / "java" / "main").mkdir(parents=True) |
| 75 | + |
| 76 | + result = strategy._resolve_project_classpath(classpath, build_root, {}, timeout=60) |
| 77 | + |
| 78 | + entries = result.split(os.pathsep) |
| 79 | + |
| 80 | + # Module A: JAR replaced with java/main classes + resources/main |
| 81 | + mod_a_java = str(build_root / "module-a" / "build" / "classes" / "java" / "main") |
| 82 | + mod_a_resources = str(build_root / "module-a" / "build" / "resources" / "main") |
| 83 | + assert mod_a_java in entries |
| 84 | + assert mod_a_resources in entries |
| 85 | + |
| 86 | + # Module B: JAR replaced with both kotlin/main and java/main classes |
| 87 | + mod_b_kotlin = str(build_root / "module-b" / "build" / "classes" / "kotlin" / "main") |
| 88 | + mod_b_java = str(build_root / "module-b" / "build" / "classes" / "java" / "main") |
| 89 | + assert mod_b_kotlin in entries |
| 90 | + assert mod_b_java in entries |
| 91 | + |
| 92 | + # External JAR preserved as-is |
| 93 | + ext_jar = str(tmp_path / "gradle-cache" / "some-lib-1.0.jar") |
| 94 | + assert ext_jar in entries |
| 95 | + |
| 96 | + # Original JAR paths should NOT be present |
| 97 | + assert str(build_root / "module-a" / "build" / "libs" / "module-a-1.0.jar") not in entries |
| 98 | + assert str(build_root / "module-b" / "build" / "libs" / "module-b-1.0.jar") not in entries |
| 99 | + |
| 100 | + |
| 101 | +def test_compiles_uncompiled_modules(strategy, build_root, tmp_path): |
| 102 | + """Modules with no compiled classes should trigger a Gradle compilation.""" |
| 103 | + classpath = _make_classpath(build_root, tmp_path) |
| 104 | + |
| 105 | + with ( |
| 106 | + patch.object(GradleStrategy, "find_executable", return_value="gradle"), |
| 107 | + patch("codeflash.languages.java.test_runner._run_cmd_kill_pg_on_timeout") as mock_run, |
| 108 | + ): |
| 109 | + mock_run.return_value = subprocess.CompletedProcess(args=["gradle"], returncode=0, stdout="", stderr="") |
| 110 | + |
| 111 | + strategy._resolve_project_classpath(classpath, build_root, {"JAVA_HOME": "/jdk"}, timeout=120) |
| 112 | + |
| 113 | + # Should have been called once to compile module-c |
| 114 | + mock_run.assert_called_once() |
| 115 | + cmd = mock_run.call_args[0][0] |
| 116 | + assert cmd[0] == "gradle" |
| 117 | + assert ":module-c:classes" in cmd |
| 118 | + assert "--no-daemon" in cmd |
| 119 | + |
| 120 | + |
| 121 | +def test_no_compilation_when_all_compiled(strategy, build_root, tmp_path): |
| 122 | + """When all modules have compiled classes, no compilation should be triggered.""" |
| 123 | + # Give module-c some compiled classes |
| 124 | + (build_root / "module-c" / "build" / "classes" / "java" / "main").mkdir(parents=True) |
| 125 | + (build_root / "module-c" / "build" / "classes" / "java" / "main" / "C.class").write_bytes(b"") |
| 126 | + |
| 127 | + classpath = _make_classpath(build_root, tmp_path) |
| 128 | + |
| 129 | + with ( |
| 130 | + patch.object(GradleStrategy, "find_executable", return_value="gradle"), |
| 131 | + patch("codeflash.languages.java.test_runner._run_cmd_kill_pg_on_timeout") as mock_run, |
| 132 | + ): |
| 133 | + strategy._resolve_project_classpath(classpath, build_root, {}, timeout=60) |
| 134 | + |
| 135 | + # No Gradle call should have been made |
| 136 | + mock_run.assert_not_called() |
| 137 | + |
| 138 | + |
| 139 | +def test_noop_when_no_missing_jars(strategy, build_root, tmp_path): |
| 140 | + """When all JARs exist, the classpath should be returned unchanged.""" |
| 141 | + # Create all the JAR files |
| 142 | + for mod in ["module-a", "module-b", "module-c"]: |
| 143 | + jar_dir = build_root / mod / "build" / "libs" |
| 144 | + jar_dir.mkdir(parents=True, exist_ok=True) |
| 145 | + (jar_dir / f"{mod}-1.0.jar").write_bytes(b"") |
| 146 | + |
| 147 | + classpath = _make_classpath(build_root, tmp_path) |
| 148 | + result = strategy._resolve_project_classpath(classpath, build_root, {}, timeout=60) |
| 149 | + assert result == classpath |
| 150 | + |
| 151 | + |
| 152 | +def test_external_missing_jar_preserved(strategy, tmp_path): |
| 153 | + """Missing external JARs (not under build_root) should be kept as-is.""" |
| 154 | + root = (tmp_path / "project").resolve() |
| 155 | + root.mkdir() |
| 156 | + |
| 157 | + external_jar = "/some/external/path/lib.jar" |
| 158 | + classpath = external_jar |
| 159 | + |
| 160 | + result = strategy._resolve_project_classpath(classpath, root, {}, timeout=60) |
| 161 | + assert result == external_jar |
| 162 | + |
| 163 | + |
| 164 | +def test_nested_gradle_module(strategy, tmp_path): |
| 165 | + """Nested Gradle modules (connect/runtime) should be handled correctly.""" |
| 166 | + root = (tmp_path / "project").resolve() |
| 167 | + root.mkdir() |
| 168 | + |
| 169 | + # Nested module: connect/runtime |
| 170 | + nested = root / "connect" / "runtime" |
| 171 | + (nested / "build" / "classes" / "java" / "main").mkdir(parents=True) |
| 172 | + (nested / "build" / "classes" / "java" / "main" / "R.class").write_bytes(b"") |
| 173 | + |
| 174 | + jar_path = str(root / "connect" / "runtime" / "build" / "libs" / "runtime-1.0.jar") |
| 175 | + classpath = jar_path |
| 176 | + |
| 177 | + result = strategy._resolve_project_classpath(classpath, root, {}, timeout=60) |
| 178 | + entries = result.split(os.pathsep) |
| 179 | + |
| 180 | + assert str(root / "connect" / "runtime" / "build" / "classes" / "java" / "main") in entries |
| 181 | + assert jar_path not in entries |
| 182 | + |
| 183 | + |
| 184 | +def test_compile_dependency_modules_single_call(strategy, tmp_path): |
| 185 | + """Multiple uncompiled modules should be compiled in a single Gradle invocation.""" |
| 186 | + root = (tmp_path / "project").resolve() |
| 187 | + root.mkdir() |
| 188 | + |
| 189 | + with ( |
| 190 | + patch.object(GradleStrategy, "find_executable", return_value="gradle"), |
| 191 | + patch("codeflash.languages.java.test_runner._run_cmd_kill_pg_on_timeout") as mock_run, |
| 192 | + ): |
| 193 | + mock_run.return_value = subprocess.CompletedProcess(args=["gradle"], returncode=0, stdout="", stderr="") |
| 194 | + |
| 195 | + strategy._compile_dependency_modules(root, {}, ["module-a", "module-b", "connect:runtime"], timeout=120) |
| 196 | + |
| 197 | + mock_run.assert_called_once() |
| 198 | + cmd = mock_run.call_args[0][0] |
| 199 | + assert ":module-a:classes" in cmd |
| 200 | + assert ":module-b:classes" in cmd |
| 201 | + assert ":connect:runtime:classes" in cmd |
| 202 | + |
| 203 | + |
| 204 | +def test_compile_dependency_modules_gradle_not_found(strategy, tmp_path): |
| 205 | + """Should not crash when Gradle executable is not found.""" |
| 206 | + root = (tmp_path / "project").resolve() |
| 207 | + root.mkdir() |
| 208 | + |
| 209 | + with patch.object(GradleStrategy, "find_executable", return_value=None): |
| 210 | + # Should not raise |
| 211 | + strategy._compile_dependency_modules(root, {}, ["module-a"], timeout=60) |
0 commit comments